PE文件格式(输入表)
可执行文件使用来自其他DLL的代码或数据的动作作为输入。例如当exe程序弹出一个消息框时,是通过调用User32.dll的MessageBoxA(或MessageBoxW)函数来实现的
Win32 API是区分字符集的,两个版本:ANSI版本与Unicode版本
输入表是一个类型为IMAGE_IMPORT_DESCRIPTOR(简称IID)的数组,每一个DLL都对应有一个IID结构,由于没有字段指明这个IID数组的长度,所以通过在末尾添加一个空的IID结构来表示数组的结束
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; // RVA,指向输入名称表(INT),INT是一个类型为IMAGE_THUNK_DATA结构的数组,同样的,通过在数组末尾附加一个空的IMAGE_THUNK_DATA结构来表示数组的结束,每一个输入的函数都有一个对应的IMAGE_THUNK_DATA结构 } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; // -1 if no forwarders DWORD Name; // 以“00”结尾的ASCII字符的RVA,指向DLL的名字,如“User32.dll” DWORD FirstThunk; // 指向输入地址表(IAT)的RVA,IAT也是一个类型为IMAGE_THUNK_DATA结构的数组,同样的,通过在数组末尾附加一个空的IMAGE_THUNK_DATA结构来表示数组的结束,每一个输入的函数都有一个对应的IMAGE_THUNK_DATA结构 } IMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk与FirstThunk极其相似,前者指向INT,后者指向IAT
INT和IAT都是类型为IMAGE_THUNK_DATA的数组,两个数组的结束都是有一个值为0的IMAGE_THUNK_DATA元素表示
typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // 指向一个转向者字符串的RVA DWORD Function; // 被输入的函数的内存地址 DWORD Ordinal; // 被输入的API的序列值 DWORD AddressOfData; // 指向 PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32;
该类型仅有的一个成员u1,是一个联合体(union),联合体内的类型都是DWORD,所以IMAGE_THUNK_DATA的大小是4字节
每个IMAGE_THUNK_DATA元素对应于一个从可执行文件输入的函数
当IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号的方式进行输入,这时候低31位(或一个64位可执行文件的低63位)的值就表示函数的序号;当该类型的最高位为0时,表示函数以字符串类型的函数名方式输入,这时候值就表示一个指向IMAGE_IMPORT_BY_NAME结构的RVA
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint:表示函数在其原始DLL文件的输出表中的序号。该域被PE装载器用来在DLL的输出表里快速查询函数。该值不是必须的
Name:含有输入函数的函数名。以“NULL”字符结尾。这是一个可变的大小
OriginalFirstThunk指向的INT数组和FirstThunk指向的IAT数组到底有什么区别
当PE文件被加载时,PE加载器会遍历INT结构中的数组项,通过其指向的IMAGE_IMPORT_BY_NAME结构来找到函数的名字,PE加载器可以通过函数的名字找到函数的地址,随后把得到的函数地址填充到IAT结构中,此后,通过IAT结构中的函数地址就可以进行函数调用了
PE装载器把导入函数输入至IAT的顺序
1、 读取IID的Name成员,获取库名称字符串(“Kernel32.dll”) 2、 装载相应库,LoadLibrary("Kernel32.dll") 3、 读取IID的OriginalFirstThunk成员,获取INT地址 4、 读取INT数组中的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA) 5、 使用IMAGE_IMPORT_BY_NAME的Hint或Name项,获取相应函数的起始地址 6、 读取IID的FirstThunk(IAT)成员,获得IAT地址 7、 将上面获得的函数地址输入相应的IAT数组值 8、 重复4-7步骤,直到INT结束(遇到NULL时)
IMAGE_OPTIONAL_HEADER32 ——> DataDirectory ——> VirtualAddress的值即是IMAGE_IMPORT_DESCRIPTOR结构体数组的起始地址(RVA值)
以某文件为例,查看IMAGE_OPTIONAL_HEADER32.DataDirectory结构体的值;第一个4字节为虚拟地址,第二个4字节为Size成员
因为RVA是2524,故文件偏移为1724,计算方法参考上面的:RAW = RVA - VirtualAddress + PointerToRawData
IID的大小为20字节
用工具计算验证一下
将上面得到的输入表第一个IID结构的数据与IID结构体的成员对应
IMAGE_IMPORT_DESCRIPTOR { OriginalFirstThunk; = 00 00 25 F8 TimeDateStamp; = 00 00 00 00 ForwarderChain; = 00 00 00 00 Name; = 00 00 26 A8 FirstThunk; = 00 00 20 34 }Name是一个字符串指针,它指向导入函数所属的库文件名称(RVA:26 A8 ——> RAW:18A8)
可以看到VCRUNTIME140.dll字符串
OriginalFirstThunk -- INT,包含导入函数信息的结构体指针数组
(RVA:25F8 ——> RAW:17F8),
每个地址值分别指向IMAGE_IMPORT_BY_NAME结构体
跟踪第一个值2684(RVA),计算参照上面方法,可以看到导入的API函数的名称字符串
0x0048为Ordinal,是库中函数固有编号,后面为函数名称字符串,以00结尾