一:简单介绍和必须知道的知识点:
在介绍PE文件格式的细节前,仔细看一下下面图,该图展示了PE文件格式的大概布局,学习时建议配合使用PE工具——stud_PE。
1.1PE的基本概念
PE文件使用的是一个平面地址空间,所以代码和数据都合并在一起,组成了一个很大的结构。文件的内容被分割为不同的区块(Section,也叫区段,节等)区块中包含代码或者数据,各个区块按照页边界对齐。区块木有大小限制,是一个连续结构。每个块都有它自己的内存中的一套属性,例如此块是否含有代码,是否可读或者写等。
知道PE文件不是作为单一内存映射文件被载入内存是很重要的。PE装载器遍历PE文件并决定文件的那一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。磁盘文件一旦被载入内存,磁盘上的数据结构布局和内存中的数据结构布局就是一致的。这样,如果在磁盘的数据结构中找一些内容,就和被载入的内存文件找到的内容一样。但是数据之间的相对位置可能会改变,某项的偏移地址可能区别于原始的偏移位置。
1.1.1 基地址
当PE文件通过加载器载入内存后,内存中的版本称为模块(Module),映射文件的起始地址称为模块句柄(hModule),可以通过模块句柄访问内存中的其他数据结构。这个起始内存地址也称为基地址(ImageBase)。
基地址的值是由PE文件本身设定的。按照默认设置,用 VsuaC++建立的EXE文件的基地址是400000h(6位),DLL文件的基地址是10000000h(8位)。
1.1.2 虚拟地址
在Windows系统中,PE文件被系统加载器映射到内存中。每个程序都有自己的虚拟空间,这个虚拟空间的内存地址被称为虚拟地址(Virtual Address,VA )。
1.1.3 相对虚拟地址
为了避免出现绝对内存地址引入了相对虚拟地址(RVA)的概念。RVA只是内存中的一个简单的,相对于PE文件载入地址的偏移量,它是一个”相对“地址(或者叫 偏移量)
例如:一个文件从400000h处载入,而且它的代码区块开始于401000h处,代码区块的RVA计算方法如下:
目标地址(401000h)- 载入地址(400000h)= RVA (1000h)
相对的知道了:
虚拟地址(VA) = 基地址(ImageBase) + 相对虚拟地址(RVA)
1.1.4 文件偏移地址
当PE文件存储在磁盘中时,某个数据的位置相当于文件头的偏移量称为文件偏移地址(FileOffset)或者物理地址(RAW Offset)。文件偏移地址从PE文件的第一个字节开始计数,起始值为0.
二:MS-DOS头部
每个PE文件都是以一个DOS程序开始的,有了它,一旦程序在DOS下执行,DOS就能执行识别出这是一个有效的执行体,然后运行紧随MZ header的DOS stub(DOS块)。DOS stub实际上是一个有效的EXE ,在不支持PE文件格式的操作系统上它将显示一个错误提示,类似于字符串”This program cannot be run in MS-DOS mode“。程序员也会根据自己的意图实现完整的DOS代码。这个stub部分不重要,我们通常把DOS MZ头与DOS stub合称为DOS文件头。
IMAGE_DOS_HEADER的结构体定义如下:
其中有俩个字段比较重要,分别是e_magic 和 e_lfanew。e_magic字段(1字节大小)的值需要被设置为 5A4D。这个值有一个#define,名为IMAGE_DOS_SIGNATURE,在ASII表示法里它的值为”MZ“,是MS-DOS的创建者之一Mark Zbikowski名字的缩写。e_lfanew 字段是真正的PE文件头的相对偏移(RVA),指向真正的PE头的文件偏移位置,占用4字节,位于文件开始偏移3ch字节处。
CPU属于小端类,字符存储时低位在低位,及在前,高位在后。将次序恢复后,e_lfanew的值为0000000Bh,这个值就是真正的偏移量。
三:PE文件头
紧跟DOS stub的是PE文件头(PE Header)。”PE Header“是PE相关结构NT映像头的简称(IMAGE_NT_HEADERS),其中包含许多重要字段。当执行体在执行PE文件结构时,装载器将会从IMAGE_DOS_HEADER结构的e_lfanew字段里找到PE Heard的起始偏移量,用其加上基地址,得到PE文件头的指针。
PNTHeard = ImageBase + dosHeader->e_lfanew
3.1 Signature字段
3.2 IMAGE_FILE_HEADER 结构
其中最重要的是指出IMAGE_OPTIONAL_HEADER的大小。下面介绍IMAGE_FILE_HEADER 结构的各个字段,并进行说明。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; ** 机器号 相对该结构的偏移0x00**
WORD NumberOfSections; **重要成员 节区数量 相对该结构的偏移0x02**
DWORD TimeDateStamp; ** 时间戳 相对该结构的偏移0x04**
DWORD PointerToSymbolTable; ** 符号表偏移 相对该结构的偏移0x08**
DWORD NumberOfSymbols; ** 符号表数量 相对该结构的偏移0x0C**
WORD SizeOfOptionalHeader; **重要成员 可选头大小 相对该结构的偏移0x10**
WORD Characteristics; **重要成员 PE文件属性 相对该结构的偏移0x12**
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
用16进制工具查看结构情况:
1.Machine
所表示的是计算机的体系结构类型,也就是说这个成员可以指定该PE文件能够在32位还是在64位CPU上执行。如果强行更改该数值程序就会报错。该成员可以是以下的数值:
2.NumherOlSections:区块(Section)的数目,块表紧跟在IMAGE NT HEADERS 后面。
3.TimeDateStamp:表示文件的创建时间。这个值是自1970年1月1日以来用格林威治时间(GMT)计算的秒数,是一个比文件系统的日期/时间更精确的文件创建时间指示器。将这个值翻译为易读的字符串需要使用_cime 函数(它是时区敏感型的)。另一个对此字段计算有用的函数是 gmtime。
4.PointerToSymbolTable:COFF 符号表的文件偏移位置(参见 Microsoft 规范的 5.4节)。因为采用了较新的 debug格式,所以 COF 符号表在 PE 文件中较为少见。在 Visual Studio.NET 出现之前COFF 符号表可以通过设置链接器开关(/DEBUGTYPE:COFF)来创建。COFF 符号表几乎总能在目标文件中找到,若没有符号表存在,将此值设置为0。
5.NumberOrSymhols:如果有 COFF 符号表,它代表其中的符号数目。COFF 符号是一个大小固定的结构,如果想找到 COFF符号表的结束处,需要使用这个域。
6.Size0f0ptionalHeader:紧跟 IMAGE_FILE HEADER,表示数据的大小。在 PE 文件中,这个数据结构叫作 IMAGE OPTIONAL, HEADER,其大小依赖于当前文件是 32 位还是 64 位文件。对 32位 PE 文件,这个域通常是 00F0h;对 64位 PE32+ 文件,这个域是 00F0h。不管怎样,这些是要求
的最小值,较大的值也可能会出现。
7. Characterislics:文件属性,有选择地通过几个值的运算得到。这些标志的有效值是定义于winnt.h 内的 IMAGE FILE xxx值,具体如表 11.2所示。普通 EXE 文件的这个字段的值一般是 010fh.DLL文件的这个字段的值一般是 2102h。
3.3IMAGE_OPTIONAL_HEARDER结构
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; **魔术字 偏移0x00
BYTE MajorLinkerVersion; **链接器主版本 偏移0x02
BYTE MinorLinkerVersion; **链接器副版本 偏移0x03
DWORD SizeOfCode; **所有含代码的节的总大小 偏移0x04
DWORD SizeOfInitializedData; **所有含初始数据的节的总大小 偏移0x08
DWORD SizeOfUninitializedData; **所有含未初始数据的节的总大小 偏移0x0C
DWORD AddressOfEntryPoint; **程序执行入口地址 偏移0x10 重要
DWORD BaseOfCode; **代码节的起始地址 偏移0x14
DWORD BaseOfData; **数据节的起始地址 偏移0x18
DWORD ImageBase; **程序首选装载地址 偏移0x1C 重要
DWORD SectionAlignment; **内存中节区对齐大小 偏移0x20 重要
DWORD FileAlignment; **文件中节区对齐大小 偏移0x24 重要
WORD MajorOperatingSystemVersion; **操作系统的主版本号 偏移0x28
WORD MinorOperatingSystemVersion; **操作系统的副版本号 偏移0x2A
WORD MajorImageVersion; **镜像的主版本号 偏移0x2C
WORD MinorImageVersion; **镜像的副版本号 偏移0x2E
WORD MajorSubsystemVersion; **子系统的主版本号 偏移0x30
WORD MinorSubsystemVersion; **子系统的副版本号 偏移0x32
DWORD Win32VersionValue; **保留,必须为0 偏移0x34
DWORD SizeOfImage; **镜像大小 偏移0x38 重要
DWORD SizeOfHeaders; **PE头大小 偏移0x3C 重要
DWORD CheckSum; **校验和 偏移0x40
WORD Subsystem; **子系统类型 偏移0x44
WORD DllCharacteristics; **DLL文件特征 偏移0x46
DWORD SizeOfStackReserve; **栈的保留大小 偏移0x48
DWORD SizeOfStackCommit; **栈的提交大小 偏移0x4C
DWORD SizeOfHeapReserve; **堆的保留大小 偏移0x50
DWORD SizeOfHeapCommit; **堆的提交大小 偏移0x54
DWORD LoaderFlags; **保留,必须为0 偏移0x58
DWORD NumberOfRvaAndSizes; **数据目录的项数 偏移0x5C
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
① Magic:这是一个标记字,说明文件是 ROM 映像(0107h)还是普通可执行的映像(010Bh),一般是 010Bh。如果是 PE32+,则是 020Bh。
④ Size0fCode:有 IMAGE SCN_CNT_CODE 属性的区块的总大小(只人不舍 ),这个值是向上对齐某一个值的整数倍。例如,本例是 200h,即对齐的是一个磁盘扇区字节数(200h)的整数倍。在通常情况下,多数文件只有1个 Code 块,所以这个字段和.text 块的大小匹配。
⑤ SizeOfInitializedData:已初始化数据块的大小,即在编译时所构成的块的大小(不包括代码段 )。但这个数据不太准确。
⑥ SizeOfUninitializedData:未初始化数据块的大小,装载程序要在虚拟地址空间中为这些数据约定空间。这些块在磁盘文件中不占空间,就像“UninitializedData”这一术语所暗示的一样,这些块在程序开始运行时没有指定值。未初始化数据通常在.bss块中。
⑦ Address0fEntnyPoint:程序执行人口 RVA。对于 DLL,这个入口点在进程初始化和关闭时及线程创建和毁灭时被调用。在大多数可执行文件中,这个地址不直接指向 Main、WinMain 或 DlMain函数,而指向运行时的库代码并由它来调用上述函数。在 DLL中,这个域能被设置为 0,此时前面
提到的通知消息都无法收到。链接器的 /OENTRY 开关可以设置这个域为 0。⑧ BaseOfCode:代码段的起始 RVA。在内存中,代码段通常在 PE 文件头之后,数据块之前。在 Microsof 链接器生成的可执行文件中,RVA 的值通常是1000h。Borland的 Tlink32 用 ImageBase加第1个 Code Section 的 RVA,并将结果存入该字段。
⑧ BaseOfCode: 代码段的起始RVA。在内存中,代码段通常在 PE文件头之后,数据块之前。 在 Microsoft链接器生成的可执行文件中, RVA的值通常是1000h 。Borland 的 Tlink32 用 ImageBase 加第1个 Code Section的 RVA, 并将结果存入该字段。
⑨BaseOfData:数据段的起始 RVA。数据段通常在内存的末尾,即 PE 文件头和 Code Section之后。可是,这个域的值对于不同版本的 Microso 链接器是不一致的,在 64 位可执行文件中是不会出现的。
⑩ ImageBase:文件在内存中的首选载入地址。如果有可能(也就是说,如果目前没有其他文件占据这块地址,它就是正确对齐的并且是一个合法的地址),加载器会试图在这个地址载入 PE 文件。如果 PE 文件是在这个地址载人的,那么加载器将跳过应用基址重定位的步骤。
⑪SectionAlignment: 载入内存时的区块对齐大小。每个区块被载人的地址必定是本字段指定数值的整数倍。默认的对齐尺寸是目标 CPU的页尺寸。对运行在Windows 9x/Me下的用户模式可执行 文件,最小的对齐尺寸是每页1000h(4KB) 。 这个字段可以通过链接器的/ALIGN 开关来设置。在 IA-64 上,这个字段是按8KB排列的。
⑫ FileAlignment: 磁盘上 PE 文件内的区块对齐大小,组成块的原始数据必须保证从本字段的 倍数地址开始。对于x86 可执行文件,这个值通常是200h 或1000h, 这是为了保证块总是从磁盘的 扇区开始,这个字段的功能等价于NE 格式文件中的段/资源对齐因子。使用不同版本的 Microsoft链 接器,默认值会改变。这个值必须是2的幂,其最小值为200h。而且,如果 SectionAlignment 小于 CPU的页尺寸,这个域就必须与SectionAlignment 匹配。链接器开关/OPT:WIN98设置x86 可执行文 件的对齐值为1000h,/OPT:NOWIN98 设置对齐值为200h。
⑳ SizeOflmage: 映像载入内存后的总尺寸,是指载入文件从 ImageBase 到最后一个块的大小。 最后一个块根据其大小向上取整。
⑳ SizeOfHeaders:MS-DOS 头部、PE 文件头、区块表的总尺寸。这些项目出现在 PE 文件中的 所有代码或数据区块之前,域值四舍五入至文件对齐值的倍数。
SizeOfHeaders = (e_lfanew/*DOS头部*/ + 4/*PE签名*/ +
sizeof(IMAGE_FILE_HEADER) +
SizeOfOptionalHeader + /*NT头*/
sizeof(IMAGE_SECTION_HEADER) * NumberOfSections) / /*节表*/
FileAlignment *
FileAlignment +
FileAlignment; /*向上舍入 一般该结果不可能是FileAlignment的整数倍,所以直接加上FileAlignment还是没问题的 */
!!NumberOfRvaAndSizes: 数据目录的项数。这个字段的值从Windows NT 发布以来 一 直是16。
!! DataDirectory[16]:数据目录表,由数个相同的IMAGE_DATA_DIRECTORY 结构组成,指向
输出表、输入表、资源块等数据。IMACE_DATA_DIRECTORY的结构定义如下。
以上太繁琐,下面是简化版:
AddressOfEntryPoint
该成员保存着文件被执行时的入口地址,它是一个RVA。如果想要在一个可执行文件中附加了一段代码并且要让这段代码首先被执行,就可以通过更改入口地址到目标代码上,然后再跳转回原有的入口地址。
ImageBase
该成员指定了文件被执行时优先被装入的地址,如果这个地址已经被占用,那么程序装载器就会将它载入其他地址。当文件被载入其他地址后,就必须通过重定位表进行资源的重定位,这就会变慢文件的载入速度。而装载到ImageBase指定的地址就不会进行资源重定位。
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 成员中,DLL 文件对应的IMAGE_FILE_RELOCS_STRIPPED
位总是为0,而EXE文件的这个标志位总是为1。
SectionAlignment
该成员指定了文件被装入内存时,节区的对齐单位。节区被装入内存的虚拟地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。该成员的默认大小为系统的页面大小。
FileAlignment
该成员指定了文件在硬盘上时,节区的对齐单位。节区在硬盘上的地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。该值应为200h到10000h(含)之间的2的幂。默认为200h。如果SectionAlignment的值小于系统页面大小,则FileAlignment的值必须等于SectionAlignment的值。
SizeOfImage
该成员指定了文件载入内存后的总体大小,包含所有的头部信息。并且它的值必须是SectionAlignment
的整数倍。