一、 输入表
1、输入表地址定位
PE文件头可选映像头中数据目录表的第二成员指向输入表,输入表以一个 IAMGE_IMPORT_DESCRITPTOR 数组开始,每个被PE文件隐式地链接进来的DLL都有一个IID,在这个数组中没有字段指出该结构数组的项数,但他最后一个单元是NULL。
数据目录表的第二成员 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] 就对应输入表。回忆上节文章内容可知,数据目录表 IMAGE_DATA_DIRECTORY 中结构成员 VirtualAddress 就是输入表的RVA了,而区块表 IMAGE_SECTION_HEADER 中结构成员 VirtualAddress 对应区块的RVA。通过输入表的RVA与区块的RVA比较,我们就能知道输入表在哪个区块里面,一般输入表都在".idata"区块里面。
2、输入表结构
IID的结构如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; //指向输入名称表(INT)的RVA
};
DWORD TimeDateStamp; //一个32位的时间标志
DWORD ForwarderChain; //这是一个被转向API的索引,一般为0
DWORD Name; //DLL名字,是个以00结尾的ASCII字符的RVA地址
DWORD FirstThunk; //指向输入地址表(IAT)的RVA
} IMAGE_IMPORT_DESCRIPTOR;
通过上图 OriginalFisrtThunk 和 FirstThunk 非常相似,两个数组都有一个IMAGE_THUNK_DATA结构类型的元素,他是一个指针大小的联合,每一个IAMGE_THUNK_DATA元素对应于一个从可执行文件输入的函数,两个数组的结束通过一个值为零的IAMGE_THUNK_DATA元素表示的,IMAGE_THUNK_DATA结构实际上是一个双字,该结构不同时刻有不同的含义,定义如下:
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
(1)OriginalFirstThunk:它指向输入名称表(简称INT),INT是一个 IMAGE_THUNK_DATA 结构的数组,数组中的每个IMAGE_THUNK_DATA 结构的成员 AddressOfData都指向IMAGE_IMPORT_BY_NAME结构。
(2)TimeDateStamp:一个32位时间标志,该字段可以忽略。
(3)ForwarderChain:当程序引用一个DLL中的API,而这个API又引用别的DLL的API时使用,这种情况很少出现。
(4)Name:它表示DLL 名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称,如:KERNEL32.DLL)。
(5)FirstThunk:它指向输入地址表(简称IAT),IAT是一个 IMAGE_THUNK_DATA 结构的数组。
IAMGE_THUNK_DATA 值的最高位为1时,表示函数以序列号方式输入,这时低31位(或者一个64位可执行文件的低63位)被看做是一个函数序号,当双字的最高位为0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个RVA,指向一个IAMGE_IMPORT_BY_NAME结构,该结构定义如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //本函数在其所驻留DLL的输出表中的序号
BYTE Name[1]; //输入函数的函数名,函数名是一个ASCII码字符串,以NULL结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
为什么由两个并行的指针数组指向IAMGE_IMPORT_BY_NAME结构呢?
第一个数组(由OriginalFirstThunk所指向)是但单独的一项,而且不可改变,成为INT,第二个数组(由FirstThunk所指向)是由PE装载器重写的,PE装载器首先搜索OriginalFirstThunk,如果找到了,加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真实入口地址来代替由FirstThunk指向的 IAMGE_IMPORT_BY_NAME 数组所指向数组里的元素值,JMP dword ptr[xxxxxxxx]中的[xxxxxxxx]指向FirstThunk
数组中的一个入口,所以当PE文件装载内存后准备执行时,所有函数入口地址被排列在一起,此时输入表中其他就不重要的,依靠IAT提供地址就可以正常运行。
有些情况,一些函数仅由序号引出,也就是说不能用函数名来调用它,只能通过位置调用它,此时 IMAGE_THUNK_DATA 的值低位字指示函数序数,而高二进位(MSB)设为1,Microsoft提供了一个方便的常量测量DWORD值的MSB位,就是 IMAGE_ORDINAL_FLAG32,其值是80000000h。第二是程序的 OriginalFirstThunk 的值为0,初始化时,系统根据FirstThunk 的值指向函数名的地址串,由地址串找到函数名,再根据函数名入口地址,然后用入口地址取代 FirstThunk
指向的地址串的原值。
二、输出表
输出表(Export Table)包含函数名称,输出序数等,序数是指定DLL中某个函数的16位数字,在所指向的DLL里是独一无二的。数据目录表中的第一个成员 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT] 指向输出表,对应区块一般为".edata"。输出表 IAMGE_EXPORT_DIRECTORY 结构如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //未使用,总为0
DWORD TimeDateStamp; //创建输出表创建时间(GMT时间)
WORD MajorVersion; //主版本号,一般为0
WORD MinorVersion; //次版本号,一般为0
DWORD Name; //模块的真实名称
DWORD Base; //基数,加上序数就是函数数组的索引值
DWORD NumberOfFunctions; //AddressOfFunctions阵列中的元素个数
DWORD NumberOfNames; //AddressOfNameS阵列中的元素个数
DWORD AddressOfFunctions; //指向函数地址数组
DWORD AddressOfNames; //函数名字的指针地址
DWORD AddressOfNameOrdinals; //指向输出序号数组
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Name:指向一个ASCII字符串的RVA,既模块的名字。
Base:序号的基数,按序号导出函数的序号值从Base开始递增。当通过序数来查询一个输出函数时,这个值从序数里被减去,结果被用作进入输出地址表(EAT)的索引。
NumberOfFunctions:输出地址表(EAT)中的条目数量,即所有导出函数的数量。
NumberOfNames:输出名称表(ENT)中的条目数量,即按名称导出函数的数量,这个值总是小于或者等于NumberOfFunctions的值。
AddressOfFunctions:EAT的RVA,指向一个DWORD数组,数组中的每一项是一个导出函数的RVA,顺序与导出序号相同。
AddressOfNames:ENT的RVA,指向一个DWORD数组,数组中的每一项仍然是一个RVA,指向一个表示函数名字。
AddressOfNameOrdinals:输出序数表的RVA,指向一个WORD数组。数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号。
下图是一个经典的输出表结构:
三、重定位表
重定位就是你本来这个程序理论上要占据这个地址,但是由于某种原因,这个地址现在不能让你霸占,你必须转移到别的地址,这就需要基址重定位。如果可执行文件不在首选的地址装入,那么文件中每一个定位都需要被修正。对加载器来说,它不需要知道关于地址如何使用的任何细节,它只需要知道有一系列的数据需要以某种一致的方式来修正就可以了。
对于EXE文件来说,每个文件总是使用独立的虚拟地址空间,所以EXE总能够按照这个地址装入,不需要重定位信息。而对于DLL来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证装入地址没有被其它的DLL使用,所以DLL文件中必须包含重定位信息。
下面以实例 DllDemo.DLL为例讲述其定位过程。
......
:0040100E 6800204000 push 00402000
......
在这个例子中汇编语句将一个指针压栈,402000h是某一字符串的指针。指令是来自一个基置为 00400000h 的DLL文件,因此这个字符串的RVA是2000h。如果DLL确实在 00400000h 处装入,那么指令能够按照现在的样子正确执行。假设当DLL执行时,Windows加载器决定将其映射到 870000h 处,此时就需要进行基址重定位,计算方式如下:
402000h + (870000h - 400000h)= 872000h
基址重定位表(Base Relocation Table)位于一个叫".reloc" 的区块内,但是找到它们正确方式是通过数据目录表的 IMAGE_DIRECTORY_ENTRY_BASERELOC 条目。基址重定位数据组织方法采用类似按页分割的方法,其许多重定位块串接在成的,每个块存放4KB(1000h)大小的重定位信息,每个重定位数据块的大小必须以DWORD(4字节)对齐,他们以IMAGE_BASE_RELOCATION 结构开始,格式如下:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; //重定位数据开始RVA地址
DWORD SizeOfBlock; //重定位块的长度
// WORD TypeOffset[1]; //重定项位数组
} IMAGE_BASE_RELOCATION;
VirtualAddress:这是一组重定位数据的开始RVA地址,各重定位项的地址加上这个值才是重定位项完整的RVA地址。
SizeOfBlock:是当前重定位结构的大小,因为VirtualAddress 和 SizeOfBlock 大小都是固定的4个字节,因此这个项减去8,则是TypeOffset 大小。
TypeOffset:是一个数组。数组每项大小为两个字节,共16位,它又分为高4位与低12位,高四位代表重定位类型,低12位是重定位的地址,与VirtualAddress 相加即是指向PE映像中需要修改地址数据的指针。
对于X86 可执行文件,所有的基址重定位类型都是IMAGE_REL_BASED_HIGHLOW,在一组重定位结束的地方会出现一个类型是IAMGE_REL_BASED_ABSOLUTE的重定位,这些重定位什么都不做,在哪里只是填充,以便下一个IAMGE_BASE_RELOCATION 是以4个字节分界线来对齐,所有重定位最终以一个 VirtualAddress 字段为0的 IAMGE_BASE_RELOCATION 结构对齐。
重定位表的结构如下图,由数个 IMAGE_BASE_RELOCATION 结构组成,每个结构VirtualAddress,SizeOfSlock 和 TypeOffset 三部分组成。