PE结构分析
引言
Windows 的 PE 文件结构是一种可执行文件格式,用于存储 Windows 应用程序、服务和驱动程序。
准备一个简单的程序
以32位为主,在后面会展示与64位的区别
1 | //没有使用到的库仅用于展示PE结构的某些变化 |
结构体系
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)。 |