PE文件解析
字数 1322 2025-08-22 12:23:00
PE文件结构解析与操作指南
1. PE文件概述
PE (Portable Executable) 文件是Windows操作系统中可执行文件的标准格式,包括:
- 应用程序 (.exe)
- 动态链接库 (.dll)
- 驱动程序 (.sys)
PE文件主要由以下几个部分组成:
- DOS头和DOS存根
- NT头
- 数据目录
- 节表
- 节数据
2. DOS头 (IMAGE_DOS_HEADER)
DOS头是为了兼容DOS系统而保留的结构,主要包含以下重要字段:
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // Magic number "MZ" (0x5A4D)
// ... 其他字段 ...
LONG e_lfanew; // 指向NT头的偏移量
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
- e_magic: 固定为"5A4D"("MZ"),用于识别PE文件
- e_lfanew: 指向NT头的偏移量,是解析PE文件的关键
3. DOS存根
紧跟在DOS头后面,在DOS环境下运行时会显示:
"This program cannot be run in DOS mode."
4. NT头 (IMAGE_NT_HEADERS)
NT头由三部分组成:
- 签名 (Signature): 固定为"00004550"("PE\0\0")
- 文件头 (File Header)
- 可选头 (Optional Header)
4.1 文件头 (IMAGE_FILE_HEADER)
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 目标平台架构
WORD NumberOfSections; // 节的数量
DWORD TimeDateStamp; // 时间戳
DWORD PointerToSymbolTable; // 符号表指针(通常为0)
DWORD NumberOfSymbols; // 符号数量(通常为0)
WORD SizeOfOptionalHeader; // 可选头大小
WORD Characteristics; // 文件特性标志
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
4.2 可选头 (IMAGE_OPTIONAL_HEADER)
typedef struct _IMAGE_OPTIONAL_HEADER32 {
WORD Magic; // 标识32位(0x10B)或64位(0x20B)
// ... 其他字段 ...
DWORD AddressOfEntryPoint; // 程序入口点RVA
DWORD ImageBase; // 首选加载基址
DWORD SizeOfImage; // 内存中总大小(内存对齐)
DWORD SectionAlignment; // 内存对齐粒度(默认0x1000)
DWORD FileAlignment; // 文件对齐粒度(默认0x200)
DWORD NumberOfRvaAndSizes; // 数据目录项数(通常16)
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
重要字段说明:
- Magic: 0x10B表示32位PE,0x20B表示64位PE
- AddressOfEntryPoint: 程序执行的起始RVA
- ImageBase: PE文件加载到进程的基地址
- DataDirectory: 包含16个数据目录项,如导入表、导出表等
5. 数据目录 (IMAGE_DATA_DIRECTORY)
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // RVA
DWORD Size; // 大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
重要数据目录项:
- 导入表 (IMAGE_DIRECTORY_ENTRY_IMPORT)
- 导出表 (IMAGE_DIRECTORY_ENTRY_EXPORT)
- 资源表 (IMAGE_DIRECTORY_ENTRY_RESOURCE)
- 重定位表 (IMAGE_DIRECTORY_ENTRY_BASERELOC)
6. 节表 (IMAGE_SECTION_HEADER)
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节名称(如".text")
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; // 节的RVA
DWORD SizeOfRawData; // 文件中对齐后大小
DWORD PointerToRawData; // 节在文件中的偏移
// ... 其他字段 ...
DWORD Characteristics; // 节属性(可读/写/执行等)
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
常见节:
- .text: 代码段
- .data: 数据段
- .rsrc: 资源段
- .reloc: 重定位表段
7. 代码实现示例
7.1 加载PE文件到内存
DWORD FileReadSize;
char path[] = "C:\\Windows\\System32\\calc.exe";
HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
DWORD dwFileSize = GetFileSize(hFile, NULL);
LPVOID FileImage = VirtualAlloc(NULL, dwFileSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
ReadFile(hFile, FileImage, dwFileSize, &FileReadSize, NULL);
CloseHandle(hFile);
7.2 获取NT头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)FileImage;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((DWORD64)FileImage + pDosHeader->e_lfanew);
7.3 解析导入表
void PrintImportTable(PIMAGE_IMPORT_DESCRIPTOR importTable, LPVOID fileImage) {
while (importTable->Name) {
const char* dllName = (const char*)((DWORD64)fileImage + importTable->Name);
std::cout << "DLL Name: " << dllName << std::endl;
// 遍历导入的函数
PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((DWORD64)fileImage + importTable->OriginalFirstThunk);
if (!thunk) {
thunk = (PIMAGE_THUNK_DATA)((DWORD64)fileImage + importTable->FirstThunk);
}
while (thunk->u1.AddressOfData) {
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) {
std::cout << " Ordinal: " << IMAGE_ORDINAL(thunk->u1.Ordinal) << std::endl;
} else {
PIMAGE_IMPORT_BY_NAME importByName = (PIMAGE_IMPORT_BY_NAME)((DWORD64)fileImage + thunk->u1.AddressOfData);
std::cout << " Function: " << importByName->Name << std::endl;
}
thunk++;
}
importTable++;
}
}
// 使用示例
IMAGE_DATA_DIRECTORY dataDir = pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
PIMAGE_IMPORT_DESCRIPTOR importTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD64)FileImage + dataDir.VirtualAddress);
PrintImportTable(importTable, FileImage);
7.4 遍历节表
WORD sectionNum = pNtHeader->FileHeader.NumberOfSections;
PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((DWORD64)pNtHeader + sizeof(IMAGE_NT_HEADERS));
for (size_t i = 0; i < sectionNum; i++) {
PIMAGE_SECTION_HEADER currentPSecHeader = pSecHeader + i;
std::cout << "Section Name: " << currentPSecHeader->Name << std::endl;
std::cout << "Virtual Address: 0x" << std::hex << currentPSecHeader->VirtualAddress << std::endl;
std::cout << "Size: " << std::dec << currentPSecHeader->SizeOfRawData << " bytes" << std::endl;
}
8. 关键概念总结
- RVA (Relative Virtual Address): 相对于ImageBase的偏移地址
- VA (Virtual Address): 实际内存地址 = ImageBase + RVA
- FOA (File Offset Address): 文件中的偏移地址
- 内存对齐与文件对齐: 通常内存对齐为0x1000,文件对齐为0x200
- 导入表与导出表: 分别处理依赖函数和导出函数
- 重定位: 当PE文件无法加载到首选基址时需要的地址修正
通过理解PE文件结构,可以深入分析Windows可执行文件的工作原理,为逆向工程、安全分析和软件开发打下坚实基础。