第六章 在用户模式下调用内核API函数 翻译:Kendiv( fcczj@263.net ) 更新:Friday, May 06, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
将系统模块和驱动程序加载到内存中 NtQuerySystemInformation()是Windows 2000系统编程中主要API函数之一,几乎所有内建的管理工具都使用了该函数,但是你不会在DDK(Device Driver Kit)文档的任何地方找到它。唯一提及该函数的就是ntddk.h中CONFIGURATION_INFORMATIONS结构的注释,这证实了这一函数的存在。如果存在“无正式文档系数”的话,并且按照在微软文档中出现频率来划分函数的有用程度,那么NtQuerySystemInformation()将当仁不让的位居榜首。随同很多其他让人振奋的功能,该函数还可返回已加载的系统模块的列表,这包括所有的系统核心组件和内核模式的驱动程序。
Spy driver的源文件中包含最少的直接代码和类型定义,以从NtQuerySystemInformation()获取已加载模块的列表。从调用者角度来看,这是一个非常简单的函数。该函数需要四个参数,如列表6-6所示。SystemInformationClass是一个从0开始的数值,它用来指定要查询的信息的类型。这里的Information可以是变长的,其具体大小依赖于所查询的信息的类型,查询到的信息将被复制到调用者提供的SystemInformation指向的缓冲区中。缓冲区的长度由SystemInformationLength参数指定。如果调用成功,复制到缓冲区的实际字节数将被写入ReturnLength指向的变量中。这个函数的问题是,在它发现缓冲区太小的情况下,它不会报告它实际想要复制多少字节。因此,调用者必须在一个循环中不断尝试,直到函数的返回代码从STATUS_INFO_LENGTH_MISMATCH(0xC0000004)变为STATUS_SUCCESS(0x00000000)。
NTSTATUS NTAPI ZwQuerySystemInformation( DWORD SystemInformationClass, PVOID SystemInformation, DWORD SystemInformationLength, PDWORD ReturnLength); 列表6-6. NtQuerySystemInformation()的原型
列表6-6没有给出NtQuerySystemInformation()自身,但给出了该函数的另一个“兄弟”:ZwQuerySystemInformation(),这一函数除了函数名前缀不同之外,其运行机理与NtQuerySystemInformation()是相同。你或许还记得第二章中的Nt*和Zw* Native API函数集合。如果从用户模式进行调用的话,这两种函数的工作方式非常相似,在用户模式下,这两组函数都将通过ntdll.dll到达相同的INT 2Eh Stub。不过,在内核模式下,情形就有些不同了。此时,ntoskrnl.exe将控制对Native API的调用,Nt*()和Zw*()的执行路径将不再相同。Zw*()函数还是通过INT 2Eh中断门,这和ntdll.dll的处理方式相同。而Nt*()却绕过了此中断门。在DDK文档的术语表中,微软是这样描述Zw*()函数集的(Microsoft 2000f):
“一组与执行体的系统服务(executive’s system services)平行的入口点。从内核模式的代码()中调用一个ZwXxx入口点将获得相应的系统服务,只是在使用Zw*()函数时,不会检查调用者的访问权限和参数的有效性,而且调用不会将先前模式(previous mode)切换到用户模式”(Windows 2000 DDK\Kernel-Mode Drivers\Design Guide\Kernel-Mode Glossary \Z\Zw routines.)
上文最后一句中提到的“先前模式(previous mode)”非常重要。Peter G. Viscarola和W. Anthony Mason: “尽管任意一组函数都可以从内核模式调用,但如果用Zw*()函数来代替Nt*()函数,则可将先前模式(此后的模式才是请求被发出的模式)切换到内核模式。”(Viscarola和Mason 1999,p.18)
对先前模式(previous-mode)的处理带来的副作用是,在没有任何附加预防措施的情况下,从内核模式的驱动程序中调用NtQuerySystemInformation()函数将返回一个出错状态代码:STATUS_ACCESS_VIOLATION(0xC0000005),而对ZwQuerySystemInformation()的调用则可成功,或者返回STATUS_INFO_LENGTH_MISMATCH。
在列表6-7中给出了SystemInformationClass所需的常量和类型的定义。已加载模块的列表将通过一个MODULE_LIST结构返回,每个模块均包含一个32位的模块计数和一个MODULE_INFO类型的数组。
#define SystemModuleInformation 11 // SYSTEMINFOCLASS
typedef struct _MODULE_INFO { DWORD dReserved1; DWORD dReserved2; PVOID pBase; DWORD dSize; DWORD dFlags; WORD wIndex; WORD wRank; WORD wLoadCount; WORD wNameOffset; BYTE abPath [MAXIMUM_FILENAME_LENGTH]; } MODULE_INFO, *PMODULE_INFO, **PPMODULE_INFO;
#define MODULE_INFO_ sizeof (MODULE_INFO) // ----------------------------------------------------------------- typedef struct _MODULE_LIST { DWORD dModules; MODULE_INFO aModules []; } MODULE_LIST, *PMODULE_LIST, **PPMODULE_LIST;
#define MODULE_LIST_ sizeof (MODULE_LIST) 列表6-7. SystemModuleInformation定义
现在调用ZwQuerySystemInformation()所需的一切都已准备好。列表6-8给出了SpyModuleList()函数的实现方式,该函数使用一个trial-and-error循环(指,出错-尝试方式的循环,在第一章提及过),和两个简单的内存管理函数---SpyMemoryCreate()和SpyMemoryDestroy(),这两个函数内部将调用Windows 2000执行体函数(Executive function)ExAllocatePoolWithTag()和ExFreePool()。SpyModuleList()函数在开始时将使用4,096Byte的缓冲区,如果ZwQuerySystemInformation()的返回值为STATUS_INFO_LENGTH_MISMATCH,则将缓冲区扩大一倍,然后再次尝试调用ZwQuerySystemInformation()。如果ZwQuerySystemInformation()返回了其他的值,将终止循环。SpyModueList()的可选参数pdData和pns,将关返回更详细的信息。如果SpyModueList()返回NULL,则表示调用失败,此时pns指向的缓冲区中将保存一个错误代码,*pdData将被设为0。如果SpyModueList()调用成功,*pdData将保存复制到缓冲区中的字节数,*pns的值将为STATUS_SUCCESS。
#define SPY_TAG '>YPS' // SPY>
PVOID SpyMemoryCreate (DWORD dSize) { return ExAllocatePoolWithTag (PagedPool, max (dSize, 1), SPY_TAG); }
// ----------------------------------------------------------------- PVOID SpyMemoryDestroy (PVOID pData) { if (pData != NULL) ExFreePool (pData); return NULL; }
// ----------------------------------------------------------------- PMODULE_LIST SpyModuleList (PDWORD pdData, PNTSTATUS pns) { DWORD dSize; DWORD dData = 0; NTSTATUS ns = STATUS_INVALID_PARAMETER; PMODULE_LIST pml = NULL;
for (dSize = PAGE_SIZE; (pml == NULL) && dSize; dSize <<= 1) { if ((pml = SpyMemoryCreate (dSize)) == NULL) { ns = STATUS_NO_MEMORY; break; } ns = ZwQuerySystemInformation (SystemModuleInformation, pml, dSize, &dData); if (ns != STATUS_SUCCESS) { pml = SpyMemoryDestroy (pml); dData = 0;
if (ns != STATUS_INFO_LENGTH_MISMATCH) break; } } if (pdData != NULL) *pdData = dData; if (pns != NULL) *pns = ns; return pml; } 列表6-8. 使用ZwQuerySystemInformation()获取模块列表
剩下的操作将用来获取给定模块的基地址,这将非常简单。列表6-9定义了两个函数:SpyModuleFind()是SpyModuleList()的增强版,它可以根据指定的模块文件名来扫描ZwQuerySystemInformation()返回的模块列表,SpyModuleBase()反复调用SpyModuleFind(),从模块的MODULE_INFO结构中提取出模块的基地址。SpyModuleHeader()函数调用SpyModuleBase()并将获取的模块基地址传递给RtlImageNtHeader()。该函数是进入已加载模块导出节(export section)的第一步。
PMODULE_LIST SpyModuleFind (PBYTE pbModule, PDWORD pdIndex, PNTSTATUS pns) { DWORD i; DWORD dIndex = -1; NTSTATUS ns = STATUS_INVALID_PARAMETER; PMODULE_LIST pml = NULL;
if ((pml = SpyModuleList (NULL, &ns)) != NULL) { for (i = 0; i < pml->dModules; i++) { if (!_stricmp (pml->aModules [i].abPath + pml->aModules [i].wNameOffset, pbModule)) { dIndex = i; break; } } if (dIndex == -1) { pml = SpyMemoryDestroy (pml); ns = STATUS_NO_SUCH_FILE; } } if (pdIndex != NULL) *pdIndex = dIndex; if (pns != NULL) *pns = ns; return pml; }
// ----------------------------------------------------------------- PVOID SpyModuleBase (PBYTE pbModule, PNTSTATUS pns) { PMODULE_LIST pml; DWORD dIndex; NTSTATUS ns = STATUS_INVALID_PARAMETER; PVOID pBase = NULL;
if ((pml = SpyModuleFind (pbModule, &dIndex, &ns)) != NULL) { pBase = pml->aModules [dIndex].pBase; SpyMemoryDestroy (pml); } if (pns != NULL) *pns = ns; return pBase; }
// ----------------------------------------------------------------- PIMAGE_NT_HEADERS SpyModuleHeader (PBYTE pbModule, PPVOID ppBase, PNTSTATUS pns) { PVOID pBase = NULL; NTSTATUS ns = STATUS_INVALID_PARAMETER; PIMAGE_NT_HEADERS pinh = NULL;
if (((pBase = SpyModuleBase (pbModule, &ns)) != NULL) && ((pinh = RtlImageNtHeader (pBase)) == NULL)) { ns = STATUS_INVALID_IMAGE_FORMAT; } if (ppBase != NULL) *ppBase = pBase; if (pns != NULL) *pns = ns; return pinh; } 列表6-9. 查找指定模块的信息
解析导出函数、变量的符号 前一小节解释了如何搜索一个PE文件映像中导出函数和变量的符号化名称,以及如何确定已加载系统模块或驱动程序的基地址。现在,是时候将这些零碎的东西整理一下了。基本上,查找一个给定模块的导出符号需要如下三个步骤: 1. 找到模块的线性基地址 2. 搜索模块导出节中的符号 3. 将找到的符号的相对偏移量和模块基地址相加
第一步已经讨论过。列表6-10提供了剩余步骤地实现细节。SpyModuleExport()需要一个文件名,如ntoskrnl.exe、hal.dll、ntfs.sys等等,pbModule参数返回一个指向模块的IMAGE_EXPORT_DIRECTORY结构的指针。可选参数ppBase和pns返回附加的信息:*ppBase在成功的情况下返回模块的基地址,*pns在出错的情况下返回错误状态的诊断信息。首先,SpyModuleExport()调用SpyModuleHeader()来定位IMAGE_NT_HEADERS;然后,它计算PE DataDirectory数组中第一个元素(该元素是一个IMAGE_DATA_DIRECTORY结构)所保存的有关导出节的信息。如果IMAGE_DATA_DIRECTORY结构的VirtualAddress |