PE结构学习

前言

这篇是关于PE结构学习的笔记,参考了两篇文章做了个综合

PE文件解析

0x01可执行文件

文件格式一般是指数据信息在计算机中存储的格式,不同计算机操作系统,不同应用的文件,在计算机中存储都有可能是不同的

可执行文件:一般是指可以被计算机操作系统识别并执行的文件

windows: 采用PE(Portable Executable)格式

linux:采用ELF(Executable and Linking Format)格式

0x02 PE文件格式

PE文件,常见的如:exe,dll,ocx,sys,com等文件后缀的文件都属于PE格式

0x03 地址的基本概念

VA(Virtual Address):虚拟地址
PE 文件映射到内存空间时,数据在内存空间中对应的地址。

ImageBase:映射基址
PE 文件在内存空间中的映射起始位置,是个 VA 地址。

RVA(Relative Virtual Address):相对虚拟地址
PE 文件在内存中的 VA 相对于 ImageBase 的偏移量。

FOA(File Offset Address,FOA):文件偏移地址
PE 文件在磁盘上存放时,数据相对于文件开头位置的偏移量,文件偏移地址等于文件地址。

节偏移 : 节偏移=RVA-文件偏移

转换关系:VA = ImageBase + RVA

文件偏移地址 = VA – ImageBase – 节偏移 = RVA – 节偏移

0x04 PE文件格式

PE 文件结构

0x05 DOS头

DOS头在winnt.h文件中定义如下:

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头部
USHORT e_magic; '// DOS签名“MZ-->Mark Zbikowski(设计了DOS的工程师)” -> 4D 5A '
USHORT e_cblp; // 文件最后页的字节数 -> 00 90 -> 144
USHORT e_cp; // 文件页数 -> 00 30 -> 48
USHORT e_crlc; // 重定义元素个数 -> 00 00
USHORT e_cparhdr; // 头部尺寸,以段落为单位 -> 00 04
USHORT e_minalloc; // 所需的最小附加段 -> 00 00
USHORT e_maxalloc; // 所需的最大附加段 -> FF FF
USHORT e_ss; // 初始的SS值(相对偏移量) -> 00 00
USHORT e_sp; // 初始的SP值 -> 00 B8 -> 184
USHORT e_csum; // 校验和 -> 00 00
USHORT e_ip; // 初始的IP值 -> 00 00
USHORT e_cs; // 初始的CS值(相对偏移量) -> 00 00
USHORT e_lfarlc; // 重分配表文件地址 -> 00 40 -> 64
USHORT e_ovno; // 覆盖号 -> 00 00
USHORT e_res[4]; // 保留字 -> 00 00 00 00 00 00 00 00
USHORT e_oemid; // OEM标识符(相对e_oeminfo) -> 00 00
USHORT e_oeminfo; // OEM信息 -> 00 00
USHORT e_res2[10]; // 保留字 -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
LONG e_lfanew; '// 指示NT头的偏移(根据不同文件拥有可变值) -> 00 00 00 C0 -> 192' PE头对于文件的偏移地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS头 占用64字节

dos头是PE文件结构的第一个头,用于保存对DOS系统的兼容,并且用于定位真正的PE头。图中的B0 00 00 00也就是e_lfanew指向了PE头的偏移。

查看e_lfanew指向位置是否为50 45 00 00即可判断是否为一个PE文件。

DOS头中4D 5Ae_lfanewB0 00 00 00中间数据和e_lfanew到指向PE头偏移位置可有可无。

0x06 DOS_STUB

_IMAGE_DOS_STUB 结构:
该结构未在 winnt.h 中定义,其内容随着链接时使用的链接器不同而不同,通常用于保存在 DOS 环境中的可执行代码。
例如:该结构中的代码用于显示字符串:”This program cannot run in DOS mode”。

0x07 NT_HEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature; // 类似于DOS头中的e_magic -> 00 00 45 50 -> PE标识符
IMAGE_FILE_HEADER FileHeader; // IMAGE_FILE_HEADER是PE文件头,定义如下
IMAGE_OPTIONAL_HEADER64 OptionalHeader; // 可选头
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // 类似于DOS头中的e_magic -> 00 00 45 50 -> PE标识符
IMAGE_FILE_HEADER FileHeader; // IMAGE_FILE_HEADER是PE文件头,定义如下
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#endif

PE 文件使用 _IMAGE_NT_HEADER 结构提供程序在 Windows 系统中的执行环境。
可以看到 _IMAGE_NT_HEADER 结构针对 32 位和 64 位有不同的定义。

0x08 FILE_HEADER

  • IMAGE_FILE_HEADER:其中有4个重要的成员,若设置不正确,将会导致文件无法正常运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_FILE_HEADER { 
WORD Machine; '// 每个CPU拥有唯一的Machine码 -> 4C 01 -> PE -> 兼容32位Intel X86芯片'

WORD NumberOfSections; '// 指文件中存在的节段(又称节区)数量,也就是节表中的项数 -> 00 04 -> 4
// 该值一定要大于0,且当定义的节段数与实际不符时,将发生运行错误。'

DWORD TimeDateStamp; // PE文件的创建时间,一般有连接器填写 -> 38 D1 29 1E
DWORD PointerToSymbolTable; // COFF 符号表的 RVA 偏移量,如果 COFF 符号表不存在,则该值为 -> 00 00 00 00
DWORD NumberOfSymbols; // 符号表的数量 -> 00 00 00 00

WORD SizeOfOptionalHeader; '// 指出IMAGE_OPTIONAL_HEADER32结构体的长度。-> 00 E0 -> 224字节
// PE32+格式文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,
// 这两个结构体尺寸是不相同的,所以需要在SizeOfOptionalHeader中指明大小。'

WORD Characteristics; '// 标识文件的属性,exe则为010f dll文件则为210e

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

0x09 可选头:OPTIONAL_HEADER

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
typedef struct _IMAGE_OPTIONAL_HEADER { 
//standard fields
WORD Magic; '// 魔数 32位为0x10B,64位为0x20B,ROM镜像为0x107'
BYTE MajorLinkerVersion; // 链接器的主版本号 -> 05
BYTE MinorLinkerVersion; // 链接器的次版本号 -> 0C
DWORD SizeOfCode; // 代码节大小,一般放在“.text”节里,必须是FileAlignment的整数倍 -> 40 00 04 00
DWORD SizeOfInitializedData; // 已初始化数大小,一般放在“.data”节里,必须是FileAlignment的整数倍 -> 40 00 0A 00
DWORD SizeOfUninitializedData; // 未初始化数大小,一般放在“.bss”节里,必须是FileAlignment的整数倍 -> 00 00 00 00
DWORD AddressOfEntryPoint; '// 指出程序最先执行的代码起始地址(RVA) -> 00 00 10 00',对于dll文件来说如果没有入口函数这个值为0,对于驱动该值时初始化函数的地址。
DWORD BaseOfCode; // 代码基址,当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 10 00

DWORD BaseOfData; // 数据基址,当镜像被加载进内存时数据节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 20 00
// 在64位文件中此处被并入紧随其后的ImageBase中。

DWORD ImageBase; '// 当加载进内存时,镜像的第1个字节的首选地址。
// WindowEXE默认ImageBase值为00400000,DLL文件的ImageBase值为10000000,也可以指定其他值。
// 执行PE文件时,PE装载器先创建进程,再将文件载入内存,
// 然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint'
//NT additional fields
'// PE文件的Body部分被划分成若干节段,这些节段储存着不同类别的数据。'
DWORD SectionAlignment; '// SectionAlignment指定了节段在内存中的最小单位, -> 00 00 10 00'
DWORD FileAlignment; '// FileAlignment指定了节段在磁盘文件中的最小单位,-> 00 00 02 00
// SectionAlignment必须大于或者等于FileAlignment'

WORD MajorOperatingSystemVersion;// 主系统的主版本号 -> 00 04
WORD MinorOperatingSystemVersion;// 主系统的次版本号 -> 00 00
WORD MajorImageVersion; // 镜像的主版本号 -> 00 00
WORD MinorImageVersion; // 镜像的次版本号 -> 00 00
WORD MajorSubsystemVersion; // 子系统的主版本号 -> 00 04
WORD MinorSubsystemVersion; // 子系统的次版本号 -> 00 00
DWORD Win32VersionValue; // 保留,必须为0 -> 00 00 00 00

DWORD SizeOfImage; '// 当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。
// 一般文件大小与加载到内存中的大小是不同的。 -> 00 00 50 00'

DWORD SizeOfHeaders; '// 所有头的总大小,向上舍入为FileAlignment的倍数。
// 可以以此值作为PE文件第一节的文件偏移量。-> 00 00 04 00'

DWORD CheckSum; // 镜像文件的校验和 -> 00 00 B4 99

WORD Subsystem; '// 运行此镜像所需的子系统 -> 00 02 -> 窗口应用程序
// 用来区分系统驱动文件(*.sys)与普通可执行文件(*.exe,*.dll),
// 参考:https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.3'

WORD DllCharacteristics; // DLL标识 -> 00 00
DWORD SizeOfStackReserve; // 最大栈大小。CPU的堆栈。默认是1MB。-> 00 10 00 00
DWORD SizeOfStackCommit; // 初始提交的堆栈大小。默认是4KB -> 00 00 10 00
DWORD SizeOfHeapReserve; // 最大堆大小。编译器分配的。默认是1MB ->00 10 00 00
DWORD SizeOfHeapCommit; // 初始提交的局部堆空间大小。默认是4K ->00 00 10 00
DWORD LoaderFlags; // 保留,必须为0 -> 00 00 00 00

DWORD NumberOfRvaAndSizes; '// 指定DataDirectory的数组个数,由于以前发行的Windows NT的原因,它只能为16。 -> 00 00 00 10'
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; '// 数据目录数组。详见下文。'
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
//下面是64位的 含义都差不多只是有些 成员所占空间大小不同
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER;
#else

来看到AddressOfEntryPointImageBaseImageBase是一个内存的虚拟地址,而AddressOfEntryPoint是相对虚拟地址。真正可执行文件的虚拟地址是ImageBase+AddressOfEntryPoint。但并不是绝对的因为ImageBase是建议装载地址。如果装载地址已经被使用过了系统会重新分配一块内存。

0x10 数据目录表

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据结构(表)的起始 RVA 偏移地址。
DWORD Size; //数据结构(表)的大小。
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

PE 文件使用 _IMAGE_DATA_DIRECTORY 结构提供 PE 文件中数据结构(输入表、输出表等)的地址和大小信息。

DataDirectory[] 数据目录数组:数组每项都有被定义的值,不同项对应不同数据结构。重点关注的IMPORT和EXPORT,它们是PE头中的非常重要的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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 调试目录
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 描术字串
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 机器值
#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信息

0x11 导出表:EXPORT_DIRECTORY

一般的简单pe文件并不存在导出表,一般的dll都有导出表,因为写有导出函数可以给到别的pe文件调用功能函数

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {                  
DWORD Characteristics; // 未使用
DWORD TimeDateStamp; // 时间戳
WORD MajorVersion; // 未使用
WORD MinorVersion; // 未使用
DWORD Name; // 指向该导出表文件名字符串
DWORD Base; // 导出函数起始序号
DWORD NumberOfFunctions; // 所有导出函数的个数
DWORD NumberOfNames; // 以函数名字导出的函数个数
DWORD AddressOfFunctions; // 导出函数地址表RVA
DWORD AddressOfNames; // 导出函数名称表RVA
DWORD AddressOfNameOrdinals; // 导出函数序号表RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

0x12 导入表:IMPORT_DESCRIPTOR

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_IMPORT_DESCRIPTOR {                    
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //RVA 指向IMAGE_THUNK_DATA结构数组
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //RVA,指向dll名字,该名字以 两个字节的0结尾
DWORD FirstThunk; //RVA,指向IMAGE_THUNK_DATA结构数组
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

0x13 节表:SECTION_HEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _IMAGE_SECTION_HEADER {  
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //8个字节的块名
union
{
DWORD PhysicalAddress; //该 Section 的物理地址
DWORD VirtualSize; //该 Section 加载到内存中时的真实大小。
} Misc; //区块尺寸

DWORD VirtualAddress; //区块的起始RVA地址
DWORD SizeOfRawData; //在磁盘上占用的空间大小(对齐) 该值必须是 FileAlignment 值的整数倍。
DWORD PointerToRawData; //在文件中偏移
DWORD PointerToRelocations; //在OBJ文件中使用,重定位开头的文件指针。
DWORD PointerToLinenumbers; //行号表的偏移(供调试使用地)
WORD NumberOfRelocations; //在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers; //行号表中行号的数目
DWORD Characteristics; //区块属性如可执行、可读、可写、初始化数据、未初始化数据等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

节名称:

img

参考链接:

PE文件结构解析

DOS头分析


PE结构学习
https://coutcin-xw.github.io/2022/06/12/PE结构学习/
作者
CoutCin
发布于
2022年6月12日
许可协议