简介:面向PE格式学习与逆向调试的实用工具包,适合软件开发者、逆向工程师及系统管理员。资源以PE格式详解为主线,覆盖DOS头、PE签名、COFF头、可选头、节区表、导入导出表、重定位表等核心结构,压缩包内配套PETools.exe及相关DLL、C++工程和示例代码,便于对照源码理解PE文件的组织与加载流程,帮助理解节区属性、映像基址与入口点等关键信息。包体共83个文件、约301KB,以DLL和EXE为主,另有CPP/DSP工程、文本说明、库文件与汇编脚本,兼顾现成工具与可扩展源码。已有808人学习下载。借助这套工具,可查看PE头信息、解析导入导出表、嗅探文件结构,甚至进行内存转储与签名校验,适合希望深入Windows可执行格式、开展逆向工程或排查系统问题的读者。无论是学习PE规范、调试异常程序,还是分析未知二进制,都能提供直观参考。
1. PE TOOLS 到底是什么:先分清这里的 PE 是哪种 PE
PE TOOLS 这个标题看着小,背后要对付的却是 Windows 可执行文件(exe、dll、sys)的通用容器——PE 格式。它全称 Portable Executable,意思是无论 32 位还是 64 位 Windows,程序在磁盘上的存在形式都遵守同一套结构约定。遇到“这个 exe 依赖哪些 DLL”“有没有加壳”“入口点在哪”“为什么换台机器就跑不起来”这类问题,靠的就是 PE TOOLS 这类查看工具。
这里说的 PE TOOLS 不是 U 盘启动盘的 PE,也不是重装系统用的维护工具。它面向的是开发和排查场景:把程序从磁盘文件到内存映像之间发生的事拆开看,理清导入表、导出表、节表、资源和重定位。会用的人,几分钟就能摸清一个陌生可执行文件的基本盘;不会用的人,面对一个报错程序只能靠日志碰运气。
适合谁也很明确:做 Windows 客户端部署的人、排查兼容性问题的运维、写驱动和动态库的 C/C++ 开发,以及入门逆向分析的人。下面从文件结构开始,一路讲到工具选型、实际操作和踩坑,目标是让你看完能对着一个真实 exe 动手。
2. 先把 PE 结构拆开看:DOS 头、NT 头、节表和导入导出表
2.1 DOS 头与 e_lfanew:所有 PE 解析都从这两个字段起步
任何一个合法 PE 文件,前两个字节一定是4D 5A,也就是 ASCII 字符MZ。这是 DOS 时代留下的遗产,Windows 加载器靠它快速判断“这文件到底是不是 PE”。真正的 PE 结构起始位置并不固定,它由一个叫e_lfanew的字段指出,这个字段位于文件偏移0x3C处,占 4 字节,用小端序存储。
用十六进制编辑器(比如 HxD)打开任意一个 exe:先看前两个字节是不是4D 5A,再跳转到偏移0x3C,读 4 字节,比如80 00 00 00,说明 NT 头在文件偏移0x80。常见值有0x40、0x80、0xF0,但不要假设它是固定值,因为加壳工具和编译器都可能调整它。
| 字段 | 偏移 | 大小 | 含义 |
|---|---|---|---|
| e_magic | 0x00 | 2 字节 | 固定为0x5A4D,即MZ |
| e_cblp | 0x02 | 2 字节 | DOS 头内字段,解析 PE 时基本用不到 |
| e_lfanew | 0x3C | 4 字节 | NT 头相对文件起始的偏移,最关键的跳转点 |
| e_cp / e_crlc 等 | 0x04~0x3B | 不等 | DOS 遗留字段,通常忽略 |
这个头还有个常见坑:有些 PE 查看工具界面里会直接列出 DOS 头全部字段,看着很长,其实真正影响解析的只有e_lfanew和e_magic。其余字段是 DOS 程序时代用于内存分配的,现代 Windows 加载器根本不看。
2.2 NT 头三件套:签名、文件头、可选头
顺着e_lfanew跳过去,先看到 4 字节签名50 45 00 00,也就是PE\0\0。紧跟着是IMAGE_FILE_HEADER(20 字节),再往后是IMAGE_OPTIONAL_HEADER。注意名字里虽然写着 Optional,但 Windows 加载器每次加载都必须读它,一个字节都不能少,这个命名是历史遗留。
IMAGE_FILE_HEADER里我最常看的字段是Machine和NumberOfSections。Machine为0x14C表示 x86,0x8664表示 x64;NumberOfSections决定节表有多少项。SizeOfOptionalHeader也得留意:32 位程序通常为0xE0,64 位为0xF0,解析节表时要拿它跳过可选头,算错一步后面全乱。
IMAGE_OPTIONAL_HEADER里关键是这几个:Magic为0x10B表示 PE32,0x20B表示 PE32+;AddressOfEntryPoint是程序入口 RVA,加壳程序这里通常指向壳的代码;ImageBase指示首选加载地址(32 位常见0x400000,64 位常见0x140000000);Subsystem区分 GUI 程序(2)和 CUI 控制台程序(3)。
| 所在结构 | 字段 | 典型值 | 用途 |
|---|---|---|---|
| FILE_HEADER | Machine | 0x14C / 0x8664 | 判断 32 位还是 64 位 |
| FILE_HEADER | NumberOfSections | 一般 3~10 | 控制节表循环次数 |
| FILE_HEADER | SizeOfOptionalHeader | 0xE0 / 0xF0 | 跳过可选头定位节表 |
| OPTIONAL_HEADER | Magic | 0x10B / 0x20B | 区分 PE32 / PE32+ |
| OPTIONAL_HEADER | AddressOfEntryPoint | 0x1000 附近 | 入口点 RVA |
| OPTIONAL_HEADER | Subsystem | 2 / 3 | GUI 或控制台子系统 |
2.3 节表:文件偏移和 RVA 的换算全靠它
节表紧跟可选头,每个条目固定 40 字节,条目数量由NumberOfSections给出。每个节有一个名字、一个内存里的虚拟大小和虚拟地址、一个磁盘里的大小和文件偏移,还有一组描述属性的标志位。常见的节名有.text(代码)、.rdata(只读数据)、.data(可读写数据)、.rsrc(资源)、.reloc(重定位)。
解析 PE 时最容易出错的就是理解两个地址空间:磁盘上按PointerToRawData定位,内存里按VirtualAddress定位,两者经过加载器的节映射建立关系。当你在工具里看到某个函数的 RVA,想知道它在文件里的哪个位置,必须做一次换算:
文件偏移 = Section.PointerToRawData + (RVA - Section.VirtualAddress)前提是这个 RVA 落在该节的虚拟地址区间内。举个例子:.text节的VirtualAddress是0x1000,PointerToRawData是0x400,一个符号的 RVA 是0x1520,那它在文件里的偏移就是0x400 + (0x1520 - 0x1000) = 0x920。工具能直接显示文件偏移,但手写解析脚本时必须自己算这一步。
为什么会有两个地址?因为磁盘文件按FileAlignment(常见0x200)对齐,而内存映像按SectionAlignment(常见0x1000)对齐。一个节在磁盘上可能只有 0x600 字节,但映射进内存后占据 0x1000 字节的空间,多出来的部分由加载器清零。
2.4 导入表与导出表:PE 文件对外依赖的窗口
数据目录(DataDirectory)位于可选头末尾,是一组{ RVA, Size }对。索引 0 是导出表,索引 1 是导入表,索引 2 是资源表,索引 5 是重定位表,索引 12 是导入地址表(IAT)。这是 PE 文件最容易被新手看晕的部分,因为导入表里藏着一层间接结构。
IMAGE_IMPORT_DESCRIPTOR是一个以全零结尾的数组,每组 20 字节,描述一个被导入的 DLL。关键字段是OriginalFirstThunk(INT 的 RVA)、FirstThunk(IAT 的 RVA)、Name(DLL 名字符串的 RVA)。在磁盘上,INT 和 IAT 都指向同一个IMAGE_THUNK_DATA数组,数组里每个元素是一个指向IMAGE_IMPORT_BY_NAME的 RVA,而IMAGE_IMPORT_BY_NAME里存放着函数序号和名字。
程序加载进入内存后,加载器会遍历 IAT,把每个元素改写为实际函数地址。这就是为什么调试器里看 IAT 是一串函数地址,而文件里看同样的位置却是一串 RVA。理解不了这一点,你会在“为什么磁盘和内存不一样”这个问题上卡很久。
验证手段很简单,装上 Visual Studio 工具链后,在命令行跑一句:
dumpbin /imports C:\path\app.exe输出会列出依赖的 DLL 名、每个 DLL 里导入的函数名和序号。dumpbin /exports对应导出表,适合分析 DLL 对外提供的接口。没有 Visual Studio 环境时,用 LLVM 工具链的llvm-readobj --coff-imports app.exe也能看到同样的信息。
3. 工具怎么选:图形界面、命令行、查壳工具各管一段
3.1 CFF Explorer:最顺手的图形化 PE 查看器
我日常的主力工具是 CFF Explorer,它是 Explorer Suite 套件里的一个独立程序,绿色免安装,打开就能用。为什么选它而不是更老的 LordPE 或者 PEiD?因为 CFF Explorer 把 PE 文件的每个结构都映射成了左侧树形菜单:DOS Header、NT Header、Section Headers、Import、Export、Resource、Relocations,点哪一项右边就显示对应字段,完全不需要手算偏移。
更重要的是它的“地址换算”能力。在第 2 章里我们还得手动用公式把 RVA 换成文件偏移,但在 CFF Explorer 里,任何 RVA 字段都可以右键选择跳转到对应文件位置,它自己完成节表换算。新手查字段时不容易翻车,熟手做批量修改时也能省一半时间。
它还自带一个很好用的功能:直接编辑字段。比如你想把某个 exe 的子系统从 GUI 改成控制台,在NT Header -> Optional Header -> Subsystem里把 2 改成 3,保存即可。这种改完能立刻验证加载行为的能力,是纯十六进制编辑器做不到的。
3.2 dumpbin 与 llvm-readobj:命令行的批量场景更好用
图形工具适合单文件细看,但当你手上有几十个 exe 要做依赖审计时,图形界面就不够用了。这时候用命令行工具。dumpbin是 Visual Studio 工具链里最直接的 PE 查看命令,没有额外安装成本,只要是装了 VS 或者 VS Build Tools 的机器就能跑。
dumpbin /headers C:\tools\demo.exe dumpbin /imports C:\tools\demo.exe dumpbin /exports C:\tools\demo.dll dumpbin /dependents C:\tools\demo.exe/headers输出 DOS 头、NT 头和节表全量字段;/imports列出所有导入的 DLL 和函数;/dependents只列依赖的 DLL 名,适合做程序集体检。注意这些是 Visual Studio 环境的命令,不是 shell 内置命令,使用时需要先打开“开发者命令提示符”,或者在普通命令行里全路径调用dumpbin.exe。
如果你不想安装庞大的 VS 工具链,装一个 LLVM 发行版也能完成大部分工作。llvm-readobj支持的参数和输出格式略有不同,但覆盖了文件头、节表、导入表、导出表、重定位表这些核心信息:
llvm-readobj --file-headers --sections app.exe llvm-readobj --coff-imports app.exe llvm-readobj --coff-exports app.dll命令行的好处是能放进批处理脚本里循环跑。我一般写一个for循环,把目录下所有 exe 的依赖导出来归档,跑完直接生成一份清单,比一个个点开图形界面快得多。
3.3 DIE 和 PEiD:查壳这一步不能少
拿到一个陌生 exe,第一件事不是看表,而是查壳。这里的“壳”指加壳程序(packer)对 PE 文件的整体压缩或加密。加壳后,你直接看导入表往往会发现它们被压缩或加密了,入口点被改到壳的代码段里,原始导入表要等程序运行时由壳在内存中还原。所以不带壳分析步骤直接去读导入表,读到的基本是一堆无意义数据。
查壳工具有两个层次。老牌 PEiD 曾经是标配,但已经多年不更新,对新版编译器生成的程序识别率很低;我更推荐 DIE(Detect It Easy),它的签名库维护活跃,还能显示入口点所在节、熵值、链接器版本这些辅助信息。用 DIE 打开一个 exe,先看主界面显示什么壳名,再看入口点落在哪个节:普通程序入口通常在.text节,加壳程序入口常常落在一个名为UPX0、ASPack或自定义名的节上。
| 工具 | 类型 | 强项 | 注意 |
|---|---|---|---|
| CFF Explorer | 图形 | 结构浏览、字段编辑、地址换算 | 单文件场景最好用 |
| dumpbin | 命令行 | 依赖清单、头信息、VS 环境自带 | 需要装 VS 工具链 |
| llvm-readobj | 命令行 | 跨平台、免 VS | 需要 LLVM 环境 |
| DIE | 图形/命令行 | 壳识别、熵分析 | 建议和 CFF Explorer 配合 |
3.4 一套离线可用的最小静态分析组合
总结下来,我的建议组合是:DIE 查壳、CFF Explorer 看结构、dumpbin 或 llvm-readobj 做批量清单、HxD 做最后的字节确认。这四个工具覆盖了 PE 静态分析的完整链路,而且全部离线可跑,不需要目标程序运行起来,也不需要网络环境。
这套组合的价值在于:它把“程序能不能跑”这个黑匣子问题,拆成了“依赖齐不齐、入口对不对、有没有壳、节表是否被破坏”几个可验证的具体问题。排查部署故障时,先用 DIE 看有没有壳,再用 CFF 看导入表确认 VC 运行库依赖,最后用 dumpbin 批量扫一遍所有模块的依赖,基本能把问题定位到具体 DLL 层面。
工具链不必一次备齐。新手先装一个 CFF Explorer,边看第 2 章的结构边对照真实文件,等需要批量处理或者写脚本了,再补上 dumpbin。工具是手段,把结构看懂才是目的。
4. 实操:对着一个真实 exe 把 PE 信息完整拉出来
4.1 先过 DIE 查壳,排除干扰项
假设你拿到一个来历不明的demo.exe,先别急着双击运行,也别直接拖进 CFF Explorer。第一步打开 DIE,把文件拖进窗口,看结果面板。
DIE 主界面会给出几行关键信息:文件类型(PE32 还是 PE32+)、壳/编译器检测结果、入口点(EP)所在节、文件熵值。没有加壳的程序通常显示为“Microsoft Visual C++”或“Microsoft Linker”,加壳程序则直接显示壳名,比如 UPX、ASPack、Themida。熵值也很有参考意义,正常编译的 PE 文件熵值一般在 5~6 之间,被压缩加壳后会升高到 7 以上,接近随机数据。
Detect It Easy v0.05 File: demo.exe Entropy: 6.77 EP: 0x00011000 Section: .upx0 Result: UPX入口点落在.upx0这种非标准节里,基本可以确定是 UPX 壳。下一步最好先脱壳再分析,否则直接看导入表会看到壳的导入信息。UPX 类壳可以用upx -d demo.exe脱掉;如果是商业壳,就不建议硬脱了,老老实实用动态调试或者找原始安装包。
4.2 用 CFF Explorer 读导入表和依赖 DLL
确认没有壳或者脱壳完成后,再把文件拖进 CFF Explorer。左侧栏第二项是Import,点开后 CFF 会把导入表解析成两栏:左边是被导入的 DLL 列表,右边是当前选中的 DLL 里导入的函数名和序号。
我一般先看左边列表,确认这个程序依赖哪些 DLL。出现KERNEL32.dll、USER32.dll、GDI32.dll是正常的 Windows 基础库;出现VCRUNTIME140.dll、MSVCP140.dll说明它依赖 Visual C++ 2015-2022 运行库;出现大量没见过的第三方 DLL 就要注意,可能带了一堆不必要的外部依赖。
DLL Name: KERNEL32.dll IsFirstParty: No Functions: CreateFileW, ReadFile, WriteFile, WaitForSingleObjectExport页签在分析 DLL 时用得上。拿到一个xxx.dll,想确认它对外提供了什么接口,直接切到Export,能看到导出函数的名称、序号和 RVA。很多运维场景里,程序报“无法找到入口点”,本质上就是 DLL 的导出表和调用方期望的函数签名对不上,这时候用这个页签查一下最直接。
CFF 还会在左侧列出Resource和Relocations,前者包含图标、版本信息、清单,后者是 ASLR(地址空间随机化)必需的偏移记录。排查“为什么换一台机器图标变了”这类问题,可以看Resource里的版本信息;排查 64 位程序加载崩溃,可以看Relocations是否完整。
4.3 改一个字段验证加载行为:Subsystem 实验
理解了 PE 结构之后,最快建立“文件字段真实影响加载”这个直觉的动手实验,是把 GUI 程序改成控制台程序。选一个样本 exe(最好是你自己写的一个没有数字签名要求的测试程序),用 CFF Explorer 打开,进入NT Header -> Optional Header,找到Subsystem字段,当前值应该是2(Windows GUI)。
把2改成3(Windows CUI),保存文件。建议先把原始文件复制一份备份,因为修改会破坏原有数字签名,正式发布的程序不能这么玩。改完直接双击运行,你会发现原本不开控制台窗口的 GUI 程序,这次启动时旁边多了一个黑色控制台窗口。这是因为加载器根据Subsystem决定是否为新进程创建控制台设备。
Subsystem = 2 (IMAGE_SUBSYSTEM_WINDOWS_GUI) -> 无控制台窗口 Subsystem = 3 (IMAGE_SUBSYSTEM_WINDOWS_CUI) -> 附加控制台窗口这个实验的意义在于,它让你看到加载器对 PE 头的依赖程度:一个字节的变化,直接改变了进程的外在表现。以后再遇到“为什么程序运行时会蹦出黑框”的部署问题,你马上就能想到是不是编译时链接器把子系统设置成了 CUI,而不是瞎猜。
注意:修改 PE 头字段会破坏 Authenticode 数字签名,被修改的文件在部分系统中可能触发 SmartScreen 拦截。验证用测试程序即可,不要拿生产环境文件做实验。
4.4 批量提取依赖清单:dumpbin 配合脚本
单文件分析够用了,但部署一个系统时往往要扫整个目录。这时写个批处理循环,把所有 exe 和 dll 的依赖导出成文本。以下是 Windows 命令行脚本:
@echo off cd /d C:\audit for %%f in (*.exe *.dll) do ( echo ===== %%f =====>> deps.txt dumpbin /dependents "%%f" | findstr /r "^[A-Za-z].*\.dll" >> deps.txt )逻辑说明:for循环遍历当前目录下所有 exe 和 dll;dumpbin /dependents输出每个文件的依赖列表;findstr /r用正则过滤掉 dumpbin 输出里的头信息和路径行,只保留 DLL 名列。运行结束后打开deps.txt,每个文件依赖哪些 DLL 一目了然。
notepad deps.txt如果机器上没有 dumpbin,改用llvm-readobj --coff-imports后把输出丢给 findstr 过滤也是一样的思路。脚本本身不值钱,值钱的是把“确认依赖”从人工逐个点开变成一条命令跑完,这在交付几十个组件的时候能省下一整晚。
5. PE 解析与查看的六个高频翻车现场:现象、原因、解决
5.1 工具位数错配和 .NET 程序集导致的假象
现象:用 32 位版 CFF Explorer 打开一个 64 位 exe,字段显示得很奇怪,Machine不是0x8664,Optional Header里的地址也读不出正常值;或者干脆提示解析失败。
原因:PE32+ 的可选头结构和 PE32 不同,文件头里SizeOfOptionalHeader多了 16 字节,ImageBase从 4 字节变成 8 字节。老的 32 位工具没有按Magic分支处理,直接把后续字段按错误长度解析,自然全部错位。
解决:打开任何 PE 工具前,先确认工具版本支持目标文件位数。CFF Explorer 同一安装包能自动识别,但旧版 LordPE、PETools 这类老程序对 x64 支持不完整。遇到字段错乱,第一反应不是怀疑文件坏了,而是换一个支持 PE32+ 的工具再试。
现象:用 dumpbin 打开一个 .NET 程序集,报错LNK1104 无法打开文件,或者提示不是有效的 Win32 程序。
原因:.NET 程序集从文件格式上仍然是 PE 文件,但它包含 CLR 头,核心代码不在传统的.text代码节里,而是在.text或单独节里的 MSIL 元数据中。dumpbin 的/imports对这个结构的理解不完整,遇到托管程序会直接拒绝或漏报。
解决:先确认是不是托管程序:用 CFF Explorer 看DataDirectory里索引 14(CLR Runtime Header)是否有值,或者用dumpbin /clr尝试。命令行场景下,llvm-readobj对 .NET 容忍度更高,勉强能列出节表和导出表。真正分析托管代码时换 ILSpy 或 dnSpy,PE 工具只做结构确认。
5.2 RVA 换算和 PE 签名定位的隐蔽错误
现象:在工具里查到某个函数的 RVA,手动加节表偏移去文件里找,跳过去看到的数据和工具显示完全对不上。
原因:RVA 换文件偏移必须先确定 RVA 落在哪个节的虚拟地址区间。很多人直接拿RVA - ImageBase或者随便加了一个节的PointerToRawData,没考虑这个 RVA 可能落在别的节里,甚至落在节与节的间隙中。
解决:严格按公式来:先找到RVA >= Section.VirtualAddress且RVA < Section.VirtualAddress + Section.VirtualSize的节,再用PointerToRawData + (RVA - VirtualAddress)换算。如果目标 RVA 不在任何节范围内,说明它可能是未映射的数据,通常对应文件末尾的证书表或有用的调试信息。
现象:手写脚本时想“搜索 PE 签名定位 NT 头”,但是0x50 0x45 0x00 0x00在文件里搜出来好几个命中,不知道该信哪一个。
原因:PE\0\0这 4 个字节只是特征签名,不是唯一标识。节数据里完全可能恰好出现同样的字节序列,尤其在某节内容是字符串或资源时,命中率还不低。直接搜第一个命中大概率是错的。
解决:不要全盘搜索,永远从e_lfanew字段跳转。先读0x3C处的 4 字节小端值作为偏移,再到该偏移验证恶魔签名。如果e_lfanew指向的位置不是PE\0\0,说明文件可疑,可能是加壳程序刻意修改了跳转点,此时继续解析没有意义,先考虑脱壳或恢复原始头。
5.3 加壳程序和异常 e_lfanew 带来的误判
现象:用 CFF Explorer 打开一个加了 UPX 壳的程序,Import页签里只有两三个不相关的 DLL,找不到程序本身需要的Win32 API,以为文件损坏了。
原因:加壳工具把原始 PE 的导入表压缩或加密后放进了壳的节里,PE 头的DataDirectory[1]指向的是壳自己的导入表占位数据。你直接读静态文件当然看不到真实依赖,这是壳的预期行为,不是文件坏了。
解决:先查壳再分析,UPX 类壳优先脱壳,脱壳后导入表会还原。如果不想脱壳,用动态调试器跑起来,等壳解压完在执行流到达原始入口点时,再去 dump 内存中的导入表。静态工具看加壳样本只能得到假信息,这一点要记牢。
现象:脚本里假设e_lfanew恒为0x80,结果对某个样本解析出的 NT 头字段全是乱码。
原因:虽然多数编译器把 NT 头放在0x80或0xF0,但一些壳、加保护工具或特殊编译器会把它挪到别的偏移,用固定偏移读必然错位。DOS 头 + DOS 存根的大小由链接器决定,没有硬性标准。
解决:任何 PE 解析脚本都必须先动态读取0x3C处的 4 字节e_lfanew,再据此确定 NT 头位置。写死偏移只适合针对单一编译器产出的样本做快速查看,不适合做通用解析。把这个习惯固化下来,能省掉大量“为什么换个文件就解析失败”的排查时间。
6. 进阶:用 Python 手写一个 200 行的 PE 解析器
工具看多了,自己动手写一个最小解析器是检验理解最直接的方式。下面这段脚本只依赖 Python 标准库struct,不做任何假设,完整实现 DOS 头跳转、NT 头验证和节表遍历:
import struct path = r"C:\audit\demo.exe" with open(path, "rb") as f: data = f.read() # 1. DOS头:读取e_lfanew e_lfanew = struct.unpack_from("<L", data, 0x3C)[0] assert data[e_lfanew:e_lfanew + 4] == b"PE\x00\x00", "PE signature not found" # 2. 文件头:Machine(2字节)+NumberOfSections(2字节)+... 共20字节 machine, nsects, _, size_opt, _ = struct.unpack_from("<HHIII", data, e_lfanew + 4) # 3. 可选头:Magic在文件头后偏移0x00处 magic = struct.unpack_from("<H", data, e_lfanew + 4 + 20)[0] pe32_plus = (magic == 0x20B) print(f"Machine: {machine:#06x} Sections: {nsects} PE32+ : {pe32_plus}") # 4. 节表:文件头(20) + 可选头(size_opt) 之后开始 sec_offset = e_lfanew + 4 + 20 + size_opt for i in range(nsects): off = sec_offset + i * 40 name = data[off:off + 8].rstrip(b"\x00").decode("ascii", "replace") vsize, vaddr, raw_size, raw_ptr = struct.unpack_from("<IIII", data, off + 8) print(f"{name:8s} VA={vaddr:#010x} RawPtr={raw_ptr:#08x}")逻辑说明:第一步用unpack_from读取0x3C处的 4 字节无符号小端整数,得出 NT 头偏移,这是整套解析的地基。第二步在e_lfanew + 4处读取文件头里的Machine、节数量、可选头大小,<HHIII表示 2+2+4+4+4 字节的小端解析。第三步读取可选头的Magic,判断目标是 PE32 还是 PE32+,这一步决定后续字段宽度,必须最先确认。第四步遍历节表,每个节条目 40 字节,前 8 字节是名字,偏移 8 处开始是虚拟大小、虚拟地址、文件大小、文件偏移。
这段脚本故意留了扩展空间:DataDirectory在可选头尾部,解析导入表时需要先定位DataDirectory[1]的 RVA,再用第 2 章讲的换算公式转到文件偏移,然后循着IMAGE_IMPORT_DESCRIPTOR数组逐个读 DLL 名和函数名。手写完整导入表解析大约 80 行代码,是个值得自己推一遍的练习。
如果赶进度,也可以直接用pefile库,pe = pefile.PE(path)一行读完所有结构,省去写实现的时间。但我建议至少手写一次节表遍历,因为所有工具和库都是在做同样的事:读e_lfanew、验证签名、分位数处理字段、按节表做地址换算。这些步骤自己走一遍,再回头用 CFF Explorer 就不仅是“会用”,而是每个字段显示什么、为什么显示成这样,心里全有数。我也是踩了几次字段错位和地址算错的坑才养成固定写跳转偏移的习惯,希望帮到你。
本文还有配套的精品资源,点击获取