必须同时验证MZ头(0x5A4D)和PE签名(0x00004550):先读前2字节确认0x4D 5A,再从偏移0x3C读e_lfanew得PE头位置,检查该处4字节是否为0x00004550且文件长度足够。

怎么用 C++ 读取 PE 文件头并验证有效性
直接读文件前 64 字节就能判断是不是合法 PE:先检查 MZ 签名,再跳到偏移 0x3C 读 DWORD 得到 NT 头起始位置,接着校验该处是否为 PE\0\0。别依赖 ImageNtHeader() 宏——它内部做指针加法,若文件不完整或内存映射越界会崩。
常见错误是把文件当内存映射后直接强转 PIMAGE_NT_HEADERS,结果访问 OptionalHeader.DataDirectory 时触发访问违例。必须先确认文件大小 ≥ NT 头结束位置,且 NumberOfRvaAndSizes ≤ 16(Windows 只定义了前 16 个数据目录)。
- 用
CreateFile()+CreateFileMapping()+MapViewOfFile()映射只读视图,避免写坏原文件 - 所有偏移计算必须用
RVA2RAW()转换:即RVA - OptionalHeader.ImageBase + OptionalHeader.SizeOfHeaders(仅适用于文件视图;内存加载后逻辑不同) - 32/64 位 PE 的
IMAGE_NT_HEADERS结构体不同,需先读OptionalHeader.Magic判断是0x010B还是0x020B
怎么手动修复导入表(IAT)中的函数地址
导入表不是“修复”而是“填充”:PE 加载器本应做的工作,你得在内存中模拟。关键在遍历 IMAGE_IMPORT_DESCRIPTOR 数组,每个描述符指向一个 DLL 名和两个函数名数组(OriginalFirstThunk 和 FirstThunk),后者最终会被填入函数地址。
容易踩的坑是混淆 INT 和 IAT:OriginalFirstThunk 指向未绑定的函数名/序号数组(可为 NULL),而 FirstThunk 是运行时实际被调用的地址槽位。你只需遍历 FirstThunk 指向的 IMAGE_THUNK_DATA 数组,对每个非零项解析其指向的函数名或序号,再用 GetProcAddress() 获取地址写回去。
立即学习“C++免费学习笔记(深入)”;
- 注意
IMAGE_THUNK_DATA的u1.AddressOfData高位为 1 表示是序号导入(Ordinal & 0x80000000),此时低 31 位才是真实序号 - 如果 DLL 未加载,必须先调用
LoadLibrary();但不要硬编码路径,优先用GetSystemDirectory()或搜索PATH环境变量 - 写地址前确保目标内存页有写权限:
VirtualProtect()改PAGE_READWRITE,完事再改回PAGE_EXECUTE_READ
怎么处理重定位表(Reloc)让代码在任意基址运行
重定位表只在 DLL 或 ASLR 启用的 EXE 中存在。它的作用是告诉加载器:哪些地址需要按实际加载基址修正。结构是 IMAGE_BASE_RELOCATION 块链表,每块含一个 RVA 基址和若干 2 字节重定位项(高 4 位是类型,低 12 位是相对于基址的偏移)。
最常遇到的类型是 IMAGE_REL_BASED_HIGHLOW(值为 3),表示要修正一个 32 位 RVA 地址。你得把该偏移处的 4 字节读出来,减去原始 ImageBase,加上当前加载地址,再写回去。
- 重定位项不是连续存储的:每个块末尾可能有填充字节,需用
SizeOfBlock控制读取范围,不能只靠项数 - 若当前加载地址等于
ImageBase,整个重定位表可跳过——很多工具忽略这点导致无谓遍历 - 写内存前务必检查重定位项指向的地址是否在代码段内(对比
SectionHeader.VirtualAddress和VirtualSize),否则可能覆盖只读数据
为什么手动修复后程序仍崩溃?几个隐蔽点
导入和重定位只是基础。真正卡住的地方往往不在结构解析本身:
-
TLSDirectory(线程局部存储)没处理:TLS 回调函数数组里的地址也需重定位,且回调会在 DllMain 之前执行 - 资源节(.rsrc)里的字符串、图标等引用的 RVA 不受重定位表管理,但若你移动了整个镜像,这些内部指针就全失效
- 某些驱动或反调试工具会扫描 IAT 是否被修改(比如检测
FirstThunk和OriginalFirstThunk是否相等),填完地址后最好把OriginalFirstThunk清零或设为相同值 - 64 位 PE 的重定位类型多了
IMAGE_REL_BASED_DIR64(值为 10),处理逻辑和 HIGHLOW 不同,不能简单位移
最麻烦的是调试符号和异常表(.pdata)——它们依赖精确的函数边界 RVA,一旦重定位出错,SEH 就直接失效,连异常都捕获不到。


















