malx-labs/pe-delayload-divergence
GitHub: malx-labs/pe-delayload-divergence
该项目通过单一异常的 PE 测试用例集,研究并记录了四种主流工具在延迟加载导入目录解析上的规范歧义与行为分歧。
Stars: 0 | Forks: 0
# PE 延迟加载跨工具解析差异
这是一项关于 PE 延迟加载导入目录在四种广泛使用的工具中如何被不同解析的研究的复现工件,该研究使用单一异常测试用例来隔离每种差异的原因。
每个测试用例与格式良好的延迟加载二进制文件在恰好一个结构属性上不同。当工具在同一个测试用例上产生不同的输出时,这种差异完全归因于该单一属性,没有混淆因素。一个格式良好的对照测试用例被所有四个工具完全相同地解析,以此作为每个发现的基础。
## 这是什么(以及不是什么)
**这是:** 一套针对故意畸形、非功能性 PE 测试用例的字节级规范,用于隔离特定的 PE 规范解释问题,并附带四个 PE 解析器针对每个用例的记录输出。
**这不是:** 恶意软件、规避载荷或旨在击败安全工具的文件。本仓库不包含任何可执行文件,仅包含字节级规范和记录的工具输出。这些规范描述的用例是非功能性的研究标本(单个畸形描述符,无可执行代码),旨在探测解析器在规范未定义处的格式行为,而非用于逃避检测。它们就像协议一致性测试套件一样,是用于研究的标本。
每个测试用例在原本有效的基线之上,仅改变单一结构属性(一个属性位、一个数组长度、一个目录大小)。目标是确定是哪种规范歧义导致了工具间的分歧,以便将该歧义报告给规范维护者。
## 研究发现
三个单一异常的测试用例在四种工具中均产生了截然不同的解析结果:
| 测试用例 | 异常 | IOCX | pefile | dumpbin | Ghidra |
|--------------------|--------------------------|--------------------|-----------|--------------|--------------|
| v0 属性模式 | Attributes 位 0 清除 | 解析 (1) | 解析 (1) | 拒绝 (0) | 拒绝 (0) |
| INT/IAT 不匹配 | INT=3, IAT=2 thunks | INT (3) | INT (3) | INT (3) | IAT (2) |
| 声明大小 vs 终止符 | 声明大小截断数组 | 遍历 (2) | 遍历 (2) | 声明 (1) | 声明 (1) |
工具的对齐方式**在不同的测试用例之间会发生变化**;dumpbin 在 INT/IAT 权威性上站在 IOCX/pefile 一边,但在目录终止上却反对它们;Ghidra 在 INT/IAT 上是唯一的异议者,但在终止问题上却加入了 dumpbin 的行列。没有哪两个工具在所有三个测试用例上的意见是一致的。
这表明存在三个*独立的*规范解释轴线,而不是简单的严格与宽松的光谱。每种歧义都是由每种实现独立解决的,而 PE 规范对这三者的定义都不够充分。
**这些都不是工具缺陷。** 每个观察到的行为都是对 PE 格式中未定义点的一种合理理解。该发现的意义在于表明合理的实现会产生分歧,而不是说其中哪一个是错误的。
## 工具版本
| Tool | Version |
|-------------|----------------|
| IOCX | 0.7.5 |
| pefile | 2024.8.26 |
| dumpbin | 14.44.35227.0 |
| Ghidra | 12.1 PUBLIC |
测量环境:IOCX/pefile 运行于 Linux (Kubuntu),Ghidra 运行于 Windows 10,dumpbin 运行于 Windows 11。所有测试用例均为 PE32+ (x64)。
## 测试用例
每个测试用例都在 fixtures/specs/ 中有逐字节的文档记录。以下是每个用例中单一异常的摘要:
### fixture_106 — v0 旧版属性模式
描述符的 `Attributes` 位 0(在 `winnt.h` 中命名为 `RvaBased`,在 `delayimp.h` 中命名为 `dlattrRva`)被**清除**。本文档使用“v0”作为位清除(旧版指针)形式的简写,使用“v1”作为位置位(RVA)形式的简写。
描述符的所有其他字段都是有效的映像内 RVA,指向 `test.dll → LegacyFunc`。
- **IOCX / pefile:** 将这些字段解释为 RVA,并解析导入。IOCX 还会额外标记 `delay_import_attributes_legacy_va_mode`。
- **dumpbin:** 识别该目录,但不枚举任何内容。
- **Ghidra:** 拒绝名称和 thunk 指针(`Invalid namePtr for 2000`,`Invalid thunkPtr for 4000`),不枚举任何内容。
*规范问题:* 解析器应如何处理现代映像中的 v0 描述符?是将字段解释为 VA、拒绝它们,还是忽略该属性位?
### fixture_105 — INT/IAT 长度不匹配
Import Name Table 声明了三个 thunk(Foo、Bar、Baz);Import Address Table 声明了两个(Foo、Bar)。这两个数组各自本身都是格式良好的。
- **IOCX / pefile / dumpbin:** 从 INT 进行枚举 — 3 个导入。IOCX 会标记 `delay_import_int_iat_mismatch`。
- **Ghidra:** 从 IAT 进行枚举 — 2 个导入。
*规范问题:* 当 INT 和 IAT 长度不一致时,在枚举导入方面哪一个具有权威性?
### fixture_101 — 描述符数组:声明大小 vs Null 终止符
延迟加载目录声明大小为 48 字节;包含一个完整的 32 字节描述符加上第二个描述符的前 16 字节,且没有 Null 描述符终止符。
- **IOCX / pefile:** 遍历至 Null 终止符,将部分加零填充的第二个描述符读取为有效:2 个导入(一个重复项)。IOCX 会标记 `delay_import_descriptor_unterminated`。
- **dumpbin / Ghidra:** 在第一个完整的描述符处停止,遵守声明的大小:1 个导入。
*规范问题:* 描述符数组是由目录声明的 `Size` 终止,还是由 Null 描述符终止?当声明的大小在描述符中途结束且没有终止符时,该不完整的描述符算作一个导入吗?
### fixture_109 — 格式良好的 v1 对照
一个结构完整的 v1 延迟加载描述符:三个按名称导入(Function1/2/3),匹配的 INT/IAT,已绑定的 IAT 已填充。**被所有四个工具完全相同地解析**(每个都枚举出 3 个导入)。此对照确认上述差异可归因于它们各自改变的单一属性,而不是由于测试用例极其简化的构造方式引起的。
## 仓库布局
```
README.md this file
results.csv the full divergence matrix (all tools, all fixtures)
fixtures/
specs/ byte-level annotated spec of each fixture
fixture_106.md
fixture_105.md
fixture_101.md
fixture_109.md
tools/
DumpDelayImports.java Ghidra headless postscript (queries the
program model for delay-load imports)
run_ghidra_batch.ps1 Script for batch-processing Ghidra fixture analysis
outputs/ recorded per-tool output (inspect without rebuilding)
fixture_106/
dumpbin.txt
ghidra.txt
pefile.txt
iocx.json
...
```
## 复现结果
这些测试用例并不作为二进制文件提供。每个用例都在 fixtures/specs/ 中有完整的逐字节规范;使用任何 PE 编写工具根据其规范重新构建您想要测试的用例,然后将其通过以下工具运行。这些规范非常简单(单个描述符,最简化的 INT/IAT,一到两个导入),可以通过手动或编程方式重构。
### dumpbin (Windows, MSVC Build Tools)
```
dumpbin /imports fixture_106.exe
```
寻找 "Section contains the following delay load imports" 块。对于 fixture_106,会打印节标题但不会列出任何导入(Ghidra 和 dumpbin 都会拒绝 v0)。
### pefile (Python)
```
import pefile
pe = pefile.PE("fixture_106.exe")
if hasattr(pe, "DIRECTORY_ENTRY_DELAY_IMPORT"):
for entry in pe.DIRECTORY_ENTRY_DELAY_IMPORT:
print(entry.dll, [imp.name for imp in entry.imports])
else:
print("no delay-load directory parsed")
```
### Ghidra (headless)
将 `tools/DumpDelayImports.java` 放置在脚本目录中,然后针对单个测试用例运行:
```
$GHIDRA_HOME/support/analyzeHeadless \
-import fixture_106.exe \
-postScript DumpDelayImports.java \
-scriptPath \
-overwrite \
-analysisTimeoutPerFile 60 \
2>&1 | grep '^DDI:'
```
在 Windows 上,使用 `analyzeHeadless.bat` 和 `Select-String '^DDI:'` 来代替 `grep`。
该后处理脚本直接查询程序的外部符号(而非 GUI 的 Symbol Tree),并打印可被 grep 抓取的 `DDI:` 行:解析状态、延迟目录位置以及枚举出的导入。对于纯延迟加载的测试用例,`external_symbol_count` 即为 Ghidra 枚举出的延迟导入数量(有关为何成立,请参阅脚本头)。
**完整测试用例集:** `tools/run_ghidra_batch.ps1` 会运行输入目录中的每个 `.exe`,并将每次运行的控制台输出(包括 `DDI:` 行)捕获到对应测试用例的 `ghidra.txt` 中。请在脚本顶部修改路径以适配您的环境。
### IOCX
```
iocx fixture_106.exe -a full
```
延迟加载导入出现在 `metadata.delayed_imports` 下;结构异常出现在 `analysis.heuristics` 下(例如 `delay_import_attributes_legacy_va_mode`)。
## 范围与诚实的注意事项
- **这些是规范歧义的发现,而不是错误报告。** 此处记录的每一种工具行为都是对规范未定义文本的一种合理且无可辩驳的解释。需要进行修正的合适对象是 PE 规范,而不是工具。
- **v0 测试用例使用有效的映像内 RVA(单次发射构造)。** IOCX 和 pefile 之所以能解析出“正确”的导入,仅仅是因为测试用例的 RVA 字段恰好是有效的。在带有原始 VA 的真正 v0 二进制文件上,同样的 RVA 解释会产生乱码导入,而 dumpbin 和 Ghidra 的拒绝可以说会更加正确。IOCX 和 pefile 的特征被描述为“忽略 v0 标志并作为 RVA 解析”,而不是“解析正确”。
- **Ghidra 也会对所有测试用例发出良性警告**(零入口点、非标准目录计数),这与解析差异无关。这些是对测试用例最小化人工构造的反应,并且在比较中是恒定的。
- **工具版本已在上方固定。** 不同版本对延迟加载的处理可能会有所不同;结果适用于所列出的版本。
- **IOCX 是作者自己的工具。** 它的行为是按照与其他工具相同的证据标准(直接输出)进行报告的,但读者应注意作者参与了该项工作。
## 许可证
测试用例规范、结果、记录的输出以及本 README 均获得 [CC-BY-4.0](LICENSE) 许可。
`tools/` 中的脚本(`DumpDelayImports.java`、`run_ghidra_batch.ps1`)均获得 [MPL-2.0](tools/LICENSE) 许可。
**署名:** “PE Delay-Load Cross-Tool Divergence, MalX Labs, https://github.com/malx-labs/pe-delayload-divergence” (CC-BY-4.0)。
## 相关内容
- PE 格式文档 PR(规范歧义报告):[待定]
- Ghidra 讨论(INT/IAT 权威性问题):[待定]
- 方法论文章:https://gist.github.com/malx-labs/ce30872e5db790f25c964b4027b2b7ee
标签:AI合规, JS文件枚举, PE文件格式, 二进制分析, 云安全运维, 域名枚举, 格式规范, 测试固件, 解析器测试, 逆向工具