也谈SSDT Hook(一)

一、原理篇

1.            关于系统服务。

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

 

图1 

 

1

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

图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:

7c 92e1aa b8ad000000      mov     eax,0ADh

7c 92e1af ba0003fe 7f       mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)

7c 92e1b4 ff12            call    dword ptr [edx]

7c 92e1b 6 c 21000          ret     10h

 

lkd> u ntdll!NtQuerySystemInformation L4

ntdll!ZwQuerySystemInformation:

7c 92e1aa b8ad000000      mov     eax,0ADh

7c 92e1af ba0003fe 7f       mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)

7c 92e1b4 ff12            call    dword ptr [edx]

7c 92e1b 6 c 21000          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

804de 44a 6a 08            push    8

804de 44c e8e0110000      call    nt!KiSystemService (804df631)

804de 451 c 21000          ret     10h

 

lkd> u nt!NtQuerySystemInformation

nt!NtQuerySystemInformation:

8057e786 6810020000      push    210h

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

8057e790 e 8a 64cf6ff      call    nt!_SEH_prolog (804e343b)

8057e795 33c 0            xor     eax,eax

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

8057e 79a 8945dc          mov     dword ptr [ebp-24h],eax

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

8057e 7a 0 64a 124010000    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

804de 44a 6a 08            push    8

804de 44c e8e0110000      call    nt!KiSystemService (804df631)

804de 451 c 21000          ret     10h

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

lkd> dd nt!ZwQuerySystemInformation

804de440  0000adb8 24548d00 086a 9c 04 0011e0e8

804de450  0010c 200 0000aeb8 24548d00 086a 9c 04

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

8055a 680  804e 36a 8 00000000 0000011c 80513eb8

是这里804e 36a 8,可以先都为快:

lkd> dd 804e 36a 8

804e 36a 8  80580302 80579b 8c 8058b7ae 805907e4

804e36b8  805905fe 806377a 0 80639931 8063997a

804e 36c 8  8057560b 806481cf 80636f 5f 8058fb85

804e36d8  8062f 0a 4 8057be31 8058cc26 806261bd

804e36e8  805dcf20 80568f 9d 805d 9ac 1 805a 2bb0

804e 36f 8  804e3cb4 806481bb 805ca 22c 804f 0e28

804e3708  80569649 80567d49 8058fff3 8064e 1c 1

804e3718  8058f 8f 5 80581225 8064e 42f f584dc90

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

lkd> u 80580302

nt!NtAcceptConnectPort:

80580302 689c 000000      push    9Ch

80580307 68d 8224f 80      push    offset nt!_real+0x128 ( 804f 22d8)

8058030c e 82a 31f 6ff      call    nt!_SEH_prolog (804e343b)

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

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

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

80580320 84c 0            test    al,al

80580322 0f 84e9080300    je      nt!NtAcceptConnectPort+0x1df (805b 0c 11)

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

804e 36a 8+0xAD*4 = 804E 395C

lkd> dd 804E 395C

804e 395c   8057e786 80590ad0 80591857 805871f 3

804e 396c   f7377b46 8056d338 80570e3b 8059068f

804e 397c   804e 303a 806477af 805710d8 805dae 6c

804e 398c   8058f 6a 6 8057b545 8057dbee 80566809

804e 399c   8058b492 80567272 8065a 3d6 8064e029

804e 39ac   f 58647c 0 8057f 307 8056ae96 8056a 9ae

804e39bc  80622b92 8062b803 8058aa 2c f584d960

804e39cc  8062b5fc 8059d753 8053c 14a f 5864a 50

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

lkd> u 8057e786

nt!NtQuerySystemInformation:

8057e786 6810020000      push    210h

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

8057e790 e 8a 64cf6ff      call    nt!_SEH_prolog (804e343b)

8057e795 33c 0            xor     eax,eax

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

8057e 79a 8945dc          mov     dword ptr [ebp-24h],eax

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

8057e 7a 0 64a 124010000    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

   +0x 00c MappedSystemVa   : Ptr32 Void

   +0x010 StartVa          : Ptr32 Void

   +0x014 ByteCount        : Uint4B

   +0x018 ByteOffset       : Uint4B

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

(待续)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值