📄 漫谈兼容内核之十一:windows dll的装入和连接.txt
字号:
PCHAR ImportedName)
{
. . . . . .
ImportModuleDirectory = (PIMAGE_IMPORT_MODULE_DIRECTORY)
RtlImageDirectoryEntryToData(Module->BaseAddress,
TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT,
NULL);
. . . . . .
while (ImportModuleDirectory->dwRVAModuleName)
{
Name = (PCHAR)Module->BaseAddress +
ImportModuleDirectory->dwRVAModuleName;
if (0 == _stricmp(Name, ImportedName))
{
Status = LdrpProcessImportDirectoryEntry(Module,
ImportedModule,
ImportModuleDirectory);
. . . . . .
}
ImportModuleDirectory++;
}
return STATUS_SUCCESS;
}[/code]
先看调用参数。第一个参数Module当然是指向当前模块的LDR_MODULE数据结构的指针;第二个参数ImportedModule同样是指针,但是指向被引入模块的LDR_MODULE数据结构;第三个参数ImportedName则是字符串指针,指向被引入模块的文件名。这段程序很简单,就是搜索引入者模块的普通引入目录,找到了要求引入给定被引入模块的那个目录项,就调用LdrpProcessImportDirectoryEntry()。注意前面在LdrFixupImports()中是按绑定引入目录项、而不是普通引入目录项在处理的,而LdrpProcessImportDirectoryEntry()要求使用普通引入目录项,所以才需要经由LdrpProcessImportDirectory()先找到相应的普通引入目录项。
事实上,LdrpProcessImportDirectoryEntry()所实现的才是本来意义上的动态连接。
[code][__true_LdrInitializeThunk > LdrPEStartup() > LdrFixupImports()
> LdrpProcessImportDirectory() > LdrpProcessImportDirectoryEntry()]
static NTSTATUS
LdrpProcessImportDirectoryEntry(PLDR_MODULE Module,
PLDR_MODULE ImportedModule,
PIMAGE_IMPORT_MODULE_DIRECTORY ImportModuleDirectory)
{
. . . . . .
/* Get the import address list. */
ImportAddressList = (PVOID *)(Module->BaseAddress +
ImportModuleDirectory->dwRVAFunctionAddressList);
/* Get the list of functions to import. */
if (ImportModuleDirectory->dwRVAFunctionNameList != 0)
{
FunctionNameList = (PULONG) (Module->BaseAddress +
ImportModuleDirectory->dwRVAFunctionNameList);
}
else
{
FunctionNameList = (PULONG)(Module->BaseAddress +
ImportModuleDirectory->dwRVAFunctionAddressList);
}
/* Get the size of IAT. */
IATSize = 0;
while (FunctionNameList[IATSize] != 0L)
{
IATSize++;
}
/* Unprotect the region we are about to write into. */
IATBase = (PVOID)ImportAddressList;
IATSize *= sizeof(PVOID*);
Status = NtProtectVirtualMemory(NtCurrentProcess(),
&IATBase,
&IATSize,
PAGE_READWRITE,
&OldProtect);
. . . . . .
/* Walk through function list and fixup addresses. */
while (*FunctionNameList != 0L)
{
if ((*FunctionNameList) & 0x80000000)
{
Ordinal = (*FunctionNameList) & 0x7fffffff;
*ImportAddressList =
LdrGetExportByOrdinal(ImportedModule->BaseAddress, Ordinal);
. . . . . .
}
else
{
IMAGE_IMPORT_BY_NAME *pe_name;
pe_name = RVA(Module->BaseAddress, *FunctionNameList);
*ImportAddressList = LdrGetExportByName(ImportedModule->BaseAddress,
pe_name->Name, pe_name->Hint);
. . . . . .
}
ImportAddressList++;
FunctionNameList++;
}
/* Protect the region we are about to write into. */
Status = NtProtectVirtualMemory(NtCurrentProcess(),
&IATBase,
&IATSize,
OldProtect,
&OldProtect);
. . . . . .
return STATUS_SUCCESS;
}[/code]
首先根据目录项中的两个位移量取得分别指向函数名字符串数组和函数指针数组的指针。这两个数组是平行的,一个函数的的函数名在字符串数组中的下标是什么,它的函数入口在指针数组中的下标也就是什么。然后对字符串数组中的元素计数,得到该数组的大小IATSize。显然,函数指针数组的大小也是IATSize。这里IAT是“引入地址表(Imported Address Table)”的缩写,其实就是函数指针数组。这个数组在映像内部,其所在的页面在装入映像时已被加上写保护,而下面要做的事正是要改变这些指针的值,所以先要通过NtProtectVirtualMemory()把这些页面的访问模式改成可读可写。做完这些准备之后,下面就是连接的过程了,那就是根据需要把被引入模块所引出的函数入口(地址)填写到引入者模块的IAT中。
与当前模块中的两个数组相对应,在被引入模块的“引出”目录中也有两个数组,说明本模块引出函数的名称和入口地址(在映像中的位移)。当然,这两个数组也是平行的。
要获取被引入模块中的函数入口有两种方法,即按序号(Ordinal)引入和按函数名引入。虽然名曰函数名数组,实际上数组中的元素既可以是个函数名指针(实际上是结构指针,见后),也可以是个非0的序号。如果数组元素的内容是(32位)序号就把最高位设成1,使序号(在形式上)都大于0x80000000。因为任何模块都不可能引出这么多的函数,所以这样做是安全的。另一方面,当数组元素的内容是函数名指针时又必须使最高位为0,这也不会有问题,因为Windows的用户空间本来就在0x80000000、即2GB边界以下。这里,按函数名获取是很好理解的,需要引入的模块以函数名的方式说明要引入那一些函数,而被引入模块也以函数名的方式说明本模块提供(引出)了哪一些函数,通过字符串比对就可以实现配对,并从而取得被引入模块提供的相应函数指针。那么“序号”又是什么呢?其实也很简单,序号本质上就是目标函数在引出数组中的下标,只不过下标是从0开始的,而序号是从一个“基序号”开始的(因为必须是非0),所以序号是下标加基序号(一般是1)。引出目录项中有个字段Base,那就是基序号。显然,按序号获取函数指针的效率更高。
回到上面的代码。根据当前模块引入目录中字符串数组各元素的内容,就可以确定所要求的是按序号引入还是按函数名引入,从而分别调用LdrGetExportByOrdinal()和LdrGetExportByName()。这两个函数都返回目标函数在本进程用户空间中的入口地址,把它填写入当前模块引入目录函数指针数组中的相应元素,就完成了一个函数的连接。当然,同样的操作要循环实施于当前模块需要从给定模块引入的所有函数,并且(在上一层)循环实施于所有的被引入模块。
完成了对一个被引入模块的连接之后,又调用NtProtectVirtualMemory()恢复当前模块中给定目录项内函数指针数组所在页面的保护。
按序号引入和按函数名引入所使用的上述两个函数基本上是一样的,只是LdrGetExportByOrdinal()略为简单一些(无需字符串比对),我们就来看这个函数的代码。
[code][__true_LdrInitializeThunk > LdrPEStartup() > LdrFixupImports()
> LdrpProcessImportDirectory() > LdrpProcessImportDirectoryEntry()
> LdrGetExportByOrdinal()]
static PVOID
LdrGetExportByOrdinal (PVOID BaseAddress, ULONG Ordinal)
{
. . . . . .
ExportDir = (PIMAGE_EXPORT_DIRECTORY)
RtlImageDirectoryEntryToData (BaseAddress,
TRUE,
IMAGE_DIRECTORY_ENTRY_EXPORT,
&ExportDirSize);
ExFunctions = (PDWORD *)RVA(BaseAddress, ExportDir->AddressOfFunctions);
Function = (0 != ExFunctions[Ordinal - ExportDir->Base]
? RVA(BaseAddress, ExFunctions[Ordinal - ExportDir->Base] )
: NULL);
if (((ULONG)Function >= (ULONG)ExportDir) &&
((ULONG)Function < (ULONG)ExportDir + (ULONG)ExportDirSize))
{
Function = LdrFixupForward((PCHAR)Function);
}
return Function;
}[/code]
这里的RVA()是个宏操作,用来根据映像内位移和映像起点计算装入后的虚拟地址,定义为:
[code]#define RVA(m, b) ((ULONG)b + m[/code]
代码的逻辑很简单,先通过RtlImageDirectoryEntryToData()从被引入模块的映像获取它的引出目录ExportDir。再根据ExportDir->AddressOfFunctions、即函数指针数组在映像中的位移、和映像的起点算出它的地址ExFunctions。然后,从序号中减去被引入模块使用的基序号,就得到了实际的下标。至于用这下标取得目标函数的入口位移,再换算成虚拟地址、即函数指针,那就是直截了当的事了。
由被引入模块实现并引出的函数当然不会落在引出目录内部,如果目标函数指针落在引出目录内部,那就是特殊的情况了,实际上说明这是个“过路”的转引函数,其实现还在另一个模块中,而此时的“函数指针”其实是个字符串指针,指向被转引模块的模块名。转引函数的连接则需要通过LdrFixupForward()作进一步的处理。
[code][__true_LdrInitializeThunk > LdrPEStartup() > LdrFixupImports()
> LdrpProcessImportDirectory() > LdrpProcessImportDirectoryEntry()
> LdrGetExportByOrdinal() > LdrFixupForward()]
static PVOID
LdrFixupForward(PCHAR ForwardName)
{
. . . . . .
strcpy(NameBuffer, ForwardName);
p = strchr(NameBuffer, '.');
if (p != NULL)
{
*p = 0;
DPRINT("Dll: %s Function: %s\n", NameBuffer, p+1);
RtlCreateUnicodeStringFromAsciiz (&DllName, NameBuffer);
Status = LdrFindEntryForName (&DllName, &Module, FALSE);
/* FIXME:
* The caller (or the image) is responsible for loading of the dll, where
* the function is forwarded.
*/
if (!NT_SUCCESS(Status))
{
Status = LdrLoadDll(NULL, LDRP_PROCESS_CREATION_TIME,
&DllName, &BaseAddress);
if (NT_SUCCESS(Status))
{
Status = LdrFindEntryForName (&DllName, &Module, FALSE);
}
}
RtlFreeUnicodeString (&DllName);
if (!NT_SUCCESS(Status))
{
DPRINT1("LdrFixupForward: failed to load %s\n", NameBuffer);
return NULL;
}
DPRINT("BaseAddress: %p\n", Module->BaseAddress);
return LdrGetExportByName(Module->BaseAddress, (PUCHAR)(p+1), -1);
}
return NULL;
}[/code]
先通过LdrFindEntryForName()在已装入模块的队列中寻找,取得指向被转引模块的LDR_MODULE数据结构指针,如果找不到就通过LdrLoadDll()装入。然后再通过LdrGetExportByName()获取目标函数的入口。
函数LdrLoadDll()是ntdll.dll的一个引出函数。它不仅供ntdll.dll内部的LdrFixupForward()调用,也供别的DLL或.exe应用程序调用。例如kernel32.dll中的LoadLibraryExA()和LoadLibraryExW()就都要调用这个函数。
[code][__true_LdrInitializeThunk > LdrPEStartup() > LdrFixupImports()
> LdrpProcessImportDirectory() > LdrpProcessImportDirectoryEntry()
> LdrGetExportByOrdinal() > LdrFixupForward() > LdrLoadDll()]
NTSTATUS STDCALL
LdrLoadDll (IN PWSTR SearchPath OPTIONAL, IN ULONG LoadFlags,
IN PUNICODE_STRING Name, OUT PVOID *BaseAddress OPTIONAL)
{
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -