PE文件是由许许多多的结构体组成的,程序在运行时就会通过这些结构快速定位到PE文件的各种资源,其结构大致如图所示,从上到下依次是Dos头、Nt头、节表、节区和调试信息(可选)。其中Dos头、Nt头和节表在本文中统称为PE文件头(因为SizeOfHeaders就是这三个头的总大小)、节区则称为节,所以也可以说PE文件是由PE文件头和节组成。
PE文件头保存着整个PE文件的索引信息,可以帮助PE装载器定位资源,而节则保存着整个PE文件的所有资源。正因为如此,所以存在着这样的说法:头是节的描述,节是头的具体化。
一:IMGAGE_DOS_HEADER
cs
typedef struct _IMAE_DOS_HEADER {
WORD e_magic; **重要成员 相对该结构的偏移0x00**
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; **重要成员 相对该结构的偏移0x3C**
} IMAGE_DOS-HEADER, *PIMAGE_DOS_HEADER;
当我们用16进制编辑器打开一个PE文件时,就会发现所有PE文件的前两个字节都是MZ
,用十六进制表示是4D 5A
,这两个字母就是Mark Zbikowski
的姓名缩写,他是最初的MS-DOS设计者之一。如果把PE文件的这两个字节修改成其他数据,运行该PE文件就会无法正常运行(跳出黑窗口打印Program too big to fit in memory
然后闪退,有兴趣的朋友可以尝试下)。这里可以证明当PE文件运行时,首先就会检测这两个字节,如果不是MZ
则会退出运行。
在该结构体中另一个重要成员就是最后一个成员e_lfanew
。该成员的大小是LONG
类型4个字节。之所以说它重要是因为它保存着IMAGE_NT_HEADERS32
这个结构体在PE文件中的偏移地址,PE文件运行时只有通过该成员才能定位到PE签名(也就是IMAGE_NT_HEADERS32
结构体的起始位置)。
二:IMAGE_DOS_STUB (了解)
在IMAGE_DOS_HEADER
结构体后面紧跟着就是IMAGE_DOS_STUB
程序,它是运行在MS-DOS
下的可执行程序,当可执行文件运行于MS-DOS
下时,这个程序会打印This program cannot be run in DOS mode
这条消息。用户可以自己更改该程序,MS-DOS程序当前是可有可无的,如果你想使文件大小尽可能的小可以省掉MS-DOS程序,同时把前面的参数都清0。
**三:**IMAGE_NT_HEADERS32
cpp
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; **重要成员 PE签名 相对该结构的偏移0x00**
IMAGE_FILE_HEADER FileHeader; **重要成员 结构体 相对该结构的偏移0x04**
IMAGE_OPTIONAL_HEADER32 OptionalHeader; **重要成员 结构体 相对该结构的偏移0x18**
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
这个结构体是整个PE文件的核心 ,它是由一个Signature
、一个IMAGE_FILE_HEADER结构体
、一个IMAGE_OPTIONAL_HEADER32结构体
组成的。所以从整体看来这个结构比较简单,但实际上其内部结构较为复杂,我将会在下方对两个结构体进行详细的介绍。
Signature
也称作PE签名,这个成员和DOS头的MZ标记一样都是一个PE文件的标准特征,只不过这个成员是DWORD
类型大小为4字节,如果把这个PE签名修改后,程序也是不会正常运行的(跳出黑窗口打印This program cannot be run in DOS mode
然后闪退,可能是因为修改PE签名后无法识别后续内容的关系吧)。
如果把MZ标志和PE签名同时改变的话,其效果和只修改MZ是一样的,可见程序在载入时是先检测MZ标志然后才检测PE签名的。
**四:**IMAGE_FILE_HEADER
cs
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;
Machine:
所表示的是计算机的体系结构类型,也就是说这个成员可以指定该PE文件能够在32位还是在64位CPU上执行。如果强行更改该数值程序就会报错。该成员可以是以下的数值:
NumberOfSections:
它的含义就是当前PE文件的节区数量,虽然它是大小是两个字节,但是在windows加载程序时会将节区的最大数量限制为96个。
TimeDateStamp:
它的含义是时间戳,用于表示该PE文件创建的时间,时间是从国际协调时间也就是1970年1月1日00:00
起开始计数的,计数单位是秒。例如0x5CFBB225的计算方法如下:
cs
通过计算,从1970年到2024年一共有*个闰年(通过闰年计算器获得),到1月有*天。
秒:0x5CFBB225 % 60 = 33
分:0x5CFBB225 / 60 % 60 = 3
时:0x5CFBB225 / 3600 % 24 = 13
日:0x5CFBB225 / 3600 / 24 - (365 * 49 + 12) - 151 + 1 = 1
月:(0x5CFBB225 / 3600 / 24 - (365 * 49 + 12)) / 30 + 1 = 1
年:0x5CFBB225 / 3600 / 24 / 365 + 1970 = 2024
结果为:2024年1月1日 13:03:33
SizeOfOptionalHeader
它存储该PE文件的可选PE头的大小,在32位PE文件中可选头大小为0xE0,64位可选头大小为0xF0。正因为如此,所以就必须通过该成员来确定可选PE头的大小。
Characteristics
它描述了PE文件的一些属性信息,比如是否可执行,是否是一个动态连接库等。该值可以是一个也可以是多个值的和,具体定义如下:
五:IMAGE_OPTIONAL_HEADER32
cs
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
这个无符号整数指出了镜像文件的状态,此成员可以是以下的值:
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
的整数倍。
SizeOfHeaders
该成员指定了PE文件头的大小,并且向上舍入为FileAlignment的倍数,值的计算方式为:
cpp
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的原因,它只能为10h。
**六:**IMAGE_SECTION_HEADER
cpp
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; **节区名 偏移0x00
union {
DWORD PhysicalAddress;
DWORD VirtualSize; **节区的虚拟大小 偏移0x08 重要
} Misc;
DWORD VirtualAddress; **节区的虚拟地址 偏移0x0C 重要
DWORD SizeOfRawData; **节区在硬盘上的大小 偏移0x10 重要
DWORD PointerToRawData; **节区在硬盘上的地址 偏移0x14 重要
DWORD PointerToRelocations; **指向重定位项开头的地址 偏移0x18
DWORD PointerToLinenumbers; **指向行号项开头的地址 偏移0x1C
WORD NumberOfRelocations; **节区的重定位项数 偏移0x20
WORD NumberOfLinenumbers; **节区的行号数 偏移0x22
DWORD Characteristics; **节区的属性 偏移0x24 重要
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name
这是一个8字节的ASCII字符串,长度不足8字节时用0x00填充,该名称并不遵守必须以"\0"结尾的规律,如果不是以"\0"结尾,系统会截取8个字节的长度进行处理。可执行文件不支持长度超过8字节的节名。对于支持超过字节长度的文件来说,此成员会包含斜杠(/),并在后面跟随一个用ASCII表示的十进制数字,该数字是字符串表的偏移量。
Misc.VirtualSize
这个成员在一个共用体中,这个共用体中还有另外一个成员,由于用处不大我们就不讲解了,主要讲解VirtualSize的含义。这个成员指定了该节区装入内存后的总大小,以字节为单位,如果此值大于SizeOfRawData的值,那么大出的部分将用0x00填充。这个成员只对可执行文件有效,如果是obj文件此成员的值为0。
VirtualAddress
指定了该节区装入内存虚拟空间后的地址,这个地址是一个相对虚拟地址(RVA),它的值一般是SectionAlignment的整数倍。它加上ImageBase后才是真正的虚拟地址。
SizeOfRawData
指定了该节区在硬盘上初始化数据的大小,以字节为单位。它的值必须是FileAlignment的整数倍,如果小于Misc.VirtualSize,那么该部分的其余部分将用0x00填充。如果该部分仅包含未初始化的数据,那么这个值将会为零。
PointerToRawData
指出零该节区在硬盘文件中的地址,这个数值是从文件头开始算起的偏移量,也就是说这个地址是一个文件偏移地址(FOA)。它的值必须是FileAlignment的整数倍。如果这个部分仅包含未初始化的数据,则将此成员设置为零。
Characteristics
该成员指出了该节区的属性特征。其中的不同数据位代表了不同的属性,这些数据位组合起来就是这个节的属性特征
总表: