Last updated on May 10, 2024 pm
IAT中的奇怪地址
在之前的文章:PE结构(9)导入表 中,我们已对IAT结构做出了基本的解析。
目前可以达成的认识是:
在PE文件加载之前,IAT与INT指向同一块函数名称空间,当PE文件加载之后,OS会将所需导入函数的绝对地址贴到IAT的地址中。
那么有没有一种情况,即在PE文件加载之前,IAT中已经存储了导入函数的地址呢?
事实上这事儿完全有可能的,例如古早时期的Windows XP版本中,系统自带的画图、记事本、计算器等EXE执行文件,都是通过这种方式直接在IAT中存储了导入函数地址。
IAT中函数地址的加载
当PE文件被装载到内存,开始计算IAT中所需的函数地址时,一个显而易见的问题就在于:依赖的DLL文件,并不能保证自身一定位于某个固定的ImageBase。
即使在OS层面没有开启入口地址随机化,也存在某个DLL计划好的ImageBase被其他DLL Module抢占,导致程序基地址发生偏差。此时OS将根据DLL内的Reloc表对DLL内的地址进行重定位修复,最终是将修复后的函数地址填入EXE文件的IAT中。
那么如果EXE里的IAT是被提前绑定的呢?当然需要重新计算了。
同理,如果DLL文件本身被修改了,导致函数地址变更,此时也需要重新计算IAT中存储的地址。
绑定导入表
如何进行这种计算呢?就需要绑定导入表的参与了。
TimeStamp比对
我们在之前的文章中探讨过导入表的结构:
1 2 3 4 5 6 7 8 9 10
| typedef struct _DATA_IMPORT_DIRECTORY { union { DWORD Characteristics; DWORD OriginalFirstThunk; }; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } DATA_IMPORT_DIRECTORY, * PDATA_IMPORT_DIRECTORY;
|
在当时我们未对我TimeDateStamp成员进行过多深究。
此时则需要重新审视该成员,当该成员存储0值时,意味着IAT中尚未绑定函数地址,当该成员存储-1时,意味着IAT已绑定函数地址。
对于使用了绑定导入表技术的PE文件来说,在程序加载之前,TimeDateStamp的值就已经是-1了。
此时我们解决了第一个问题:如何判断IAT表中已经绑定了导入函数?
但是还未解决另一个问题,在IAT被提前(加载前)绑定时,如何确认绑定的地址是真实有效的呢?
结构概述
1 2 3 4 5 6 7 8 9 10 11
| typedef struct _DATA_BOUND_IMPORT_DESCRIPTOR { DWORD TimeDateStamp; WORD OffsetModuleName; WORD NumberOfModuleForwarderRefs; } DATA_BOUND_IMPORT_DESCRIPTOR, *PDATA_BOUND_IMPORT_DESCRIPTOR;
typedef struct _DATA_BOUND_FORWARDER_REF { DWORD TimeDateStamp; WORD OffsetModuleName; WORD Reserved; } DATA_BOUND_FORWARDER_REF, *PDATA_BOUND_FORWARDER_REF;
|
该接口即为绑定导入表结构,当DATA_BOUND_IMPORT_DESCRIPTOR中存储的TimeDateStamp成员,与对应的DLL文件的标准PE头中存储的TimeDateStamp一致的情况下,我们认为导入函数的地址不需要重新计算(即DLL文件并未发生修改)。
很明显,为了确认这件事情,OS会需要遍历全部的绑定导入表结构,来判断是否需要将某个DLL的导出函数地址重新计算后填入EXE的IAT中。
遍历方法
DATA_BOUND_IMPORT_DESCRIPTOR结构中的NumberOfModuleForwarderRefs成员,描述了该dll依赖多少个其他dll。
若该成员为0,则下一个结构体仍然是DATA_BOUND_IMPORT_DESCRIPTOR。
若该成员不为0,则下N个结构体为DATA_BOUND_FORWARDER_REF结构。
DLL名称的奇怪设计
尝试获取DATA_BOUND_FORWARDER_REF结构或DATA_BOUND_IMPORT_DESCRIPTOR结构的名称时,使用第一个DATA_BOUND_IMPORT_DESCRIPTOR结构的地址加上OffsetModuleName(无论当前遍历到第几个),即是DLL名称字符串所在地址(RVA)。
编码实现打印绑定导入表
很不容易找到一个还在用这古老技术的文件。
以下示例使用从winxp中提取的记事本程序演示。
notepad.exe
主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void work07() { PEFILE peFile{ 0 }; FILE* pFile = fopen("D:\\notepad.exe", "rb");
int fileLength = LoadFileToMemory(pFile, &peFile); int r = fclose(pFile);
if (peFile.h_pe == nullptr) { return; }
PDATA_BOUND_IMPORT_DESCRIPTOR pTable = nullptr; GetBoundImportTable(&pTable, &peFile);
PrintBoundImportTable(pTable, &peFile); }
|
打印绑定导入表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void PrintBoundImportTable(PDATA_BOUND_IMPORT_DESCRIPTOR table, lpPEFILE lpPefile) { if (table == NULL) { return; } DWORD firstTableAddress = (DWORD)table;
while (table->TimeDateStamp != NULL) { printf("\n"); DWORD address = (DWORD)table; char* dllName = (char*)(table->OffsetModuleName + firstTableAddress); DWORD timeStamp = table->TimeDateStamp; WORD numberOfRefs = table->NumberOfModuleForwarderRefs; printf("address: 0x%x, dllName: %s, timeStamp: 0x%x, numberOfRefs: %d \n", address, dllName, timeStamp, numberOfRefs); if (numberOfRefs > 0) { printf("Refs:\n"); PrintBoundImportRef( (PDATA_BOUND_FORWARDER_REF)(table + 1), firstTableAddress, numberOfRefs, lpPefile); }
table += numberOfRefs > 0 ? numberOfRefs + 1 : 1; printf("\n"); printf("*********************************************************************\n"); } }
|
打印Ref(其实是完全相同的结构)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void PrintBoundImportRef( PDATA_BOUND_FORWARDER_REF table, DWORD firstTableAddress, DWORD size, lpPEFILE lpPefile) { int index = 0; while (table->TimeDateStamp != NULL && index < size) { DWORD address = (DWORD)table; char* dllName = (char*)(table->OffsetModuleName + firstTableAddress); DWORD timeStamp = table->TimeDateStamp; WORD Reserved = table->Reserved; printf(" address: 0x%x, dllName: %s, timeStamp: 0x%x, Reserved: %d \n", address, dllName, timeStamp, Reserved);
table++; index++; } }
|
测试结果
