PE文件解析
字数 1322 2025-08-22 12:23:00

PE文件结构解析与操作指南

1. PE文件概述

PE (Portable Executable) 文件是Windows操作系统中可执行文件的标准格式,包括:

  • 应用程序 (.exe)
  • 动态链接库 (.dll)
  • 驱动程序 (.sys)

PE文件主要由以下几个部分组成:

  1. DOS头和DOS存根
  2. NT头
  3. 数据目录
  4. 节表
  5. 节数据

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头由三部分组成:

  1. 签名 (Signature): 固定为"00004550"("PE\0\0")
  2. 文件头 (File Header)
  3. 可选头 (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. 关键概念总结

  1. RVA (Relative Virtual Address): 相对于ImageBase的偏移地址
  2. VA (Virtual Address): 实际内存地址 = ImageBase + RVA
  3. FOA (File Offset Address): 文件中的偏移地址
  4. 内存对齐与文件对齐: 通常内存对齐为0x1000,文件对齐为0x200
  5. 导入表与导出表: 分别处理依赖函数和导出函数
  6. 重定位: 当PE文件无法加载到首选基址时需要的地址修正

通过理解PE文件结构,可以深入分析Windows可执行文件的工作原理,为逆向工程、安全分析和软件开发打下坚实基础。

PE文件结构解析与操作指南 1. PE文件概述 PE (Portable Executable) 文件是Windows操作系统中可执行文件的标准格式,包括: 应用程序 (.exe) 动态链接库 (.dll) 驱动程序 (.sys) PE文件主要由以下几个部分组成: DOS头和DOS存根 NT头 数据目录 节表 节数据 2. DOS头 (IMAGE_ DOS_ HEADER) DOS头是为了兼容DOS系统而保留的结构,主要包含以下重要字段: 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) 4.2 可选头 (IMAGE_ OPTIONAL_ HEADER) 重要字段说明: Magic : 0x10B表示32位PE,0x20B表示64位PE AddressOfEntryPoint : 程序执行的起始RVA ImageBase : PE文件加载到进程的基地址 DataDirectory : 包含16个数据目录项,如导入表、导出表等 5. 数据目录 (IMAGE_ DATA_ DIRECTORY) 重要数据目录项: 导入表 (IMAGE_ DIRECTORY_ ENTRY_ IMPORT) 导出表 (IMAGE_ DIRECTORY_ ENTRY_ EXPORT) 资源表 (IMAGE_ DIRECTORY_ ENTRY_ RESOURCE) 重定位表 (IMAGE_ DIRECTORY_ ENTRY_ BASERELOC) 6. 节表 (IMAGE_ SECTION_ HEADER) 常见节: .text : 代码段 .data : 数据段 .rsrc : 资源段 .reloc : 重定位表段 7. 代码实现示例 7.1 加载PE文件到内存 7.2 获取NT头 7.3 解析导入表 7.4 遍历节表 8. 关键概念总结 RVA (Relative Virtual Address) : 相对于ImageBase的偏移地址 VA (Virtual Address) : 实际内存地址 = ImageBase + RVA FOA (File Offset Address) : 文件中的偏移地址 内存对齐与文件对齐 : 通常内存对齐为0x1000,文件对齐为0x200 导入表与导出表 : 分别处理依赖函数和导出函数 重定位 : 当PE文件无法加载到首选基址时需要的地址修正 通过理解PE文件结构,可以深入分析Windows可执行文件的工作原理,为逆向工程、安全分析和软件开发打下坚实基础。