上一次篇我们留下了WinLdrpScanImportAddressTable函数。
当WinLdrScanImportDescriptorTable调用这个函数时,当前需要处理的DLL已经加载到内存而且已经初始化完毕。现在我们需要调用WinLdrpScanImportAddressTable来填写模块本身的IAT。
这个函数有三个参数,WinLdrBlock是Freeldr的LOADER_PARAMETER_BLOCK结构。DllBase是刚刚加载的模块的基地址。ImageBase是需要处理IAT的模块的基地址。ThunkData是IAT表。
WinLdrScanImportDescriptorTable -> WinLdrpScanImportAddressTable (freeldr/freeldr/peloader.c)
- BOOLEAN WinLdrpScanImportAddressTable(IN OUT PLOADER_PARAMETER_BLOCK WinLdrBlock,
- IN PVOID DllBase,
- IN PVOID ImageBase,
- IN PIMAGE_THUNK_DATA ThunkData)
- {
- PIMAGE_EXPORT_DIRECTORY ExportDirectory = NULL;
- BOOLEAN Status;
- ULONG ExportSize;
- /* 获得新加载DLL的导出表 */
- if (DllBase == NULL)
- return FALSE;
- else
- ExportDirectory =
- (PIMAGE_EXPORT_DIRECTORY)RtlImageDirectoryEntryToData(VaToPa(DllBase),
- TRUE,
- IMAGE_DIRECTORY_ENTRY_EXPORT,
- &ExportSize);
- if (ExportDirectory == NULL)
- return FALSE;
- /* 每个IAT表项循环一次, 调用WinLdrpBindImportName进行IAT地址填充 */
- while (((PIMAGE_THUNK_DATA)VaToPa(ThunkData))->u1.AddressOfData != 0)
- {
- /* 填写IAT */
- Status = WinLdrpBindImportName(
- WinLdrBlock,
- DllBase,
- ImageBase,
- ThunkData,
- ExportDirectory,
- ExportSize,
- FALSE);
- /* 下一个IAT表项 */
- ThunkData++;
- if (!Status)
- return Status;
- }
- /* Return success */
- return TRUE;
- }
获得刚刚加载的DLL的导出表后,为每个IAT表项进行一次循环,使用WinLdrpBindImportName填写该IAT表项。
WinLdrpBindImportName的作用是根据IAT存储的内容,在DLL的导出表中搜索到函数地址,并将地址覆盖IAT的原始值。
WinLdrpBindImportName有7个参数:
WinLdrBlock —— Freeldr的LOADER_PARAMETER_BLOCK结构
DllBase —— 拥有导出函数的DLL的基地址
ImageBase —— 需要处理IAT的模块的基地址
ThunkData —— 当前处理的IAT表项指针
ExportDirectory —— DllBase模块的导出表指针
ExportSize —— 导出表大小
ProcessForwards —— 是否在处理DLL Forwards (我们后面会说)
WinLdrScanImportDescriptorTable -> WinLdrpScanImportAddressTable -> WinLdrpBindImportName(freeldr/freeldr/peloader.c)
- BOOLEAN WinLdrpBindImportName(IN OUT PLOADER_PARAMETER_BLOCK WinLdrBlock,
- IN PVOID DllBase,
- IN PVOID ImageBase,
- IN PIMAGE_THUNK_DATA ThunkData,
- IN PIMAGE_EXPORT_DIRECTORY ExportDirectory,
- IN ULONG ExportSize,
- IN BOOLEAN ProcessForwards)
- {
- ULONG Ordinal;
- PULONG NameTable, FunctionTable;
- PUSHORT OrdinalTable;
- LONG High, Low, Middle, Result;
- ULONG Hint;
- if(DllBase == NULL)
- {
- return FALSE;
- }
- ThunkData = VaToPa(ThunkData);
- /* 使用序号导入函数, 并且没有处理Forward */
- if (IMAGE_SNAP_BY_ORDINAL(ThunkData->u1.Ordinal) && !ProcessForwards)
- {
- /* 直接获得函数在ExportTable中的索引 */
- Ordinal = (ULONG)(IMAGE_ORDINAL(ThunkData->u1.Ordinal) - (UINT32)ExportDirectory->Base);
- }
- else
- {
- /* 按照名称导入函数, 如果不是处理Forward请求, 需要先把IMAGE_IMPORT_BY_NAME的RVA转化成物理地址 */
- if (!ProcessForwards)
- {
- ThunkData->u1.AddressOfData =
- (ULONG_PTR)RVA(ImageBase, ThunkData->u1.AddressOfData);
- }
- /* 获得导出表的AddressOfNames和AddressOfNameOrdinals的物理地址 */
- NameTable = (PULONG)VaToPa(RVA(DllBase, ExportDirectory->AddressOfNames));
- OrdinalTable = (PUSHORT)VaToPa(RVA(DllBase, ExportDirectory->AddressOfNameOrdinals));
- /* IMAGE_IMPORT_BY_NAME的第一个元素是Hint值, 这个值是个搜索建议, 可以加快搜索速度 */
- Hint = ((PIMAGE_IMPORT_BY_NAME)VaToPa((PVOID)ThunkData->u1.AddressOfData))->Hint;
- /* 首先查看AddressOfName中Hint个名称, 是否是需要查询的函数名。如果是,直接获得函数的序号 */
- if (
- (Hint < ExportDirectory->NumberOfNames) &&
- (
- strcmp(VaToPa(&((PIMAGE_IMPORT_BY_NAME)VaToPa((PVOID)ThunkData->u1.AddressOfData))->Name[0]),
- (PCHAR)VaToPa( RVA(DllBase, NameTable[Hint])) ) == 0
- )
- )
- {
- Ordinal = OrdinalTable[Hint];
- }
- else
- {
- /* Hint值不正确, 使用二分查找在NameTable中查找函数名 */
- Low = 0;
- High = ExportDirectory->NumberOfNames - 1;
- /* 二分查找 */
- while (High >= Low)
- {
- Middle = (Low + High) >> 1;
- Result = strcmp(VaToPa(&((PIMAGE_IMPORT_BY_NAME)VaToPa((PVOID)ThunkData->u1.AddressOfData))->Name[0]),
- (PCHAR)VaToPa(RVA(DllBase, NameTable[Middle])));
- if (Result < 0)
- High = Middle - 1;
- else if (Result > 0)
- Low = Middle + 1;
- else
- break;
- }
- /* 没找到 */
- if (High < Low)
- return FALSE;
- /* 获得函数序号 */
- Ordinal = OrdinalTable[Middle];
- }
- }
- /* 序号不合法*/
- if (Ordinal >= ExportDirectory->NumberOfFunctions)
- return FALSE;
- /* 获得导出函数表 */
- FunctionTable = (PULONG)VaToPa(RVA(DllBase, ExportDirectory->AddressOfFunctions));
- /* 获得函数的实际地址或者Forward字符串 */
- ThunkData->u1.Function = (ULONG_PTR)RVA(DllBase, FunctionTable[Ordinal]);
- /* 如果函数地址在导出表的范围内, 那它实际是Forwarder字符串 (DLLNAME.FUNCNAME形式) */
- if (((ULONG_PTR)VaToPa((PVOID)ThunkData->u1.Function) > (ULONG_PTR)ExportDirectory) &&
- ((ULONG_PTR)VaToPa((PVOID)ThunkData->u1.Function) < ((ULONG_PTR)ExportDirectory + ExportSize)))
- {
- PLDR_DATA_TABLE_ENTRY DataTableEntry;
- CHAR ForwardDllName[255];
- PIMAGE_EXPORT_DIRECTORY RefExportDirectory;
- ULONG RefExportSize;
- /* 分离出DLLNAME */
- RtlCopyMemory(ForwardDllName, (PCHAR)VaToPa((PVOID)ThunkData->u1.Function), sizeof(ForwardDllName));
- *strchr(ForwardDllName,'.') = '/0';
- /* 如果ForwardDllName没有加载, 出错 */
- if (!WinLdrCheckForLoadedDll(WinLdrBlock, ForwardDllName, &DataTableEntry))
- return FALSE;
- /* 获得ForwardDllName模块的导出表RefExportDirectory */
- RefExportDirectory = (PIMAGE_EXPORT_DIRECTORY)
- RtlImageDirectoryEntryToData(VaToPa(DataTableEntry->DllBase),
- TRUE,
- IMAGE_DIRECTORY_ENTRY_EXPORT,
- &RefExportSize);
- if (RefExportDirectory)
- {
- UCHAR Buffer[128];
- IMAGE_THUNK_DATA RefThunkData;
- PIMAGE_IMPORT_BY_NAME ImportByName;
- PCHAR ImportName;
- BOOLEAN Status;
- /* 分离出FUNCNAME */
- ImportName = strchr((PCHAR)VaToPa((PVOID)ThunkData->u1.Function), '.') + 1;
- /* 做一个假的IMAGE_IMPORT_BY_NAME和IMAGE_THUNK_DATA */
- ImportByName = (PIMAGE_IMPORT_BY_NAME)Buffer;
- RtlCopyMemory(ImportByName->Name, ImportName, strlen(ImportName)+1);
- ImportByName->Hint = 0;
- RefThunkData.u1.AddressOfData = (ULONG_PTR)ImportByName;
- /* 调用WinLdrpBindImportName填充RefThunkData, 注意最后一个参数 */
- Status = WinLdrpBindImportName(
- WinLdrBlock,
- DataTableEntry->DllBase,
- ImageBase,
- &RefThunkData,
- RefExportDirectory,
- RefExportSize,
- TRUE);
- /* 填写真正的ThunkData */
- ThunkData->u1 = RefThunkData.u1;
- return Status;
- }
- else
- {
- return FALSE;
- }
- }
- return TRUE;
- }
这里导入表导出表的结构就不说了,网上有不少教程。
这个函数的最后一个参数很有意思。因为导出表有一种Forwarder机制, 可以把导出的函数传递给其他模块。比如A模块导出的A函数是个Forwarder指向B模块的B函数,那么在exe程序导入A!A时实际上导入的是B!B。最后一个参数就表明了当前的调用是否是在处理一个Forwarder。下面会有说明。
如果ThunkData的最高位是1说明按照序号方式导入函数,直接减去Export表中的Base值就可以得到实际的函数序号(20-24行)。
如果最高位不是1说明按照名称导入,则需要在导出表中查找所需的函数名,之后得到函数序号。
28 - 32行处理了Forwarder与普通ThunkData绑定的唯一区别,处理Forwarder时ThunkData里面存的不是IMAGE_IMPORT_BY_NAME的RVA,而是真实的内存地址。这里将RVA转化成内存地址。
之后首先使用IMAGE_IMPORT_BY_NAME中的Hind值查看导出表是不是需要找的函数,如果是直接得到函数序号Ordinal(39 - 48)。
如果Hint不正确则使用二分查找导出表的NameTable,找到函数后从OrdinalTable中获得函数索引(49 - 72)。
来到74行, 这时已经获得函数的Ordinal, 根据Ordinal在导出表中找到函数地址, 并把这个地址赋值给ThunkData。如果地址不是一个Forwarder字符串则函数完成。
而如果这个函数地址指向的是导出表的内存空间,说明它并不是真正地址,而是一个Forwarder字符串。形如KERNEL32.GetProcAddress。82 - 126就是处理这种情况的,它把这个字符串分解为KERNEL32和GetProcAddress,并且重新调用WinLdrpBindImportName来绑定ThunkData, 这时这个ThunkData是函数伪造的, 里面存储的不是RVA, 而是指向IMAGE_IMPORT_BY_NAME地址。所以这次调用时最后一个参数为TRUE。调用完成后把伪造的ThunkData的值赋值给正式的ThunkData,函数完成。
至此镜像加载所有的关键点都说完了。
Freeldr的这组函数设计有一个比较不好的地方,就是没有将加载(WinLdrLoadImage)、生成DTE(WinLdrAllocateDataTableEntry)和处理导入表(WinLdrScanImportDescriptorTable)三个函数综合起来。每次手工加载一个模块的时候都要一次调用这三个函数,其实WinLdrScanImportDescriptorTable中的辅助函数WinLdrpLoadAndScanReferencedDll就是做这件事的,Freeldr完全可以把它改写成一个API接口,这样加载镜像时就更加方便了。