PE 文件格式 详解 二
内容引用自:看雪《逆向工程原理》。如有错误,欢迎留言。
1、 区块表(节表)
区块表紧跟在PE头后面,所有区块的属性都被定义在区块表中。区块表中的数据仅仅是因为属性相同被放到一起,对程序的各种方法、数据的追溯还是要用到DataDirectory。
区块表是由一组IMAGE_SETION_HEADER结构组成,每个结构描述一个区块,各结构的排列顺序与其所描述的区块在文件中的排列顺序是一致的。
区块表最后以一个空的IMAGE_SETION_HEADER结构作为结尾。IMAGE_SETION_HEADER (长度为28h)定义如下:
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef strutct _IMAGE_SETION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //注意 此处是 8 个字节
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 区块的大小
}
DWORD VirtualAddress; // 节区的 RVA 地址
DWORD SizeOfRawData; // 在文件中对齐后的尺寸
DWORD PointerToRawData; // 在文件中的偏移量
DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移,无用
DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地),无用
WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目,无用
WORD NumberOfLinenumbers; // 行号表中行号的数目,无用
DWORD Characteristics; // 节属性如可读,可写,可执行等
} IMAGE_SETION_HEADER
需要注意的点:
Name:区块名 实际上没有任何意义,只要不重复可以任意命名,设置为特定的名字仅仅是正规编程方便查看。
VirtualSize:对应区块的实际大小,未进行对齐处理前的大小
VirtualAddress:对应区块装入内存中的RVA地址
SizeOfRawData:对应区块在磁盘中的大小,在可执行文件中,该值是已经被FileAligment处理过的长度。
PointerToRawData:对应区块在磁盘中的偏移,从文件头开始算起
Characteristics:按位指出对应区块的属性 (bit OR),常见值如下:
此处涉及到RVA to RAW,即 相对虚拟地址 到 文件物理偏移地址的转换。比如在DataDirectory中存放的为RVA地址,就需要转换为物理偏移。
其转换方法为:RVA - VisualAddress + PointerToRawData
2、 导入表
导入表是要提供程序执行时需要调用的导入函数的所属DLL、函数名、内存地址等。该表在区块中,由DataDirectory第二个数组项指向。
导入表是由一组IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每一个结构对应一个DLL文件,以一个内容全为 0 的IMAGE_IMPORT_DESCRIPTOR作为结束。
IMAGE_IMPORT_DESCRIPTOR定义:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; #一般定义该值,指向 INT (Import Name Table) RVA
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; #指向 DLL 名称 的地址 RVA
DWORD FirstThunk; #指向 IAT (Import Address Table) RVA
} IMAGE_IMPORT_DESCRIPTOR;
需要注意的点:
Name : 不会直接给出DLL的名字 此处是一个RVA 地址。 该地址指向了 DLL 名称。
OriginalFirstThunk 和 FirstThunk : 虽然一个指向 INT 一个指向 IAT ,但实际这两个表均是由一系列 IMAGE_THUNK_DATA结构组成的数组(静态存储时一般指向相同的内容)。数组最后以一个全为0 的结构作为结束。OriginalFirstThunk指向的INT不会变化,当PE文件加载进内存后,FirstThunk指向的IAT会存储导入函数真实的内存地址。
IMAGE_THUNK_DATA 定义如下:
typedef _IMAGE_THUNK_DATA {
union {
DWORD ForwarderString; #RVA 指向forwarder string
DWORD Function; #被导入函数的入口地址
DWORD Ordinal; #该函数的序数
DWORD AddressOfData; #指向 IMAGE_IMPORT_BY_NAME 结构体
};
} IMAGE_THUNK_DATA32
对于可执行文件,IMAGE_THUNK_DATA 中存储的要么是 Ordinal(最高位为1,其余31位为序号) 要么是AddressOfData。在DLL中对每个函数都进行了编号(导出表中的导出序号),访问函数即可以通过名称访问,也可以通过编号访问。
当IMAGE_THUNK_DATA存放的为 AddressOfData 时,该地址指向一个 IMAGE_IMPORT_BY_NAME 结构体
IMAGE_IMPORT_BY_NAME 定义如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; #Ordinal 函数的序号
BYTE Name[1]; #此处为函数名字,实际为变长数组,以00结尾,即字符串结束符
} IMAGE_IMPORT_BY_NAME
常见寻找导入函数的方法(静态存储):
根据 DataDirectory 中RVA 找到 导入表的文件偏移地址, 根据 OriginalFirstThunk 找到 INT (Import Name Table), 根据 AddressOfData 找到 IMAGE_IMPORT_BY_NAME 即可找到函数名。
3、导出表
为了让其他程序调用本程序提供的导出函数,需要定义导出表。导出表定义如下:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; # 通常为0
DWORD TimeDateStamp; # 时间戳
WORD MajorVersion; # 通常为0
WORD MinorVersion; # 通常为0
DWORD Name; # 指向真实的DLL名字的RVA,PE装载器用的是这个名字
DWORD Base; # 序号基数,等于所有函数导出序号最小的值,该值也是导出地址表中第一个地址对应的导出函数的导出序号
DWORD NumberOfFunctions; # 导出函数的总数,不一定是真实导出函数的个数。等于导出序号的最小值依次递增一到最大值的个数。导出函数的序号可以自定义,所以可能是不连续的不规则的。
DWORD NumberOfNames; # 通过函数名导出的函数的总数。
DWORD AddressOfFunctions; # 指向导出函数地址表的RVA。 导出函数地址表(可以当成数组)存放的是所有导出函数所在地址的RVA。 导出函数地址表个数和NumberOfFunctions 相等。 该地址表对应函数的导出序号从 Base 开始,依次递增一。
DWORD AddressOfNames; # 指向导出函数名称地址表的RVA。 导出函数名称地址表存放的是指向按名称导出函数的函数名的RVA。 导出函数名称地址表的大小和 NumberOfNames 相等。
DWORD AddressOfNameOrdinals; # 指向导出序号表的RVA。 导出序号表存放的是以函数名称导出的函数的导出序号,每个成员都是WORD类型,其表的大小和 NumberOfNames 相等。
} IMAGE_EXPORT_DIRECTORY
查找函数的入口地址
依照导出序号(ordinal)查找:
1、根据 序号 减去 IMAGE_EXPORT_DIRECTORY中的 Base,得到索引(比如index),如果 index 大于NumberOfFunctions 则放弃 出错
2、找到 IMAGE_EXPORT_DIRECTORY中的 AddressOfFunctions,在导出地址表中从 0 开始找到 index 处的地址,即为函数的入口偏移地址
依照 导出函数名(比如名字为MyFunction) 查找:
1、以 NumberOfNames 为次数开始对 AddressOfNames 遍历,可以从 0 ,可以从 1,只要前后遵循一致即可。 找到指向对应 MyFunction 的函数的地址所在的相对函数名称地址开始的偏移(比如index)
2、根据 index 找到 AddressOfNameOrdinals 在该偏移处的值(比如为index_Ordinal),该值 + Base 其实就是 导出序号,不过找入口偏移地址不需要 + Base。
3、找到AddressOfFunctions ,在导出地址表中从0 开始找到 index_Ordinal 处的地址,即为函数的入口偏移地址。