深入分析PE结构(一)
字数 1234 2025-08-07 08:22:18

深入分析PE结构教学文档

0x0 前言

PE(Portable Executable)是可移植可执行文件的格式,是Windows操作系统下可执行文件的标准格式。学习PE结构是Windows系统编程和逆向工程的基本功。

0x1 准备阶段

宏定义

宏定义在预编译时进行简单替换:

#define TRUE 1
#define FALSE 0

带参数的宏定义:

#define MAX(A, B) ((A)>(B)?(A):(B))

宏与函数的区别:

  • 宏只是简单替换,不分配额外堆栈空间
  • 函数需要分配堆栈空间
  • 宏执行效率更高但可能带来副作用

注意事项:

  1. 宏名与左括号之间不能有空格
  2. 宏参数应加上括号避免优先级问题
  3. 末尾不需要分号
  4. 多行宏定义需使用\连接

头文件

头文件包含方式:

  • <>:从系统目录查找
  • "":从当前目录查找

头文件重复包含解决方案:

#if !defined(ZZZ)
#define ZZZ
// 头文件内容
#endif

0x2 内存分配与释放

内存分配核心代码

int* ptr = (int *)malloc(sizeof(int)*128); // 分配128个int空间
if(ptr == NULL) return 0;  // 必须检查分配是否成功
memset(ptr,0,sizeof(int)*128); // 初始化分配的内存
free(ptr);  // 使用完毕后释放
ptr = NULL; // 指针置NULL

注意事项:

  1. 使用sizeof(类型)*n定义申请大小
  2. malloc返回void*需要强制转换
  3. 必须检查分配是否成功
  4. 使用前要初始化
  5. 使用后必须释放
  6. 指针置NULL

0x3 文件读写

关键函数

  1. fopen - 打开文件
  2. fseek - 移动文件指针
  3. ftell - 获取当前位置
  4. fclose - 关闭文件
  5. fread - 读取文件内容

文件到内存

FILE* fstream = fopen("notepad.exe","ab+");
int FstreamSizes = ftell(fstream);
int* FileBuffer = (int*)malloc(FstreamSizes);
fread(FileBuffer,FstreamSizes,1,fstream);

内存到文件

FILE* fstream1 = fopen("notepad.exe","ab+");
FILE* fstream2 = fopen("newnotepad.exe","ab+");
fread(FileBuffer,FstreamSizes+1,1,fstream1);
fwrite(FileBuffer,FstreamSizes,1,fstream2);

0x4 PE头解析

DOS头结构

typedef struct _IMAGE_DOS_HEADER {
    WORD e_magic;    // 0x00 MZ标记
    // ...其他字段...
    DWORD e_lfanew;  // 0x3C PE头偏移
} IMAGE_DOS_HEADER;

关键字段:

  • e_magic:必须为0x5A4D(MZ)
  • e_lfanew:PE头相对于文件起始的偏移

NT头结构

包含三部分:

  1. PE标记:0x00004550(PE\0\0)
  2. 标准PE头
  3. 可选PE头

标准PE头

typedef struct _IMAGE_FILE_HEADER {
    WORD Machine;               // 运行平台
    WORD NumberOfSections;      // 节区数量
    DWORD TimeDateStamp;        // 时间戳
    DWORD PointerToSymbolTable; // 符号表指针
    DWORD NumberOfSymbols;      // 符号数量
    WORD SizeOfOptionalHeader;  // 可选头大小
    WORD Characteristics;       // 文件特性
} IMAGE_FILE_HEADER;

可选PE头

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD Magic;                 // 文件类型
    // ...其他字段...
    DWORD AddressOfEntryPoint;  // 程序入口RVA
    DWORD ImageBase;            // 镜像基址
    DWORD SectionAlignment;     // 内存对齐
    DWORD FileAlignment;        // 文件对齐
    DWORD SizeOfImage;          // 内存中整个PE映像尺寸
    DWORD SizeOfHeaders;        // 所有头+节表大小
    // ...其他字段...
} IMAGE_OPTIONAL_HEADER;

关键字段:

  • AddressOfEntryPoint + ImageBase = 程序真正入口地址
  • SectionAlignment:内存对齐(通常0x1000)
  • FileAlignment:文件对齐(通常0x200)

0x5 节表解析

节表结构

typedef struct _IMAGE_SECTION_HEADER {
    BYTE Name[8];               // 节区名称
    union {
        DWORD PhysicalAddress;
        DWORD VirtualSize;      // 节区真实大小
    } Misc;
    DWORD VirtualAddress;       // 内存中偏移地址(RVA)
    DWORD SizeOfRawData;        // 文件中对齐后大小
    DWORD PointerToRawData;     // 文件中偏移地址
    DWORD PointerToRelocations; // 重定位信息
    DWORD PointerToLinenumbers; // 行号信息
    WORD NumberOfRelocations;   // 重定位项数
    WORD NumberOfLinenumbers;   // 行号项数
    DWORD Characteristics;      // 节区属性
} IMAGE_SECTION_HEADER;

关键字段:

  • Name:8字节节区名
  • VirtualAddress:内存中相对偏移(RVA)
  • PointerToRawData:文件中偏移
  • SizeOfRawData:文件中对齐后大小
  • Characteristics:节区属性(可读/可写/可执行)

0x6 PE加载过程

PE文件从硬盘加载到内存的过程:

  1. 根据SizeOfImage分配内存空间并初始化为0
  2. 拷贝所有头(SizeOfHeaders大小)
  3. 按节表循环拷贝每个节:
    • PointerToRawData决定从文件哪里开始拷贝
    • VirtualAddress决定拷贝到内存什么位置
    • SizeOfRawData决定拷贝大小

关键函数实现

读取PE文件到缓冲区

DWORD ReadPEFile(LPSTR lpszFile, LPVOID* pFileBuffer) {
    FILE* pFile = fopen(lpszFile,"rb");
    fseek(pFile,0,SEEK_END);
    DWORD fileSize = ftell(pFile);
    *pFileBuffer = malloc(fileSize);
    fread(*pFileBuffer,fileSize,1,pFile);
    fclose(pFile);
    return fileSize;
}

FileBuffer转ImageBuffer

DWORD CopyFileBufferToImageBuffer(LPVOID pFileBuffer, LPVOID* pImageBuffer) {
    // 解析各种头
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer+pDosHeader->e_lfanew);
    PIMAGE_OPTIONAL_HEADER32 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pNTHeader+24);
    
    // 分配内存
    *pImageBuffer = malloc(pOptionHeader->SizeOfImage);
    memset(*pImageBuffer,0,pOptionHeader->SizeOfImage);
    
    // 拷贝头部
    memcpy(*pImageBuffer,pDosHeader,pOptionHeader->SizeOfHeaders);
    
    // 拷贝节区
    PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pNTHeader->FileHeader.SizeOfOptionalHeader);
    for(int i=0;i<pNTHeader->FileHeader.NumberOfSections;i++) {
        memcpy((void*)((DWORD)*pImageBuffer + pSectionHeader[i].VirtualAddress),
               (void*)((DWORD)pFileBuffer + pSectionHeader[i].PointerToRawData),
               pSectionHeader[i].SizeOfRawData);
    }
    return pOptionHeader->SizeOfImage;
}

RVA转FOA

DWORD RvaToFileOffset(LPVOID pFileBuffer, DWORD dwRva) {
    // 解析各种头
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
    PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer+pDosHeader->e_lfanew);
    PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNTHeader+24+pNTHeader->FileHeader.SizeOfOptionalHeader);
    
    // 查找RVA所在节
    for(int i=0;i<pNTHeader->FileHeader.NumberOfSections;i++) {
        if(dwRva >= pSectionHeader[i].VirtualAddress && 
           dwRva < pSectionHeader[i].VirtualAddress+pSectionHeader[i].Misc.VirtualSize) {
            return dwRva - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData;
        }
    }
    return dwRva; // 如果在头部,直接返回
}

总结

PE结构是Windows可执行文件的基础,理解PE结构对于:

  • 程序开发
  • 逆向工程
  • 病毒分析
  • 软件保护

都具有重要意义。掌握PE文件的磁盘格式与内存布局的转换关系,是深入Windows系统编程的关键。

深入分析PE结构教学文档 0x0 前言 PE(Portable Executable)是可移植可执行文件的格式,是Windows操作系统下可执行文件的标准格式。学习PE结构是Windows系统编程和逆向工程的基本功。 0x1 准备阶段 宏定义 宏定义在预编译时进行简单替换: 带参数的宏定义: 宏与函数的区别: 宏只是简单替换,不分配额外堆栈空间 函数需要分配堆栈空间 宏执行效率更高但可能带来副作用 注意事项: 宏名与左括号之间不能有空格 宏参数应加上括号避免优先级问题 末尾不需要分号 多行宏定义需使用 \ 连接 头文件 头文件包含方式: <> :从系统目录查找 "" :从当前目录查找 头文件重复包含解决方案: 0x2 内存分配与释放 内存分配核心代码 注意事项: 使用 sizeof(类型)*n 定义申请大小 malloc 返回 void* 需要强制转换 必须检查分配是否成功 使用前要初始化 使用后必须释放 指针置NULL 0x3 文件读写 关键函数 fopen - 打开文件 fseek - 移动文件指针 ftell - 获取当前位置 fclose - 关闭文件 fread - 读取文件内容 文件到内存 内存到文件 0x4 PE头解析 DOS头结构 关键字段: e_magic :必须为 0x5A4D (MZ) e_lfanew :PE头相对于文件起始的偏移 NT头结构 包含三部分: PE标记: 0x00004550 (PE\0\0) 标准PE头 可选PE头 标准PE头 可选PE头 关键字段: AddressOfEntryPoint + ImageBase = 程序真正入口地址 SectionAlignment :内存对齐(通常0x1000) FileAlignment :文件对齐(通常0x200) 0x5 节表解析 节表结构 关键字段: Name :8字节节区名 VirtualAddress :内存中相对偏移(RVA) PointerToRawData :文件中偏移 SizeOfRawData :文件中对齐后大小 Characteristics :节区属性(可读/可写/可执行) 0x6 PE加载过程 PE文件从硬盘加载到内存的过程: 根据 SizeOfImage 分配内存空间并初始化为0 拷贝所有头( SizeOfHeaders 大小) 按节表循环拷贝每个节: PointerToRawData 决定从文件哪里开始拷贝 VirtualAddress 决定拷贝到内存什么位置 SizeOfRawData 决定拷贝大小 关键函数实现 读取PE文件到缓冲区 FileBuffer转ImageBuffer RVA转FOA 总结 PE结构是Windows可执行文件的基础,理解PE结构对于: 程序开发 逆向工程 病毒分析 软件保护 都具有重要意义。掌握PE文件的磁盘格式与内存布局的转换关系,是深入Windows系统编程的关键。