一、原理篇
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:
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 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
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修改过来,就是上述操作的逆过程,大同小异。
(待续)