PE文件结构

[TOC]

(图片未加载请使用科学上网)

PE文件结构

PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等

一般来说 PE文件结构依次为DOS头,NT头,节表,以及各种节

PE 结构是由若干个复杂的结构体组合而成的,不是单单的一个结构体那么简单,它的结构就像文件系统的结构是由多个结构体组成的。

fb8ac66be60f0bcc6b81d1566c2fd371_2508687-20211118234239741-787510298

1.1 DOS头

DOS头由两部分组成:DOS MZ文件头和DOS块。

DOS头是一个结构体 16位的程序标准情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE 头部
WORD e_magic; // 魔术数字
WORD e_cblp; // 文件最后一页的字节数
WORD e_cp; // 文件中的页数
WORD e_crlc; // 重定位数量
WORD e_cparhdr; // 头部大小(以段为单位)
WORD e_minalloc; // 所需的最小附加段数
WORD e_maxalloc; // 所需的最大附加段数
WORD e_ss; // 初始 SS(堆栈段寄存器)值
WORD e_sp; // 初始 SP(堆栈指针寄存器)值
WORD e_csum; // 校验和
WORD e_ip; // 初始 IP(指令指针寄存器)值
WORD e_cs; // 初始 CS(代码段寄存器)值
WORD e_lfarlc; // 重定位表在文件中的地址
WORD e_ovno; // 覆盖号
WORD e_res[4]; // 保留字段(4个字)
WORD e_oemid; // OEM标识符(用于e_oeminfo)
WORD e_oeminfo; // OEM信息,具体由e_oemid定义
WORD e_res2[10]; // 保留字段(10个字)
LONG e_lfanew; // 新EXE头部在文件中的地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

e_magic: DOS文件的魔术数字,通常为0x5A4D(’MZ’)。这是用来标识该文件为一个DOS可执行文件。

e_cblp: 文件最后一页的字节数。

e_cp: 文件中总共有多少个页面。

e_crlc: 重定位项的数量。

e_cparhdr: 文件头的大小,以段为单位。

e_minalloc: 该程序需要的最小附加段数。

e_maxalloc: 该程序允许的最大附加段数。

e_ss: 初始堆栈段寄存器(SS)的值。

e_sp: 初始堆栈指针寄存器(SP)的值。

e_csum: 用于校验文件的校验和。

e_ip: 初始指令指针寄存器(IP)的值。

e_cs: 初始代码段寄存器(CS)的值。

e_lfarlc: 文件中重定位表的位置。

e_ovno: 文件的覆盖号,指示是否支持覆盖。

e_res: 一组保留字段,通常用于未来扩展。

e_oemide_oeminfo: 用于OEM特定的信息,通常在某些特定的设备或应用中使用。

e_res2: 另一组保留字段。

e_lfanew: 新的EXE头部的文件地址,指向PE头部的起始位置,通常是文件的后面部分,标志着从DOS格式过渡到PE格式。

但是平时我们分析的都是32位64位程序 仅仅使用了其中两个成员部分:

e_magice_lfanew,它们是标识和Windows寻找PE头的位置的

其他的成员可以使用0x00填充,程序不受影响可以正常运行,但是它们两个不可以

使用010打开Hello_re查看

QQ_1733488640252

前两字节就是e_magic(0x5A4D–MZ)

这两个字母就是Mark Zbikowski的姓名缩写,他是最初的MS-DOS设计者之一。如果把PE文件的这两个字节修改成其他数据,运行该PE文件就会无法正常运行,跳出黑窗口打印Program too big to fit in memory然后闪退

010很贴心的标记了各个部分 第一段红色部分就是DOS MZ文件头(0x40h)64字节

由于这不是16位程序 我们可以跳过中间部分

最后的e_lfanew是DWORD类型 4个字节 也就是图中标红部分的最后四个字节 代表着PE头所在的偏移量

显然是80 00 00 00也就是指向了00000080 如图:

QQ_1733489106422

注意到在DOS MZ文件头和PE头中间还有一部分数据(橘黄色部分)

也就是我们所谓的DOS块

而DOS块不是结构体,而是由单个字节组成的数据,可以填写任何内容。
而注意到其中的值是可以翻译为可读字符串的(右方)

“This program cannot be run in DOS mode.”

这涉及到Dos头的另一个作用

DOS头是用来兼容MS-DOS操作系统的,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode.还有一个目的,就是指明NT头在文件中的位置。(PE头有时又叫NT头)

这部分数据在实际操作中常常可以忽略

1.2 IMAGE_NT_HEADERS–PE头(NT头)

在MS-DOS头下main,就是PE头,PE头是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,其中包含许多PE装载器用到的重要字段。

NT头仍然是一个结构体样式

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32/64 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

跟随偏移我们找到了NT头的起始位置

QQ_1733489789855

很容易注意到开头四个字节(DWORD)是有意义的字符PE

也就是所谓的PE标识部分(Signature)

全部应该为”PE\0\0” 标志着PE文件头的开始 而DOS头最后的e_lfanew数据实际指向的也是这个位置

接下来两个成员为文件头(IMAGE_FILE_HEADER)和可选文件头(IMAGE_OPTIONAL_HEADER32)

注意到是没有大小定义的 实际上它们也是结构体

1.2.1 IMAGE_FILE_HEADER

同样也有结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_FILE_HEADER {

WORD Machine;//运行平台

WORD NumberOfSections;//文件的区块数目

DWORD TimeDateStamp;//文件创建的用时间戳标识的日期

DWORD PointerToSymbolTable;//指向符号表(用于调试)

DWORD NumberOfSymbols;//符号表中符号的个数

WORD SizeOfOptionalHeader;//IMAGE_OPTIONAL_HEADER32结构大小

WORD Characteristics;//文件属性

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

注意到WORD二字节 DWORD四字节 所以我们可以给程序分段

QQ_1733490297760

第一个框说明了运行的平台标识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
机器 标识

#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_AM33 0x01d3
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon
#define IMAGE_FILE_MACHINE_CEF 0x0CEF
#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE 0xC0EE

说明该程序是AMD64的

第三个字段是时间戳

用于表示该PE文件创建的时间,时间是从国际协调时间也就是1970年1月1日00:00起开始计数的,计数单位是秒

第四到第五个字段略 见注释即可

第六个字段SizeOfOptionalHeader(倒数第二个二字节框)

标识了IMAGE_OPTIONAL_HEADER结构的大小 也能看出是64位还是32位的文件

32位文件值通常是00EOh,对于64位值通常为00F0h

可见该程序是64位的

最后一个字段是文件属性

不一定是以下某个值 也可以是某几个异或得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // 文件中的重定位信息已被去除。
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件是可执行的(即没有未解决的外部引用)。
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 文件中的行号信息已被去除。
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 文件中的局部符号已被去除。
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // 积极修剪工作集(优化内存使用)。
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 应用程序可以处理超过2GB的地址。
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // 机器字节顺序被反转(低位字节在前)。
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32位机器。
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // 调试信息已从文件中去除(通常存放在.DBG文件中)。
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 如果文件在可移动媒体上,复制并从交换文件中运行。
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // 如果文件在网络上,复制并从交换文件中运行。
#define IMAGE_FILE_SYSTEM 0x1000 // 系统文件。
#define IMAGE_FILE_DLL 0x2000 // 文件是一个DLL(动态链接库)。
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 文件仅能在单处理器(UP)机器上运行。
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // 机器字节顺序被反转(高位字节在前)。


实际上 010仍然很贴心的给出了结构体定义 鼠标放在某段数据上即可 例如在本程序中

QQ_1733492097612

1.2.2 IMAGE_OPTIONAL_HEADER

注:在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。

这个结构是IMAGE_FILE_HEADER结构的补充。这两个结构合起来才能对整个PE文件头进行描述。

结构体如下(左边是相对于文件头的偏移量 也就是刚刚图片红框后面的部分开始)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
typedef struct _IMAGE_OPTIONAL_HEADER 
{
//
// Standard fields.
//
+18h WORD Magic; // 标志字
+1Ah BYTE MajorLinkerVersion; // 链接程序的主版本号
+1Bh BYTE MinorLinkerVersion; // 链接程序的次版本号
+1Ch DWORD SizeOfCode; // 所有含代码的节的总大小
+20h DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
+24h DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h DWORD AddressOfEntryPoint; // 程序执行入口RVA
+2Ch DWORD BaseOfCode; // 代码的区块的起始RVA
+30h DWORD BaseOfData; // 数据的区块的起始RVA
//
// NT additional fields. 以下是属于NT结构增加的领域。
//
+34h DWORD ImageBase; // *********程序的首选装载地址
+38h DWORD SectionAlignment; // *********内存中的区块的对齐大小
+3Ch DWORD FileAlignment; // *********文件中的区块的对齐大小
+40h WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
+42h WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
+44h WORD MajorImageVersion; // 可运行于操作系统的主版本号
+46h WORD MinorImageVersion; // 可运行于操作系统的次版本号
+48h WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
+4Ah WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
+4Ch DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
+50h DWORD SizeOfImage; // 映像装入内存后的总尺寸
+54h DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小
+58h DWORD CheckSum; // 映像的校检和
+5Ch WORD Subsystem; // 可执行文件期望的子系统
+5Eh WORD DllCharacteristics; // DllMain()函数何时被调用,默认为 0
+60h DWORD SizeOfStackReserve; // 初始化时的栈大小
+64h DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
+68h DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
+6Ch DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
+70h DWORD LoaderFlags; // 与调试有关,默认为 0
+74h DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来 // 一直是16
+78h DWORD DataDirctory[16]; // ********* 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;


第一个字段

1
2
3
\#define IMAGE_NT_OPTIONAL_HDR32_MAGIC    0x10b  // 32位PE可选头
\#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b // 64位PE可选头
\#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107

注意

1
2
3
4
5
6
7
8
9
字段6:AddressOfEntryPoint 表 程序入口RVA,即OEP:
``EOP:程序入口点,壳相关概念
``OEP:原本的程序入口点(实际为偏移,+模块基址=实际入口点)
``EP: 被加工后的入口点
字段9:ImageBase 表 模块加载基地址,exe默认0x400000,dll默认0x10000000
``建议装载地址:exe映射加载到内存中的首地址= PE 0处,即实例句柄hInstance
``一般而言,exe文件可遵从装载地址建议,但dll文件无法满足
尾字段:DataDirectory 表 数据目录表,用来定义多种不通用处的数据块。
``存储了PE中各个表的位置,详情参考IMAGE_DIRECTORY_ENTRY...系列宏

​ AddressOfEntryPoint

​ 是入口地址 如果想在一个可执行文件中附加了一段代码并且要让这段代码首先被执行,就可以通过更改入口地址到目标代码上,然后再跳转回原有的入口地址

​ 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的整数倍。整个PE文件在内存中展开后的大小

  SizeOfHeaders
  该成员指定了PE文件头的大小,并且向上舍入为FileAlignment的倍数,值的计算方式为:

1
2
3
4
5
6
7
SizeOfHeaders = (e_lfanew/*DOS头部*/ + 4/*PE签名*/ +
sizeof(IMAGE_FILE_HEADER) +
SizeOfOptionalHeader + /*NT头*/
sizeof(IMAGE_SECTION_HEADER) * NumberOfSections) / /*节表*/
FileAlignment *
FileAlignment +
FileAlignment; /*向上舍入 一般该结果不可能是FileAlignment的整数倍,所以直接加上FileAlignment还是没问题的 */

1.2.2.1 DataDirectory表

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

对于这个结构体数组 每一个成员都包含一个地址和一个大小 定义了一个区域 事实上 数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 导出目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // 导入目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // 资源目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // 异常目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // 安全目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // 基址重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // 调试目录
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86使用)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // 架构特定数据
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // 全局指针的RVA(相对虚拟地址)
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS(线程本地存储)目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // 加载配置目录
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // 头文件中的绑定导入目录
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // 导入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // 延迟加载导入描述符
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM运行时描述符

QQ_1733492361878

注意到这里是NumberOfRvaAndSizes字段 那么之后的就是这个结构体数组 16*8共128个字节

1.2.2.1.1 IMAGE_EXPORT_DIRECTORY——导出表
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 未使用,总为0
DWORD TimeDateStamp; // 文件创建时间戳
WORD MajorVersion; // 未使用,总为0
WORD MinorVersion; // 未使用,总为0
DWORD Name; // **重要 指向一个代表此 DLL名字的 ASCII字符串的 RVA
DWORD Base; // **重要 函数的起始序号
DWORD NumberOfFunctions; // **重要 导出函数地址表的个数
DWORD NumberOfNames; // **重要 以函数名字导出的函数个数
DWORD AddressOfFunctions; // **重要 导出函数地址表RVA
DWORD AddressOfNames; // **重要 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // **重要 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

导出表简介:在导出表中前四个成员基本没有用,我们就不用去管他,但是剩下的成员都是非常重要的。现在我们来说说导出表的作用,简单来说导出表就是用来描述模块中的导出函数的结构,导出函数就是将功能的提供给外部使用的函数,如果一个PE文件导出了函数,那么这个函数的信息就会记录PE文件的导出表中,方便外部程序加载该文件进行动态调用。可能有时函数在导出表中只有一个序号而没有名字,也就造成了导出表中有了三个子表的存在,分别是:函数地址表、函数名称表和函数序号表。使得外部程序可以通过函数名称和函数序号两种方式获取该函数的地址。

1
2
3
4
5
6
//系统中获取函数地址的两种方法:
HMODULE hModule = LoadLibraryA("User32.dll");
//1、函数名获取
DWORD FuncAddress = GetProcAddress(hModule, "MessageBoxA");
//2、序号获取
DWORD FuncAddress = GetProcAddress(hModule, 12);

​ 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个。

1.2.2.1.2 IMAGE_IMPORT_DESCRIPTOR——导入表
1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //导入名称表(INT)的RVA地址
} DUMMYUNIONNAME;
DWORD TimeDateStamp; //时间戳多数情况可忽略 如果是0xFFFFFFFF表示IAT表被绑定为函数地址
DWORD ForwarderChain;
DWORD Name; //导入DLL文件名的RVA地址
DWORD FirstThunk; //导入地址表(IAT)的RVA地址
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

导入表简介:PE文件使用来自于其他DLL的代码或数据是,称作导入(或者输入)。当PE文件装入时,Windows装载器的工作之一就是定位所有被输入的函数和数据,并且让正在被装入的程序可以使用这些地址。这个过程就是通过PE文件的导入表来完成的,导入表中保存的是函数名和其驻留的DLL名等动态链接所需的信息。

  OriginalFirstThunk
  这个值是一个4字节的RVA地址,这个地址指向了导入名称表(INT),INT是一个IMAGE_THUNK_DATA结构体数组,这个结构体的最后一个成员内容为0时数组结束。这个数组的每一个成员又指向了一个IMAGE_IMPORT_BY_NAME结构体,这个结构体包含了两个成员函数序号和函数名,不过这个序号一般没什么用,所以有的编译器会把函数序号置0。函数名可以当作一个以0结尾的字符串。(注:这个表不在目录项中。)
  Name
  DLL名字的指针,是一个RVA地址,指向了一个以0结尾的ASCII字符串。
  FirstThunk
  这个值是一个4字节的RVA地址,这个地址指向了导入地址表(IAT),这个IAT和INT一样,也是一个IMAGE_THUNK_DATA结构体数组,不过它在程序载入前和载入后由两种状态,在程序载入前它的结构和内容和INT表完全一样,但却是两个不同的表,指向了IMAGE_IMPORT_BY_NAME结构体。在程序载入后,他的结构和INT表一样,但内容就不一样了,里面存放的都是导入函数的地址。(注:这个表在目录项中,需要注意。)
IMAGE_THUNK_DATA——INT、IAT的结构体:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME 的地址RVA
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

//注:这个结构体是联合类型的,每一个成员都是4字节,所以为了编程方便,完全可以用一个4字节的数组取代它。

IMAGE_IMPORT_BY_NAME 结构体:

1
2
3
4
5
6
7
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可能为空,编译器决定,如果不为空是函数在导出表中的索引
CHAR Name[1]; //函数名称,以0结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

//注:这个结构体由两个成员组成,大致一看它的大小是3个字节,其实它的大小是不固定的,
// 因为无法判断函数名的长度,所以最后一个成员是一个以0结尾的字符串。
1.2.2.1.3 IMAGE_RESOURCE_DIRECTORY——资源表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//资源目录头
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //资源属性 一般为0
DWORD TimeDateStamp; //资源创建时间戳 一般为0
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries; //以名称命名的目录项数量 重要
WORD NumberOfIdEntries; //以ID命名的目录项数量 重要
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;

//资源目录项
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31; //字符串的偏移(不是RVA、FOA,相对特殊)
DWORD NameIsString:1; //判断名字是否是字符串 1:是 0:不是
} DUMMYSTRUCTNAME;
DWORD Name;
WORD Id; //目录项的ID(在一级目录指资源类型,二级目录指资源编号,三级目录指代码的页号)
} DUMMYUNIONNAME;
union {
DWORD OffsetToData; //如果不是目录,这里指数据的偏移(不是RVA、FOA,相对特殊)
struct {
DWORD OffsetToDirectory:31;//目录的偏移(不是RVA、FOA,相对特殊)
DWORD DataIsDirectory:1; //判断子资源项是否是目录 1:是 0:不是
} DUMMYSTRUCTNAME2;
} DUMMYUNIONNAME2;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

//数据项
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; //数据的偏移 重要
DWORD Size; //数据的大小 重要
DWORD CodePage; //代码页(一般为0)
DWORD Reserved; //保留
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

//名字字符串结构
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length; //Unicode字符串长度
WCHAR NameString[ 1 ]; //Unicode字符串
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

资源表简介:在Windows程序中其各种界面被称作为资源,其中被系统预先定义的资源类型包括:鼠标指针,位图, 图标,菜单,对话框, 字符串列表,字体目录, 字体,加速键,非格式化资源,消息列表,鼠标指针组,图标组,版本信息。当然还有用户自定义的资源类型,这些资源的就不举例了。这些资源都是以二进制的形式保存到PE文件中,而保存资源信息的结构就是资源表,它位于目录项的第三位。在PE文件的所有结构中,资源表的结构最为复杂,这是因为资源表用类似于文件目录结构的方式进行保存的,从根目录开始,下设一级目录、二级目录和三级目录,三级目录下才是资源文件的信息,而且资源表的结构定位也是最为特殊的
  一级目录是按照资源类型分类的,如位图资源、光标资源、图标资源。
  二级目录是按照资源编号分类的,同样是菜单资源,其子目录通过资源ID编号分类,例如:IDM_OPEN的ID号是2001h,IDM_EXIT的ID号是2002h等多个菜单编号。
  三级目录是按照资源的代码页分类的,即不同语言的代码页对应不同的代码页编号,例如:简体中文代码页编号是2052。
  三级目录下是节点,也称为资源数据,这是一个IMAGE_RESOURCE_DATA_ENTRY的数据结构,里面保存了资源的RVA地址、资源的大小,对所有资源数据块的访问都是从这里开始的。

注:资源表的一级目录、二级目录、三级目录的目录结构是相同的都是由一个资源目录头加上一个资源目录项数组组成的,可以将这个结构称作资源目录结构单元。

IMAGE_RESOURCE_DIRECTORY.NumberOfNamedEntries和IMAGE_RESOURCE_DIRECTORY.NumberOfIdEntries
  在资源目录头结构中这两个字段是最为重要的,其他字段大部分为0。NumberOfNamedEntries表示在该资源目录头后跟随的资源目录项中以IMAGE_RESOURCE_DIR_STRING_U结构命名的资源目录项数量。NumberOfIdEntries表示在该资源目录头后跟随的资源目录项中以ID命名的资源目录项数量。两个字段加起来就是本资源目录头后的资源目录项的数量总和。也就是后面IMAGE_RESOURCE_DIRECTORY_ENTRY结构的总数量。

  IMAGE_RESOURCE_DIRECTORY_ENTRY.DUMMYUNIONNAME
  在资源目录项中该字段是一个联合体类型,大小为4个字节,它决定这个资源目录的名字是字符串还是ID号。如果这个字段的最高位是1,则表示该资源的名字是字符串类型,该字段的低31位是IMAGE_RESOURCE_DIR_STRING_U结构的偏移,但这个偏移既不是FOA也不是RVA,它是以首个资源表的地址为基址,加上低31位的值才是字符串结构的地址。如果最高位为0,则表示该资源的名字是一个ID号,整个字段的值就是该资源的ID。(如果是一级目录的资源项,该ID有14个号码被预先定义了)

一级目录中预定义的资源ID:

1
2
3
4
5
6
7
8
9
资源ID	含义	 	资源ID	含义
0x01 鼠标指针(Cursor) 0x08 字体(Font)
0x02 位图(Bitmap) 0x09 加速键(Accelerators)
0x03 图标(Icon) 0x0A 非格式化资源(Unformatted)
0x04 菜单(Menu) 0x0B 消息列表(Message Table)
0x05 对话框(Dialog) 0x0C 鼠标指针组(Group Cursor)
0x06 字符串列表(String) 0x0E 图标组(Group Icon)
0x07 字体目录(Font Directory) 0x10 版本信息(Version Information)
 IMAGE_RESOURCE_DIRECTORY_ENTRY.DUMMYUNIONNAME2

  在资源目录项中该字段是一个联合体类型,大小为4个字节,它决定这个资源目录的目录中子节点的类型(是目录还是节点)。如果这个字段的最高位是1,则表示该资源的子节点是一个目录类型,该字段的低31位是子目录的资源目录头结构的偏移,但这个偏移既不是FOA也不是RVA,它是以首个资源表的地址为基址,加上低31位的值才是资源目录头结构的地址。如果最高位为0,则表示该资源的子节点是一个节点,它也以首个资源表的地址为基址,整个字段的值就是该资源节点的偏移。这个节点是IMAGE_RESOURCE_DATA_ENTRY类型的结构体。(一般在三级目录中该字段的最高位位0,而在其他两个目录中该字段的最高位为1)

注:为了编程方便,IMAGE_RESOURCE_DIRECTORY_ENTRY的联合体中出现了一组特殊的struct结构体,其成员声明格式为:[类型] [变量名] : [位宽表达式], 这个格式就是C语言中位段的声明格式。NameOffset字段的值等于该联合体的低31位,NameIsString字段的值等于该联合体的最高位。将一个4字节的类型拆成这样两个字段就可以方便的避免了繁琐的位操作了,而且该结构的总大小不会发生变化。

IMAGE_RESOURCE_DATA_ENTRY
  这个结构体就是目录资源的三级目录下的子目录,里面存储的就是资源文件的信息,如OffsetToData字段存储的就是资源文件的RVA地址,它指向了资源的二进制信息,Size字段存储的就是资源文件的大小,CodePage字段存储资源的代码页但大多数情况为0。
  注:在其指向的资源数据中,字符串都是Unicode的编码方式,每个字符都是由一个16位(一个单字)的值表示,并且都是以UNICODE_NULL结束(其实就是两个0x00)。
  IMAGE_RESOURCE_DIR_STRING_U
  该结构体就是目录资源的名称结构,里面存在两个字段,都是2个字节,Length字段存储的是目录资源名称的长度,以2个字节为单位。NameString字段是一个Unicode字符串的第一个字符,并不以0结尾,其长度是由Length字段限制。该结构的总大小并不是表面上的4个字节,而是根据名字长度变化的,计算方式为:Size = SizeOf(WCHAR) * (Length + 1); 这里的1是Length字段的大小。

1.2.2.1.4 IMAGE_BASE_RELOCATION——重定位表
1
2
3
4
5
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; 重定位数据所在页的RVA
DWORD SizeOfBlock; 当前页中重定位数据块的大小
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

重定位表简介:正如我们所知,在程序运行时系统首先会给程序分配一个4GB的虚拟内存空间,低2G空间用于放置EXE文件和DLL文件,高2G空间则是用于取得程序使用(这个空间所有程序共享)。系统随后就会将EXE文件第一个贴入低2G空间占据文件指定的ImageBase,所以EXE文件有时会没有重定位表,因为ImageBase区域大多数情况是可以使用的,也就不需要重定位。贴完EXE文件后接下来就会将大量程序使用的DLL文件贴入虚拟空间,然而这些DLL文件的ImageBase可能会发生冲突,所以有些DLL文件就不会被贴入指定的地址,但是为了让程序正常运行就只能将这些DLL贴入其他的地址。但是在PE文件中很多地址都是被编译器写死固定的(例子在下方代码块),如果基址改变这些地址就会无法使用,为了避免这样的事情发生就需要修正这些固定的地址,所以就有了重定位表。重定位表就是记录了这些需要修正的地址,在ImageBase发生改变时就会进行修正重定位表。
  修正方法:需要重定位的地址 - 以前的基址 + 当前的基址。

VirtualAddress
  这个虚拟地址是一组重定位数据的开始RVA地址,只有重定位项的有效数据加上这个值才是重定位数据真正的RVA地址。
  SizeOfBlock
  它是当前重定位块的总大小,因为VirtualAddress和SizeOfBlock都是4字节的,所以(SizeOfBlock - 8)才是该块所有重定位项的大小,(SizeOfBlock - 8) / 2就是该块所有重定位项的数目。
  重定位项
  重定位项在该结构中没有体现出来,他的位置是紧挨着这个结构的,可以把他当作一个数组,宽度为2字节,每一个重定位项分为两个部分:高4位和低12位。高4位表示了重定位数据的类型(0x00没有任何作用仅仅用作数据填充,为了4字节对齐。0x03表示这个数据是重定位数据,需要修正。0x0A出现在64位程序中,也是需要修正的地址),低12位就是重定位数据相对于VirtualAddress的偏移,也就是上面所说的有效数据。之所以是12位,是因为12位的大小足够表示该块中的所有地址(每一个数据块表示一个页中的所有重定位数据,一个页的大小位0x1000)。

注:如果修改了EXE文件的ImageBase,就要手动修复它的重定位表,因为系统会判断程序载入地址和ImageBase是否一致,如果一致就不会自动修复重定位表,双击运行时就会报错。

1.3 节表

一个节表(IMAGE_SECTION_TABLE)是由很多个节(IMAGE_SECTION_HEADER)组成的,实际上是一个IMAGE_SECTION_HEADER类型的数组,数组的成员个数被定义在IMAGE_FILE_HEADER中的NumberOfSections成员

1.3.1 IMAGE_SECTION_HEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#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
  该成员指出了该节区的属性特征。其中的不同数据位代表了不同的属性,这些数据位组合起来就是这个节的属性特征,具体数值定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
宏定义	数值	描述
0x00000001 保留
0x00000002 保留
0x00000004 保留
IMAGE_SCN_TYPE_NO_PAD 0x00000008 废弃 替换为IMAGE_SCN_ALIGN_1BYTES
0x00000010 保留
IMAGE_SCN_CNT_CODE 0x00000020 节中包含可执行代码。
IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 节中包含已初始化数据。
IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 节中包含未初始化数据。
IMAGE_SCN_LNK_OTHER 0x00000100 保留
IMAGE_SCN_LNK_INFO 0x00000200 节中包含注释或其他信息,对目标文件有效。
0x00000400 保留
IMAGE_SCN_LNK_REMOVE 0x00000800 该节不会成为镜像文件的一部分,对目标文件有效。
IMAGE_SCN_LNK_COMDAT 0x00001000 该节包含COMDAT数据,对目标文件有效。
0x00002000 保留
IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 重新计算异常处理TLB项中的位
IMAGE_SCN_GPREL 0x00008000 节中包含通过全局指针引用的数据。
0x00010000 保留
IMAGE_SCN_MEM_PURGEABLE 0x00020000 保留
IMAGE_SCN_MEM_LOCKED 0x00040000 保留
IMAGE_SCN_MEM_PRELOAD 0x00080000 保留
IMAGE_SCN_ALIGN_1BYTES 0x001000001字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_2BYTES 0x002000002字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_4BYTES 0x003000004字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_8BYTES 0x004000008字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_16BYTES 0x0050000016字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_32BYTES 0x0060000032字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_64BYTES 0x0070000064字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_128BYTES 0x00800000128字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_256BYTES 0x00900000256字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_512BYTES 0x00A00000512字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_1024BYTES 0x00B000001024字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_2048BYTES 0x00C000002048字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_4096BYTES 0x00D000004096字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_ALIGN_8192BYTES 0x00E000008192字节边界上对齐数据,对目标文件有效。
IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 此节包含扩展的重定位信息。
IMAGE_SCN_MEM_DISCARDABLE 0x02000000 此节可以在需要时被丢弃。
IMAGE_SCN_MEM_NOT_CACHED 0x04000000 此节无法缓存。
IMAGE_SCN_MEM_NOT_PAGED 0x08000000 此节无法分页。
IMAGE_SCN_MEM_SHARED 0x10000000 此节可以在内存中共享。
IMAGE_SCN_MEM_EXECUTE 0x20000000 此节可以作为代码执行。
IMAGE_SCN_MEM_READ 0x40000000 此节可读。
IMAGE_SCN_MEM_WRITE 0x80000000 此节可写。

1.4 实践

1.4.1 扩大最后节

  1. 在文件的最后新增4096个字节(0x1000)

  2. 修改最后一个节的SizeOfRawData和VirtualSize为N

N = max(SizeOfRawData, VirtualSize) + 0X1000

3.修改SizeOfImage的大小

4 为最后一个节添加可执行属性

在文件的最后新增4096个字节(0x1000)
QQ_1733819786056

修改最后一个节的SizeOfRawData和VirtualSize为N

img

max(5f10 , 6000 ) + 0X1000 = 7000

img

修改SizeOfImage的大小

100+4(PE指纹)+14(PE标准头)+38(镜像大小的偏移) = 150

img

c0000 + 1000 = c1000

img

1.4.2 新增节

在节表后面新增一个节,拷贝.text这个节的信息,因为我们的节和它一样也是可执行的

QQ_1733819813989

Name改为.abcd

VirtualSize内存中块大小:0x1000H

VirtaualAddress内存中块RVA值:最后一个节rsrc的VirtaualAddress + VirtualSize = BA000 + 7000 = C1000

SizeOfRawData文件中块大小:0x1000H

PointerToRawData文件中块偏移:最后一个节rsrc的PointerToRawData + SizeOfRawData = A9000 + 7000 = B0000

后面的和.text一样即可

修改标准PE头中的块数目(NumberOfSections) 4改成5

修改SizeOfImage的大小 原值+0x1000

在文件的最后插入0x1000字节


PE文件结构
http://example.com/2024/12/24/PE文件结构/
作者
QYQS
发布于
2024年12月24日
许可协议