引言

Windows 的 PE 文件结构是一种可执行文件格式,用于存储 Windows 应用程序、服务和驱动程序。

准备一个简单的程序

以32位为主,在后面会展示与64位的区别

1
2
3
4
5
6
7
8
9
10
11
12
//没有使用到的库仅用于展示PE结构的某些变化
#include <iostream>
#include <stdio.h>
#pragma comment(lib,"WS2_32.lib")
#include<windows.h>
using namespace std;

void main() {
int flag = 1;
flag++;
cout << flag << endl;
}

结构体系

PE 文件结构主要由以下几部分组成:

DOS 头

存储一些文件信息,如NT头的位置,主要用来兼容MS-DOS操作系统,目的是当这个文件在MS-DOS系统上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode.

NT头

存储PE文件的主要信息

节头

描述PE文件中各个节的名称,位置和属性。

部分不太重要的字段本文会省略

节是PE文件中存储代码和数据的部分,每个节都有一个名称和一些属性。不同的节担任着不同的存储任务。

节名 作用
.text 代码段,存放程序执行代码,只读和可执行
.data 数据段,存放程序中已初始化的全局变量和静态变量,可读和可写
.bss BSS段,存放程序中未初始化的全局变量和静态变量,可读和可写
.rdata 只读数据段,存放常量数据,如字符串常量、调试信息等,只读
.idata 导入数据段,存放导入函数的信息
.edata 导出数据段,存放导出函数的信息
.reloc 重定位数据段,存放重定位项的信息,用于在加载时修正内存地址,可读和可写
.rsrc 资源数据段,存放资源数据,如图标、菜单、对话框等,可读和可写

特殊节

有一些节是Visual Studio在Debug模式下使用的特殊的节,它不是PE文件结构中的标准部分。

.textbss

.textbss节为了支持Visual Studio在调试过程中动态编译和更新代码的功能(“编辑并继续”功能)。

当你在Debug模式下修改了代码,Visual Studio会将被修改的函数放到.textbss节里,然后修改对应的ILT表项(增量同步表),使它指向这个位置

.textbss节有以下几个特点

  • 它是未初始化的可执行代码节,也就是说,它具有可执行属性,在PE文件中不占用硬盘文件空间,在加载到内存时不填充数据。
  • 它是在.text节之后的一个额外的节,它的大小和位置是由Visual Studio动态分配和调整的。
  • 它只在Debug模式下有效,Release模式下默认禁用Incremental Linking,所以不会生成这个节。

.msvcjmc

.msvcjmc节是一个用于存储调试信息的节,它是由Visual Studio在Debug模式下生成的。

它包含了一些用于支持“编辑并继续”功能的符号,比如函数的入口点、返回地址等。

这个功能可以让你在调试过程中修改代码,并且不需要重新链接程序。

在Release模式下,这个节通常会被删除或合并到其他节中。

.00cfg

.00cfg节是一个用于存储控制流保护(Control Flow Guard)相关信息的节,它也是由Visual Studio生成的。

控制流保护是一种用于防止缓冲区溢出攻击的技术,它可以检查间接调用的目标是否有效。

.00cfg节中包含了一些指向检查函数的指针。

全面梳理

以010Editor的EXE模板为基准

struct IMAGE_DOS_HEADER DosHeader

名称 作用
MZSignature 标识符,用来表示这是一个可执行文件,它的值必须为0x5A4D,即MZ的ASCII码
AddressOfNewExeHeader 偏移量,表示PE头部在文件中的位置

struct IMAGE_DOS_STUB DosStub

名称 作用
Data[64] 在DOS系统(16位)运行时,用于提示This program cannot be run in DOS mode
RichHeader 富头部,包含了PE文件编译和链接时使用的链接库和链接器的信息,例如产品ID、版本号和计数器

struct IMAGE_NT_HEADERS NtHeader

名称 作用
Signature PE文件的有效签名,其值为”P E \0 \0”
FileHeader 文件头信息,存储了文件的创建时间,可运行的CPU平台,可选头大小等信息
OptionalHeader 可选头信息

struct IMAGE_FILE_HEADER FileHeader

名称 作用
Machine 运行平台,比如x86,x64,Arm
NumberOfSections PE文件中节的数量
TimeDateStamp PE文件的创建时间,以秒为单位的时间戳(from 01/01/1970 12:00 AM)
PointerToSymbolTable 表示PE文件中符号表的偏移量。
NumberOfSymbols PE文件中符号表中符号的数量
SizeOfOptionalHeader 表示OptionalHeader可选头的大小
Characteristics PE文件的一些特征信息

符号表是一种用于语言翻译器(例如编译器和解释器)中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。符号表可以帮助编译器检查语法错误,分配内存空间,生成目标代码等

注意:局部变量在栈中存储,所以局部变量并不会生成对应的符号

struct FILE_CHARACTERISTICS Characteristics

名称 作用
IMAGE_FILE_RELOCS_STRIPPED 如果这个位为1,表示重定位信息被删除,这样文件就只能被加载到指定的地址。这通常用于系统文件或驱动程序
IMAGE_FILE_EXECUTABLE_IMAGE 如果这个位为1,表示文件是可执行的
IMAGE_FILE_LINE_NUMS_STRIPPED 如果这个位为1,表示文件的行号信息被删除,这样可以减少文件的大小
IMAGE_FILE_LOCAL_SYMS_STRIPPED 如果这个位为1,表示本地符号表和字符串表被删除,这样也可以减小文件的大小
IMAGE_FILE_AGGRESIVE_WS_TRIM 如果这个位为1,表示操作系统可以回收工作集中未使用的内存页。
IMAGE_FILE_LARGE_ADDRESS_AWARE 如果这个位为1,表示应用程序可以处理大于2GB的地址空间
IMAGE_FILE_BYTES_REVERSED_LO 已弃用
IMAGE_FILE_32BIT_MACHINE 如果这个位为1,表示文件是32位的机器码
IMAGE_FILE_DEBUG_STRIPPED 如果这个位为1,表示调试信息被单独存放在.DBG文件中
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 如果这个位为1,表示如果文件在可移动介质上(如软盘或光盘),则在运行时将其复制到交换文件中
IMAGE_FILE_SYSTEM 如果这个位为1,表示文件是系统文件,如驱动程序或动态链接库
IMAGE_FILE_DLL 如果这个位为1,表示文件是动态链接库(DLL)
IMAGE_FILE_UP_SYSTEM_ONLY 如果这个位为1,表示文件只能在单处理器机器上运行
IMAGE_FILE_BYTES_REVERSED_HI 已弃用

struct IMAGE_OPTIONAL_HEADER32 OptionalHeader

名称 作用
Magic 标识PE文件的类型。常见的值有010Bh(普通可执行文件)和020Bh(64位可执行文件)。
MajorLinkerVersion 表示链接器的主版本号,表示生成PE文件的编译器或工具的版本。
MinorLinkerVersion 表示链接器的次版本号
SizeOfCode 表示所有含代码的节的总大小,以字节为单位。
SizeOfInitializedData 表示所有含已初始化数据的节的总大小,以字节为单位。
SizeOfUninitializedData 表示所有含未初始化数据的节的总大小,以字节为单位。
AddressOfEntryPoint 程序执行入口的相对虚拟地址(RVA),表示程序开始执行时跳转到的内存地址。它需要加上基址才能得到真正的虚拟地址(VA)。
BaseOfCode 代码节的起始RVA,表示代码节在内存中的相对位置。
BaseOfData 数据节的起始RVA,表示数据节在内存中的相对位置。
ImageBase 程序的首选装载地址,表示程序希望被加载到内存中的哪个位置。如果该位置已经被占用或者开启了ASLR,系统会重新分配一个基址,并进行重定位。
SectionAlignment 内存中的节对齐大小,表示每个节在内存中占用的空间必须是该值的整数倍。
FileAlignment 文件中的节对齐大小,表示每个节在文件中占用的空间必须是该值的整数倍。
MajorOperatingSystemVersion 要求操作系统最低版本号的主版本号,表示程序需要运行在哪个版本以上的操作系统上。
MinorOperatingSystemVersion 要求操作系统最低版本号的次版本号
MajorImageVersion 程序自身的主版本号,表示程序开发者给出的版本信息。
MinorImageVersion 程序自身的次版本号。
MajorSubsystemVersion 要求最低子系统的主版本号,表示程序需要运行在哪个子系统(如控制台、图形界面等)上,并且该子系统至少要达到哪个版本。
MinorSubsystemVersion 要求最低子系统的次版本号
Win32VersionValue 保留字段,无意义
SizeOfImage 装入内存后的总大小,以字节为单位。它包括了所有头部、所有节、以及内存对齐产生的空洞。
SizeOfHeaders 所有头部和节表占用的大小,以字节为单位。它通常等于第一个节在文件中的偏移量。
CheckSum 校验和,用于检测PE文件是否被修改或损坏。
Subsystem 指定可执行文件期望的子系统。常见的值有1(本机)、2(Windows图形界面)、3(Windows控制台)、5(OS/2控制台)、7(POSIX控制台)等。
DllCharacteristics 这是一个结构体,用来表示DLL文件的一些特性,比如是否支持安全检查、是否可以动态基址、是否可以终止进程等。每个特性用一个位来表示,为1表示启用,为0表示禁用。
SizeOfStackReserve 初始化时保留的栈大小,以字节为单位。栈是用来存储函数调用、局部变量、参数等临时数据的内存区域。
SizeOfStackCommit 初始化时实际提交的栈大小,以字节为单位。提交的内存是已经分配并可以使用的内存,而保留的内存只是预留了一个地址空间,还没有实际分配。
SizeOfHeapReserve 初始化时保留的堆大小,以字节为单位。堆是用来存储动态分配的内存数据的内存区域。
SizeOfHeapCommit 初始化时实际提交的堆大小,以字节为单位。同上。
NumberOfRvaAndSizes 数据目录表的项数,表示有多少个数据目录项。这个字段自Windows NT发布以来一直是16。
DataDirArray 这是一个结构体数组,用来存储数据目录表的信息。数据目录表是用来描述PE文件中一些特殊数据的位置和大小,比如导入表、导出表、资源表、重定位表等。每个结构体包含两个字段:一个是数据的RVA,另一个是数据的大小。

struct DLL_CHARACTERISTICS DllCharacteristics

名称 作用
IMAGE_LIBRARY_PROCESS_INIT 表示DLL是否有一个初始化函数,当进程加载DLL时会被调用。如果为1,表示有这样的函数
IMAGE_LIBRARY_PROCESS_TERM 表示DLL是否有一个终止函数,当进程卸载DLL时会被调用。如果为1,表示有这样的函数
IMAGE_LIBRARY_THREAD_INIT 表示DLL是否有一个线程初始化函数,当进程创建新线程时会被调用。如果为1,表示有这样的函数
IMAGE_LIBRARY_THREAD_TERM 表示DLL是否有一个线程终止函数,当进程销毁线程时会被调用。如果为1,表示有这样的函数
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 这个字段表示DLL是否支持动态基址,也就是说是否可以被加载到不同的内存地址,并进行重定位。如果为1,表示支持动态基址
IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY 这个字段表示DLL是否强制进行完整性检查),也就是说是否要求系统验证DLL的数字签名和校验和。如果为1,表示强制进行完整性检查。强制进行完整性检查可以防止DLL被篡改或损坏。
IMAGE_DLLCHARACTERISTICS_NX_COMPAT 这个字段表示DLL是否兼容数据执行保护,也就是说是否要求系统禁止在非可执行内存区域执行代码。如果为1,表示兼容数据执行保护。兼容数据执行保护可以防止缓冲区溢出攻击。
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE 这个字段表示DLL是否支持终端服务,也就是说是否能够在多用户环境下正确地工作。如果为1,表示支持终端服务。终端服务是一种机制,可以让多个用户远程访问同一台服务器。

struct IMAGE_DATA_DIRECTORY_ARRAY DataDirArray

以导入表为例,其他表同理

名称 作用
struct IMAGE_DATA_DIRECTORY Import 存储导入表的数据信息
struct IMAGE_DATA_DIRECTORY Import
名称 作用
VirtualAddress 该节表的相对虚拟地址RVA
Size 该节表的大小

struct IMAGE_SECTION_HEADER SectionHeaders

以.text节为例,其他节同理

名称 作用
struct IMAGE_SECTION_HEADER SectionHeaders[0] 存储.text节头的信息

struct IMAGE_SECTION_HEADER SectionHeaders[0]

名称 作用
Name[8] 节名称
Misc 这是一个联合体,有两个成员:一个是DWORD PhysicalAddress,表示节在内存中的物理地址;另一个是DWORD VirtualSize,表示节在内存中占用的字节数。通常使用后者。
VirtualAddress 节表的RVA
SizeOfRawDat 节在文件中占用的字节数。它必须是文件中的节对齐大小(FileAlignment)的整数倍。
PointerToRawData 这是一个文件偏移地址(FOA),表示节在文件中的偏移量。
PointerToRelocations 这是一个FOA,表示重定位表在文件中的偏移量。重定位表是一种数据结构,用来描述程序在被加载到不同地址时需要修改哪些内存位置。
PointerToLinenumbers 这是一个FOA,表示行号表在文件中的偏移量。行号表是一种数据结构,用来描述程序中每条指令对应源代码中的哪一行。
NumberOfRelocations 表示重定位表的长度,也就是有多少个重定位项。已弃用
NumberOfLinenumbers 表示行号表的长度,也就是有多少个行号项。已弃用
Characteristics 节表的特征信息

struct SECTION_CHARACTERISTICS Characteristics

名称 作用
IMAGE_SCN_TYPE_NO_PAD 该节不需要填充字节(文件大小和内存大小相同),现已废弃
IMAGE_SCN_CNT_CODE 该节包含可执行代码
IMAGE_SCN_CNT_INITIALIZED_DATA 该节包含已初始化的数据
IMAGE_SCN_CNT_UNINITIALIZED_DATA 该节包含未始化的数据
IMAGE_SCN_LNK_OTHER 保留字段
IMAGE_SCN_LNK_INFO 该节包含注释或其他类型的信息
IMAGE_SCN_LNK_REMOVE 该节不会被加载到内存中
IMAGE_SCN_LNK_COMDAT 包含comdat数据
IMAGE_SCN_GPREL 表示该节包含全局指针相对地址的数据
IMAGE_SCN_MEM_16BIT 表示该节只能在16位环境下运行,比如DOS系统
IMAGE_SCN_MEM_LOCKED 表示该节被锁定在内存中,不会被交换出去。这意味着该节包含了一些重要的或者敏感的数据,不能被写入到磁盘或者其他设备上。
IMAGE_SCN_MEM_PRELOAD 表示该节在程序启动时被预加载到内存中。这意味着该节包含了一些程序运行时必须的或者经常使用的数据,为了提高程序的性能,它们会在程序开始执行之前就被加载到内存中
IMAGE_SCN_ALIGN_1BYTES 表示该节在内存中按照1字节对齐,也就是说,该节的起始地址和大小可以是任意值,不受限制
IMAGE_SCN_ALIGN_2BYTES 表示该节在内存中按照2字节对齐,也就是说,该节的起始地址和大小必须是2的整数倍,后面的8BYTES,128BYTES同理
IMAGE_SCN_LNK_NRELOC_OVFL 表示该节包含超过65,535个重定位项的扩展重定位信息。
IMAGE_SCN_MEM_DISCARDABLE 表示该节可以被丢弃或合并。这意味着该节包含了一些不重要的或者可选的数据。
IMAGE_SCN_MEM_NOT_CACHED 表示该节不能被缓存。这意味着该节包含了一些对时效性要求高的或者不可预测的数据,比如实时输入或者输出的数据。
IMAGE_SCN_MEM_NOT_PAGED 表示该节不能被分页。这意味着该节包含了一些对内存地址要求固定的或者不可移动的数据,比如中断处理程序或者驱动程序。
IMAGE_SCN_MEM_SHARED 表示该节可以被多个进程共享。这意味着该节包含了一些公共的或者只读的数据,比如常量或者字符串。如果多个进程加载了同一个PE文件,那么它们可以共享这些节的内存空间,而不需要为每个进程分配独立的内存空间。这样可以节省内存资源,提高程序的效率。
IMAGE_SCN_MEM_EXECUTE 表示该节可以被执行。这意味着该节包含了一些可执行的代码,比如函数或者指令。操作系统会把这些节加载到内存中,并且给它们分配执行权限,这样程序就可以调用这些代码来完成一些任务。
IMAGE_SCN_MEM_READ 表示该节可以被读取。这意味着该节包含了一些可读的数据,比如常量或者字符串。操作系统会把这些节加载到内存中,并且给它们分配读取权限,这样程序就可以从这些数据中获取信息。
ULONG IMAGE_SCN_MEM_WRITE 表示该节可以被写入。这意味着该节包含了一些可写的数据,比如变量或者缓冲区。操作系统会把这些节加载到内存中,并且给它们分配写入权限,这样程序就可以向这些数据中存储信息。

struct IMAGE_SECTION_DATA Section[0]

各个节的具体内容

struct IMAGE_IMPORT_DESCRIPTOR ImportDescriptor[0]

名称 作用
Characteristics 0表示导入表的结束,也可以存储其他一些标志,但是不会被系统使用,所以没什么意义
OriginalFirstThunk RVA,指向一个导入名称表INT
TimeDateStamp 表示DLL被绑定到本模块的时间戳,如果该PE文件还没有绑定,则为0。绑定是一种优化技术,可以在编译时确定导入函数或变量的实际地址,从而加快加载速度。
ForwarderChain 个变量用来表示转发链的索引,如果它是-1,那么说明没有转发链。转发链是一种机制,可以让一个导入符号指向另一个DLL中的符号,而不是原本导入的DLL中的符号。这样可以实现一些重定向或者封装的功能。
Name 一个RVA,指向一个字符串,表示导入DLL的名称
FirstThunk RVA,指向一个导入地址表IAT

在PE没有被加载到内存时,IAT和INT指向的是同一张表

在PE文件被加载到内存中后,IAT表中的内容被替换为函数的入口地址

换句话说,IAT和INT表一样的,只是用途以及用法不同

struct IMAGE_IMPORT_BY_NAME ImportByName[0]

名称 作用
Hint 一个16位的数字,它是导入DLL中符号的序号(ordinal)。提示可以帮助加载器快速地定位到导入符号的地址,而不需要遍历整个导出表(export table)。
Name 表示导入符号的名称。名称是一个以空字符结尾的ASCII字符串,它是导入DLL中符号的标识符。名称可以用来在导出表中查找到导入符号的序号和地址。

struct RESOURCE_DIRECTORY_TABLE ResourceDirectoryTable

资源目录表(Resource Directory Table),它用来存储PE文件中的资源数据,比如图标,光标,字符串,菜单等

名称 作用
Characteristics 保留字段,目前为0
TimeDateStamp 资源目录表创建的时间戳
MajorVersion 资源目录表的主版本号
MinorVersion 资源目录表的副版本号
NumberOfNameEntries 资源目录表中以名称作为标识符的资源项的数量
NumberOfIDEntries 资源目录表中以ID作为标识符的资源项的数量
struct RESOURCE_DIRECTORY_ENTRY IDEntries 资源目录项数组,每个项指向一个资源目录或者一个资源数据。(省略)

struct BASE_RELOCATION_TABLE RelocTable

重定位表(Relocation Table),它用来存储PE文件中的重定位数据。

重定位表的作用是在程序加载到内存中时,进行内存地址的修正。因为PE文件可能会被加载到不同的基址,而其中一些指令或数据是根据基址固定的,所以需要根据实际加载的基址和默认的基址之差来调整这些指令或数据的值。

名称 作用
struct IMAGE_BASE_RELOCATION BaseReloc[0] 一个重定位块的结构体,它有两个成员变量和一个变长数组。

struct IMAGE_BASE_RELOCATION BaseReloc[0]

名称 作用
VirtualAddress 当前页面起始地址的RVA值,本块中所有重定位项中的偏移量加上这个起始地址后就得到了真正的RVA值。
SizeOfBlock 当前重定位块的大小,从这个字段的值可以算出块中重定位项的数量
Block[38] 一个变长数组,每个元素是一个16位的数据,其中低12位是需要重定位的数据在页面中的偏移量,高4位是描述当前重定位项的类型。虽然高4位定义了多种重定位项的属性,但实际上在PE文件中只能看到0和3这两种情况。当类型为0时,表示该重定位项无效;当类型为3时,表示该重定位项需要修正为高低32位(HIGHLOW)。