也谈SSDT Hook(一)

一、原理篇

1.            关于系统服务。

系统服务是由操作系统提供一组函数,使得开发者能够通过APIs直接或间接的调用。一个API可以对应一个系统服务,也可以一个API依赖多个系统服务。比如,WriteFile API对应的系统服务是ntoskrnl.exe中的NtWriteFile。系统服务分发属于陷阱分发的范畴,更详细的资料可参考’Windows Internal4th edition相关章节。从APIs到系统服务的分发过程可简化为图1

 

 

 

1

1只表现了ntdll.dll分发系统服务陷阱的过程,对于GDI/USER过程,它是负责管理图形界面的,暂不作考虑。要钩住系统服务当然要修改服务分发表了(要搞系统服务当然不只值一个方法,但是本文只考虑怎样通过SSDT来做),所以,关键是要找到服务分发列表的索引号(012,…,n),就可以找到相应的系统服务内存入口地址。系统服务分发表的结构可以直观的简化为图2

2

 

Windows系统服务是Nt*系列的Native APIs,他们在内存中的入口地址保存在SSDT中。另外,还应该注意Zw*系列的Native APIs,这是以Nt开头的系统服务入口点的镜像,它把原先的访问模式设置为内核模式,从而消除了参数的有效性检查过程,因为Nt系统服务只有当原来的访问模式为ring 3时才进行参数检查。多说几句,除了在ring 0ntoskrnl.exe有导出中,在ring 3ntdll.dll中也有这个两系列的函数。这四者的关系怎样呢?以NtQuerySystemInformation系统服务为例:

 

Ring 3

lkd>  u ntdll!ZwQuerySystemInformation L4

ntdll!ZwQuerySystemInformation:

7c92e1aa b8ad000000      mov     eax,0ADh

7c92e1af ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)

7c92e1b4 ff12            call    dword ptr [edx]

7c92e1b6 c21000          ret     10h

 

lkd> u ntdll!NtQuerySystemInformation L4

ntdll!ZwQuerySystemInformation:

7c92e1aa b8ad000000      mov     eax,0ADh

7c92e1af ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)

7c92e1b4 ff12            call    dword ptr [edx]

7c92e1b6 c21000          ret     10h

 

由此可见,在Ring 3ntdll.dll中,这两个函数是完全一样的。

Ring 0

lkd> u nt!ZwQuerySystemInformation L6

nt!ZwQuerySystemInformation:

804de440 b8ad000000      mov     eax,0ADh

804de445 8d542404        lea     edx,[esp+4]

804de449 9c              pushfd

804de44a6a08            push    8

804de44c e8e0110000      call    nt!KiSystemService (804df631)

804de451 c21000          ret     10h

 

lkd> u nt!NtQuerySystemInformation

nt!NtQuerySystemInformation:

8057e786 6810020000      push    210h

8057e78b 6830ab4e80      push    offset nt!ExTraceAllTables+0x1eb (804eab30)

8057e790 e8a64cf6ff      call    nt!_SEH_prolog (804e343b)

8057e795 33c0            xor     eax,eax

8057e797 8945e4          mov     dword ptr [ebp-1Ch],eax

8057e79a 8945dc          mov     dword ptr [ebp-24h],eax

8057e79d 8945fc          mov     dword ptr [ebp-4],eax

8057e7a0 64a124010000    mov     eax,dword ptr fs:[00000124h]

 

Ring 0下,ZwQuerySystemInformation实现了对KiSystemService(系统服务分发器)的调用,并在阿函数开始的时候将索引号放入eax寄存器(mov eax,0ADh),这是我们需要的,通过0ADh可以找到系统服务NtQuerySystemInformation,下节详细讨论。

’ Undocumented Windows 2000 Secrets’中有所阐述,这里让大家看到事实了。找几个其他的APIs尝试一下,自己去悟吧,没悟性成不了佛的。

 

1.            找到Hook入口

系统服务分发表是一个C的数据结构,ntolkrnl.exe导出了该结构的指针(符号为KeServiceDescriptorTable)。其实,内核还维护了一个替代的SDT,其名称为:KeServiceDescriptorTableShadow,但这个SDT并没有被ntolkrnl.exe导出。KeServiceDescriptorTable定义如下:

struct _KeServiceDescriptorTableEntry

{

unsigned int *ServiceTableBase;

unsigned int *ServiceCounterTableBase; //Used only in checked build

nsigned int NumberOfServices;

unsigned char *ParamTableBase;

} KeServiceDescriptorTableEntry, *PKeServiceDescriptorTableEntry

其第一个成员ServiceTableBase就是系统服务列表数组的其实地址。

首先,就是要获取KeServiceDescriptorTableEntry的内存地址。由于KeServiceDescriptorTable已经被导出,所以导入KeServiceDescriptorTable即可:

extern PServiceDescriptorTableEntry KeServiceDescriptorTable;

创建访问参考:

PServiceDescriptorTableEntry pSDT = KeServiceDescriptorTable;

不过,hoglund是这样做的

__declspec(dllimport)  ServiceDescriptorTableEntry_t KeServiceDescriptorTable;

异曲同工,都是导入KeServiceDescriptorTable

现在找到了SDT,有了一个好的开头,接下来就是要找到关注的系统服务了,才能做一些想做的事情。回到ZwQuerySystemInformation的那段反汇编的代码:

lkd> u nt!ZwQuerySystemInformation L6

nt!ZwQuerySystemInformation:

804de440 b8ad000000      mov     eax,0ADh

804de445 8d542404        lea     edx,[esp+4]

804de449 9c              pushfd

804de44a6a08            push    8

804de44c e8e0110000      call    nt!KiSystemService (804df631)

804de451 c21000          ret     10h

它索引号0ADh放到了eax寄存器,dd一下:

lkd> dd nt!ZwQuerySystemInformation

804de440  0000adb8 24548d00 086a9c04 0011e0e8

804de450  0010c200 0000aeb8 24548d00 086a9c04

804de440是它的入口地址,AD就存放在那段机器码里。这样就可以了:

DWORD dwIndex= *(*ULONG)((UCHAR*)ZwQuerySystemInformation+1);

好多小星星(*),慢慢理解吧。转换成汇编就容易理解了:

mov     ecx, DWORD PTR [ZwQuerySystemInformation];

mov     edx, [ecx+1];

最后,有了KeServiceDescriptorTable.ServiceTableBase的地址,又找到了索引号,这样所关注系统服务就找到了。

KeServiceDescriptorTable.ServiceTableBase+ dwIndex*4

手工试一下,还是以ZwQuerySystemInformation为例子。

通过KeServiceDescriptorTable.ServiceTableBase获取系统服务数组的起始地址

lkd> dd KeServiceDescriptorTable

8055a680  804e36a8 00000000 0000011c 80513eb8

是这里804e36a8,可以先都为快:

lkd> dd 804e36a8

804e36a8  80580302 80579b8c 8058b7ae 805907e4

804e36b8  805905fe 806377a0 80639931 8063997a

804e36c8  8057560b 806481cf 80636f5f 8058fb85

804e36d8  8062f0a4 8057be31 8058cc26 806261bd

804e36e8  805dcf20 80568f9d 805d9ac1 805a2bb0

804e36f8  804e3cb4 806481bb 805ca22c804f0e28

804e3708  80569649 80567d49 8058fff3 8064e1c1

804e3718  8058f8f5 80581225 8064e42f f584dc90

试一下第一个系统服务80580302是谁呢?

lkd> u 80580302

nt!NtAcceptConnectPort:

80580302 689c000000      push    9Ch

80580307 68d8224f80      push    offset nt!_real+0x128 (804f22d8)

8058030c e82a31f6ff      call    nt!_SEH_prolog (804e343b)

80580311 64a124010000    mov     eax,dword ptr fs:[00000124h]

80580317 8a8040010000    mov     al,byte ptr [eax+140h]

8058031d 884590          mov     byte ptr [ebp-70h],al

80580320 84c0            test    al,al

80580322 0f84e9080300    je      nt!NtAcceptConnectPort+0x1df (805b0c11)

果然是NtAcceptConnectPort!套用算法公式找一下NtQuerySystemInformation

804e36a8+0xAD*4 = 804E395C

lkd> dd 804E395C

804e395c  8057e786 80590ad0 80591857 805871f3

804e396c  f7377b46 8056d338 80570e3b 8059068f

804e397c  804e303a 806477af 805710d8 805dae6c

804e398c  8058f6a6 8057b545 8057dbee 80566809

804e399c  8058b492 80567272 8065a3d6 8064e029

804e39ac  f58647c0 8057f307 8056ae96 8056a9ae

804e39bc  80622b92 8062b803 8058aa2c f584d960

804e39cc  8062b5fc 8059d753 8053c14a f5864a50

那就是8057e786的位置了,反汇编:

lkd> u 8057e786

nt!NtQuerySystemInformation:

8057e786 6810020000      push    210h

8057e78b 6830ab4e80      push    offset nt!ExTraceAllTables+0x1eb (804eab30)

8057e790 e8a64cf6ff      call    nt!_SEH_prolog (804e343b)

8057e795 33c0            xor     eax,eax

8057e797 8945e4          mov     dword ptr [ebp-1Ch],eax

8057e79a 8945dc          mov     dword ptr [ebp-24h],eax

8057e79d 8945fc          mov     dword ptr [ebp-4],eax

8057e7a0 64a124010000    mov     eax,dword ptr fs:[00000124h]

真的是NtQuerySystemInformation。搞定了!

有些网上流传的代码将新的系统服务函数命名为NewZwQuerySystemInformation在语法角度是没有什么错误,但是实际上它并不是替换了ZwQuerySystemInformation而是NtQuerySystemInformation,这种命名让读者产生误解,应该是NewNtQuerySystemInformation更为妥当。我们只是通过ZwQuerySystemInformation来找到NtQuerySystemInformation,最终都是在Nt*系列的函数上做文章的。对于那些“钩住Zw*”文章的提法,也不敢苟同,坏事都是新的Nt*干的,Zw*只是提供了线索,有点受冤了。

 

2.            系统服务替换及还原

万事俱备,是不是可以“动手”了?不妨试一下,Windows 2000及以上必定是BSOD,伤心的蓝色海洋。Why?该内存区域写保护。点解?去掉写保护,修改标识寄存器CR0

31

30

...

18

17

16

...

5

4

3

2

0

1

P/G

C/D

...

A/M

 

W/P

...

N/E

E/T

T/S

E/M

M/P

P/E

我们主要注意这个WP这位,其他的请参考IA-32 Volume 3A

WP——Write Protect,当设置为1时只提供读页权限;

PE——Paging,当设置为1时提供分页;

MP——Protection Enable,当设置为1时进入保护模式;

因此,只要把WP这一位设置为0时,就可以修改SSDT了。

 

去除写保护标示:

unsigned long _cr0;

_asm

{

cli;

mov eax,cr0

mov _cr0,eax

and eax,0fffeffffh

mov cr0,eax

}

恢复写保护:

_asm

{

mov eax, _cr0

mov cr0,eax

sti

}

还有更绅士的做法,将整个SSDT的存储数组映射到一个非分页MDL(Memory Description List)的内存空间,然后就方便对这块内存区域修改属性、改写内容... Greg Hoglund那个例子的做法。

lkd> dt _mdl

nt!_MDL

   +0x000 Next             : Ptr32 _MDL

   +0x004 Size             : Int2B

   +0x006 MdlFlags         : Int2B

   +0x008 Process          : Ptr32 _EPROCESS

   +0x00c MappedSystemVa   : Ptr32 Void

   +0x010 StartVa          : Ptr32 Void

   +0x014 ByteCount        : Uint4B

   +0x018 ByteOffset       : Uint4B

最后,服务卸载时当然不能忘了把SSDT修改过来,就是上述操作的逆过程,大同小异。

(待续)

你可能感兴趣的:(hook,c,windows,汇编,api,数据结构)