Windows PE结构解析
郑重声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,如果您不同意请关闭该页面!任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!
前言
不管你是逆向领域,还是开发领域,异或是病毒编写者(红蓝对抗),都需要对PE文件有详细的了解,由于自己是个菜逼然后需要用到修改PE结构来达到某些免杀的操作,既然这样我就祭出菜逼大杀器Google百度来自行学习,本篇文章主要都是摘抄各位师傅们的文章,加上菜逼我的理解混合在一起的,如果有哪些地方理解错了或者填错了请各位师傅斧正!!

总体介绍
可执行文件(Executable File
)是指可以由操作系统直接加载执行的文件,在Windows
操作系统中可执行文件就是PE文件结构,在Linux
下则是ELF文件,下面这张图就是PE文件格式的图片(来自看雪),非常大一张图片,其实PE格式就是各种结构体的结合,Windows
下PE文件的各种结构体在WinNT.h
这个头文件中,可以在VS(宇宙无敌第一编译器)中查询。
PE文件整体结构
PE结构可以大致分为:
- DOS部分
- PE文件头
- 节表(块表)
- 节数据(块数据)
- 调试信息
依旧用看雪的图来,看雪NB~奥利给!
PE指纹(DOS头)
我们需要清楚的概念是PE指纹,也就是判断一个文件是否是PE文件的依据,首先是根据文件的前两个字节是否为4D 5A
,也就是MZ
,接着第四行的最后4个字节表示的是NT头的起始位置,我们使用32位程序作为分析
DOS头里面分为(分Header
和DOS
存根)
Header结构(00000000 - 0000003F,共64个字节)
typedef struct _IMAGE_DOS_HEADER { |
注意Win+Intel的电脑上大部分采用”小端法”,字节在内存中储存方式是倒过来的。
重要参数为e_magic
和e_lfanew
,且e_lfanew
的值在文件偏移量处0x3c的位置,大小4字节
前者是标识PE指纹的一部分,后者则是寻找PE文件头的部分,除了这两个成员,其他成员全部用0填充都不会影响程序正常运行,所以我们不需要过多的对其他部分深究
DOS存根(00000040 - X)
存根的大小是变的,起始位置为40h
,结束位置由30h
中最后面4个字节决定的
DOS存根则是一段简单的DOS程序,主要用来输出类似This program cannot be run in DOS mode.
的提示语句。即使没有DOS存根,程序也能正常执行。
NT头(PE头)
PE最重要的头,长度由DOS头 e_lfanew
决定
IMAGE_NT_HEADERS
从PE字符串开始到.text
字符串结束,如下图画出的,大小为f8h
// 32位程序的NT_HEADERS |
然后里面分别有3
个字段分别是Signature
,IMAGE_FILE_HEADER结构体
,IMAGE_OPTIONAL_HEADER结构体
大小分别是04h
,14h
,e0h
Signatur
也称作PE签名,这个成员和DOS头的MZ标记一样都是一个PE文件的标准特征,如果把这个PE签名修改后,程序也是不会正常运行的(跳出黑窗口打印This program cannot be run in DOS mode
然后闪退,可能是因为修改PE签名后无法识别后续内容的关系吧)。
IMAGE_FILE_HEADER
其中有4个重要的成员,若设置不正确,将会导致文件无法正常运行。
typedef struct _IMAGE_FILE_HEADER { |
第1部分(20个字节)
偏移 | 大小 | 英文名 | 中文名 | 描述 |
---|---|---|---|---|
0 | 2 | Machine | 机器数 | 标识CPU的数字。 |
2 | 2 | NumberOfSections | 节数 | 它的含义就是当前PE文件的节区数量,虽然它是大小是两个字节,但是在windows加载程序时会将节区的最大数量限制为96个。 |
4 | 4 | TimeDateStamp | 时间/日期标记 | UTC时间1970年1月1日00:00起的总秒数的低32位,它指出文件何时被创建。 |
8 | 8 | 已经废除 | ||
16 | 2 | SizeOfOptionalHeader | 可选头大小 | 第2部分+第3部分的总大小。这个大小在32位和64位文件中是不同的。对于32位文件来说,它是224;对于64位文件来说,它是240。 |
18 | 2 | Characteristics | 文件特征值 | 指示文件属性的标志。参考特征 |
-
Machine
所表示的是计算机的体系结构类型,也就是说这个成员可以指定该PE文件能够在32位还是在64位CPU上执行。如果强行更改该数值程序就会报错。镜像文件仅能运行于指定处理器或者能够模拟指定处理器的系统上。
具体机器类型如下图
值 描述 0x0 适用于任何类型处理器 0x1d3 Matsushita AM33处理器 0x8664 x64处理器 0x1c0 ARM小尾处理器 0xebc EFI字节码处理器 0x14c Intel 386或后继处理器及其兼容处理器 0x200 Intel Itanium处理器 0x9041 Mitsubishi M32R小尾处理器 0x266 MIPS16处理器 0x366 带FPU的MIPS处理器 0x466 带FPU的MIPS16处理器 0x1f0 PowerPC小尾处理器 0x1f1 带符点运算支持的PowerPC处理器 0x166 MIPS小尾处理器 0x1a2 Hitachi SH3处理器 0x1a3 Hitachi SH3 DSP处理器 0x1a6 Hitachi SH4处理器 0x1a6 Hitachi SH5处理器 0x1c2 Thumb处理器 0x169 MIPS小尾WCE v2处理器 -
NumberOfSections
从
010 Editor
模版上验证节区数量 -
TimeDateStamp
它的含义是时间戳,用于表示该PE文件创建的时间,时间是从国际协调时间也就是
1970年1月1日00:00
起开始计数的,计数单位是秒 -
SizeOfOptionalHeader
它存储该PE文件的可选PE头的大小,在32位PE文件中可选头大小为0xE0,64位可选头大小为0xF0。正因为如此,所以就必须通过该成员来确定可选PE头的大小。 -
Characteristics
域包含镜像文件属性的标志。以下加粗的是常用的属性。当前定义了以下值(由低位往高位)
位置 描述 0 它表明此文件不包含基址重定位信息,因此必须被加载到其首选基地址上。如果基地址不可用,加载器会报错。 1 它表明此镜像文件是合法的。看起来有点多此一举,但又不能少。 2 保留,必须为0。 3 4 5 应用程序可以处理大于2GB的地址。 6 保留,必须为0。 7 8 机器类型基于32位体系结构。 9 调试信息已经从此镜像文件中移除。 10 如果此镜像文件在可移动介质上,完全加载它并把它复制到交换文件中。几乎不用 11 如果此镜像文件在网络介质上,完全加载它并把它复制到交换文件中。几乎不用 12 此镜像文件是系统文件,而不是用户程序。 13 此镜像文件是动态链接库(DLL)。 14 此文件只能运行于单处理器机器上。 15 保留,必须为0。
IMAGE_OPTIONAL_HEADER
其中有9个重要参数,设置错误会导致文件无法运行
typedef struct _IMAGE_OPTIONAL_HEADER { |
还需要知道的是,程序的真正入口点 = ImageBase + AddressOfEntryPoint
-
Magic
这个无符号整数指出了镜像文件的状态,此成员可以是以下的值宏定义 数值 描述 IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x010B 表明这是一个32位镜像文件。 IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x020B 表明这是一个64位镜像文件。 IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x0107 表明这是一个ROM镜像。 -
AddressOfEntryPoint
该成员保存着文件被执行时的入口地址,它是一个RVA。如果想要在一个可执行文件中附加了一段代码并且要让这段代码首先被执行,就可以通过更改入口地址到目标代码上,然后再跳转回原有的入口地址。 -
ImageBase
该成员指定了文件被执行时优先被装入的地址,如果这个地址已经被占用,那么程序装载器就会将它载入其他地址。当文件被载入其他地址后,就必须通过重定位表进行资源的重定位,这就会变慢文件的载入速度。而装载到ImageBase指定的地址就不会进行资源重定位。 -
SectionAlignment
该成员指定了文件被装入内存时,节区的对齐单位。节区被装入内存的虚拟地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。该成员的默认大小为系统的页面大小。 -
FileAlignment
该成员指定了文件在硬盘上时,节区的对齐单位。节区在硬盘上的地址必须是该成员的整数倍,以字节为单位,并且该成员的值必须大于等于FileAlignment的值。该值应为200h到10000h(含)之间的2的幂。默认为200h。如果SectionAlignment的值小于系统页面大小,则FileAlignment的值必须等于SectionAlignment的值。 -
SizeOfImage
该成员指定了文件载入内存后的总体大小,包含所有的头部信息。并且它的值必须是SectionAlignment
的整数倍。 -
Subsystem
定义了以下值以确定运行镜像所需的
Windows
子系统(如果存在)值 描述 0 未知子系统 1 设备驱动程序和Native Windows进程 2 Windows图形用户界面(GUI)子系统(一般程序) 3 Windows字符模式(CUI)子系统(从命令提示符启动的) 7 Posix字符模式子系统 9 Windows CE 10 可扩展固件接口(EFI)应用程序 11 带引导服务的EFI驱动程序 12 带运行时服务的EFI驱动程序 13 EFI ROM镜像 14 XBOX -
DllCharacteristics
域定义了以下值位置 描述 1 保留,必须为0。 2 3 4 5 官方文档缺失 6 官方文档缺失 7 DLL可以在加载时被重定位。 8 强制进行代码完整性校验。 9 镜像兼容于NX。 10 可以隔离,但并不隔离此镜像。 11 不使用结构化异常(SE)处理。 12 不绑定镜像。 13 保留,必须为0。 14 WDM驱动程序。 15 官方文档缺失 16 可以用于终端服务器。
IMAGE_DATA_DIRECTORY
数据目录结构,里面列举了导入表、导出表、重定位表等一系列表
typedef struct _IMAGE_DATA_DIRECTORY { |
在010 Editor
中打开的目录如下,里面每个表都是一个完整的IMAGE_DATA_DIRECTORY
结构体
节表
节表的结构如下
typedef struct _IMAGE_SECTION_HEADER { |
以kernel23.dll举例,里面有6个节表,每个节表代表一个IMAGE_SECTION_HEADER
结构体
节表格式如下
偏移 | 大小 | 英文名 | 描述 |
---|---|---|---|
0 | 8 | Name | 这是一个8字节ASCII编码的字符串,不足8字节时用NULL填充,必须使其达到8字节。如果它正好是8字节,那就没有最后的NULL字符。可执行镜像不支持长度超过8字节的节名。 |
8 | 4 | VirtualSize | 当加载进内存时这个节的总大小。如果此值比SizeOfRawData大,那么多出的部分用0填充。这是节的数据在没有进行对齐处理前的实际大小,不需要内存对齐。 |
12 | 4 | VirtualAddress | 内存中节相对于镜像基址的偏移。必须是SectionAlignment的整数倍。 |
16 | 4 | SizeOfRawData | 磁盘文件中已初始化数据的大小。它必须是NT头中FileAlignment域的倍数。当节中仅包含未初始化的数据时,这个域应该为0。 |
20 | 4 | PointerToRawData | 节中数据起始的文件偏移。它必须是NT头中FileAlignment域的倍数。当节中仅包含未初始化的数据时,这个域应该为0。 |
24 | 4 | PointerToRelocations | 重定位项开头的文件指针。对于可执行文件或没有重定位项的文件来说,此值应该为0。 |
28 | 4 | 已经废除。 | |
32 | 2 | NumberOfRelocations | 节中重定位项的个数。对于可执行文件或没有重定位项的文件来说,此值应该为0。 |
34 | 2 | 已经废除。 | |
36 | 4 | Characteristics | 描述节特征的标志。参考节标志。 |
单独一个节表的详细结构如下图
-
Name
这是一个8字节的ASCII字符串,长度不足8字节时用0x00填充可执行文件不支持长度超过8字节的节名。对于支持超过字节长度的文件来说,此成员会包含斜杠(/),并在后面跟随一个用ASCII表示的十进制数字,该数字是字符串表的偏移量。 -
VirtualAddress
指定了该节区装入内存虚拟空间后的地址,这个地址是一个相对虚拟地址(RVA),它的值一般是SectionAlignment的整数倍。它加上ImageBase后才是真正的虚拟地址。 -
SizeOfRawData
指定了该节区在硬盘上初始化数据的大小,以字节为单位。它的值必须是FileAlignment的整数倍,如果小于Misc.VirtualSize,那么该部分的其余部分将用0x00填充。如果该部分仅包含未初始化的数据,那么这个值将会为零。 -
PointerToRawData
指出零该节区在硬盘文件中的地址,这个数值是从文件头开始算起的偏移量,也就是说这个地址是一个文件偏移地址(FOA)。它的值必须是FileAlignment的整数倍。如果这个部分仅包含未初始化的数据,则将此成员设置为零。 -
Characteristics
该成员指出了该节区的属性特征。其中的不同数据位代表了不同的属性,这些数据位组合起来就是这个节的属性特征,比较常见的定义如下,更多信息见winnt.h
的定义
虽然每个节的名称可以随意定义,但是也有一些约定俗成的名称,如下
名称 | 描述 |
---|---|
.text | 代码段,Borland C++编译器代码段为code |
.data | 可读写的数据段,存放全局变量或静态变量 |
.rdata | 只读数据段,存放常量信息 |
.idata | 导入数据段,存放导入表信息 |
.edata | 导出数据段,存放导出表信息 |
.rsrc | 资源段,存放图标、菜单等资源信息 |
.bss | 未初始化数据段 |
.crt | 存放用于支持C++运行时库(CRT)所添加的数据 |
.tls | 存放用于支持通过_declspec(thread)声明的线程局部存储数据 |
.reloc | 存放重定位信息 |
.sdata | 存放可被全局指针定位的可读写数据 |
.srdata | 存放可被全局指针定位的只读数据 |
.pdata | 存放异常表,结构体为IMAGE_RUNTIME_FUNTCION_ENTRY |
.debug$S | 存放OBJ文件中Codeview格式符号 |
.debug$T | 存放OBJ文件中Codeview格式类型符号 |
.debug$P | 存放使用预编译头时的一些信息 |
.drectve | 存放编译时的一些链接命令 |
.didat | 存放延迟装入的数据 |
导入表
导出表(Import Table),主要就是标注了需要导入使用的一些DLL文件
typedef struct _IMAGE_IMPORT_DESCRIPTOR { |
如下图可知道,需要用到的dll文件
EXE文件载入后对应的导入表结构图
EXE文件载入前对应的导入表结构图
导出表
导出表(Export Table)一般是DLL
文件用的比较多,exe
文件很少有导出表,导出表的数据结构如下
typedef struct _IMAGE_EXPORT_DIRECTORY { |
AddressOfFunctions
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的地址表,这个地址表可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfFunctions进行限定,地址表中的成员也是一个RVA地址,在内存中加上ImageBase后才是函数真正的地址。AddressOfNames
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的名称表,这个名称表也可以当作一个成员宽度为4的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员也是一个RVA地址,在FIleBuffer状态下需要进行RVA到FOA的转换才能真正找到函数名称。AddressOfNameOrdinals
这个值是一个4字节的RVA地址,他可以用来定位导出表中所有函数的序号表,这个序号表可以当作一个成员宽度为2的数组进行处理,它的长度由NumberOfNames进行限定,名称表的成员是一个函数序号,该序号用于通过名称获取函数地址。NumberOfFunctions
注意,这个值并不是真的函数数量,他是通过函数序号表中最大的序号减去最小的序号再加上一得到的,例如:一共导出了3个函数,序号分别是:0、2、4,NumberOfFunctions = 4 - 0 + 1 = 5个。
通过函数名查找函数地址
通过函数序号查找函数地址
重定位表
系统在加载DLL文件时,并不是每次都能加载到预期的 ImageBase
基址上,所以DLL都存在 基址重定位表
,
用来修正相关的地址信息,另外EXE的 动态基址
技术,也是用 基址重定位表
实现的。
PE文件中的重定位信息是由多个 IMAGE_BASE_RELOCATION
结构体组成的,每个结构体只描述一个 4KB
大小
的分页内重定位信息,也就是 0x1000
字节,因此结构体中 VirtualAddress
的值总是为 0x1000
的倍数。
typedef struct _IMAGE_BASE_RELOCATION { |
重定位的本质非常简单,就是比较实际加载地址与 ImageBase
的值,如果相等则不需要做任何操作,如果
不相等就需要把重定位表中指定的地址处加上这个差值。
TypeOffset
由两部分数据组成,高4位
表示 类型
,低12位
表示 偏移
。类型定义如下
值 | 信息 | 宏定义 |
---|---|---|
0 | 无重定位操作,填0后用于4字节对齐 | IMAGE_REL_BASED_ABSOLUTE |
1 | 重定位偏移指向位置的高2个字节需要被修正 | IMAGE_REL_BASED_HIGH |
2 | 重定位偏移指向位置的高2个字节需要被修正 | IMAGE_REL_BASED_LOW |
3 | 重定位偏移指向的4个字节的地址需要被修正 | IMAGE_REL_BASED_HIGHLOW |
4 | 需要使用两项TypeOffset才能完成索引操作 | IMAGE_REL_BASED_HIGHADJ |
5 | 基址重定位应用于MIPS jump指令 | IMAGE_REL_BASED_MIPS_JMPADDR |
6 | 保留 | IMAGE_REL_BASED_RESERVED |
9 | 基址重定位应用于MIPS16 jump指令 | IMAGE_REL_BASED_IA64_IMM64 |
10 | 重定位偏移指向的8个字节(64位)地址需要被修正 | IMAGE_REL_BASED_DIR64 |
重定位表结构如下图所示
资源表
资源表用来存储程序的各种界面数据,比如菜单、图标、版本信息等,其结构体如下
typedef struct _IMAGE_RESOURCE_DIRECTORY { |
资源在PE文件中是以目录结构的形式存在的,一般情况下这个目录分3层,从根目录开始分别为资源类型、
目录资源ID、资源代码页。每层的头部是一个 IMAGE_RESOURCE_DIRECTORY
结构,并且在其后面跟着一个
IMAGE_RESOURCE_DIRECTORY_ENTRY
结构数组,然后结构数组的每个成员则分别指向下一层目录结构。
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY { |
第一个联合体的信息,是根据当前结构体所处的目录层次来决定的,位于 第1层
目录时 Name
有效,保存的
信息是 资源类型
。位于 第2层
目录时 Id
或 结构体
有效,取决于此资源的 索引方式
,如果用的是 编号索引
就是 Id
有效,否则 结构体
有效。位于 第3层
目录时 Name
有效,保存的信息是 语言类型
。
第二个联合体的信息,理论上是根据具体情况而定的,如果下级是一个 子目录
的话,那么就是 结构体
生效,
如果下级是 资源数据
则是字段 OffsetToData
生效。
当 NameIsString
为 1
时,NameOffset
指向一个 IMAGE_RESOURCE_DIR_STRING_U
结构体
typedef struct _IMAGE_RESOURCE_DIR_STRING_U { |
当 Name
位于 第1层
目录时表示的 资源类型
如下表所示
类型值 | 资源类型 | 类型值 | 资源类型 |
---|---|---|---|
0x00000001 | 鼠标指针(Cursor) | 0x00000008 | 字体(Font) |
0x00000002 | 位图(Bitmap) | 0x00000009 | 快捷键(Accelerators) |
0x00000003 | 图标(Icon) | 0x0000000A | 非格式化资源(Unformatted) |
0x00000004 | 菜单(Menu) | 0x0000000B | 消息列表(Message Table) |
0x00000005 | 对话框(Dialog) | 0x0000000C | 鼠标指针组(Group Cursor) |
0x00000006 | 字符串列表(String Table) | 0x0000000E | 图标组(Group Icon) |
0x00000007 | 字体目录(Font Directory) | 0x00000010 | 版本信息(Version Information) |
在经过3层目录的索引后,最后是一个 IMAGE_RESOURCE_DATA_ENTRY
结构体,定义如下
typedef struct _IMAGE_RESOURCE_DATA_ENTRY { |
资源表的3层目录关系,如下图所示
参考文章
https://thunderjie.github.io/2019/03/27/PE%E7%BB%93%E6%9E%84%E8%AF%A6%E8%A7%A3/ |