SSDT技术,外挂制作,防毒制作

注明:本文是转帖。希望中国计算机技术越来越好。

论技术,我还差得远,而且网上关于SSDT的文章也多不胜数。但是还是想自己写一下,因为我想试试我能不能用最简单的语言来描述SSDT——这个对一般来人来说比较神秘的属于内核的地带。引用EVA说的一句话,“以为写个驱动就是内核,还远着了”——大概是这么个意思,记得不是很清楚。

关于SSDT,描述得最清楚的应该算《SSDT Hook的妙用-对抗ring0 inline hook》一文了,作者是堕落天才。这里引用一下他写的开头部分,略有个别字符的修改:

内核中有两个系统服务描述符表,一个是KeServiceDescriptorTable,由ntoskrnl.exe导出,一个是KeServieDescriptorTableShadow,没有导出。这两者都是一个结构体,结构下面会给出。他们的区别是,KeServiceDescriptorTable仅有 ntoskrnel一项,而KeServieDescriptorTableShadow则包含了ntoskrnel和win32k。一般的Native API的服务地址由KeServiceDescriptorTable分派,而gdi.dll和
user.dll的内核API调用服务地址,由 KeServieDescriptorTableShadow分派。还有要清楚一点的是win32k.sys只有在GUI线程中才加载,一般情况下是不加载的。

他们的结构如下:
代码:


typedef struct _SYSTEM_SERVICE_TABLE
{
     PVOID ServiceTableBase;     //这个指向系统服务函数地址表
     PULONG ServiceCounterTableBase;
     ULONG NumberOfService;      //服务函数的个数,NumberOfService*4 就是整个地址表的大小
     ULONG ParamTableBase;
}SYSTEM_SERVICE_TABLE,*PSYSTEM_SERVICE_TABLE;

typedef struct _SERVICE_DESCRIPTOR_TABLE
{
     SYSTEM_SERVICE_TABLE ntoskrnel;     //ntoskrnl.exe的服务函数
     SYSTEM_SERVICE_TABLE win32k;     //win32k.sys的服务函数,(gdi.dll/user.dll的内核支持)
     SYSTEM_SERVICE_TABLE NotUsed1;
     SYSTEM_SERVICE_TABLE NotUsed2;
}SYSTEM_DESCRIPTOR_TABLE,*PSYSTEM_DESCRIPTOR_TABLE;


当系统需要使用一个本机API的时候,就会去查找SYSTEM_DESCRIPTOR_TABLE这个表,也就是由ntoskrnl.exe导出的KeServiceDescriptorTable:

代码:

nt!RtlpBreakWithStatusInstruction:
80527fc8 cc               int     3
kd> dd KeServiceDescriptorTable
80553380   805021fc 00000000 0000011c 80502670
80553390   00000000 00000000 00000000 00000000
805533a0   00000000 00000000 00000000 00000000
805533b0   00000000 00000000 00000000 00000000
805533c0   00002710 bf80c227 00000000 00000000
805533d0   f9e6da80 f963a9e0 816850f0 806e0f40
805533e0   00000000 00000000 00000000 00000000
805533f0   97c5ac40 01c7abf5 00000000 00000000

可以看到,KeServiceDescriptorTable的地址是80553380。现在看看这个地址保存的是什么,因为这个地址的值就是SYSTEM_SERVICE_TABLE的起始地址。好了,我们看到这个地址保存的是805021fc,那么也就是说,系统服务的地址表起始地址为805021fc了。看看这个表是些什么鬼东西:
代码:

kd> dd 805021fc
805021fc   80599746 805e6914 805ea15a 805e6946
8050220c   805ea194 805e697c 805ea1d8 805ea21c
8050221c   8060b880 8060c5d2 805e1cac 805e1904
8050222c   805ca928 805ca8d8 8060bea6 805ab334
8050223c   8060b4be 8059dbbc 805a5786 805cc406
8050224c   804ffed0 8060c5c4 8056be64 805353f2
8050225c   80604b90 805b19c0 805ea694 80619a56
8050226c   805eeb86 80599e34 80619caa 805996e6

这个过程是这样的,最开始是SYSTEM_DESCRIPTOR_TABLE(80553380)保存了SYSTEM_SERVICE_TABLE的地址(805021fc),SYSTEM_SERVICE_TABLE的地址(805021fc)又保存了很多地址,这个地址就是系统服务的地址了,类似NtOpenProcess这样的ring0的函数地址。这样,系统就可以方便的找到每一个ring0函数去调用。

我们先看看第一个地址80599746是个什么函数,反汇编一下:
代码:

kd> u 80599746
nt!NtAcceptConnectPort:
80599746 689c000000       push     9Ch
8059974b 6820a14d80       push     offset nt!_real+0x128 (804da120)
80599750 e8abebf9ff       call     nt!_SEH_prolog (80538300)
80599755 64a124010000     mov     eax,dword ptr fs:[00000124h]
8059975b 8a8040010000     mov     al,byte ptr [eax+140h]
80599761 884590           mov     byte ptr [ebp-70h],al
80599764 84c0             test     al,al
80599766 0f84b9010000     je       nt!NtAcceptConnectPort+0x1df (80599925)

原来是NtAcceptConnectPort函数,第二个805e6914呢?我们也看一下,
代码:

kd> u 805e6914
nt!NtAccessCheck:
805e6914 8bff             mov     edi,edi
805e6916 55               push     ebp
805e6917 8bec             mov     ebp,esp
805e6919 33c0             xor     eax,eax
805e691b 50               push     eax
805e691c ff7524           push     dword ptr [ebp+24h]
805e691f ff7520           push     dword ptr [ebp+20h]
805e6922 ff751c           push     dword ptr [ebp+1Ch]

原来是NtAccessCheck函数。

这样我们可以清楚的看到,在这个起始地址为0x805021fc的表中,保存了各个ring0函数的地址。下面我来做个简单的比喻。

从前有一个很大的帮派,名字叫做Windows,功能很多并且很强大。因为这些各方面的能力由各个专人负责,他们一个人做一件事情。随着人员增多,帮主发现联系起来越来越困了。有一天帮主要找竟然NtOpenProcess来调查一下他的一个手下是不是别的帮派派来的间谍,但是他发现NtOpenProcess跑不见了。

于是军师就想出了一个好办法来解决这个问题:先建立一个封闭的密室,这个密室只有八袋长老以上的人才能进去。密室中间有一张纸条,上面写着一个地址——温家堡,还有这个地址放着多少人的联系信息等内容。这个密室就是Ntdll.dll,这个纸条就是SYSTEM_DESCRIPTOR_TABLE,上写的地址就是SYSTEM_SERVICE_TABLE,也就是温家堡了。这个温家堡是一个有很多大房间的地方,每个房子有个房间号
,房间里面又放着一张纸条,上面写着各个手下的住所。比如说编号为7A的房间,里面放的是NtOpenProcess的家庭住址。

这样一来,帮主要找人就容易了。先去密室找到纸条,看看上面写的是温家堡还是白云城,那个地方有多少个人的联系信息等。如果是温家堡就跑到那里去,看看要找谁,找NtOpenProcess就去7A房间。在这个房间里一看,啊,里面写着NtOpenProcess现在就住在密室的旁边……搞定。

这里就有一个新的问题,帮主假设这个里面写的东西都是正确的,没有被人改过。于是就有了别派的间谍发现了,偷偷溜进密室,然后根据纸条的内容,又跑到温家堡。进到7A房间,神不知鬼不觉的把里面记录的NtOpenProcess的地址改成了自己的家。于是,帮主再找人,发现找到对头家里去了。这个就是传说中的SSDT Hook了。

攻击者进入ring0之后,找到KeServiceDescriptorTable地址的值,即SYSTEM_SERVICE_TABLE的地址(进入密室,找到纸条写的地址——温家堡)。然后改写SYSTEM_SERVICE_TABLE中一个特定函数的地址为自己定义的函数入口处,截获了系统调用(来到温家堡,改掉7A房间里面写的住所,改成自己家)。一次HOOK就完成了。

下面我给一段简单的代码,演示怎么样让一个特定的PID不会被杀死。这段代码基本和《SSDT Hook的妙用-对抗ring0 inline hook》一文一样,我只是注释了一下而已,另外在MyNtOpenProcess处加了个判断是不是某个特定PID的功能。
代码:


/*
演示HOOK系统服务调用表中的NtOpenProcess函数,保护需要保护的进程被,防止被杀掉
*/

#include<ntddk.h>

/*
KeServiceDescriptorTable仅有ntoskrnel一项,没有包含win32k,而且后面的两个字段都没有使用,所

以为了简便直接把SystemServiceDescriptorTable定义成SYSTEM_SERVICE_TABLE,免得访问多个结构体的

字段,麻烦。这里明白就行了。
*/
typedef struct _SystemServiceDescriptorTable
{
     PVOID     ServiceTableBase;
     PULONG     ServiceCounterTableBase;
     ULONG     NumberOfService;
     ULONG     ParamTableBase;
}SystemServiceDescriptorTable,*PSystemServiceDescriptorTable;

// KeServiceDescriptorTable为ntoskrnl.exe导出
extern     PSystemServiceDescriptorTable     KeServiceDescriptorTable;

// 定义一下NtOpenProcess的原型,下面如果用汇编调用就不用定义了,但是我想尽量不用汇编
typedef     NTSTATUS     (__stdcall *NTOPENPROCESS)( OUT PHANDLE ProcessHandle,
                                                

IN ACCESS_MASK AccessMask,
                                                

IN POBJECT_ATTRIBUTES ObjectAttributes,
                                                

IN PCLIENT_ID ClientId
                                                

);

NTOPENPROCESS     RealNtOpenProcess;

// 定义函数原型
VOID Hook();
VOID Unhook();
VOID OnUnload(IN PDRIVER_OBJECT DriverObject);

// 真实的函数地址,我们会在自定义的函数中调用
ULONG     RealServiceAddress;

// 需要被驱动保护的进程ID
HANDLE     MyPID;

// 自定义的NtOpenProcess函数
NTSTATUS __stdcall MyNtOpenProcess( OUT     PHANDLE ProcessHandle,
                     IN     ACCESS_MASK DesiredAccess,
                     IN     POBJECT_ATTRIBUTES ObjectAttributes,
                     IN     PCLIENT_ID ClientId )
{
     NTSTATUS     rc;
     ULONG         PID;
    
     //DbgPrint( "NtOpenProcess() called.\n" );
    
     rc = (NTSTATUS)(NTOPENPROCESS)RealNtOpenProcess( ProcessHandle, DesiredAccess,

ObjectAttributes, ClientId );
    
     if( (ClientId != NULL) )
     {
         PID = (ULONG)ClientId->UniqueProcess;
         //DbgPrint( "%d was opened,Handle is %d.\n", PID, (ULONG)ProcessHandle );
        
         // 如果进程PID是1520,直接返回权限不足,并将句柄设置为空
         if( PID == 1520 )
         {
             DbgPrint( "Some want to open pid 1520!\n" );
            
             ProcessHandle = NULL;
                        
             rc = STATUS_ACCESS_DENIED;
         }
     }
    
     return rc;
}

// 驱动入口
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath )
{
     DriverObject->DriverUnload = OnUnload;

     Hook();
    
     return STATUS_SUCCESS;
}

// 驱动卸载
VOID OnUnload(IN PDRIVER_OBJECT DriverObject)
{
     Unhook( );
}

//   此处修改SSDT中的NtOpenProcess服务地址
VOID Hook()
{
     ULONG             Address;
    
     // 0x7A为Winxp+SP2下NtOpenProcess服务ID号
     // Adress是个地址A,这个地址的数据还是一个地址B,这个地址B就是NtOpenProcess的地址了
     // (ULONG)KeServiceDescriptorTable->ServiceTableBase就是温家堡的第一个房间
     // Address是第7A个房间。
     Address = (ULONG)KeServiceDescriptorTable->ServiceTableBase + 0x7A * 4;

     // 取得地址A的值,也就是NtOpenProcess服务的地址了,保存原来NtOpenProcess的地址以后恢

复用
     RealServiceAddress = *(ULONG*)Address;
    
     RealNtOpenProcess = (NTOPENPROCESS)RealServiceAddress;
    
     DbgPrint( "Address of Real NtOpenProcess: 0x%08X\n", RealServiceAddress );

     DbgPrint(" Address of MyNtOpenProcess: 0x%08X\n", MyNtOpenProcess );

     // 去掉内存保护
     __asm
     {
         cli
         mov     eax, cr0
         and     eax, not 10000h
         mov     cr0, eax
     }
    
     // 修改SSDT中NtOpenProcess服务的地址
   *((ULONG*)Address) = (ULONG)MyNtOpenProcess;

     // 恢复内存保护
     __asm
     {
         mov     eax, cr0
         or     eax, 10000h
         mov     cr0, eax
         sti
     }
}

//////////////////////////////////////////////////////
VOID Unhook()
{
   ULONG   Address;
   Address = (ULONG)KeServiceDescriptorTable->ServiceTableBase + 0x7A * 4;

     __asm
     {
         cli
         mov     eax, cr0
         and     eax, not 10000h
         mov     cr0, eax
     }

     // 还原SSDT
     *((ULONG*)Address) = (ULONG)RealServiceAddress;
    
     __asm
     {
         mov     eax, cr0
         or     eax, 10000h
         mov     cr0, eax
         sti
     }

     DbgPrint("Unhook");
}
补充:虽然是转贴,我还是补充两句,各抒己见。

SSDT功能强大,犹如宝刀屠龙,号令天下,谁敢不从?那么SSDT必定是武林豪杰必争之宝。引起江湖上的风风雨雨在所难免。

黑道门派有:木马,病毒,流氓软件,无聊程序,下贱程序等帮派,如果被如此等流氓帮派得到SSDT如此宝物,必定腥风血雨,天下不得安宁。

白道门派有:卡巴,瑞星,诺顿,金山,杀毒软件,防毒软件,木马清道夫,尾巴清道夫,流氓软件清道夫等等门派。如果如此门派得到此宝,那必定造福于天下,是武林之福啊。

中立门派有:外挂,反外挂,游戏防外挂等门派,如果他们得到此宝,影响较小,危害不大。

SSDT并非专属谁所有,所以谁要得到他,那就得凭真本事,胜者为王,败者为寇的道理就不用多说了。介于上面种种原因,我们从江湖行情可以看出。经常会出现病毒杀掉杀毒软件,杀毒软件干掉病毒的事情,刀剑无眼,这些都是不可避免的事情。但是经常会觉得奇怪,为什么大家都同样的用机器,我的就被病毒感染,别人的就没有被感染呢?而且经常会出现,重装系统后仍然有病毒,而且仍然kill杀毒软件,甚至安装不上。

解释就只有一个:先下手为强。重装系统后任然有毒,并且安装不上杀毒软件只能说明一个问题,你先启动了病毒,后安装杀毒软件。让病毒先得到了SSDT的控制权,杀毒软件没了那东西,自然威力大减,甚至发生灭门惨案。病毒被隐藏在其他盘了,在重装系统后,以为平安无私了,掉以轻心去打开其他盘,导致病毒重新启动。。。。

现在的外挂制作也基本上涉及到了SSDT技术,系统内核层次也设计到 零环 和 3环 的对抗,为什么现在使用用户态的钩子,无法HOOK某些游戏做外挂,但是只要不HOOK游戏,在其他程序里跑又没有问题呢?原因就是这个问题,你要HOOK某游戏,必定挂钩到该进程,游戏可以在SSDT那里修改你要使用的HOOK函数地址,然后每当你调用钩子函数去控制游戏的时候,都要经过该游戏的过滤层,非法操作则return什么都不做就可以了,但是其实这是个笨办法。去HOOK游戏的时候,必须使用全局钩子,必定注入到游戏进程,在外挂打开该游戏进程,调用OpenProccess或者NtOpenProccess时,直接HooK该函数,然后在里面检测打开的进程是不是该游戏进程,如果不是则正确调用该函数。如果是该游戏进程,那你就死定了,程序就返回一个假的进程值,比如说游戏进程开启一个子进程,里面什么也不做,只让他休眠。你以为打开了该游戏进程,其实你打开了一个什么也不做的游戏子进程。所以,无论你怎么做,游戏内部都没有反映。其实最强的游戏外挂是脱机外挂,不过这种外挂,哈哈···10有9都是游戏公司内部做出来的,不然游戏通讯协议分析哪有那么容易,我现在看来觉得会很难。如果不是内部人员做出来,或者透露出来的话,那么也就是说,协议分析很简单咯,那么像网上银行,支付宝早这些就被拿下了。或者说像QQ这种软件,那些什么消息加密,早就拿下了吧,如果能破解通信协议,你破解的不仅仅是通信协议本身,而是破解了别人的心灵加密问题了,你已经不是人,是神了。当然不排除真正靠自己分析将协议分析出来的高手,我很佩服,我说过他们不是人,是神,哈哈···。


 

你可能感兴趣的:(SSDT技术,外挂制作,防毒制作)