桥:把 GUI 逆向工具变成可调用函数
Hopper 不是被 shell 出去的,Ghidra 也不是。它们各自被包装成一条带随机令牌、私有 socket、请求期限和进程组清理的本地服务。
桥:把 GUI 逆向工具变成可调用函数
反编译器大多不是为 Agent 设计的。Hopper 是一个以 GUI 为中心的逆向工具(macOS 原生,Linux 通过私有显示运行),Ghidra 是一个 Java 桌面套件,IDA 的强项是交互式数据库。REA 没有重写它们,而是在每个引擎旁边放了一个”桥”,把引擎 API 变成一行行 JSON。
难点不在协议,而在生命周期:谁启动、谁认证、谁超时、谁清理、失败了算什么。
Hopper:在它自己的 Python 线程里说话
Hopper 提供了一个 --python 启动参数。REA 用它在 Hopper 进程内执行 bridge/hopper_bridge.py,启动前注入两个环境变量:REA_SOCKET 和随机生成的 REA_TOKEN。
桥的文档字符串第一句就把约束说清楚了:
Authenticated REA adapter executed on Hopper’s dedicated Python thread. Keep all Hopper API access on this thread: moving dispatch to a worker can deadlock Hopper.
所以桥不搞线程池,也不把请求丢给 worker;它就在 Hopper 给的那个 Python 线程里,一条一条处理。
传输是 NDJSON:一行一个请求,一行一个回复。请求的形状被严格限制为四个字段:
{"id": 7, "token": "…", "method": "decompile", "params": {…}}
认证不是”连上 socket 就算自己人”:
if (not isinstance(request["token"], str)
or not hmac.compare_digest(request["token"], REA_TOKEN)):
raise PermissionError("Invalid bridge capability")
socket 文件权限是 0600,listen(1) 只接受一个连接;每次会话换一个随机令牌。请求有字节上限(默认 16 MiB),读取时带 MAX_REQUEST_BYTES + 1 的边界,超长帧直接拒绝,不会无限读下去。
回复有两种:{id, result} 和 {id, error};处理过程中还会插入 progress 事件,告诉客户端”这条请求开始了 / 完成了”。对大函数反编译这种慢操作,进度不是装饰,是让上层能区分”还在跑”和”已经死了”。
Ghidra:临时工程,读完就删
Ghidra 的桥是另一种形态。REA 不连 GUI,而是启动 analyzeHeadless,把目标导入一个临时工程,再用 -postScript 执行打包好的 ReaGhidraBridge.java。简化后的命令行是:
analyzeHeadless <projectRoot> rea-project \
-import <target> -readOnly -deleteProject \
-scriptPath <bridgeDir> -postScript ReaGhidraBridge.java
三个参数决定了它的性格:
-readOnly:这次会话不允许改数据库。-deleteProject:关闭即删除,工程不落地。- 一个 headless 会话只有一个 Program,不像 Hopper 可以开多个文档。
桥只在默认自动分析完成后才暴露 socket。客户端连上去,发一行带 token 的 JSON,等一行回复;Unix socket 不可用时,Windows 上走 127.0.0.1 的认证回环 TCP,端点信息写在一个只有当前用户可读的私有文件里。
启动期限也不同:Hopper 默认 120 秒,Ghidra 默认 330 秒。大二进制可以调,但调的是”新一次启动尝试”的期限,不是已经卡住的那一次。
IDA 与 Kuna:适配而不是重写
IDA 走的是上游 mrexodia/ida-pro-mcp:GUI 代理模式按 SHA-256 绑定已经打开的输入,headless 模式则创建自己的数据库并在关闭时释放。Kuna 更轻,它读的是整程序导出的缓存 C/ASM,调用图单独维护;缓存键包含目标摘要和 Kuna 版本,目标或引擎变了就重新导出。
这两条路说明 REA 的 provider 抽象不是”统一所有引擎”,而是”统一调用方看到的那部分”:能力可以不同,限制必须显式。
所有桥共享的三件事
第一,认证。 每个会话一个随机 token,token 不出现在命令行参数里,而是通过环境或私有文件传给桥;请求逐条校验。
第二,期限与取消。 启动有 deadline,请求有 deadline 或 caller cancellation,队列里的请求可以被取消。超时不是”再等等”,而是一个有类型的错误。
第三,清理。 桥进程不是普通的子进程,它属于一个 owned POSIX 进程组(Windows 上是 Job Object)。关闭时先发 SIGTERM,等 250 ms;不退就 SIGKILL,再等 1 秒。仍然清不掉,就返回 incomplete,把清理失败挂到错误上,允许重试——而不是假装已经关干净。
私有运行时目录也一样:POSIX 上用 mkdtemp 原子拿到一个 owner-only 目录,socket、令牌文件、临时工程都放在里面;关闭时递归删除,删不掉同样上报,不吞。
这一层的价值
如果没有这层,Agent 调反编译器的典型写法是:
把命令拼成字符串 → 起一个 shell → 解析 stdout → 猜哪里是错误
这套写法在 demo 里能用,在调查里会出事:stdout 里混着进度和警告,错误没有类型,超时只能 kill 整个进程树,token 可能出现在 ps 里,临时文件散落在 /tmp。
桥层把这些都收进了协议:结构化请求、结构化错误、进度事件、认证、期限、进程组、私有目录。引擎仍然是原来的引擎,但它现在是一个可以被安全调用的本地服务。
下一篇文章进入 REA 最核心的一层:引擎返回的结果,如何变成一条不可变、可引用、带局限说明的 Evidence。