第六章 在用户模式下调用内核API函数
翻译:Kendiv( fcczj@263.net )
更新:Thursday, May 05, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
在运行时链接到系统模块
在实现了基本的内核调用接口之后,接下来的问题是将符号化的函数名解析为线性地址,这一地址时CALL指令所需要的(见列表6-2)。这一步很重要,因为你不能确定内核API函数的入口地址在很长一段时间内都不发生变化。只要可能,就应该按照名称调用函数。按照地址调用系统函数只是一种例外,典型的受限函数是指那些目标模块没有导出的函数(但它们确实存在)。在大多数情况下,使用符号化的名字更为清晰,这些符号化的名字位于模块的导出节(export section)中。
在PE映像中查找导出的符号名
对一个Win32程序员来说,在运行时链接到DLL的导出函数几乎每天都会碰到。例如,如果你想编写一个使用了 Windows 2000某些增强特性(译注:这些特性在旧版本中也存在,只不过Windows 2000对其进行了某种程度的增强而已。)的DLL,而且还要求在一些老的操作系统(如Windows 95或98)中也能运行,此时,你应该在运行时链接到DLL的导出函数(指仅在Windows 2000中有效的函数),并且,如果这些函数无法使用(指,此时使用的系统可能是Windows 9x),则应该“悄无声息”的采用默认的行为(意即:此时,不要做弹出一个出错对话框等类似的操作,这里的默认行为指的是使用旧版本中的功能)。在这种情况下,你应该调用GetModuleHandle()函数(如果DLL已经位于内存中,并且可以保证在足够长的时间内该DLL都是有效的),或者调用LoadLibrary()函数(如果DLL还没有加载到内存中,或者为了防止过早的卸载该DLL)。返回的模块句柄可以在多次GetProcAddress()调用中被重复使用,这样就可以找出应用程序所需的DLL的导出函数的所有入口地址。理论上来讲,可以使用相同的方法来调用ntoskrnl.exe、hal.dll或者其他系统模块导出的内核函数。不过,事实上这些系统模块导出的内核函数没有一个能在这种情况下工作!GetModuleHandle()将报告:“此模块还未加载”,并且,如果你强行向GetProcAddress()传入一个硬编码(hard-coded)的系统模块句柄,则GetProcAddress()将返回NULL,例如,ntoskrnl.exe的句柄HMODULE为0x80400000。进一步来来看的话,这也是合理的。这些模块被设计为运行于用户模式的Win32组件,因此可以将它们加载到4GB线性地址空间中的低2GB地址空间中。
如果Win32子系统不知道模块是否位于内核空间,那么理论上,我们就可以使内核驱动程序按照我们的想法工作了---这就是贯穿本书的思路。未文档化的MmGetSystemRoutineAddress()函数(由ntoskrnl.exe导出),显然就可以完成这一工作,不过,很不走运,该函数不支持Windows NT 4.0。因为本书所给出示例代码的一大前提就是在兼容Windows 2000先前版本的基础上,提供最大的扩展性,所以,我放弃使用这一函数,转而寻找一种不需要系统帮助就能确定函数入口地址的方法。Windows 2000运行时库仅为映像文件的解析提供了有限的支持,如未文档化的RtlImageNtHeader()函数,列表6-4给出了该函数的原型。该函数可将模块映像的基地址映射到线性地址空间中(即,一个指向IMAGE_DOS_HEADER结构的指针,该结构体定义于Win32 SDK头文件winnt.h中)并返回一个指向PE表头(PE Header)的指针,DOS表头的e_Ifanew成员指出了PE表头的相对偏移量(译注,这里的DOS表头指的是IMAGE_DOS_HEADER结构,该结构体定义于winnt.h中,具体细节参见后面的译注)。使用RtlImageNtHeader()函数必须小心,因为它并对传入的指针的有效性进行全面的检查。它仅测试指针是否为NULL或0xFFFFFFFF,并验证指针指向的内存块的起始处包含MZ标志。这意味着,如果你传入一个伪造的地址(该地址不为NULL或0xFFFFFFFF),那么,当RtlImageNtHeader()读取DOS header标志时将会立即触发蓝屏。奇怪的是,Windows NT 4.0将这部分代码包含在一个SEH帧中,而Windows 2000却没有这样做。
PIMAGE_NT_HEADER NTAPI RtlImageNtHeader(PVOID base);
列表6-4. RtlImageNtHeader()的原型
译注:
PE表头(PE Header)并非在PE文件的最开始位置。PE文件最前面的数百个位是所谓的DOS Stub(即,DOS Header):这是一个极小的DOS程序,它用来输出像“This Program cannot be run in DOS mode”这样的信息。这里的DOS Stub实际上是一个IMAGE_DOS_HEADER结构,该结构定义于winnt.h中。DOS表头的e_Ifanew成员实际是一个相对偏移值(RVA)。下面的公式可以获取实际的PE表头的地址:
pNTHeader = dosHeader + dosHeader->e_Ifanew;
下面是PE表头的大致布局:
有关PE文件结构的具体细节,请参考《Windows 95 System Programming SECRETS》一书的第八章PE与COFF OBJ格式。
列表6-4给出的RtlImageNtHeader()函数返回一个指向IMAGE_NT_HEADER结构的指针。有关PE文件的结构体均定义于winnt.h中。很不幸,在DDK头文件没有这些结构体的定义,因此这些定义必须手工添加进来。我的Spy driver在它的头文件w2k_spy.h中包含了它用来查找符号的结构体定义(见列表6-5)。IMAGE_NT_HEADERS由PE标志”PE\0\0”、一个IMAGE_FILE_HEADER结构和一个IMAGE_OPTIONAL_HEADER结构顺序连接而成。IMAGE_OPTIONAL_HEADER结构的最后一部分是一个IMAGE_DATA_DIRECTORY类型的数据,可使用该数组来快速的查找文件的节(section)。数组的第一项为IMAGE_DIRECTORY_ENTRY_EXPORT(参见列表6-5,该数组的第二个元素指向导入函数表---imported functions table),它指向文件的导出节(也称作导出函数表),在导出节中包含模块导出的所有函数的名字和其相对地址。我们就是在导出节中查找传递给内核调用接口的函数的名字,从而计算出它们的入口地址。
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8
#define IMAGE_DIRECTORY_ENTRY_TLS 9
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11
#define IMAGE_DIRECTORY_ENTRY_IAT 12
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
// =================================================================
// =================================================================
// WINDOWS 2000 IMAGE STRUCTURES
// =================================================================
typedef struct _IMAGE_FILE_HEADER
typedef struct _IMAGE_FILE_HEADER
{
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
}
IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
// -----------------------------------------------------------------
typedef struct _IMAGE_DATA_DIRECTORY
typedef struct _IMAGE_DATA_DIRECTORY
{
DWORD VirtualAddress;
DWORD Size;
}
IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
// -----------------------------------------------------------------
typedef struct _IMAGE_OPTIONAL_HEADER
typedef struct _IMAGE_OPTIONAL_HEADER
{
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory
[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
}
IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
// -----------------------------------------------------------------
typedef struct _IMAGE_NT_HEADERS
typedef struct _IMAGE_NT_HEADERS
{
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
}
IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
// -----------------------------------------------------------------
// -----------------------------------------------------------------
typedef struct _IMAGE_EXPORT_DIRECTORY
typedef struct _IMAGE_EXPORT_DIRECTORY
{
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
}
IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
列表6-5. PE文件基本结构的子集
PE文件中的导出节(export section)的布局,是由IMAGE_EXPORT_DIRECTORY结构来管理的,可在列表6-5的底部找到IMAGE_EXPORT_DIRECTORY结构体的定义。一个导出节基本上包含一个由IMAGE_EXPORT_DIRECTORY结构体构成的表头,再加上三个大小可变的数组和一个存放以零结尾的ANSI字符串的数组。通常情况下,一个导出项由如下三个部分构成:
1. 一个以零结尾的名称,由8个ANSI字符组成。
2. 一个16位的序列号
3. 一个32位的相对偏移量(针对文件映像的基地址)
图6-1. PE文件导出节的典型布局
导出机制并不仅适用于函数。它只是为PE映像中的地址提供了符号化的名称。对于函数来说,其符号(即函数名)将与其入口地址相关联。对于公开的变量,其符号(即变量名)将指向该变量的基地址。可通过用符号的特征参数填充三个平行的数组来完成符号和地址的关联过程。在图6-1中,这三个平行的数组将作为目标地址数组、名称偏移量数组和序列号数组来引用。它们分别对应IMAGE_EXPORT_DIRECTORY结构中的AddressOfFunctions、AddressOfNames和AddressOfNameOrdinals成员。这三个结构体成员中保存的都是相对于映像文件基地址的偏移量(即,相对偏移量)。IMAGE_EXPORT_DIRECTORY结构的Name成员包含文件自身名称字符串的相对偏移量。如果可执行文件被重新命名,可使用Name来找回文件的原始名称。图6-1只是导出节的一个常见布局,符号和名称数组(Name string sequence)的位置并不是固定的。PE文件编写者可以按照他们的喜好来移动它们,只要IMAGE_EXPORT_DIRECTORY结构的成员可以正确的引用它们。对于Name成员指向的文件字符串来说也是一样的,尽管,一般情况下,文件自身的名称总是位于名称数组中的开始位置,但这并不是必须的。记住,永远不要假定导出节中变量的位置。
IMAGE_EXPORT_DIRECTORY结构的NumberOfFunctions和NumberOfNames成员指出了AddressOfFunctions和AddressOfNames所引用的数组各包含了多少项。没有针对AddressOfNameOrdinals所引用的数组的计数,因为该数组所包含的项数总是和AddressOfNames所引用的数组相同。独立维护地址和名称数组的项数暗示着,可以建立这样一个可执行文件,该可执行文件导出了没有名称的地址。不过,到目前为止,我还没有见过这样的文件,但是在访问这些数组时,还是应该记住这种可能性的存在。再次强调,不要依赖于假设。
按名称查找导出函数或变量所对应的地址的,可按照如下步骤进行,首先,需要一个模块基地址(在Win32中,就是HMODULE):
1. 使用模块基地址调用RtlImageNtHeader(),以获取模块的IMAGE_NT_HEADERS。如果函数返回NULL,则说明地址没有指向一个有效的PE映像。
2. 用IMAGE_DIRECTORY_ENTRY_EXPORT作为索引,来访问OptionalHeader的DataDirectory成员,以找出导出节(export section)的相对偏移量。
3. 通过计算IMAGE_EXPORT_DIRECTORY表头的AddressOfNames成员,来定位导出节中的名称数组(即图6-1中的Sequence of Name Strings)。
4. 遍历名称数组,直到找到匹配的项,或者到达数组末尾(此时表示已到达NumberOfNames)。
5. 如果找到了匹配项,则可到AddressOfNameOrdinals所引用的数组的对应位置读取该名字的序列号。该数组是从0开始计数的,所以,可以使用读取到的序列号作为索引来直接访问AddressOfFunctions所引用的数组。
6. 将模块的基地址与AddressOfFunctions所引用的数组中取出的偏移量相加,即可得到指定名称(函数或者变量)的线性地址。
上述步骤看似简单清晰。但,还存在一个未知量:模块的基地址。尽管上述操作基本上反映出了GetProcAddress()函数的基本行为,但是要找到模块的地址,则意味着需要模仿GetModuleHandle()的行为。如果你查看ntoskrnl.exe的导出函数,你会发现没有函数可以完成这一工作。这是因为Windows 2000内核提供了一个更强大的函数来完成上述工作以及其他任务,这些任务包括:访问系统内部数据。这个函数就是:NtQuerySystemInformation()。
Next:
在下一节,我们将讨论如何将系统模块或驱动程序加载到内存中。
…………….待续…………..