Windows PE结构

1、Windows PE文件基础知识

1、课程目标

  1. 理解PE文件格式的历史和作用
  2. 掌握PE文件的整体结构
  3. 学会使用工具查看PE文件
  4. 理解RVA和FOA的概念和转换

2、名词解释

名词全称解释
PEPortable Executable可移植可执行文件格式
COFFCommon Object File Format通用目标文件格式
RVARelative Virtual Address相对虚拟地址
VAVirtual Address虚拟地址
FOAFile 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、课后作业

  1. 编写PE文件验证器

    • 验证DOS头和PE签名
    • 检查区段是否有效
    • 检测常见的PE异常
  2. 实现地址转换器

    • 支持RVA到FOA的转换
    • 支持FOA到RVA的转换
    • 支持VA的计算
  3. 分析常见PE文件

    • 分析notepad.exe
    • 分析kernel32.dll
    • 对比32位和64位PE的区别