PsLoadedModuleList等重要内核变量并未被ntoskrnl.exe导出,也没有公开的函数可以获取。而这些内核变量对于Rootkit、Anti-Rootkit 以及内核溢出的利用等都是至关重要的。
下面我们以PsLoadedModuleList、PsActiveProcessHead 等为例,介绍得到这些变量的方法。
对于Windows NT 4.0和Windows 2000,尚没有“温柔”的办法可以获取这些变量,比较理想的办法也就是特征代码搜索,这种方法虽然暴力,但通常都很有效,一般也不会出问题;对于Windows XP和Windows 2003,我们找到了一些更加优雅的选择。
下面首先介绍特征代码搜索的方法。
[DWORD KernelBase]
要进行特征代码搜索,首先要定位ntoskrnl.exe在内核的加载地址KernelBase。这个地址可以通过ZwQuerySystemInformation Class 10的SystemModuleInformation来得到。参考资源[1]中给出了相关代码。事实上,KernelBase 这个值对同一操作系统来说非常固定,可以作为常量来用:
Windows NT:
0x80100000
Windows 2000:0x80400000
Windows XP:
0x804d1000
Windows 2003: 0x804e0000
Windows NT 4.0 ntoskrnl.exe 的OptionalHeader-ImageBase = 0x80100000,ntldr 也会按照这个值来加载内核,但是从Windows 2000开始就不是这样了。可能基于这个历史原因,各系统的*(DWORD *)PsNtosImageBase始终初始化为0x80100000。
另外,内核变量PsNtosImageBase、KdpNtosImageBase等也指向KernelBase:
KernelBase = *(DWORD *)PsNtosImageBase
KernelBase = *(DWORD *)KdpNtosImageBase
[LIST_ENTRY PsLoadedModuleList]
PsLoadedModuleList这个全局变量指向一个保存着所加载驱动信息的双向链表。通过它可以枚举系统中所有的驱动模块。
虽然很多内核函数都用到了PsLoadedModuleList,但是大部分并没有被导出,而从基址开始搜会很花时间。对于Windows 2000来说,从下面这个地方入手是个好主意:
nt!MmGetSystemRoutineAddress+0x66:
804f0ed0 8b35f0e84680
mov
esi,[nt!PsLoadedModuleList (8046e8f0)]
804f0ed6 81fef0e84680
cmp
esi,0x8046e8f0
流程如下:
1、ImageBase = LoadLibraryA("ntoskrnl.exe")
2、GetProcAddress(ImageBase,"MmGetSystemRoutineAddress")
3、搜索特征代码:
*(WORD *)(MmGetSystemRoutineAddress + i) = 0x358b && *(WORD *)(MmGetSystemRoutineAddress + i + 6) = 0xfe81
&&
*(DWORD *)(MmGetSystemRoutineAddress + i + 2) == *(DWORD *)(MmGetSystemRoutineAddress + i + 8)
4、定位内核中的地址:
PsLoadedModuleList = *(DWORD *)(MmGetSystemRoutineAddress + i + 2) + (KernelBase - ImageBase)
从SP0到SP4,i值并不相同,但是肯定不大于0x100。
对Windows NT来说,就没这么好运气了,没有理想的可用于定位的API,只能从头开始搜索。下面这段代码至少对SP1~SP6a的来说都具有很好的稳定性和唯一性:
801CEB1C: 8B 4D 08
mov
ecx,dword ptr [ebp+8]
801CEB1F: 89 01
mov
dword ptr [ecx],eax
801CEB21: 8B 45 0C
mov
eax,dword ptr [ebp+0Ch]
801CEB24: 89 10
mov
dword ptr [eax],edx
801CEB26: 8B 36
mov
esi,dword ptr [esi]
801CEB28: 81 FE 70 0B 15 80
cmp
esi,80150B70h
//PsLoadedModuleList
如果是用驱动做这件事情,就不必暴力搜索了,fuzen_op(fuzen_op@yahoo.com)在FU_Rootkit2.0(参考资源[2])中使用了一段比较巧妙的代码:
DWORD FindPsLoadedModuleList (IN PDRIVER_OBJECT DriverObject)
{
PMODULE_ENTRY pm_current;
if (DriverObject == NULL)
return 0;
pm_current = *((PMODULE_ENTRY*)((DWORD)DriverObject + 0x14));
if (pm_current == NULL)
return 0;
gul_PsLoadedModuleList = pm_current;
while ((PMODULE_ENTRY)pm_current-le_mod.Flink != gul_PsLoadedModuleList)
{
if ((pm_current-unk1 == 0x00000000) &&
(pm_current-driver_Path.Length == 0))
{
return (DWORD) pm_current;
}
pm_current = (MODULE_ENTRY*)pm_current-le_mod.Flink;
}
return 0;
}
[LIST_ENTRY PsActiveProcessHead]
理论上PsActiveProcessHead 也可以用搜索代码的方法来取,但是还有更简单的方法。
ntoskrnl.exe导出的PsInitialSystemProcess 是一个PEPROCESS,指向system进程的EPROCESS。这个EPROCESS的结构成员EPROCESS.ActiveProcessLinks.Blink 就是PsActiveProcessHead:
kd dt _EPROCESS ActiveProcessLinks.Blink poi(PsInitialSystemProcess)
+0x0a0 ActiveProcessLinks
:
[ 0x81356900 - 0x8046e728 ]
+0x004 Blink
: 0x8046e728
[ 0x81a2fb00 - 0xff5a4ce0 ]
kd ? PsActiveProcessHead
Evaluate expression: -2142836952 = 8046e728
EPROCESS这个结构在不同的操作系统上各不相同,需要分别对待。
[struct _KDDEBUGGER_DATA64 KdDebuggerDataBlock]
Windows 2000 开始,系统引入了变量KdDebuggerDataBlock。其中包含了大量的内核变量。如果能够获取到的话,可以解决许多问题。遗憾的是,Windows NT上没有这个变量。WinDBG SDK的wdbgexts.h中包含了它的结构:
typedef struct _KDDEBUGGER_DATA64
因为比较长,这里就不引用了。
从对5.0.2195.6902版本ntoskrnl.exe 的逆向工程结果来看,只有两个函数使用了该变量,并且,两个函数都未导出,且代码前后没有明显特征,无法靠直接搜索代码来获取。
但是,我们发现,ntoskrnl.exe导出了KdEnableDebugger,KdEnableDebugger会调用KdInitSystem,而KdInitSystem 中引用了KdDebuggerDataBlock:
n < 100
Windows 2000:
KdEnableDebugger + n:
6A 00
push
0
6A 00
push
0
C6 05 28 41 48 00 01
mov
_PoHiberInProgress, 1
E8 1C DC 10 00
call
_KdInitSystem@8 ; KdInitSystem(x,x)
KdInitSystem + n:
68 70 02 00 00
push
270h
// sizeof(KdDebuggerDataBlock)
B9 50 D1 54 00
mov
ecx, offset _KdpDebuggerDataListHead
68 D8 FA 46 00
push
offset KdDebuggerDataBlock
8B 40 18
mov
eax, [eax+18h]
68 4B 44 42 47
push
4742444Bh // "KDBG",可以用作搜索的定位标志
A3 3C D1 54 00
mov
ds:_KdpNtosImageBase, eax
89 0D 54 D1 54 00
mov
ds:dword_54D154, ecx
89 0D 50 D1 54 00
mov
ds:_KdpDebuggerDataListHead, ecx
Windows XP
KdEnableDebugger + n:
6A 00
push
0
6A 00
push
0
C6 05 8C 98 47 00 01
mov
_PoHiberInProgress, 1
E8 2B 17 13 00
call
_KdInitSystem@8 ; KdInitSystem(x,x)
KdInitSystem + n:
68 90 02 00 00
push
290h
68 E0 9D 46 00
push
offset KdDebuggerDataBlock
BE 74 96 59 00
mov
esi, offset _KdpDebuggerDataListHead
68 4B 44 42 47
push
4742444Bh
89 35 78 96 59 00
mov
ds:dword_599678, esi
89 35 74 96 59 00
mov
ds:_KdpDebuggerDataListHead, esi
Windows 2003
KdEnableDebugger + n:
56
push
esi
56
push
esi
C6 05 0C 08 49 00 01
mov
PoHiberInProgres, 1
E8 CB AD 15 00
call
_KdInitSystem@8 ; KdInitSystem(x,x)
KdInitSystem + n:
68 18 03 00 00
push
318h
68 D0 A3 47 00
push
offset KdDebuggerDataBlock
BE 18 10 5D 00
mov
esi, offset _KdpDebuggerDataListHead
68 4B 44 42 47
push
4742444Bh
89 35 1C 10 5D 00
mov
ds:dword_5D101C, esi
89 35 18 10 5D 00
mov
ds:_KdpDebuggerDataListHead, esi
可以看出,上面代码特征的唯一性很好。用于搜索是没有问题的。我在上面同时列出了三个系统的代码,仅仅只是为了比较,事实上,对Windows XP和Windows 2003是完全没有必要采取如此暴力手段的。
下面介绍针对Windows XP和Windows 2003的方法。
[struct _DBGKD_GET_VERSION64 KdVersionBlock]
Opc0de和Edgar Barbosa在参考资源[3]中提到,Windows XP和Windows 2003引入