一、原理篇
1. 关于系统服务。
系统服务是由操作系统提供一组函数,使得开发者能够通过APIs直接或间接的调用。一个API可以对应一个系统服务,也可以一个API依赖多个系统服务。比如,WriteFile API对应的系统服务是ntoskrnl.exe中的NtWriteFile。系统服务分发属于陷阱分发的范畴,更详细的资料可参考’Windows Internal(4th edition)’相关章节。从APIs到系统服务的分发过程可简化为图1:
图1
图1只表现了ntdll.dll分发系统服务陷阱的过程,对于GDI/USER过程,它是负责管理图形界面的,暂不作考虑。要钩住系统服务当然要修改服务分发表了(要搞系统服务当然不只值一个方法,但是本文只考虑怎样通过SSDT来做),所以,关键是要找到服务分发列表的索引号(0,1,2,…,n),就可以找到相应的系统服务内存入口地址。系统服务分发表的结构可以直观的简化为图2:
图2
Windows系统服务是Nt*系列的Native APIs,他们在内存中的入口地址保存在SSDT中。另外,还应该注意Zw*系列的Native APIs,这是以Nt开头的系统服务入口点的镜像,它把原先的访问模式设置为内核模式,从而消除了参数的有效性检查过程,因为Nt系统服务只有当原来的访问模式为ring 3时才进行参数检查。多说几句,除了在ring 0的ntoskrnl.exe有导出中,在ring 3的ntdll.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 3下ntdll.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修改过来,就是上述操作的逆过程,大同小异。
(待续)