PE文件解析(二)
之前已经对PE文件的总体结构进行了解析,但PE文件中的很多重要的数据还存放在各类数据表中,因此我们还需要对这些表信息进行解析,这里主要包块输入表与输入地址表、输出表、重定位表、资源表五个部分
输入表与输入地址表
什么是输入表
可执行文件使用来自其他DLL的代码与数据的行为称为输入,当PE文件载入内存时,Windows加载器的工作之一就是找到这些输入的函数与数据,并让文件可以使用这些代码与数据的正确地址,而这个过程正是通过输入表(Import Table)完成的,输入表中保存了输入的函数名和这些函数所在的DLL名称
什么是输入地址表
当我们调用DLL中的函数时,调用者程序无法得知这些函数的实际地址,因为这些函数的实际地址只有当PE文件载入内存中时才得以确定,所以我们采取了一种间接调用的形式,即在内存中仅保留函数名(或序号),通过此方法告知Windows加载器,我们需要这样的一个函数,当PE文件载入内存后,Windows加载器便将相关DLL载入内存,同时将函数名替换成函数实际所处的地址,而记录这些中间函数信息并最终被替换成实际地址的结构即为输入地址表IAT,记住这个转换过程,之后我们会做详细讲解
输入表的结构
在了解什么是输入表、什么是输入地址表之后,我们了解下它们载入内存前的大致结构

我们发现图中一共包含了4张表,分别是输入表IMAGE_IMPORT_DESCRIPTOR、输入名称表(INT)、输入地址表(IAT)、函数表IMAGE_IMPORT_BY_NAME,现在我们分别给出这四个表在winnt.h的具体定义
输入表 1
2
3
4
5
6
7
8
9
10typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
} DUMMYUNIONNAME; // INT表的RVA
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // DLL名字的RVA
DWORD FirstThunk; // IAT表的RVA
} IMAGE_IMPORT_DESCRIPTOR
- OriginalFirstThunk:指向输入名称表INT的RVA,INT是一个IMAGE_THUNK_DATA结构的数组,数组以一个全0内容来标识数组的结束
- TimeDateStamp:32位时间标记
- ForwarderChain:当程序使用一个DLL中的API,而该API又使用其他DLL中的API时使用,通常为0
- Name:DLL名字的RVA
- FirstThunk:指向输入地址表IAT的RVA,与INT类似,IAT也是一个IMAGE_THUNK_DATA结构的数组
IMAGE_THUNK_DATA的结构 1
2
3
4
5
6
7
8typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
- 当该数的最高位为1时,表示该函数以序号方式输入,此时低31位就是该函数的序号
- 当该数的最高位为0时,表示该函数以名称方式输入,此时整个32位就代表一个指向IMAGE_IMPORT_BY_NAME的RVA
IMAGE_IMPORT_BY_NAME的结构 1
2
3
4typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME
- Hint:本函数在其所驻留的DLL中输出表的序号,但该值不是必须的,一些链接器直接将其置为0
- Name:输入函数函数名的变长数组,以0结尾
输入表如何工作
原理
我们之前提到IAT表承担了将函数名转换成实际地址的功能,其实这就是输入表的主要功能,即载入时替换IAT表中的内容,一旦替换工作完成,那么整个输入表的替他部分就不关键了,程序依靠IAT表中的内容就可以正常运行
我们现在来看PE文件载入内存后输入表的结构,我们可以发现IAT此时不再指向IMAGE_IMPORT_BY_NAME表,其中的内容被填入了函数的实际地址

具体的工作流程是
- ①PE装载器先通过INT表找出每个IMAGE_IMPORT_BY_NAME结构指向的函数地址
- ②装载器用函数真正的入口地址来替代IAT中的值
也就是说在实际的程序中,当我们调用一个DLL中的函数,其采用了一种间接调用的形式call dword ptr [xxxxxxx],这个xxxxx就是IAT表的地址,当载入内存前,[xxxxxx]指向了IMAGE_IMPORT_BY_NAME中的对应的函数,而载入内存后[xxxxxx]就指向了函数的实际地址,这就是转换工作在程序中的体现
实例
为了解释这一过程,我们通过一个例子来理解其中的转换过程,例如此处我们的程序使用了一个TestDLL.dll中的Plus函数,首先我们在反汇编程序中观察反汇编代码,可以发现我们我们并不是call了一个绝对地址,而是call了ds:[0x402000]内存里的内容,我们在内存窗口里观察到0x402000处的4字节信息为0x10001050

之后我们跟进call中,可以发现来到了Plus函数内部,而地址正是0x10001050,也就是说调用Plus函数是以一种间接调用的形式实现的,且ds:[0x402000]中存了Plus函数载入内存后的实际地址0x10001050

此时程序已经载入内存,而我们需要查看其载入内存前ds:[0x402000]的内容,因为该程序的ImageBase为0x400000,那么RVA
=
0x2000,将其转换为FOA即是0x1200,现在我们打开16进制编辑器,找到此处观察发现里面存了一个RVA偏移值0x2778(指向IMAGE_IMPORT_BY_NAME中的对应的函数)

我们将0x2778转换为其对应的FOA,即0x1978,我们来到该处可以看到这里记录了函数的名字

现在我们已经分析完了输入表的整个工作流程,通过PE tools我们可以查看完整的输入表信息

需要注意的是,我们的程序使用了多少个DLL,就会有多少个输入表,且输入表也是以一个全0的结构作为判空标识的,最后,我们用一个简单的图来回顾整个过程
