Windows PE结构
1、Windows PE文件基础知识
1、课程目标
- 理解PE文件格式的历史和作用
- 掌握PE文件的整体结构
- 学会使用工具查看PE文件
- 理解RVA和FOA的概念和转换
2、名词解释
| 名词 | 全称 | 解释 |
|---|---|---|
| PE | Portable Executable | 可移植可执行文件格式 |
| COFF | Common Object File Format | 通用目标文件格式 |
| RVA | Relative Virtual Address | 相对虚拟地址 |
| VA | Virtual Address | 虚拟地址 |
| FOA | File Offset Address | 文件偏移地址 |
| ImageBase | - | 映像基址 |
| Section | 区段/节 | PE文件的数据分区 |
| Header | 头部 | PE文件头信息 |
3、使用工具
- CFF Explorer(PE编辑器)
- PE-bear
- PEview
- x64dbg
- Visual Studio(dumpbin工具)
- 010 Editor(PE模板)
4、技术原理
4.1、PE文件结构概览
+---------------------------+ 0x0000
| DOS Header |
| (64 bytes) |
+---------------------------+ 0x0040
| DOS Stub |
| (可选,通常包含 |
| "This program..."消息) |
+---------------------------+ e_lfanew指向
| PE Signature | "PE\0\0"
| (4 bytes) |
+---------------------------+
| File Header |
| (20 bytes) |
+---------------------------+
| Optional Header |
| (PE32: 224 bytes |
| PE32+: 240 bytes) |
+---------------------------+
| Section Headers |
| (每个40 bytes) |
+---------------------------+
| |
| Section Data |
| (.text, .data, |
| .rdata, .rsrc等) |
| |
+---------------------------+
4.2、PE文件类型
文件扩展名 类型描述
.exe 可执行文件
.dll 动态链接库
.sys 驱动程序
.ocx ActiveX控件
.scr 屏幕保护程序
.cpl 控制面板程序
.efi EFI应用程序
4.3、RVA与FOA转换
VA = ImageBase + RVA
RVA = VA - ImageBase
FOA = RVA - Section.VirtualAddress + Section.PointerToRawData
转换条件:
RVA必须落在某个区段的范围内:
Section.VirtualAddress <= RVA < Section.VirtualAddress + Section.VirtualSize
5、代码实现
5.1、PE文件基础结构定义
#include <windows.h>
#include <stdio.h>
// 读取PE文件
LPVOID LoadPEFile(const char* filePath, DWORD* fileSize) {
HANDLE hFile = CreateFileA(
filePath,
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] 无法打开文件: %s\n", filePath);
return NULL;
}
*fileSize = GetFileSize(hFile, NULL);
LPVOID fileData = VirtualAlloc(
NULL,
*fileSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
if (!fileData) {
CloseHandle(hFile);
return NULL;
}
DWORD bytesRead;
if (!ReadFile(hFile, fileData, *fileSize, &bytesRead, NULL)) {
VirtualFree(fileData, 0, MEM_RELEASE);
CloseHandle(hFile);
return NULL;
}
CloseHandle(hFile);
return fileData;
}
// 验证PE文件
BOOL ValidatePEFile(LPVOID fileData) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
// 检查DOS签名
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
printf("[-] 无效的DOS签名: 0x%04X\n", dosHeader->e_magic);
return FALSE;
}
// 获取NT头
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
(PBYTE)fileData + dosHeader->e_lfanew
);
// 检查PE签名
if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) {
printf("[-] 无效的PE签名: 0x%08X\n", ntHeaders->Signature);
return FALSE;
}
printf("[+] 有效的PE文件\n");
return TRUE;
}
// 判断PE位数
BOOL IsPE64(LPVOID fileData) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
(PBYTE)fileData + dosHeader->e_lfanew
);
return (ntHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC);
}
5.2、RVA到FOA转换
// 获取区段头数组
PIMAGE_SECTION_HEADER GetSectionHeaders(LPVOID fileData) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
(PBYTE)fileData + dosHeader->e_lfanew
);
return IMAGE_FIRST_SECTION(ntHeaders);
}
// 获取区段数量
WORD GetNumberOfSections(LPVOID fileData) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
(PBYTE)fileData + dosHeader->e_lfanew
);
return ntHeaders->FileHeader.NumberOfSections;
}
// RVA转FOA
DWORD RvaToFoa(LPVOID fileData, DWORD rva) {
PIMAGE_SECTION_HEADER sections = GetSectionHeaders(fileData);
WORD numSections = GetNumberOfSections(fileData);
// 遍历所有区段
for (WORD i = 0; i < numSections; i++) {
DWORD sectionStart = sections[i].VirtualAddress;
DWORD sectionEnd = sectionStart + sections[i].Misc.VirtualSize;
// 检查RVA是否在此区段范围内
if (rva >= sectionStart && rva < sectionEnd) {
// 计算FOA
DWORD offset = rva - sections[i].VirtualAddress;
return sections[i].PointerToRawData + offset;
}
}
// RVA在头部区域(未映射到区段)
// 直接返回RVA作为FOA
return rva;
}
// FOA转RVA
DWORD FoaToRva(LPVOID fileData, DWORD foa) {
PIMAGE_SECTION_HEADER sections = GetSectionHeaders(fileData);
WORD numSections = GetNumberOfSections(fileData);
for (WORD i = 0; i < numSections; i++) {
DWORD sectionStart = sections[i].PointerToRawData;
DWORD sectionEnd = sectionStart + sections[i].SizeOfRawData;
if (foa >= sectionStart && foa < sectionEnd) {
DWORD offset = foa - sections[i].PointerToRawData;
return sections[i].VirtualAddress + offset;
}
}
return foa;
}
// 通过RVA获取数据指针
LPVOID GetDataByRva(LPVOID fileData, DWORD rva) {
DWORD foa = RvaToFoa(fileData, rva);
return (LPVOID)((PBYTE)fileData + foa);
}
5.3、PE文件信息打印
// 打印PE基本信息
void PrintPEInfo(LPVOID fileData, DWORD fileSize) {
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileData;
printf("\n=== DOS Header ===\n");
printf("e_magic: 0x%04X (MZ)\n", dosHeader->e_magic);
printf("e_lfanew: 0x%08X\n", dosHeader->e_lfanew);
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
(PBYTE)fileData + dosHeader->e_lfanew
);
printf("\n=== PE Signature ===\n");
printf("Signature: 0x%08X (PE\\0\\0)\n", ntHeaders->Signature);
// 判断位数
BOOL is64 = IsPE64(fileData);
printf("\n=== File Header ===\n");
printf("Machine: 0x%04X (%s)\n",
ntHeaders->FileHeader.Machine,
ntHeaders->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 ? "x64" :
ntHeaders->FileHeader.Machine == IMAGE_FILE_MACHINE_I386 ? "x86" : "Unknown");
printf("NumberOfSections: %d\n", ntHeaders->FileHeader.NumberOfSections);
printf("TimeDateStamp: 0x%08X\n", ntHeaders->FileHeader.TimeDateStamp);
printf("SizeOfOptionalHeader: %d\n", ntHeaders->FileHeader.SizeOfOptionalHeader);
printf("Characteristics: 0x%04X\n", ntHeaders->FileHeader.Characteristics);
// 解析特征
WORD chars = ntHeaders->FileHeader.Characteristics;
printf(" -> ");
if (chars & IMAGE_FILE_EXECUTABLE_IMAGE) printf("EXECUTABLE ");
if (chars & IMAGE_FILE_DLL) printf("DLL ");
if (chars & IMAGE_FILE_LARGE_ADDRESS_AWARE) printf("LARGE_ADDRESS_AWARE ");
if (chars & IMAGE_FILE_32BIT_MACHINE) printf("32BIT ");
printf("\n");
if (is64) {
PIMAGE_NT_HEADERS64 nt64 = (PIMAGE_NT_HEADERS64)ntHeaders;
PIMAGE_OPTIONAL_HEADER64 opt = &nt64->OptionalHeader;
printf("\n=== Optional Header (PE32+) ===\n");
printf("Magic: 0x%04X (PE32+)\n", opt->Magic);
printf("AddressOfEntryPoint:0x%08X\n", opt->AddressOfEntryPoint);
printf("ImageBase: 0x%016llX\n", opt->ImageBase);
printf("SectionAlignment: 0x%08X\n", opt->SectionAlignment);
printf("FileAlignment: 0x%08X\n", opt->FileAlignment);
printf("SizeOfImage: 0x%08X\n", opt->SizeOfImage);
printf("SizeOfHeaders: 0x%08X\n", opt->SizeOfHeaders);
printf("Subsystem: %d (%s)\n", opt->Subsystem,
opt->Subsystem == IMAGE_SUBSYSTEM_WINDOWS_GUI ? "GUI" :
opt->Subsystem == IMAGE_SUBSYSTEM_WINDOWS_CUI ? "Console" : "Other");
printf("NumberOfRvaAndSizes:%d\n", opt->NumberOfRvaAndSizes);
} else {
PIMAGE_OPTIONAL_HEADER32 opt = &ntHeaders->OptionalHeader;
printf("\n=== Optional Header (PE32) ===\n");
printf("Magic: 0x%04X (PE32)\n", opt->Magic);
printf("AddressOfEntryPoint:0x%08X\n", opt->AddressOfEntryPoint);
printf("ImageBase: 0x%08X\n", opt->ImageBase);
printf("SectionAlignment: 0x%08X\n", opt->SectionAlignment);
printf("FileAlignment: 0x%08X\n", opt->FileAlignment);
printf("SizeOfImage: 0x%08X\n", opt->SizeOfImage);
printf("SizeOfHeaders: 0x%08X\n", opt->SizeOfHeaders);
}
}
// 打印区段信息
void PrintSections(LPVOID fileData) {
PIMAGE_SECTION_HEADER sections = GetSectionHeaders(fileData);
WORD numSections = GetNumberOfSections(fileData);
printf("\n=== Sections (%d) ===\n", numSections);
printf("%-8s %-10s %-10s %-10s %-10s %-10s\n",
"Name", "VirtAddr", "VirtSize", "RawAddr", "RawSize", "Chars");
printf("%s\n", "--------------------------------------------------------------");
for (WORD i = 0; i < numSections; i++) {
char name[9] = {0};
memcpy(name, sections[i].Name, 8);
printf("%-8s 0x%08X 0x%08X 0x%08X 0x%08X 0x%08X\n",
name,
sections[i].VirtualAddress,
sections[i].Misc.VirtualSize,
sections[i].PointerToRawData,
sections[i].SizeOfRawData,
sections[i].Characteristics);
// 解析区段特征
DWORD chars = sections[i].Characteristics;
printf(" -> ");
if (chars & IMAGE_SCN_CNT_CODE) printf("CODE ");
if (chars & IMAGE_SCN_CNT_INITIALIZED_DATA) printf("IDATA ");
if (chars & IMAGE_SCN_CNT_UNINITIALIZED_DATA) printf("UDATA ");
if (chars & IMAGE_SCN_MEM_EXECUTE) printf("EXEC ");
if (chars & IMAGE_SCN_MEM_READ) printf("READ ");
if (chars & IMAGE_SCN_MEM_WRITE) printf("WRITE ");
printf("\n");
}
}
// 主函数
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("用法: %s <PE文件路径>\n", argv[0]);
return 1;
}
DWORD fileSize;
LPVOID fileData = LoadPEFile(argv[1], &fileSize);
if (!fileData) {
return 1;
}
printf("[*] 文件大小: %d bytes\n", fileSize);
if (!ValidatePEFile(fileData)) {
VirtualFree(fileData, 0, MEM_RELEASE);
return 1;
}
printf("[*] PE类型: %s\n", IsPE64(fileData) ? "PE32+ (64-bit)" : "PE32 (32-bit)");
PrintPEInfo(fileData, fileSize);
PrintSections(fileData);
VirtualFree(fileData, 0, MEM_RELEASE);
return 0;
}
5.4、使用dumpbin工具
:: 查看PE头信息
dumpbin /headers target.exe
:: 查看导入表
dumpbin /imports target.exe
:: 查看导出表
dumpbin /exports target.dll
:: 查看所有信息
dumpbin /all target.exe
:: 反汇编
dumpbin /disasm target.exe
6、课后作业
-
编写PE文件验证器
- 验证DOS头和PE签名
- 检查区段是否有效
- 检测常见的PE异常
-
实现地址转换器
- 支持RVA到FOA的转换
- 支持FOA到RVA的转换
- 支持VA的计算
-
分析常见PE文件
- 分析notepad.exe
- 分析kernel32.dll
- 对比32位和64位PE的区别