安全读取 PE 文件的 _IMAGE_NT_HEADERS 需先验证 DOS 头魔数、用 e_lfanew 定位并校验偏移有效性;修改 ImageBase 后须重算校验和、处理 ASLR 与重定位;解析时必须按 Machine 和 SizeOfOptionalHeader 动态选择 32/64 位头结构,校验和计算需置零自身、16 位累加、奇数补零、二次折叠。

怎么安全读取 PE 文件的 _IMAGE_NT_HEADERS?
直接用 fread 读整个文件再强转指针是常见错误——PE 文件头部位置不固定,DOS Header 末尾的 e_lfanew 字段才是关键跳转点。跳错地址会导致读到垃圾数据,甚至程序崩溃。
- 先用
fopen以"rb"模式打开文件,避免文本模式换行符干扰 - 读前 64 字节(足够覆盖
_IMAGE_DOS_HEADER),检查e_magic是否为0x5A4D(即"MZ") - 提取
e_lfanew值,必须验证是否在文件范围内,否则可能越界读取 - 再 fseek 到该偏移,读入
_IMAGE_NT_HEADERS(通常 248 字节,但实际长度取决于可选头大小)
示例关键判断:
if (e_lfanew < 64 || e_lfanew > file_size - sizeof(IMAGE_NT_HEADERS)) { /<em> 无效 PE </em>/ }
修改 OptionalHeader.ImageBase 后为什么加载失败?
改了 ImageBase 却无法加载,大概率是因为没同步更新校验和、没重定位、或忽略了 ASLR 兼容性。Windows 加载器对校验和有严格校验逻辑,哪怕只是改一个字节,也得重新计算。
- 修改前先调用
CheckSumMappedFile(Windows SDK 提供)或手动实现校验和算法(RFC 1071 标准) - 若目标文件启用了 ASLR(
DllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE),改ImageBase可能被忽略,加载器仍会随机基址重定位 - 修改后必须用
SetFilePointer+WriteFile精确写回原位置,不能只写结构体——PE 头部前后都有其他字段,写错长度会破坏后续节表
注意:修改 ImageBase 不等于绕过 DEP/ASLR,它只是告诉加载器“首选基址”,现代系统下作用有限。
立即学习“C++免费学习笔记(深入)”;
用 IMAGE_OPTIONAL_HEADER64 解析 32 位文件会怎样?
会解析出完全错误的字段布局,比如把 32 位的 AddressOfEntryPoint 当成高位 32 位,导致入口点地址变成 0 或极大值,调试器直接跳飞。
- 必须通过
NT_HEADERS.FileHeader.Machine判断架构:IMAGE_FILE_MACHINE_I386对应 32 位,IMAGE_FILE_MACHINE_AMD64对应 64 位 -
OptionalHeader的大小由NT_HEADERS.FileHeader.SizeOfOptionalHeader决定,不是硬编码;32 位通常是 224 字节,64 位是 240 字节 - 手动解析时别依赖
sizeof(IMAGE_OPTIONAL_HEADER64),它在 32 位编译环境下也可能被定义,但内容不对
一个典型坑:SizeOfStackReserve 在 32 位头里偏移是 0x40,在 64 位头里是 0x50——偏移错了,后面全错。
PE 头修改后校验和不匹配,自己算容易漏什么?
自己实现校验和最常漏的是:跳过校验和字段本身、没按 16 位无符号整数累加、没处理奇数字节数、没做最终折叠(两次求和)。Windows 的 CheckSumMappedFile 实际做了两轮累加,且把校验和字段临时置零再算。
- 计算前,先将当前校验和字段(
OptionalHeader.CheckSum)设为 0,否则会把自己也算进去 - 按每 2 字节为单位解释内存,累加为
DWORD,溢出不丢弃(即带进位加) - 若总字节数为奇数,最后一个字节按低位补 0 构成 16 位再加(不是忽略)
- 最终结果要再做一次“高 16 位 + 低 16 位”,直到结果落在 16 位内,然后加上原始和的低 16 位
这一步看似机械,但少一个细节,生成的校验和就通不过 LoadLibrary 验证。
PE 头解析不是纯结构体 memcpy 能搞定的事,每个字段背后都连着加载器的一条路径。改得越多,越要确认你动的是加载器真正在意的那个值,而不是你以为它在意的。



















