PE 文件的学习:

1.了解

PE( Portable Execute)文件是Windows下可执行文件的总称,常见的有 DLL,EXE,OCX,SYS 等。它是微软在 UNIX 平台的 COFF(通用对象文件格式)基础上制作而成。最初设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在 Windows 系列操作系统下。**PE文件是指 32 位可执行文件,也称为PE32。64位的可执行文件称为 PE+ 或 PE32+,是PE(PE32)的一种扩展形式(请注意不是PE64)**。

事实上,一个文件是否是 PE 文件与其扩展名无关,PE文件可以是任何扩展名。那 Windows 是怎么区分可执行文件和非可执行文件的呢?我们调用 LoadLibrary 传递了一个文件名,系统是如何判断这个文件是一个合法的动态库呢?这就涉及到PE文件结构了。

2.结构学习

一、DOS头

DOS头 的作用是兼容 MS-DOS 操作系统中的可执行文件,对于 32位PE文件来说,DOS 所起的作用就是显示一行文字,提示用户:我需要在32位windows上才可以运行。我认为这是个善意的玩笑,因为他并不像显示的那样不能运行,其实已经运行了,只是在 DOS 上没有干用户希望看到的工作而已,好吧,我承认这不是重点。但是,至少我们看一下这个头是如何定义的:

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 header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

二、NT头

顺着 DOS 头中的 e_lfanew,我们很容易可以找到 NT头,这个才是 32位PE文件中最有用的头,定义如下:

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

类似于 DOS头中的 e_magic,其高16位是0,低16是0x4550,用字符表示是 ‘PE‘ 。⭐⭐⭐

2.IMAGE_FILE_HEADER(文件头)
1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;⭐⭐⭐
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

Machine:该文件的运行平台,是 x86、x64 还是 I64 等等;

NumberOfSections: 该PE文件中有多少个节,也就是节表中的项数。
TimeDateStamp: PE文件的创建时间,一般有连接器填写。
PointerToSymbolTable: COFF文件符号表在文件中的偏移。
NumberOfSymbols: 符号表的数量。
SizeOfOptionalHeader: 紧随其后的可选头的大小。
Characteristics: 可执行文件的属性

3.IMAGE_OPTIONAL_HEADER(可选头)

别看他名字叫可选头,其实一点都不能少,不过,它在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。为了简单起见,我们只看32位。

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
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD 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;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
  • Magic:表示可选头的类型。

    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
  • MajorLinkerVersionMinorLinkerVersion:链接器的版本号。

  • SizeOfCode:代码段的长度,如果有多个代码段,则是代码段长度的总和。

  • SizeOfInitializedData:初始化的数据长度。

  • SizeOfUninitializedData:未初始化的数据长度。

  • AddressOfEntryPoint:程序入口的 RVA,对于exe这个地址可以理解为WinMain的RVA。对于DLL,这个地址可以理解为DllMain的RVA,如果是驱动程序,可以理解为DriverEntry的RVA。当然,实际上入口点并非是WinMain,DllMain和DriverEntry,在这些函数之前还有一系列初始化要完成,当然,这些不是本文的重点。⭐⭐⭐

  • BaseOfCode:代码段起始地址的RVA。

  • BaseOfData:数据段起始地址的RVA。

  • ImageBase:映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。⭐⭐⭐

  • SectionAlignment:节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。⭐⭐⭐

  • FileAlignment:节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。

  • MajorOperatingSystemVersion、MinorOperatingSystemVersion:所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。

  • MajorImageVersionMinorImageVersion:映象的版本号,这个是开发者自己指定的,由连接器填写。

  • MajorSubsystemVersionMinorSubsystemVersion:所需子系统版本号。

  • Win32VersionValue:保留,必须为0。

  • SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。

  • SizeOfHeaders:所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。

  • CheckSum:映象文件的校验和。

  • Subsystem:运行该PE文件所需的子系统。

  • SizeOfStackReserve:运行时为每个线程栈保留内存的大小。

  • SizeOfStackCommit:运行时每个线程栈初始占用内存大小。

  • SizeOfHeapReserve:运行时为进程堆保留内存大小。

  • SizeOfHeapCommit:运行时进程堆初始占用内存大小。

  • LoaderFlags:保留,必须为0。

  • NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数。

  • ⭐⭐⭐⭐⭐DataDirectory:数据目录,这是一个数组,数组的项定义如下:

    1
    2
    3
    4
    typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress; //是一个RVA的偏移地址
    DWORD Size; // 对应表的大小
    } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

    这两个数有什么用呢 ?一个是地址,一个是大小,可以看出这个数据目录项定义的是一个区域那他定义的是什么东西的区域呢?前面说了,DataDirectory 是个数组数组中的每一项对应一个特定的数据结构包括导入表,导出表等等根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory ⭐⭐⭐   
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory ⭐⭐⭐
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory ⭐⭐
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table ⭐⭐ //IAT
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor

三.IMAGE_SECTION_HEADER(节表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _IMAGE_SECTION_HEADER {
Name //8个字节的块名
union
{
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; //区块尺寸
DWORD VirtualAddress; //区块的RVA地址
DWORD SizeOfRawData; //在文件中对齐后的尺寸
DWORD PointerToRawData; //在文件中偏移
DWORD PointerToRelocations; //在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; //行号表的偏移(供调试使用地)
WORD NumberOfRelocations; //在OBJ文件中使用,重定位项数目
WORD NumberOfLinenumbers; //行号表中行号的数目
DWORD Characteristics; //区块属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

四.节

每个区块的名称都是唯一的,不能有同名的两个区块。
但事实上节的名称不表示任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” (一般为.text)或者说将包含数据的区块命名为“.Code”(一般为.rdata等) 都是合法的。
当我们要从PE 文件中读取需要的区块的时候,不能以区块的名称作为定位的标准和依据,正确方法是

在节头(IMAGE_SECTION_HEADER)中。要定位节的地址,你可以按照以下步骤进行:

  1. 获取文件头:从 PE 文件的起始部分读取 DOS 头(IMAGE_DOS_HEADER),然后找到 PE 头的位置。
  2. 读取可选头:从 PE 头中读取可选头(IMAGE_OPTIONAL_HEADER32),确认文件类型(如 PE32)。
  3. 读取节表:可选头后面会跟着一个或多个节头。节头的数量在可选头中指定,通常可以通过 NumberOfSections 字段获取。
  4. 计算节的地址:每个节头都有一个 VirtualAddress 字段,表示该节在内存中的地址。你可以通过遍历节头来查找特定节的信息。
  5. 加载地址:当 PE 文件被加载到内存中时,你可以将 VirtualAddress 和基址相加,以确定节的实际内存地址。

各种节的描述:

五.其他注意

1.RVA 和文件偏移换算

在 PE 文件格式中,RVA(Relative Virtual Address)和文件偏移之间的换算是非常重要的。以下是相关术语和它们在 PE 文件结构中的定义,以及如何进行换算的方法:

术语及其定义

  1. RVA(Relative Virtual Address)

    • 定义:相对虚拟地址,是指某个节或数据在内存中的相对地址,通常是相对于加载基址(ImageBase)的偏移。
    • 结构:在 IMAGE_SECTION_HEADER 中的 VirtualAddress 字段。
  2. FOA(File Offset Address)

    • 定义:文件偏移地址,指的是文件中某个特定数据或结构的偏移位置。
    • 结构:在 IMAGE_SECTION_HEADER 中的 PointerToRawData 字段。
  3. ImageBase

    • 定义:基址,指 PE 文件在内存中加载时的起始地址。
    • 结构:在 IMAGE_OPTIONAL_HEADER 中的 ImageBase 字段。

RVA 与 FOA 之间的换算

1. 从 RVA 转换到 FOA

给定一个 RVA,可以使用以下公式转换为 FOA:

1
FOA = RVA - Section.VirtualAddress + Section.PointerToRawData
  • 解释
    • Section.VirtualAddress:对应节头中的 VirtualAddress 字段。
    • Section.PointerToRawData:对应节头中的 PointerToRawData 字段。

2. 从 FOA 转换到 RVA

给定一个 FOA,可以使用以下公式转换为 RVA:

1
RVA = FOA - Section.PointerToRawData + Section.VirtualAddress

示例

假设有以下节头信息:

  • 节名.text
  • VirtualAddress0x1000
  • PointerToRawData0x200

那么,对于某个特定的 RVA 0x1010,可以计算 FOA 如下:

1
FOA = 0x1010 - 0x1000 + 0x200 = 0x210

如果要将 FOA 0x210 转换回 RVA:

1
RVA = 0x210 - 0x200 + 0x1000 = 0x1010

总结

  • RVA 是节在内存中的相对虚拟地址。
  • FOA 是节在文件中的具体偏移。
  • 使用 IMAGE_SECTION_HEADER 中的 VirtualAddressPointerToRawData 字段进行 RVA 和 FOA 之间的换算。

这些计算在 PE 文件的解析、调试和分析中是非常重要的,理解这些术语及其对应的结构定义将有助于处理 PE 文件格式。

同时计算时也要考虑对齐会对RVA 与 FOA 之间的换算影响

节对齐和文件对齐是 PE 文件格式中两个重要的概念,它们的主要区别如下:

节对齐(Section Alignment)

  1. 定义

    • 节对齐是指在 PE 文件加载到内存中时,节在内存中的对齐要求。它决定了每个节的起始地址必须是某个特定值的倍数。
  2. 位置

    • 节对齐的值在 IMAGE_OPTIONAL_HEADER 结构中,字段名称为 SectionAlignment
  3. 作用

    • 确保加载到内存中的节按页对齐,以优化内存访问和提高性能。通常,SectionAlignment 的值为 4096 字节(一个内存页的大小)。
  4. 加载时的对齐

    • 节对齐是在 PE 文件加载到内存后生效的。加载过程会根据 SectionAlignment 进行调整。

文件对齐(File Alignment)

  1. 定义

    • 文件对齐是指在 PE 文件中,节在文件中的对齐要求。它决定了每个节在文件中开始的位置必须是某个特定值的倍数。
  2. 位置

    • 文件对齐的值同样在 IMAGE_OPTIONAL_HEADER 结构中,字段名称为 FileAlignment
  3. 作用

    • 确保在 PE 文件中,节的起始位置按照 FileAlignment 对齐,以便于文件的读取和解析。通常,FileAlignment 的值为 512 字节或 4096 字节。
  4. 未加载时的对齐

    • 文件对齐是在 PE 文件未加载时适用的。它影响文件的存储格式,而不是内存中的布局。

总结

  • 节对齐:适用于 PE 文件加载到内存后的内存布局,确保节按页对齐。
  • 文件对齐:适用于 PE 文件的存储格式,确保节在文件中按字节对齐。

因此,节对齐和文件对齐是针对不同上下文的对齐要求,它们的值和作用有所不同。

2.PE文件与内存映射

就是把 PE 文件 硬盘中 放到 *内存中***,然后 CPU 从 内存中读取指令并执行。

文件中使用偏移(offset),内存中使用 VA(Virtual Address,虚拟地址)来表示位置。

VA 指进程虚拟内存的绝对地址RVA(Relative Virtual Address,相对虚拟地址)是指从某基准位置(ImageBase)开始的相对地址。VA 与 RVA 满足下面的换算关系: RVA + ImageBase = VA


PE 头内部信息大多是 RVA 形式存在。
原因在于(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他的 PE文件(DLL)。
此时必须通过重定向(Relocation)将其加载到其他空白的位置,若 PE头信息使用的是 VA,则无法正常访问。
因此使用 RVA 来重定向信息,即使发生了重定向,只要相对于基准位置的相对位置没有变化,就能正常访问到指定信息,不会出现任何问题。


**当 PE 文件被执行时,PE 装载器会为 *进程* 分配 4GB 的 *虚拟地址空间**( Virtual address spaces 官方文档:**https://docs.microsoft.com/zh-tw/windows-hardware/drivers/gettingstarted/virtual-address-spaces\* *)*,然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到 *虚拟地址空间* 中的 0X400000的位置。

Last

写的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
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
93
94
95
96
97
98
99
100
101
#include <iostream>
#include <fstream>
#include <windows.h>

using namespace std;

void PrintDataDirectory(IMAGE_DATA_DIRECTORY dir, const char* name) {
cout << name << " - RVA: 0x" << hex << dir.VirtualAddress << ", Size: 0x" << dir.Size << dec << endl;
}

void ParsePEFile(const char* filePath) {
// 打开文件
ifstream file(filePath, ios::binary | ios::in);
if (!file) {
cerr << "无法打开文件: " << filePath << endl;
return;
}

// 读取DOS头
IMAGE_DOS_HEADER dosHeader;
file.read(reinterpret_cast<char*>(&dosHeader), sizeof(IMAGE_DOS_HEADER));
if (dosHeader.e_magic != IMAGE_DOS_SIGNATURE) {
cerr << "不是有效的PE文件" << endl;
return;
}

// 跳转到PE头
file.seekg(dosHeader.e_lfanew, ios::beg);
DWORD signature;
file.read(reinterpret_cast<char*>(&signature), sizeof(DWORD));

if (signature != IMAGE_NT_SIGNATURE) {
cerr << "PE签名无效" << endl;
return;
}

// 读取文件头
IMAGE_FILE_HEADER fileHeader;
file.read(reinterpret_cast<char*>(&fileHeader), sizeof(IMAGE_FILE_HEADER));

// 读取可选头(32位/64位)
IMAGE_OPTIONAL_HEADER optionalHeader;
file.read(reinterpret_cast<char*>(&optionalHeader), sizeof(IMAGE_OPTIONAL_HEADER));

cout << "PE文件解析:" << filePath << endl;
cout << "-----------------------------------" << endl;

// 输出加载基地址、入口地址
cout << "加载基地址: 0x" << hex << optionalHeader.ImageBase << endl;
cout << "入口地址: 0x" << hex << optionalHeader.AddressOfEntryPoint << endl;

// 判断是否启用了DEP、ASLR、控制流保护、SEH
bool depEnabled = (optionalHeader.DllCharacteristics & IMAGE_DLLCHARACTERISTICS_NX_COMPAT);
bool aslrEnabled = (optionalHeader.DllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE);
bool cfGuardEnabled = (optionalHeader.DllCharacteristics & IMAGE_DLLCHARACTERISTICS_GUARD_CF);
bool sehEnabled = !(fileHeader.Characteristics & IMAGE_FILE_DLL); // SEH一般禁用DLL

cout << "DEP启用: " << (depEnabled ? "是" : "否") << endl;
cout << "ASLR启用: " << (aslrEnabled ? "是" : "否") << endl;
cout << "控制流保护启用: " << (cfGuardEnabled ? "是" : "否") << endl;
cout << "SEH启用: " << (sehEnabled ? "是" : "否") << endl;

// 输出数据目录项信息
cout << "-----------------------------------" << endl;
cout << "数据目录项:" << endl;

PrintDataDirectory(optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT], "Export Table");
PrintDataDirectory(optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT], "Import Table");
PrintDataDirectory(optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE], "Resource Table");
PrintDataDirectory(optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION], "Exception Table");
PrintDataDirectory(optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_SECURITY], "Security Table");
PrintDataDirectory(optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC], "Base Relocation Table");
PrintDataDirectory(optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG], "Debug Directory");
PrintDataDirectory(optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS], "TLS Table");

// 输出所有节的名字、相对偏移、大小
cout << "-----------------------------------" << endl;
cout << "节信息:" << endl;

file.seekg(dosHeader.e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) + fileHeader.SizeOfOptionalHeader, ios::beg);
IMAGE_SECTION_HEADER sectionHeader;
for (int i = 0; i < fileHeader.NumberOfSections; i++) {
file.read(reinterpret_cast<char*>(&sectionHeader), sizeof(IMAGE_SECTION_HEADER));
cout << "节名: " << string(reinterpret_cast<char*>(sectionHeader.Name), 8) << endl;
cout << "相对虚拟地址: 0x" << hex << sectionHeader.VirtualAddress << endl;
cout << "大小: 0x" << hex << sectionHeader.Misc.VirtualSize << dec << endl;
cout << "-----------------------------------" << endl;
}

file.close();
}

int main(int argc, char* argv[]) {
if (argc != 2) {
cerr << "用法: " << argv[0] << " <PE文件路径>" << endl;
return 1;
}
ParsePEFile(argv[1]);
return 0;
}

1
使用命令:./pe.exe target.exe