深入分析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))
宏与函数的区别:
- 宏只是简单替换,不分配额外堆栈空间
- 函数需要分配堆栈空间
- 宏执行效率更高但可能带来副作用
注意事项:
- 宏名与左括号之间不能有空格
- 宏参数应加上括号避免优先级问题
- 末尾不需要分号
- 多行宏定义需使用
\连接
头文件
头文件包含方式:
<>:从系统目录查找"":从当前目录查找
头文件重复包含解决方案:
#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
注意事项:
- 使用
sizeof(类型)*n定义申请大小 malloc返回void*需要强制转换- 必须检查分配是否成功
- 使用前要初始化
- 使用后必须释放
- 指针置NULL
0x3 文件读写
关键函数
fopen- 打开文件fseek- 移动文件指针ftell- 获取当前位置fclose- 关闭文件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头结构
包含三部分:
- PE标记:
0x00004550(PE\0\0) - 标准PE头
- 可选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文件从硬盘加载到内存的过程:
- 根据
SizeOfImage分配内存空间并初始化为0 - 拷贝所有头(
SizeOfHeaders大小) - 按节表循环拷贝每个节:
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系统编程的关键。