专栏 REA 解剖

会话:一次 open_binary 绑定一个引擎

一个目标只能有一个深分析绑定。REA 宁可返回 ambiguous,也不让 auto 在 Hopper 和 Ghidra 之间悄悄挑一个——因为选择引擎本身就是一次分析决策。

会话:一次 open_binary 绑定一个引擎

open_binary 看起来只是一个”打开文件”的动作。在 REA 里,它同时完成了四件事:确认目标身份、挑选深分析引擎、建立一条不可变的绑定、为这次分析生成一份配置指纹。

目标身份不是一个路径

BinaryTarget 的基础字段只有三个:

interface BinaryTargetIdentity {
  readonly path: string;
  readonly sha256: string;
  // 以及由格式决定的 kind / format / architecture
}

路径告诉你文件在哪,SHA-256 告诉你是哪一个文件。后面所有 Evidence 的 subject 都会带这两个值;如果分析过程中文件被替换,证据就对不上目标。这个约束听起来很基础,但很多工具只记路径,最后没人能证明”你分析的是不是现在这个二进制”。

候选不是越多越好

深分析候选有四个:Hopper、Ghidra、IDA、Kuna。它们不是随便挑一个就能互换:

引擎 形态 会话特征
Hopper GUI(macOS / Linux 私有显示)+ Python 桥 一个文档,可读写,UI 可能弹出
Ghidra analyzeHeadless 一个临时工程、一个 Program,只读,关闭即删
IDA 上游 ida-pro-mcp GUI 代理或自有 headless 数据库
Kuna 缓存导出 读取缓存的 C/ASM,调用图另算

除此之外还有一组”辅助提供者”:objdump、Rizin、pwndbg、JADX、Binwalk、tshark……它们各自声明自己支持的操作,并且必须与深分析提供者的操作不相交。SessionProviderRouter.selectable() 在构造时就会检查这一点,重叠直接抛错:

for (const operation of registry.declaredOperations())
  if (auxiliaryOperations.has(operation))
    throw new TypeError(
      `Deep and auxiliary providers both declare operation ${operation}`,
    );

这条检查的意思是:一个操作只能有一个权威实现。否则同一份 Evidence 可能来自两个引擎,而调用方无从知道是哪一个。

auto 只在没有歧义时自动

选择器有三种来源:请求显式指定、环境变量默认值、auto。auto 并不做”优先级排序”,它只做一件事:如果恰好一个候选可用,就选它;如果两个以上可用,返回 ambiguous。

if (usable.length > 1)
  return err(
    selectionError("ambiguous", "auto", /* candidates */),
  );

这段代码值得多看一眼。多数工具在自动选择时会按注册顺序取第一个,或者按”精度从高到低”静默回退。REA 把这种选择交还给调用方,因为 Hopper 和 Ghidra 的反编译结果、符号恢复能力、地址语义并不完全一致。偷偷换一个引擎,等于偷偷换了一种观测方法。

注册顺序也不影响结果,源码注释写得很明确:Registration order never affects selection or caller-visible output.

一个目标,一个深绑定

BinarySession 为每个目标保存一条不可变的 route:provider 身份、能力表、analysis_profile 和客户端工厂。analysis_profile 是”这次分析到底用了什么配置”的承诺:

{
  "provider": { "id": "ghidra", "name": "Ghidra", "version": "<引擎版本>" },
  "parameters": { "...": "影响分析结果的参数" },
  "digest": "sha256(canonical(profile))"
}

它会被写进每一条 Evidence。两个结果即使长得一样,只要 profile 不同,就不能当作同一次观测来比较。

绑定之后,所有走深分析的操作都路由到这条 route;辅助提供者只接自己那部分操作,按需启动(LazyAnalysisProvider),不会因为你要读一个 PE 导入表就把 Ghidra 拉起来。

关闭不是删除

close_binary 可以带一个 snapshot_path。关闭时 REA 会先导出分析快照,再释放引擎资源;下次 open_binary 可以用 snapshot 把快照导入,恢复原来的分析状态。这个设计解决了一个很现实的问题:Ghidra 冷启动可能要几分钟,而调查经常跨多个回合。

但快照不是证据。快照保存的是引擎状态,Evidence 保存的是观测记录;前者可以重建会话,后者才能被引用。REA 在关闭时会把两者分开处理,证据留在账本里,快照写到调用方指定的路径。

为什么值得这么麻烦

因为逆向分析最怕的不是”没看到”,而是”看到了,但不知道是用什么看到的”。

一个目标一个绑定、一个操作一个权威、一个 profile 一条记录,这三件事加起来,才能让后面的 compare_functions、verify_reconstruction 有意义。否则你比较的可能是两个引擎对同一段代码的不同猜测,而不是同一观测方法下的两次结果。

下一站是桥:这些引擎并不是被 exec() 一下就完事,它们各自被包装成了一条带认证、带期限、带生命周期的本地服务。