深入Windows NT/2000模块的组织
WebCrazy(http://webcrazy.yeah.net/)
在《小议Windows NT/2000分页机制 》中我对x86平台Windows NT/2000的非分页内存内部机制有了较为详细的说明,从中也可以看出地址空间可以分为进程空间与系统空间,其中每个进程有各自的进程空间,而所有的进程则共享同一个系统空间。所以Windows NT/2000在牵涉到模块管理时也涉及到进程私有的模块管理与系统共享模块管理两部分,下面我将从这两方面分别进行介绍。
正因为所有的进程共享同一个系统空间,所以系统模块主要是些操作系统代码模块或是些设备驱动程序代码等(它们一般进程都需要使用到),其位于系统4G内存的高端。Windows NT/2000内部由一系统变量PsLoadedModuleList指出,其具体结构为一双向链表。熟悉Windows NT/2000的人,都知道系统在发生蓝屏死机时,默认情况下都会将系统此时转储到MEMORY.DMP文件中,系统内核调试器i366kd或windbg在调试跟踪这个转储文件时也会根据这个系统变量重新装入系统崩溃前的已装载模块。我下面列出根据这个变量枚举系统模块的代码:
//-----------------------------------------------
//
// EnumKernelModules
// Only test on Windows 2000 Server Chinese Edition
// Build 2195(Free)!Programmed By WebCrazy
// (tsu00@263.net ) on 10-27-2000!
//
//-----------------------------------------------
ULONG PsLoadedModuleList=0x8046a4c0; //fetch from symbol file
#define KERNELMOD_IMAGEBASE_OFFSET 0x18
#define KERNELMOD_IMAGENAME_OFFSET 0x24
void EnumKernelModules()
{
PLIST_ENTRY pKernelModuleListHead, pKernelModuleListPtr;
PUNICODE_STRING pImageName;
if(((USHORT)NtBuildNumber)!=2195){
DbgPrint("Only test on Windows 2000 Server Build 2195!\n");
return;
}
DbgPrint("\n Base Addr\tModule Name");
DbgPrint("\n ---------\t-----------\n");
pKernelModuleListHead=pKernelModuleListPtr=(PLIST_ENTRY)(ULONG *)PsLoadedModuleList;
do{
pKernelModuleListPtr=pKernelModuleListPtr->Flink;
DbgPrint(" %08X",
*(ULONG *)((char *) pKernelModuleListPtr+KERNELMOD_IMAGEBASE_OFFSET));
pImageName = (PUNICODE_STRING)(ULONG *)((char *)
pKernelModuleListPtr+KERNELMOD_IMAGENAME_OFFSET);
DbgPrint("\t%S\n",pImageName->Buffer);
}while(pKernelModuleListPtr->Flink!=pKernelModuleListHead);
}
上面PsLoadedModuleList的值我直接从Symbol文件中获得,你可根据实际情况予以调整。好,先看看EnumKernelModules输出结果:
Base Addr Module Name
--------- -----------
80400000 \WINNT\System32\ntoskrnl.exe
80062000 \WINNT\System32\hal.dll
. .
. .
FD0F8000 \SystemRoot\System32\Drivers\Cdfs.SYS
FCDB1000 \SystemRoot\System32\DRIVERS\ipsec.sys
. .
. .
与Softice的mod命令基本相同,但值得注意的是Softice的mod命令不仅输出进程内核模块,而且也列出特定进程的用户态模块列表,那么系统又是如何管理进程特定模块的呢?
由于每个进程都拥有各自的模块,而所有这些模块都要求从用户态可以访问到。因此进程模块组织的数据结构应该位于用户态地址空间中,实际上Windows NT/2000进程模块列表是由PEB(Process Environment Block)结构中的成员指定,Windows NT/2000均将每个拥有用户态代码的进程的PEB置于0x7FFDF000处(2G空间以下,用户态代码可直接访问)。不过,Windows NT/2000一般通过TEB间接得到PEB的地址,即通过以下代码取得:
mov eax,fs:[18]
mov eax,[eax+30]
第一条语句获得当前线程的TEB地址,关于TEB及TEB地址的获得,请参阅《Windows NT/2000内部数据结构探究》,第二条语句获得位于TEB偏移30h处获得PEB地址。我想Windows NT/2000使用这种方法可能是考虑兼容性吧,我以下提供的代码直接使用了常量地址。
看看Windbg的分析吧:
> !ntsdexts.version
Version 5.0 (Build 2195) Uniprocessor Free
> !ntsdexts.peb
PEB at 7FFDF000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: Yes
ImageBaseAddress: 01000000
Ldr.Initialized: Yes
Ldr.InInitializationOrderModuleList: 71f80 . 72808
Ldr.InLoadOrderModuleList: 71ee0 . 727f8
Ldr.InMemoryOrderModuleList: 71ee8 . 72800
01000000 D:\winnt\system32\calc.exe
77F80000 D:\WINNT\System32\ntdll.dll
77560000 D:\WINNT\system32\SHELL32.dll
77F40000 D:\WINNT\system32\GDI32.DLL
77E60000 D:\WINNT\system32\KERNEL32.DLL
77DF0000 D:\WINNT\system32\USER32.DLL
77D90000 D:\WINNT\system32\ADVAPI32.DLL
77D20000 D:\WINNT\system32\RPCRT4.DLL
77C50000 D:\WINNT\system32\SHLWAPI.DLL
77B30000 D:\WINNT\system32\COMCTL32.DLL
78000000 D:\WINNT\system32\MSVCRT.dll
SubSystemData: 0
ProcessHeap: 70000
ProcessParameters: 20000
WindowTitle: 'D:\winnt\system32\calc.exe'
ImageFile: 'D:\winnt\system32\calc.exe'
. .
. .
. .
Windbg的以上输出详细的显示了PEB各字段值,对其中数据跟踪分析后,我写了以下程序段直接读取系统结构获取进程模块列表:
//-----------------------------------------------
//
// EnumUserModules-information from PEB
// Only test on Windows 2000 Server Chinese Edition
// Build 2195(Free)!Programmed By WebCrazy
// (tsu00@263.net ) on 10-27-2000!
//
//-----------------------------------------------
#define PEBADDRESS 0x7ffdf000
#define PEB_LDR_DATA_OFFSET 0x0c
#define LDRDATA_IMAGEBASE_OFFSET 0x10
#define LDRDATA_IMAGENAME_OFFSET 0x1c
#pragma pack(4)
typedef struct _PEB_LDR_DATA
{
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
#pragma pack()
void EnumUserModules(void *kpeb)
{
PLIST_ENTRY pUserModuleListHead, pUserModuleListPtr;
PPEB_LDR_DATA pLdrData;
PUNICODE_STRING pImageName;
if(((USHORT)NtBuildNumber)!=2195){
DbgPrint("Only test on Windows 2000 Server Build 2195!\n");
return;
}
KeAttachProcess(kpeb);
pLdrData=(PPEB_LDR_DATA)(ULONG *)(*(ULONG *)(PEBADDRESS+PEB_LDR_DATA_OFFSET));
if(!pLdrData->Initialized){
DbgPrint("Process:%08X Not Initialized!\n",(ULONG)kpeb);
KeDetachProcess();
return;
}
DbgPrint("\n Base Addr\tModule Name");
DbgPrint("\n ---------\t-----------\n");
pUserModuleListHead=pUserModuleListPtr=
(PLIST_ENTRY)&(pLdrData->InMemoryOrderModuleList);
do{
pUserModuleListPtr=pUserModuleListPtr->Flink;
DbgPrint(" %08X",*(ULONG *)((char *)
pUserModuleListPtr+LDRDATA_IMAGEBASE_OFFSET));
pImageName = (PUNICODE_STRING)(ULONG *)((char *)
pUserModuleListPtr+LDRDATA_IMAGENAME_OFFSET);
DbgPrint("\t%S\n",pImageName->Buffer);
}while(pUserModuleListPtr->Flink!=pUserModuleListHead);
KeDetachProcess();
}
EnumUserModules程序段实现对特定进程(由参数kpeb所指定)模块的枚举,函数段并未实现对PEB合法性的检查,如Idle与system进程是纯内核态进程,他们并不存在用户态的PEB,解决办法可以通过检查TEB的合法性,这些进程TEB一般情况下都为0。EnumUserModules也没有对kpeb的合法性检查,其假设所有kpeb当前在系统中都存在,否则将出现意想不到的结果。虽然只涉及到用户态数据的读取,但程序段中使用了KeAttachProcess/KeDetachProcess内核例程,所以程序段只能在驱动代码中实现。EnumUserModules使用InMemoryOrderModuleList成员枚举模块列表的(见上Windbg输出结果,EnumUserModules的输出结果与其一致),当然你也可以使用InInitializationOrderModuleList或InLoadOrderModuleList成员。
上面已经说明了SoftICE中的mod命令将系统模块与进程模块列出,也就是说其实现了我提供的这两个程序段(SoftICE还输出PE模块的PE Header段,可以根据Base Addr按照PE规范取出PE Header的位置,另外我不确信SoftICE是不是使用同样的方法实现的)。
不论用户态的可执行Win32模块(.exe)或是核心态的驱动程序(.sys),还是系统动态链接库(.dll)在Windows NT/2000中均是以PE格式存在的,但也未必所有模块均为此格式,实际上所有文件均可以作为模块出现,如常见的nls文件等等。关于PE文件的装载,Windows NT/2000提供了以ldr开头的函数族,至于其结构已众所周知,我将不予介绍。
Windows 2000中实现了枚举系统模块的PSAPI与ToolHelp API(ToolHelp API在Win9X中早已实现,但Windows NT却仅使用PSAPI函数),我在跟踪分析这部分代码时原以为多少可以参阅一下这些函数的头文件中的一些譬如MODULEENTRY32或MODULEINFO等的定义,但系统内部却使用完全不同的格式.可以这样说系统内部的结构大而全且仅使用UNICODE格式,而这些API仅呈现API使用的部分定义,向用户隐藏了好多的内部特征。不过ToolHelp等使用Section对象(Win32 API称为FileMapping对象)映射整个模块实现模块的枚举,不过其最终都使用到EnumUserModules所引用的PEB数据。关于PEB还包含着许多系统数据,如ProcessHeap、ProcessParameters等等,感兴趣的话可以使用windbg/Softice好好挖掘挖掘!
参考资料:
1.David Solomom《Inside Windows NT,2nd Edition》