PE文件格式详解(五)
0x00 前言
前一篇了解了区块虚拟地址和文件地址转换的相关知识,这一篇该把我们所学拿出来用用了。这篇我们将了解更为重要的一个知识点——输入表和输出表的知识。
0x01 输入表
首先我们有疑问。这个输入表是啥?为啥有输入表?其实输入表就是记录PE输入函数相关信息的一张表。那为什么要有这张表?答:原来PE文件运行过程中并不是独立运行的,它必须要借助window系统的需对函数才能完成其功能。常见的如USER32,KERNEL32等DLL。
输入表所起的作用就是帮助载入的PE找到所需调用的函数。
在PE文件中,有个专门的数组,他们分别对每个被输入的DLL程序。每个这样的结构都给出了被输入DLL的名称并且指向一组函数指针,这组函数指针就是输入地址表(Import Address Table 简称ITA)。
在之前讲过的PE文件头的IMAGE_OPTIONAL_HAEDER结构中的数据目录表即DataDirectory[16]中第二个成员Imaport Table指向输入表。输入表是一个由IMAGE_IMPORT_DESCRIPORT(简称IID)的结构组成的数组。IID的结构如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORD OriginalFirstThunk;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
下面介绍几个重要字段:
OriginalFirstThunk:这个字段包含指向输入名称表(简称INT)的RVA,INT是一个IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构指向IMAGE_IMPORT_BY_NAME结构,数组最后以一个内容为0的IMAGE_THUNK_DATA结构结束。
直接看这个描肯定感觉很绕,下面看看这几个结构的关系图,希望能够帮助理解:
其实就是1指向2再指向3
Name:输入的DLL的名字指针,它是一个以00结尾的ASCII字符的RVA地址,该字符串包含输入的DLL名。例如:KERNEL32.DLL,或者USER32.DLL。
FirstThunk:包含指向输入地址表(IAT)的RVA。IAT也是指向IAMGE_THUNK_DATA结构。
这里的FristThunk和OringinalFristThunk极为相似,作用也很类似,但是作用的先后有不同,后面将会做详细讲解。
下面来重点看看这个起着巨大作用的IMAGE_THUNK_DATA结构。
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
ForwarderString 指向一个转向者字符串的RVA;
Function 被输入的函数的内存地址;
Ordinal 被输入的API的序数值
AddressOfData 指向IMAGE_IMPORT_BY_NAME
标识位黄色的这几个字段都很重要,是的这个结构的字段全都很重要!
下面来看看AddressOfData字段指向的IMAGE_IMPORT_BY_NAME结构
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME;
这个结构很简单,只有两个字段:
Hint字段:指示本函数在其所驻留的输出表的中序号该域被PE装载器用来在DLL的输出表里快速查询。该值不是必须的,一些链接器将此值设为0;
NAME字段:这个字段比较重要。它含有输入函数的函数名,函数名是一个ASCII码字符串,并以NULL结尾。注意,这里虽然将NAME的大小定义为字节,其实他是可变的。
0x02 输入地址表
接下来要讲的才是本文里最为关键的部分。通过上面的了解大概我们都会疑惑为啥这两个数组都要指向IMAGR_IMPORT_BY_NAME结构?原因如下:
第一,第一个由OriginalFrist通过IMAGE_THUNK_DATA结构所指向的IMAGE_IMPORT_BY_NAME是单独的一项,而且IMAGE_THUNK_DATA的值不可以更改,这个IMAGE_THUNK_DATA组成的数组就是INT,其实它是为FristThunk做为提示用的。
第二,第二个由FristThunk所指向的IMAGE_THUNK_DATA的值是由PE装载器填写的,他们的值构成了IAT。PE装载器首先搜索OringinalFristThunk,通过它所指向的INT结构中的每个IMAGE_IMPORT_BY_NAME所指向的每个被载入函数的地址。然后通过加载器将值填充到FristThunk指向的IAT表中。
接下来对比一下加载前后的INT表和IAN表值变化:
加载前
加载后,显示IAT的值已经填充了函数地址
我们最后要用到的即使PE加载后的ITA表。
0x03 实例讲解如何找到输入表的FileOffset
前面我们已经详细讲解了怎么找到数据目录表,这里就不再缀述。我们直接找到数据目录表的第二项,它的位置在180h处如下图:
由此可知输入表的RVA值为3000h,大小为52h。
其实我们也可以可以直接在lordPE的区段表中找到FileOffset的值如下图:
值为A00h。
用hexwrokshop直接跳转到该位置(大小为52h)。如下图:
由于IMAGE_THUNK_DATA的五个字段都是双字,因此按照八个字节依次读出数组中第一个结构的每个字段:第一个OriginnalFristThunk为:0000 3028 第一个TimeDateStamp为00000000。第一个ForwardChain为:00000000。第一个Name值为:00003038,第一个FristThunk值为:00003030
也可以用LordPE查看IMAGE_THUNK_DATA的字段值及名字如下图:
可以知道调用了的是USE32.DLL。
数组中的其余值也可以依次读写出来