Windows PE结构
8、阶段合集
1、课程目标
- 综合运用PE结构知识
- 实现完整的PE解析器
- 实现PE加载器和修改器
- 掌握PE在安全开发中的应用
2、名词解释
| 名词 | 全称 | 解释 |
|---|---|---|
| PE Loader | PE加载器 | 手动将PE映射到内存并执行 |
| PE Packer | PE加壳器 | 压缩/加密PE文件 |
| PE Injector | PE注入器 | 向目标进程注入PE |
| Reflective DLL | 反射式DLL | 自加载的DLL |
| RunPE | - | 镂空进程执行技术 |
| PE Parser | PE解析器 | 分析PE结构的工具 |
3、使用工具
- Visual Studio 2022
- CFF Explorer
- x64dbg
- IDA Pro
- PE-bear
4、技术原理
4.1、PE结构综合图
+--------------------------------------------------+
| PE文件布局 |
+--------------------------------------------------+
| DOS Header (64 bytes) |
| - e_magic: MZ |
| - e_lfanew: -> PE Signature |
+--------------------------------------------------+
| DOS Stub (可变长度) |
| - "This program cannot be run in DOS mode" |
+--------------------------------------------------+
| PE Signature (4 bytes): "PE\0\0" |
+--------------------------------------------------+
| File Header (20 bytes) |
| - Machine, NumberOfSections |
| - TimeDateStamp, SizeOfOptionalHeader |
| - Characteristics |
+--------------------------------------------------+
| Optional Header (PE32: 224 / PE32+: 240 bytes) |
| - Magic, EntryPoint, ImageBase |
| - SectionAlignment, FileAlignment |
| - SizeOfImage, SizeOfHeaders |
| - Subsystem, DllCharacteristics |
| - DataDirectory[16] |
| [0] Export Table |
| [1] Import Table |
| [2] Resource Table |
| [5] Base Relocation Table |
| [12] IAT |
+--------------------------------------------------+
| Section Headers (每个40 bytes) |
| - .text (代码) |
| - .data (已初始化数据) |
| - .rdata (只读数据) |
| - .rsrc (资源) |
| - .reloc (重定位) |
+--------------------------------------------------+
| Section Data |
| - 代码、数据、资源等 |
+--------------------------------------------------+
5、代码实现
5.1、完整PE解析器
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "imagehlp.lib")
// PE解析器上下文
typedef struct _PE_CONTEXT {
LPBYTE fileData;
DWORD fileSize;
BOOL is64;
PIMAGE_DOS_HEADER dosHeader;
PIMAGE_NT_HEADERS32 ntHeaders32;
PIMAGE_NT_HEADERS64 ntHeaders64;
PIMAGE_FILE_HEADER fileHeader;
PIMAGE_SECTION_HEADER sections;
WORD sectionCount;
// 通用访问
ULONGLONG imageBase;
DWORD entryPoint;
DWORD sizeOfImage;
DWORD sizeOfHeaders;
DWORD sectionAlignment;
DWORD fileAlignment;
WORD subsystem;
WORD dllCharacteristics;
// 数据目录
PIMAGE_DATA_DIRECTORY dataDirectories;
DWORD dataDirectoryCount;
} PE_CONTEXT, *PPE_CONTEXT;
// 初始化PE上下文
BOOL InitPEContext(PPE_CONTEXT ctx, LPBYTE fileData, DWORD fileSize) {
ZeroMemory(ctx, sizeof(PE_CONTEXT));
ctx->fileData = fileData;
ctx->fileSize = fileSize;
// 验证DOS头
ctx->dosHeader = (PIMAGE_DOS_HEADER)fileData;
if (ctx->dosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
printf("[-] 无效的DOS签名\n");
return FALSE;
}
// 验证PE签名
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(
fileData + ctx->dosHeader->e_lfanew
);
if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) {
printf("[-] 无效的PE签名\n");
return FALSE;
}
ctx->fileHeader = &ntHeaders->FileHeader;
ctx->sectionCount = ctx->fileHeader->NumberOfSections;
// 判断位数
ctx->is64 = (ntHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC);
if (ctx->is64) {
ctx->ntHeaders64 = (PIMAGE_NT_HEADERS64)ntHeaders;
PIMAGE_OPTIONAL_HEADER64 opt = &ctx->ntHeaders64->OptionalHeader;
ctx->imageBase = opt->ImageBase;
ctx->entryPoint = opt->AddressOfEntryPoint;
ctx->sizeOfImage = opt->SizeOfImage;
ctx->sizeOfHeaders = opt->SizeOfHeaders;
ctx->sectionAlignment = opt->SectionAlignment;
ctx->fileAlignment = opt->FileAlignment;
ctx->subsystem = opt->Subsystem;
ctx->dllCharacteristics = opt->DllCharacteristics;
ctx->dataDirectories = opt->DataDirectory;
ctx->dataDirectoryCount = opt->NumberOfRvaAndSizes;
ctx->sections = IMAGE_FIRST_SECTION(ctx->ntHeaders64);
} else {
ctx->ntHeaders32 = (PIMAGE_NT_HEADERS32)ntHeaders;
PIMAGE_OPTIONAL_HEADER32 opt = &ctx->ntHeaders32->OptionalHeader;
ctx->imageBase = opt->ImageBase;
ctx->entryPoint = opt->AddressOfEntryPoint;
ctx->sizeOfImage = opt->SizeOfImage;
ctx->sizeOfHeaders = opt->SizeOfHeaders;
ctx->sectionAlignment = opt->SectionAlignment;
ctx->fileAlignment = opt->FileAlignment;
ctx->subsystem = opt->Subsystem;
ctx->dllCharacteristics = opt->DllCharacteristics;
ctx->dataDirectories = opt->DataDirectory;
ctx->dataDirectoryCount = opt->NumberOfRvaAndSizes;
ctx->sections = IMAGE_FIRST_SECTION(ctx->ntHeaders32);
}
return TRUE;
}
// RVA转FOA
DWORD PERvaToFoa(PPE_CONTEXT ctx, DWORD rva) {
for (WORD i = 0; i < ctx->sectionCount; i++) {
DWORD start = ctx->sections[i].VirtualAddress;
DWORD end = start + ctx->sections[i].Misc.VirtualSize;
if (rva >= start && rva < end) {
return ctx->sections[i].PointerToRawData + (rva - start);
}
}
return rva;
}
// 获取数据指针
LPVOID PEGetDataByRva(PPE_CONTEXT ctx, DWORD rva) {
if (rva == 0) return NULL;
DWORD foa = PERvaToFoa(ctx, rva);
return ctx->fileData + foa;
}
// 打印完整PE信息
void PrintFullPEInfo(PPE_CONTEXT ctx) {
printf("\n");
printf("================================================================================\n");
printf(" PE File Analysis Report\n");
printf("================================================================================\n");
// 基本信息
printf("\n[基本信息]\n");
printf(" 文件大小: %d bytes (0x%X)\n", ctx->fileSize, ctx->fileSize);
printf(" PE类型: %s\n", ctx->is64 ? "PE32+ (64-bit)" : "PE32 (32-bit)");
printf(" 映像基址: 0x%llX\n", ctx->imageBase);
printf(" 入口点: 0x%08X (RVA)\n", ctx->entryPoint);
printf(" 映像大小: 0x%08X (%d KB)\n", ctx->sizeOfImage, ctx->sizeOfImage / 1024);
printf(" 头部大小: 0x%08X\n", ctx->sizeOfHeaders);
printf(" 区段对齐: 0x%08X (%d bytes)\n", ctx->sectionAlignment, ctx->sectionAlignment);
printf(" 文件对齐: 0x%08X (%d bytes)\n", ctx->fileAlignment, ctx->fileAlignment);
// 安全特性
printf("\n[安全特性]\n");
WORD dll = ctx->dllCharacteristics;
printf(" ASLR: %s\n", (dll & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) ? "启用" : "禁用");
printf(" DEP/NX: %s\n", (dll & IMAGE_DLLCHARACTERISTICS_NX_COMPAT) ? "启用" : "禁用");
printf(" CFG: %s\n", (dll & IMAGE_DLLCHARACTERISTICS_GUARD_CF) ? "启用" : "禁用");
printf(" 高熵ASLR: %s\n", (dll & IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA) ? "启用" : "禁用");
printf(" SEH: %s\n", (dll & IMAGE_DLLCHARACTERISTICS_NO_SEH) ? "禁用" : "启用");
// 区段信息
printf("\n[区段信息] (%d个区段)\n", ctx->sectionCount);
printf(" %-8s %-10s %-10s %-10s %-10s %-10s\n",
"Name", "VirtAddr", "VirtSize", "RawAddr", "RawSize", "Flags");
printf(" %s\n", "------------------------------------------------------------");
for (WORD i = 0; i < ctx->sectionCount; i++) {
char name[9] = {0};
memcpy(name, ctx->sections[i].Name, 8);
printf(" %-8s 0x%08X 0x%08X 0x%08X 0x%08X 0x%08X\n",
name,
ctx->sections[i].VirtualAddress,
ctx->sections[i].Misc.VirtualSize,
ctx->sections[i].PointerToRawData,
ctx->sections[i].SizeOfRawData,
ctx->sections[i].Characteristics);
}
// 数据目录
printf("\n[数据目录]\n");
const char* dirNames[] = {
"Export", "Import", "Resource", "Exception", "Security",
"BaseReloc", "Debug", "Architecture", "GlobalPtr", "TLS",
"LoadConfig", "BoundImport", "IAT", "DelayImport", "CLR", "Reserved"
};
for (DWORD i = 0; i < ctx->dataDirectoryCount && i < 16; i++) {
if (ctx->dataDirectories[i].VirtualAddress != 0) {
printf(" [%2d] %-12s RVA: 0x%08X Size: 0x%08X\n",
i, dirNames[i],
ctx->dataDirectories[i].VirtualAddress,
ctx->dataDirectories[i].Size);
}
}
printf("\n================================================================================\n");
}
5.2、完整PE加载器
// PE加载器
typedef struct _LOADED_PE {
LPBYTE imageBase;
DWORD imageSize;
DWORD entryPoint;
BOOL is64;
BOOL isDll;
} LOADED_PE, *PLOADED_PE;
// 完整的PE加载器
BOOL LoadPEIntoMemory(LPBYTE peData, DWORD peSize, PLOADED_PE loaded) {
PE_CONTEXT ctx;
if (!InitPEContext(&ctx, peData, peSize)) {
return FALSE;
}
// 分配内存
loaded->imageBase = (LPBYTE)VirtualAlloc(
NULL,
ctx.sizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (!loaded->imageBase) {
printf("[-] 无法分配内存: %d bytes\n", ctx.sizeOfImage);
return FALSE;
}
loaded->imageSize = ctx.sizeOfImage;
loaded->entryPoint = ctx.entryPoint;
loaded->is64 = ctx.is64;
loaded->isDll = (ctx.fileHeader->Characteristics & IMAGE_FILE_DLL) != 0;
printf("[+] 映像基址: 0x%p\n", loaded->imageBase);
// 复制头部
memcpy(loaded->imageBase, peData, ctx.sizeOfHeaders);
// 复制区段
for (WORD i = 0; i < ctx.sectionCount; i++) {
if (ctx.sections[i].SizeOfRawData > 0) {
memcpy(
loaded->imageBase + ctx.sections[i].VirtualAddress,
peData + ctx.sections[i].PointerToRawData,
ctx.sections[i].SizeOfRawData
);
}
char name[9] = {0};
memcpy(name, ctx.sections[i].Name, 8);
printf("[+] 复制区段: %-8s -> 0x%p\n",
name, loaded->imageBase + ctx.sections[i].VirtualAddress);
}
// 执行重定位
LONGLONG delta = (LONGLONG)loaded->imageBase - (LONGLONG)ctx.imageBase;
if (delta != 0) {
printf("[*] 执行重定位 (Delta: 0x%llX)\n", delta);
PIMAGE_DATA_DIRECTORY relocDir = &ctx.dataDirectories[IMAGE_DIRECTORY_ENTRY_BASERELOC];
if (relocDir->VirtualAddress == 0) {
printf("[-] 没有重定位表,无法加载到其他地址\n");
VirtualFree(loaded->imageBase, 0, MEM_RELEASE);
return FALSE;
}
PIMAGE_BASE_RELOCATION reloc = (PIMAGE_BASE_RELOCATION)(
loaded->imageBase + relocDir->VirtualAddress
);
int fixupCount = 0;
while (reloc->VirtualAddress != 0) {
DWORD numEntries = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
WORD* entries = (WORD*)(reloc + 1);
for (DWORD j = 0; j < numEntries; j++) {
WORD type = entries[j] >> 12;
WORD offset = entries[j] & 0x0FFF;
LPBYTE target = loaded->imageBase + reloc->VirtualAddress + offset;
if (type == IMAGE_REL_BASED_HIGHLOW) {
*(DWORD*)target += (DWORD)delta;
fixupCount++;
} else if (type == IMAGE_REL_BASED_DIR64) {
*(ULONGLONG*)target += delta;
fixupCount++;
}
}
reloc = (PIMAGE_BASE_RELOCATION)((LPBYTE)reloc + reloc->SizeOfBlock);
}
printf("[+] 重定位完成: %d 处修复\n", fixupCount);
}
// 解析导入表
PIMAGE_DATA_DIRECTORY importDir = &ctx.dataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT];
if (importDir->VirtualAddress != 0) {
printf("[*] 解析导入表\n");
PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(
loaded->imageBase + importDir->VirtualAddress
);
while (importDesc->Name != 0) {
char* dllName = (char*)(loaded->imageBase + importDesc->Name);
HMODULE hDll = LoadLibraryA(dllName);
if (!hDll) {
printf("[-] 无法加载: %s\n", dllName);
VirtualFree(loaded->imageBase, 0, MEM_RELEASE);
return FALSE;
}
printf("[+] 加载DLL: %s -> 0x%p\n", dllName, hDll);
// 填充IAT
PIMAGE_THUNK_DATA originalThunk = (PIMAGE_THUNK_DATA)(
loaded->imageBase + (importDesc->OriginalFirstThunk ?
importDesc->OriginalFirstThunk :
importDesc->FirstThunk)
);
PIMAGE_THUNK_DATA firstThunk = (PIMAGE_THUNK_DATA)(
loaded->imageBase + importDesc->FirstThunk
);
while (originalThunk->u1.AddressOfData != 0) {
FARPROC func;
if (IMAGE_SNAP_BY_ORDINAL(originalThunk->u1.Ordinal)) {
func = GetProcAddress(hDll,
MAKEINTRESOURCEA(IMAGE_ORDINAL(originalThunk->u1.Ordinal)));
} else {
PIMAGE_IMPORT_BY_NAME hint = (PIMAGE_IMPORT_BY_NAME)(
loaded->imageBase + originalThunk->u1.AddressOfData
);
func = GetProcAddress(hDll, hint->Name);
}
if (!func) {
printf("[-] 无法获取函数地址\n");
}
firstThunk->u1.Function = (ULONGLONG)func;
originalThunk++;
firstThunk++;
}
importDesc++;
}
}
// 设置区段保护属性
for (WORD i = 0; i < ctx.sectionCount; i++) {
DWORD protect = PAGE_READONLY;
DWORD chars = ctx.sections[i].Characteristics;
if (chars & IMAGE_SCN_MEM_EXECUTE) {
if (chars & IMAGE_SCN_MEM_WRITE) {
protect = PAGE_EXECUTE_READWRITE;
} else {
protect = PAGE_EXECUTE_READ;
}
} else if (chars & IMAGE_SCN_MEM_WRITE) {
protect = PAGE_READWRITE;
}
DWORD oldProtect;
VirtualProtect(
loaded->imageBase + ctx.sections[i].VirtualAddress,
ctx.sections[i].Misc.VirtualSize,
protect,
&oldProtect
);
}
printf("[+] PE加载完成\n");
return TRUE;
}
// 执行加载的PE
BOOL ExecuteLoadedPE(PLOADED_PE loaded) {
if (loaded->entryPoint == 0) {
printf("[-] 没有入口点\n");
return FALSE;
}
LPBYTE entry = loaded->imageBase + loaded->entryPoint;
printf("[*] 执行入口点: 0x%p\n", entry);
if (loaded->isDll) {
typedef BOOL (WINAPI* DllMain_t)(HINSTANCE, DWORD, LPVOID);
DllMain_t DllMain = (DllMain_t)entry;
return DllMain((HINSTANCE)loaded->imageBase, DLL_PROCESS_ATTACH, NULL);
} else {
typedef int (*EntryPoint_t)(void);
EntryPoint_t EntryPoint = (EntryPoint_t)entry;
return EntryPoint();
}
}
5.3、PE修改器
// 添加区段
BOOL PEAddSection(PPE_CONTEXT ctx, DWORD* newFileSize,
const char* name, DWORD size, DWORD characteristics) {
// 检查空间
DWORD headersEnd = ctx->sizeOfHeaders;
DWORD newSectionHeaderOffset = (DWORD)((LPBYTE)&ctx->sections[ctx->sectionCount] - ctx->fileData);
if (newSectionHeaderOffset + sizeof(IMAGE_SECTION_HEADER) > headersEnd) {
printf("[-] 没有空间添加区段头\n");
return FALSE;
}
// 获取最后一个区段
PIMAGE_SECTION_HEADER lastSection = &ctx->sections[ctx->sectionCount - 1];
PIMAGE_SECTION_HEADER newSection = &ctx->sections[ctx->sectionCount];
// 计算新区段位置
DWORD alignedVA = (lastSection->VirtualAddress + lastSection->Misc.VirtualSize +
ctx->sectionAlignment - 1) & ~(ctx->sectionAlignment - 1);
DWORD alignedRaw = (lastSection->PointerToRawData + lastSection->SizeOfRawData +
ctx->fileAlignment - 1) & ~(ctx->fileAlignment - 1);
DWORD alignedSize = (size + ctx->fileAlignment - 1) & ~(ctx->fileAlignment - 1);
// 填充新区段头
ZeroMemory(newSection, sizeof(IMAGE_SECTION_HEADER));
strncpy((char*)newSection->Name, name, 8);
newSection->Misc.VirtualSize = size;
newSection->VirtualAddress = alignedVA;
newSection->SizeOfRawData = alignedSize;
newSection->PointerToRawData = alignedRaw;
newSection->Characteristics = characteristics;
// 更新PE头
ctx->fileHeader->NumberOfSections++;
DWORD newImageSize = (alignedVA + size + ctx->sectionAlignment - 1) &
~(ctx->sectionAlignment - 1);
if (ctx->is64) {
ctx->ntHeaders64->OptionalHeader.SizeOfImage = newImageSize;
} else {
ctx->ntHeaders32->OptionalHeader.SizeOfImage = newImageSize;
}
*newFileSize = alignedRaw + alignedSize;
printf("[+] 添加区段: %s\n", name);
printf(" VirtualAddress: 0x%08X\n", alignedVA);
printf(" PointerToRawData: 0x%08X\n", alignedRaw);
printf(" Size: 0x%08X\n", alignedSize);
return TRUE;
}
// 修改入口点
void PESetEntryPoint(PPE_CONTEXT ctx, DWORD newEntryPoint) {
if (ctx->is64) {
ctx->ntHeaders64->OptionalHeader.AddressOfEntryPoint = newEntryPoint;
} else {
ctx->ntHeaders32->OptionalHeader.AddressOfEntryPoint = newEntryPoint;
}
printf("[+] 入口点修改为: 0x%08X\n", newEntryPoint);
}
// 禁用ASLR
void PEDisableASLR(PPE_CONTEXT ctx) {
if (ctx->is64) {
ctx->ntHeaders64->OptionalHeader.DllCharacteristics &=
~IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE;
} else {
ctx->ntHeaders32->OptionalHeader.DllCharacteristics &=
~IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE;
}
printf("[+] ASLR已禁用\n");
}
// 清除调试信息
void PEClearDebugInfo(PPE_CONTEXT ctx) {
ctx->dataDirectories[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress = 0;
ctx->dataDirectories[IMAGE_DIRECTORY_ENTRY_DEBUG].Size = 0;
printf("[+] 调试信息已清除\n");
}
5.4、PE分析工具主程序
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("PE Analyzer v1.0\n");
printf("用法: %s <PE文件路径> [选项]\n", argv[0]);
printf("选项:\n");
printf(" -i 显示完整信息\n");
printf(" -e 显示导出表\n");
printf(" -m 显示导入表\n");
printf(" -r 显示资源表\n");
printf(" -l 显示重定位表\n");
printf(" -x 执行PE文件\n");
return 1;
}
// 加载PE文件
HANDLE hFile = CreateFileA(argv[1], GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] 无法打开文件: %s\n", argv[1]);
return 1;
}
DWORD fileSize = GetFileSize(hFile, NULL);
LPBYTE fileData = (LPBYTE)VirtualAlloc(NULL, fileSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
DWORD bytesRead;
ReadFile(hFile, fileData, fileSize, &bytesRead, NULL);
CloseHandle(hFile);
// 初始化PE上下文
PE_CONTEXT ctx;
if (!InitPEContext(&ctx, fileData, fileSize)) {
VirtualFree(fileData, 0, MEM_RELEASE);
return 1;
}
// 处理选项
BOOL showInfo = TRUE;
for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "-i") == 0) {
PrintFullPEInfo(&ctx);
showInfo = FALSE;
} else if (strcmp(argv[i], "-e") == 0) {
ParseExportTable(fileData);
showInfo = FALSE;
} else if (strcmp(argv[i], "-m") == 0) {
ParseImportTable(fileData);
showInfo = FALSE;
} else if (strcmp(argv[i], "-r") == 0) {
ParseResourceTable(fileData);
showInfo = FALSE;
} else if (strcmp(argv[i], "-l") == 0) {
ParseRelocationTable(fileData);
showInfo = FALSE;
} else if (strcmp(argv[i], "-x") == 0) {
LOADED_PE loaded;
if (LoadPEIntoMemory(fileData, fileSize, &loaded)) {
ExecuteLoadedPE(&loaded);
}
showInfo = FALSE;
}
}
if (showInfo) {
PrintFullPEInfo(&ctx);
}
VirtualFree(fileData, 0, MEM_RELEASE);
return 0;
}
6、课后作业
-
编写完整PE分析工具
- 支持所有数据目录解析
- 支持导出到JSON/XML
- 支持PE对比功能
-
实现反射式DLL加载器
- 无文件落地加载DLL
- 自动处理导入和重定位
- 支持卸载功能
-
研究PE保护技术
- 分析常见加壳方式
- 研究反调试技术
- 了解代码混淆方法