第六章 在用户模式下调用内核API函数
翻译:Kendiv( [email protected] )
更新: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成员不是NULL,并且Size成员是一个合理的值,则可以断定该PE Image包含至少一个导出节。此时,SpyModuleExport()使用PTR_ADD()宏(见列表6-10)将VirtualAddress加上模块基地址,从而得到IMAGE_EXPORT_DIRECTORY的绝对线性地址。否则,SpyModuleExport()将返回NULL,并将状态代码设为STATUS_DATA_ERROR(0xC000003E)。
#define PTR_ADD(_base,_offset) /
((PVOID) ((PBYTE) (_base) + (DWORD) (_offset)))
// -----------------------------------------------------------------
PIMAGE_EXPORT_DIRECTORY SpyModuleExport (PBYTE pbModule,
PPVOID ppBase,
PNTSTATUS pns)
{
PIMAGE_NT_HEADERS pinh;
PIMAGE_DATA_DIRECTORY pidd;
PVOID pBase = NULL;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
PIMAGE_EXPORT_DIRECTORY pied = NULL;
if ((pinh = SpyModuleHeader (pbModule, &pBase, &ns)) != NULL)
{
pidd = pinh->OptionalHeader.DataDirectory
+ IMAGE_DIRECTORY_ENTRY_EXPORT;
if (pidd->VirtualAddress &&
(pidd->Size >= IMAGE_EXPORT_DIRECTORY_))
{
pied = PTR_ADD (pBase, pidd->VirtualAddress);
}
else
{
ns = STATUS_DATA_ERROR;
}
}
if (ppBase != NULL) *ppBase = pBase;
if (pns != NULL) *pns = ns;
return pied;
}
// -----------------------------------------------------------------
PVOID SpyModuleSymbol (PBYTE pbModule,
PBYTE pbName,
PPVOID ppBase,
PNTSTATUS pns)
{
PIMAGE_EXPORT_DIRECTORY pied;
PDWORD pdNames, pdFunctions;
PWORD pwOrdinals;
DWORD i, j;
PVOID pBase = NULL;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
PVOID pAddress = NULL;
if ((pied = SpyModuleExport (pbModule, &pBase, &ns)) != NULL)
{
pdNames = PTR_ADD (pBase, pied->AddressOfNames);
pdFunctions = PTR_ADD (pBase, pied->AddressOfFunctions);
pwOrdinals = PTR_ADD (pBase, pied->AddressOfNameOrdinals);
for (i = 0; i < pied->NumberOfNames; i++)
{
j = pwOrdinals [i];
if (!strcmp (PTR_ADD (pBase, pdNames [i]), pbName))
{
if (j < pied->NumberOfFunctions)
{
pAddress = PTR_ADD (pBase, pdFunctions [j]);
}
break;
}
}
if (pAddress == NULL)
{
ns = STATUS_PROCEDURE_NOT_FOUND;
}
}
if (ppBase != NULL) *ppBase = pBase;
if (pns != NULL) *pns = ns;
return pAddress;
}
列表6-10. 在模块的导出节中查找符号
SpyModuleSymbol()函数将完成最后的工作。在这里你会发现访问列表6-1所示结构体的代码。在从SpyModuleExport()返回一个IMAGE_EXPORT_DIRECTORY指针后,地址、名称和序列号数组的线性地址就可以确定下来了,再次使用PTR_ADD()宏。很幸运,PE文件格式规定指向其内部数据结构的指针总是一个相对于映像文件基地址的偏移量,因此PTR_ADD()宏提供了一个通用的转换方式,可根据偏移量计算线性地址。在查找地址时,要特别注意序列号数组。如果在相同的数组中找到了符号,变量i中将包含该符号在数组中的索引,此索引从0开始计数。这个索引值不能直接拿来访问地址数组,它必须经过序列号数组的转换。j = pwordinals[i];就是完成这一工作的。注意,这里的序列号是一个16位的值,而其他两个数组包含的都是32位的值。如果传给SpyModuleSymbol()的符号名(由pbName引用)无法解析,则SpyModuleSymbol()将返回一个NULL,此时的返回代码将为STATUS_PROCEDURE_NOT_FOUND(0xC 000007A )。
尽管看上去SpyModuleSymbol()提供了我们按名字调用内核函数所需的一切,我还是引入了该函数的另一个外包函数。列表6-11给出了最终结果:SpyModuleSymbolEx()函数使用一个由模块/符号名组成的单一字符串,其格式为”module!symbol”,当然符号的解析还是要有SpyModuleSymbol()来完成。SpyModuleSymbolEx()中的大部分代码都用来将输入的字符串解析为一个模块名和一个符号。如果没有找到”!”分隔符,SpyModuleSymbolEx()将假定ntoskrnl.exe为目标模块,因为该模块是最常用的一个。
PVOID SpyModuleSymbolEx (PBYTE pbSymbol,
PPVOID ppBase,
PNTSTATUS pns)
{
DWORD i;
BYTE abModule [MAXIMUM_FILENAME_LENGTH] = "ntoskrnl.exe";
PBYTE pbName = pbSymbol;
PVOID pBase = NULL;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
PVOID pAddress = NULL;
for (i = 0; pbSymbol [i] && (pbSymbol [i] != '!'); i++);
if (pbSymbol [i++])
{
if (i <= MAXIMUM_FILENAME_LENGTH)
{
strcpyn (abModule, pbSymbol, i);
pbName = pbSymbol + i;
}
else
{
pbName = NULL;
}
}
if (pbName != NULL)
{
pAddress = SpyModuleSymbol (abModule, pbName, &pBase, &ns);
}
if (ppBase != NULL) *ppBase = pBase;
if (pns != NULL) *pns = ns;
return pAddress;
}
列表6-11. 一个强大的符号查找函数
Next:
在这一节里,我们讨论了如何调用系统模块中的导出函数,在下一节里,我们将讨论如何在用户模式下的应用程序中执行这一调用操作。
………………待续……………..