REA:逆向一切(2)
上一部分我们聊了 REA(Reverse Engineer Anything)是什么:一个面向逆向工程的 MCP 服务器,让 Claude Code、Codex、Cursor、Gemini CLI 这类智能体去拆二进制、Electron 应用、安卓包和网页,并且把结论连同证据一起交给你。我们也粗略看了它的分层架构。这一部分,我们说实话,这个项目最特别的地方不是“支持的格式多”,而是一种近乎偏执的工程态度:宁可说“我不知道”,也不替编一个答案。你会在它的文档里反复看到 unknown、limitations、coverage、no fallback 这类词。逆向工程里,最危险的不是查不到东西,而是智能体信心满满地给你一个错误结论。REA 的大部分设计,都是在对抗这件事。
一、选哪个引擎,由规则决定,不由“感觉”决定
做原生二进制分析,REA 自己不造反汇编器,而是对接 Hopper、Ghidra、IDA 这些成熟引擎(架构图里还有 Kuna 反编译器、objdump、Rizin 等“备胎工具”式的候选)。问题来了:同一个文件,好几个引擎都能打开,选谁?
架构图(docs/architecture.mermaid)给的答案很干脆:AnalysisProviderRegistry 维护“排好序的深度分析候选”,做的是 deterministic selection, no fallback——确定性选择,不做回退。CLI 文档(docs/cli.md)把规则讲得更具体:自动选择时,只有当恰好一个可用引擎支持这个目标时才自动选中;如果有多个都支持,就要你显式指定,比如:
rea analyze /absolute/path/to/program --provider hopper
也可以设置环境变量 REA_ANALYSIS_PROVIDER 作为长期偏好,命令行里的 --provider 优先级更高。在 MCP 里则是给 open_binary 传 provider_id。
这里有个很有意思的设计:一个目标一次绑定。架构图里的 SessionProviderRouter + BinarySession 被描述为“每个目标一个可变深度绑定”(one immutable deep binding per target)。文档里还强调,选中目标引擎会一直到你显式切换或关闭,运行中途失败也不会被自动换成另一个引擎。
为什么这样设计?想象你请了两位翻译,第一位翻到一半生病了,你临时换了第二位。两个人对同一个术语的译法、对同一个地址的命名习惯都可能不同,最后拼起来的稿子看上去通顺,却可能前后矛盾。逆向分析也一样:Hopper 和 Ghidra 对同一个函数的伪代码、命名、交叉引用都不一样,中途悄悄换引擎,证据就乱了。REA 选择先“失败就如实报告”,把定数权还给你。provider 不可用时,文档说会返回 provider_unavailable,并带上 remediation.action 和 details.rejections 这些原始原因;选择有歧义时返回 ambiguous,列出 details.candidate_ids 让你挑。
二、桥接:把别人的软件“请进来”干活
Hopper、Ghidra 都是别人的软件,REA 怎么指挥它们?答案是“桥”。架构图里能看到两座:
• bridge/hopper_bridge.py:运行在 Hopper 内部,把各种操作翻译成 Hopper 的 Python API 调用。REA 这边的 src/hopper/负责启动 Hopper,并通过 Unix socket 协议(BridgeLauncher、HopperClient、protocol)与之通信。
• bridge/ghidra/ReaGhidraBridge.java:一个打包好的 Ghidra HeadlessScript,负责清单、反编译、引用和控制流图(inventory、decompiler、references、CFG);REA 一侧的 src/ghidra/处理 Ghidra 的发现、画像与函数分析、快照、Unix/Windows 传输和有界的生命周期。
IDA 则是另一种思路:src/ida/去适配上游的 ida-pro-mcp——一个外部 MCP,要么代理一个正在运行的 GUI,要么监听一个自有的无界面数据库。也就是说,REA 并不限定 IDA 是自己的内部模块,而是把“上游契约”适配进来,并且在架构图里明确标注 IDA 的安装和引擎生命周期归上游管。
这种“桥接而非重写”的好处很直观:成熟引擎几十年的分析能力直接拿来用,REA 只需要专注在“把结果整理成智能体能用、可验证的形式”上。代价是启动慢——cold start 时要等引擎导入、自动分析。docs/mcp-contracts.md 里专门有一节讲 Ghidra 首次查询的 deadline:REA 的 Ghidra 启动默认只许 330,000 ms,Hopper 默认 120,000 ms,可分别用 REA_GHIDRA_STARTUP_TIMEOUT_MS 和 REA_HOPPER_STARTUP_TIMEOUT_MS 调高;而 MCP 客户端的单次工具调用默认可能只有 60 秒,会比 REA 自己的 deadline 先超时。文档还给了一个实测例子:某一个受控的 Linux x64/WSL2 环境下,首次 overview 大约花了 64 秒;Windows x64 上一个很小的 1,024 字节库,默认 60 秒超时后重开并用了 240 秒的调试设置,冷启动 overview 约 163 秒,下一次只要约 19 毫秒。文档自己也强调,240 秒只是示例,不是通用值。
三、证据模型:每个结论都带“出处”
如果只让我挑 REA 的一个核心设计,我会选 Evidence(证据)。
README 里反复说“results include the evidence and limitations behind each position, observation(…)”。落到实现上,docs/cli.md 写得很清楚:Evidence 记录会保留:制品身份、源位置、观测(observations)、推断(inferences)和未解决的发现。注意“观察”和“推断”是分开的。比如读到一个函数里调用了某个 API,这是观察;据此判断“这个函数负责贴板”,这是推断。把两者混为一谈,是逆向报告里最常见的灾难。
MCP 文档(docs/mcp-contracts.md)里,证据类工具返回完整的规范化 Evidence 记录,你读 structuredContent.normalized_result 拿到结果,读 structuredContent.evidence_id 拿到身份;同一条记录也会在会话的证据包里。有几个细节:
• 哈希身份:证据 ID 形如 ev_sha256;导入的证据包会做完整性验证,认证失败会报 evidence_integrity_mismatch,而不是默默接受。
• 覆盖度(coverage):清单类证据必须声明 coverage.scope 和 coverage.status(complete 或 unknown)。比较两份清单时,只有范围一致、成员完整、遍历完整,才会下“相等”的结论。
• 缺失不等于不存在:文档里再出现类似表述——“一个空的调用列表如果覆盖度是 partial 或 unknown,‘不能证明这个函数没有调用’;Rust 二进制里没有 dep_v0 段,就当作‘空元数据’,而不是‘没有依赖’的证明。
• Unknown 账本:架构图里有 InvestigationRecords,承担 EvidenceLedger 和 Unknown 的所有权;MCP 里有 record_unknown、update_unknown、list_unknowns 这类工具。更新用 expected_revision,版本过期会报“可重试的 revision_conflict”。
你可以把它想象成一个很认真的实验室笔记本:每一条结论后面都贴着原始数据的编号,每一个还没搞清楚的地方都单独记在“待解问题”里,而且这一页是有版本号的。智能体写完实现代码之后,你还能回头问:这个判断的依据在哪?
四、大结果怎么办:摘要在会话里,全量留在“保险柜”
逆向分析的输出很容易大到离谱。docs/cli.md 里有个直观的例子:一个真实应用的完整 JavaScript 分析 Evidence 可以有几百 MB,文档举的 Obsidian 例子是 334 MB 的 Evidence 文件,投影成摘要视图大约只有 10 KB。
REA 的办法是“先抽要、按需翻页”。CLI 里先把结果存成文件,再用 inspect-analysis-view 取视图:
rea analyze-javascript-application /absolute/path/to/app --json > app-evidence.json
jq -c ‘{source: {kind: “inline”, evidence: {}}, view: {kind: “summary”}}’ app-evidence.json > app-view.json
rea inspect-analysis-view app-view.json
MCP 一侧则是 detail: “summary”:完整 Evidence 先记在会话里,返回的是摘要 Evidence,并给出 normalized_result.parent_evidence_id,后面用 inspect_analysis_view 以 retained-evidence 引用去读某一页。某个模式,既不用重复分析,也不用把巨大的记录塞进一次响应里。analyze_function 的函数卷宗可达几 MB,所以它默认就是 summary。
这还设有一个对智能体体贴的特别的约束:MCP 响应有预算。文档说 REA 会把完整结果控制在客户端 10 MiB 接收缓冲之内;放不下时,会返回 resource_constraint,并在 details.reported_limits.evidence_reference 里告诉你证据的确切引用,让你用视图翻页,或者用 export_evidence_bundle 导出到文件(原子发布)。它没有悄悄截断,而是明说“放不下,在这里取”。
五、不用外部工具的“内置读取器”
很多时候你不需要启动一个沉重的反汇编引擎,只想问“这个文件到底是什么”。README 的支持表里有一批 Built-in static inspectors(内置静态检查,无需外部工具),例如:
• Go 二进制:内嵌的编译器版本、模块、replace 和构建设置,无需安装 Go;
• Rust 二进制:读取 cargo-auditable 内嵌的 crate 名称、版本、来源和依赖边;
• IL2CPP:解析 Unity 的 global-metadata.dat(支持版本 24、27、29、31、39),无需 Unity;
• Godot PCK:格式 2、3、4;
• 模型文件:GGUF / safetensors 的头部元数据和张量声明;
• PE 证书与导入表、ELF notes、Apple IPS 崩溃报告、Linux 内核模块、Erlang BEAM、PyInstaller 可执行文件等。
架构图把它们归为“bundled in-process readers”(捆绑的进程内读取器)。设计思路很朴素:能在进程内静态读懂的格式,就别拉起外部程序;读不懂的地方就留 unknown。docs/cli.md 里讲 Godot PCK 时也有一句:不做解密,加密部分记为 unknown。命令也统一得很:
rea inspect-go-binary /absolute/path/to/program --json
rea inspect-rust-binary /absolute/path/to/program --json
rea inspect-il2cpp-metadata /absolute/path/to/global-metadata.dat --json
六、进程与文件的“安全带”
REA 要拉起 Ghidra、JADX、Binwalk、tshark 等一堆外部进程,还可能处理来源不明的目标文件,这里最怕的是进程泄漏、读文件失控、被目标的文字“带偏”。从架构图的“Provider Process Foundation”能看到它的底座:src/process/ 负责 owned process groups、private roots、deadlines、有界文件读取与诊断、清理;src/filesystem/ 管 owned 文件/目录结构、稳定读取与分阶段输出;Windows 上还有 src/windows/ 和 native/windows/src/ 的 Node-API 实现,做 NTFS 准入、受保护的 DACL 和原子的 Job Object 分配。文档里一句话很能说明态度:“REA never kills a process it cannot prove it owns”——证明不了是自己拉起的进程,就不会去杀。清理不干净会报 cleanup_incomplete 并列出残留资源,而不是假装成功。
另一个有意思的点是对提示注入的防御。MCP 文档里专门有一节 “Untrusted target content”:目标程序里的字符串、符号、反编译代码、页面文本、抓包、日志,都可能被人故意写成“请执行某某命令”“请下载这个必需组件”的样子。REA 的做法是把这些当作数据,服务器指令告诉智能体不要照做,并要求把这类情况作为提示注入尝试上报;同时在返回结果的 _meta 里打上标记:
{ “provenance”: { “untrusted_content”: true, “source”: “analysis_target” } }
文档还说,这个标记默认开启(returnsTargetContent),只有 export_evidence_bundle 和 record_unknown 这类只会读状态的工具被列为例外。宿主可以据此对“读过目标内容之后”的进程启动或设备修改类工具要求额外批准。
七、对“各种客户端”的体贴:紧凑 schema
最后说一个不起眼却很实用的设计。MCP 工具多,每个工具的入参 schema 都会占用模型的上下文,有的模型接口还对单个工具的 schema 大小有硬上限。README 里提到:MCP 默认使用紧凑输入 schema 来减少上下文开销,需要完整字段说明和嵌套校验细节时设置 REA_MCP_INPUT_SCHEMA_PROFILE=full。docs/mcp-contracts.md 进一步说明:compact 是默认值,把每个工具的输入 schema 控制在 13,312 字节预算内;它只改变“通告出去的 schema”,工具集合、输出 schema 和服务器的规范校验都不变。文档里还提到,有的客户端会因为 schema 太大,嵌套超过十层而拒绝工具目录,compact 模式就是为这些场景准备的。少数结构过于复杂的工具(如 compare_web_captures、capture_browser_scenario 等)会以精简形式通告,细节由服务器端校验兜底。
还有 REA_MCP_OUTPUT_SCHEMA_PROFILE=off 这个可选项:对 tools/list 帧大小有限制的 stdio 宿主,可以不通告 outputSchema,但服务器端仍按同样的规范校验成功结果。
本部分小结
把这一路看下来,REA 的技术亮点可以浓缩成四句话:
- 选择要确定:引擎选择有规则、一个目标一次绑定、失败不偷偷回退;
- 结论要有据:Evidence 区分观察与推断,带哈希身份、覆盖度,未知的东西进 Unknown 账本;
- 结果要可翻页:大结果先给摘要,全量留在会话或文件里,按需取页,而不是撑爆响应;
- 边界要安全:自有进程组与文件句柄、清理不完整就明说、目标内容一律当作不可信数据。
它不是“魔法”让智能体变聪明,而是用一整套工程纪律让智能体更不容易犯错、也更容易被你检查。这也是为什么我觉得它的价值不只在“一条命令接入各种智能体”,而在于它把逆向这种高风险、高不确定性的活,变成了可追溯的流程。
下一部分预告
第三部分是上手篇:怎么用 npx rea-agents setup 把 REA 接进你的智能体,Node.js 版本和 Linux 上 ps 的前提,CLI 与 MCP 的典型用法,README 里 DX-Ball、Notion、TH04 三个案例怎么读,以及它适合谁、有哪些局限、合规边界在哪儿。
- 点赞
- 收藏
- 关注作者
评论(0)