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++; 	} }
 
  | 
 
测试结果
