可执行文件是指可以由操作系统直接加载执行的文件,而PE文件是Windows下的可执行文件。我们既然要学习PE文件,了解文件格式就是必须的。我们一般称具有PE结构的文件为PE文件,常见的有EXE、DLL。一个完整的PE文件主要有四部分组成:DOS头,NT头,节表以及节数据。
PE文件结构说明
DOS头
DOS头 是用来兼容 MS-DOS 操作系统的,目的是当这个文件在 MS-DOS 上运行时提示一段文字:This program cannot be run in DOS mode. 还有一个目的,就是指明 NT 头在文件中的位置。
NT头
NT头 包含 windows PE 文件的主要信息,其中包括一个 ‘PE’ 字样的签名,PE文件头(IMAGE_FILE_HEADER)和 PE可选头(IMAGE_OPTIONAL_HEADER32)。
节表
节表是 PE 文件后续节的描述,windows 根据节表的描述加载每个节。
节数据
每个节实际上是一个容器,可以包含 代码、数据 等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义。
DOS头
首先看到文件前两个字节4D 5A
也就是”MZ”,“MZ”是MS-DOS开发者之一的马克·茨柏克沃斯基(Mark Zbikowski)的姓名首字母缩写。PE文件的第一个字节位于一个传统的MS-DOS头部,叫作IMAGE_DOS_HEADERDOS,主要是为了向后兼容以前的DOS系统,DOS部分可以分为DOS MZ文件头(IMAGE_DOS_HEADER)和DOS块(DOS Stub)。DOS头的总长为0x40h,DOS块总长不定。
1 | typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header |
在DOS头我们只需要重点关注e_magic
和e_lfanew
即可。前者就是MZ
标识,后者指向PE结构开始的地方。
NT头
NT头由PE文件头标志,标准PE头,扩展PE头三部分组成。PE文件头标志是50 40 00 00
,也就是PE,我们从结构体的角度看一下PE文件头的详细信息。
PE文件头标识
1 | typedef struct _IMAGE_NT_HEADERS { |
这是用来判断是否是一个有效的PE文件的标识
我们使用十六进制工具载入一个EXE文件查看
在第四排四个字节指向的地址为PE头开始的地方,50 45
也就是PE,在PE标识后存在机器码用来标识程序为x64(0x8664)
还是x32(0x014C)
,例如这里用来展示的文件就是一个64位可执行程序。
标准PE头
共有20个字节
1 | typedef struct _IMAGE_FILE_HEADER { |
扩展PE头
包含以下重要信息
- 所有含代码的节的总大小
- 所有含已初始化数据的节的总大小
- 所有含未初始化数据的节的大小
- 程序执行入口RVA
- 代码的节的起始RVA
- 数据的节的起始RVA
- 程序的建议装载地址
- 内存中的节的对齐粒度
- 文件中的节的对齐粒度
- 内存中整个PE映像尺寸
- 所有头+节表的大小
- 导出表
- 导入表
- 资源
- 重定位表
- 调试信息
- 版权信息
- 导入函数地址表
1 | typedef struct _IMAGE_OPTIONAL_HEADER { |
如果想要编写一个PE解析器,那么就需要关注一下FileAlignment
以及 SizeOfHeaders
这两个成员。SizeOfHeaders
表示所有的头加上节表文件对齐之后的值,对齐的大小参考的就是 FileAlignment
成员,如果所有的头加上节表的大小为320,FileAlignment
为 200,那么 SizeOfHeaders
大小就为 400。下图中0x20000就是在内存中对齐的大小,0x400是程序在文件中的对其大小。
最后一个成员数据目录表很重要。PE扩展头的最后一个成员是IMAGE_DATA_DIRECTORY
结构体,定义如下
1 | typedef struct _IMAGE_DATA_DIRECTORY { |
存储了导出表和导入表
导出表
结构如下
1 | typedef struct _IMAGE_EXPORT_DIRECTORY { |
导出表的作用就是记载着动态链接库的一些导出信息。通过导出表,DLL文件可以向系统提供导出函数的名称、序号和入口地址等信息,以便Windows加载器通过这些信息来完成动态连接的整个过程。exe文件不存在导出表,一般存在于Dll文件中,导出 函数给别人用。不清楚Dll文件作用的可自行百度。
导入表
结构如下
1 | typedef struct _IMAGE_IMPORT_DESCRIPTOR { |
.exe 文件存在导入表,就是导入函数,然后自己使用。导入表在PE文件加载时,会根据这个表里的内容加载依赖的DLL ,并填充所需函数的地址。
OriginalFirstThunk
指向INT,FirstThunk
指向IAT,当文件在磁盘中时,实际上 INT 和 IAT 的内容是一样的。
而当文件加载到内存中时
INT表的结构不变,而IAT表则是直接存储了函数的地址
节表
节表的结构如下,整体为40个字节。存储了PE代码和数据的结构数据,指示装载系统代码段在哪里,数据段在哪里等。
在标准PE头中定义好了文件节的数目。
1 | typedef struct _IMAGE_SECTION_HEADER { |
节数据
常见的节数据:
.text:代码段,是在编译或汇编结束时产生的一种块,它的内容全部是指令代码。也有的编译器将该段命名为.code
.data:初始化的数据块,是初始化的数据块,包含那些编译时被初始化的变量、字符串
.idata:输入表,包含其他外来dll的函数和数据信息,也就是输入表,也有人称之为导入表。
.rsrc:资源数据块,包含模块的全部资源数据,如图标、菜单、位图等。
.reloc:重定位表,用于保存基址的重定位表。即当装在程序不能按照连接器所指定的地址装载文件是,需要对指令或已经初始化的变量进行调整,该块中也包含了调整过程中所需要的一些数据,如果装载能够正常装在则忽略此段中的数据。
.edata:导出表,是pe文件的输出表,以供其他模块使用,并不是每个pe文件都有此数据段,因为有的文件并不需要输出一些函数,该数据段常见于动态连接库文件中。
.radata:存放调试目录、说明字符串,该数据块并不常见主要是用于存放一些调试信息。