给 REA 套一层 GUI:153 个命令的界面是怎么自动长出来的
做过一个「界面驱动」的逆向分析项目,最后卡在一个很具体的地方:一个工具要人手动配置 153 个不同的参数面板,写到手软,而且一升级就全废。
最近 REA 更新到 6.5.0,我重做了一遍,这次只写了不到 2000 行就把这事解决了。
这篇是完整复盘,重点是三个踩了才发现的坑:cmd.exe 的标签解析跟换行符有关 、一类只在个别命令上发作的沉默 bug ,以及怎么把存在浏览器里的模型配置挖出来。
REA 是什么,它缺一个 GUI
REA (Reverse Engineer Anything) 是 morluto 做的开源项目,10 月 7 日上线后十天里从 0 冲到 GitHub trending 第一,10 月中旬公开数据是 20 万星。Replit 创始人兼 CEO Amjad Masad 评价它:「AI 驱动的逆向工程和反编译领域正在发生的事情简直不可思议。很快,所有软件都可能事实上变成开源软件。」
它解决的是这么一件事:
你在某个 App 里看到一个好用的功能,想知道它怎么实现的,然后在自己的产品里做一个类似的。问题是------你没有这个软件的源码。
REA 的做法是让 AI Agent 通过 MCP 协议去逆向。装好之后,你在 Claude Code 或 Codex 里直接说人话:
Understand how search works in the Notes app, show me the evidence,
and build a similar feature for my project.
它就自己去调 Ghidra、Hopper 这些逆向工具,把结果分析成伪代码给你,还能顺带把下载的 APK 拆开看清单、反编译 Java 方法、观察浏览器行为、比对两个版本的差异。
官方仓库里几个已验证的案例挺能说明它到底能干什么:
- DX-Ball --- 逆向 DOS 游戏里声音随位置左右偏移的计算逻辑,重写的 C 函数通过了 3205 组测试,连编译出的 63 字节机器码都和原版一致
- Notion --- 追踪 Electron 渲染进程的剪贴板 API 调用,穿过 preload 和 IPC 一直到主进程,途中还顺带分析了富文本剪贴板格式
- TH04 --- 分析 PC-98 版《东方幻想乡》的 16 位指令,还原出固定角度和自瞄角度两套计算,再把重写的 C++ 和当年编译器的输出做对比
但有个问题:REA 目前的形态只有两个------命令行 CLI 和 MCP Server。
对已经在用 Claude Code 的开发者没问题,但这东西的门槛比想象中高。你得先装 Node、配 MCP、理解一堆 provider 概念(Hopper/Ghidra/IDA 谁在谁不在、JDK 版本对不对),然后才能开始干活。
我想要的,是一个能双击就用的东西。
153 个命令,不该我写的东西
我最初想的是:那就老老实实给每个命令做一个表单。
然后我去数了一下 CLI 有多少命令。
153 个。
里面还有 analyze-javascript-application、inspect-managed-members、trace-native-ui-action 这种名字长到要换行的。涉及的目标类型有原生二进制、JS/Electron、.NET、APK、网站、固件、pcap、SCTP 信号记录、SQLite、WASM、EVM 字节码、Flutter......每个类型的参数还不一样。
写 153 套表单?光想想就头皮发麻。而且 REA 更新频繁------它 10 月 7 日到 11 日之间连发了 5.0、6.0、6.1、6.2、6.3、6.4 六个版本,每个都标记了 breaking changes。今天新加两个命令、明天改一个参数,我这个界面就全废了。
转念一想:这事儿不该我干,REA 自己知道。
我去翻 REA 的 CLI,发现它自带 introspection(自省)能力:
| 命令 | 作用 |
|---|---|
rea <命令> --schema --format json |
该命令的完整 JSON Schema,包含参数、类型、枚举值、取值范围、默认值、每一项的说明文字 |
rea --llms |
全部命令清单和描述 |
rea capabilities --json |
当前哪些 provider 可用、不可用的原因是什么 |
这意味着:表单可以直接从命令的 Schema 生成。
我写了 tools/build-catalog.mjs,驱动 REA 自己的 CLI 把 153 个命令的 Schema 全部导出成一份 catalog.json(10 个并发进程,95 秒跑完)。前端再读这份 Schema 渲染表单:
- 参数名、类型(字符串/整数/布尔/数组/枚举)从 Schema 读
- 默认值、最小最大限制直接预填
- 描述文字作为提示显示在输入框下面
- 必填项标红,没填满就不让点运行
- 底部实时显示将要执行的命令行,可以直接复制到终端用
结果是:代码里没有一个命令名是硬编码的。
REA 以后加新命令、改参数,我跑一下 刷新命令列表.bat 就行,不用改一行前端代码。
界面:从一次点击到一条证据链
主体就是命令面板。左边 153 个命令按目标类型分组,带搜索;右边表单;下方是实时输出。
跑一个 Electron 应用的分析,17 个阶段的进度流式刷新,结果是 139 KB 的 JSON,带 evidence_id、各种 sha256 摘要、confidence 字段,还有 limitations ------ REA 会明确告诉你哪些结论是推断的、哪些是观测到的、哪些地方它不确定。这个诚实度比很多商业工具强。
结果视图我做了折叠处理,不然一个真实应用的 Evidence 能到几百 MB,直接把浏览器撑爆。数组超过 12 项时只展开头尾各 6 项,中间标注总数和省略数;超过 400 字符的字符串会截断并标出原始长度。需要时切「原始输出」看完整内容。
「证据链」标签页会自动把 evidence、provenance、sha256、confidence 这类字段抽出来单独展示,不用在几万行 JSON 里翻。
深色主题是跟着系统走的:
进度:宁可显示「java.exe 内存 93 MB」,也别放个假进度
最开始我的进度条是个定时器,转圈配个百分比。后来我自己看着都觉得心虚------那个百分比跟实际进度毫无关系,Ghidra 卡在第 4 分钟和第 40 分钟,界面上长一个样。
分析一个原生二进制,用户真正想知道的只有三件事:它死了没有、现在跑到哪一步、还要等多久。前两件是可以从真实信号里读出来的。
所以我把进度面板改成了完全由信号驱动:
- 阶段 来自 REA 子进程 stdout 里的
{"rea_progress":{...}}行分隔 JSON,边跑边解析,解析到什么阶段就显示什么阶段 - 引擎状态 靠轮询进程树:每 2.5 秒用
wmic找一次 REA 进程下面的java.exe,取内存占用最大的那个------Ghidra 分析时内存是持续增长的,这个数字能粗略反映它干到哪了 - 时长是真实计时器,和状态角标同一个数据源
- 取消 用
taskkill /T杀整棵进程树,Ghidra 的 JVM 不会变成孤儿进程
界面上显示的是「Ghidra 引擎分析中 · java.exe PID 21088,内存 93 MB · 00:12」。这不是好看的数字,是这台机器上正在发生的事。
这里有个取舍值得说:轮询进程树的代价比 WebSocket 推送高 ,每 2.5 秒一次 wmic 查询在 Windows 上并不便宜。但我只需要在「有任务在跑」的时候轮询,空闲时完全不动,实际开销可以忽略。数值没变化时也不推送,避免 SSE 通道被无意义的重复帧塞满。
缺引擎的时候,别只丢个退出码
这是我踩过的一个坑。
你第一次用原生分析功能,如果没装 Ghidra,REA 会返回一个非零退出码。绝大多数工具的做法就是打印一行错误,然后你就自己去猜吧。
REA 其实返回了很结构化的诊断信息------它明确告诉你每个引擎缺什么、怎么修。但命令行下这坨 JSON 没人愿意读。
所以我在界面上把它渲染成人话了:
右边这个面板会逐个列出每个 provider 的可用状态:
- ghidra ---
Set GHIDRA_INSTALL_DIR to an extracted Ghidra 12.1.x release directory - hopper ---
Hopper integration is not supported on win32(Windows 上根本用不了) - ida ---
Set REA_IDA_MCP_CONFIG to an upstream IDA MCP JSON registration
点一下具体命令,如果失败原因是「引擎不可用」,界面顶部会弹一条横幅,写清楚缺哪个引擎、为什么、怎么修------而不是甩给你一个 exit code 1。
绿色包:62 MB,双击即用
不想装 Node、不想配环境变量、不想 npm install------所以打了个 Windows 免安装包。
REA-UI-green/
├── 启动 REA UI.bat ← 双击这个
├── 批量反编译.bat
├── 刷新命令列表.bat
├── server.mjs 本地服务,包装 REA CLI
├── public/ 界面 + catalog.json
├── runtime/node.exe 内置 Node 24.22.0
├── tools/ Ghidra 批量脚本(含编译好的 .class)
└── app/node_modules/ rea-agents 及其依赖
解压后双击 启动 REA UI.bat,服务就绪后浏览器自动打开 http://127.0.0.1:7823。关掉命令行窗口就停止服务。端口被占用会自动顺延;如果 7823 上已经跑着一个 REA UI,再双击不会起第二个实例,而是直接把已有那个的页面打开。
启动脚本会自己找 Ghidra。 这地方有个坑值得说:我这台机器的系统环境变量 JAVA_HOME 指向 Java 8,而 REA 要求 JDK 21+,直接沿用会让所有原生分析失败。所以脚本会校验主版本号,不是 21 或 25 就自动换成探测到的 JDK,并且在启动时明确告诉你最终用了哪个:
Engine: Ghidra D:\rea\ghidra | JDK D:\rea\jdk
如果没装引擎,它也会直说,而不是默默降级:
No Ghidra engine. Static analysis works; native binaries need Ghidra 12.1.x + JDK 21.
这个启动器我修了四次
上面这段听起来很顺,实际上这个 .bat 是我返工最多的文件。有一次打包完,双击之后浏览器根本没打开,什么都没发生。排查下来是四个叠在一起的坑,每个单独看都不起眼:
① 换行符。 仓库里所有 .bat 都是 LF 换行。cmd.exe 解析 goto :label 和 call :label 是按字节偏移定位的 ,LF-only 的批处理会让它报「系统找不到指定的批处理标签」,然后脚本当场终止。启动器第一段流程就是 call :find_ghidra,也就是说它在找到引擎之前就死了,后面的 node 压根没机会启动。
这是我这几年踩过的最阴的一个坑:同样一份代码,CRLF 就能跑,LF 就不行,而且报错信息完全不指向真正的问题。 现在打包脚本里加了一条断言------.bat 的 CR 数必须等于 LF 数,不等就构建失败。
② find 不支持正则。 端口占用检测原来写的是 netstat -ano | find ":%PORT%.*LISTENING"。find 根本没有正则,.* 被按字面比较,永远不匹配 ,于是每次启动都认为 7823 是空闲的。改用 findstr /r 才正常。
③ 浏览器抢跑。 原来的顺序是先 start http://... 再启动 node。冷启动时杀软要扫一遍 94 MB 的 node.exe,十几秒后服务才就绪------浏览器早就把「无法访问」显示出来了。现在改成 REA_UI_OPEN=1,由 server 在 listen 回调里开页面,就绪才打开。
④ 版本号没去引号。 批处理里用 for /f "tokens=3" 从 java -version 的输出里取版本号,拿到的是 "21.0.12"------带引号 。拿它跟 21 做子串比较永远不相等,于是脚本认为这个 JAVA_HOME 不可用,把它清空重来。修法是 %%~V 去掉外层引号。
四个坑里三个都是「批处理太老了,谁还在乎」的心态养出来的。但恰恰是这种文件,坏了最没有报错可言。
体积方面,打包产物目录 216 MB、zip 62 MB。压缩比主要靠剔掉 frida------它是 rea-agents 的 optionalDependency,却带了 130 MB 的 C 源码构建中间产物,运行时根本不加载。删掉它,包小了一半以上。
对话模式:从「我没法验证」到真跑通
界面上有个对话式面板:用自然语言描述目标,模型自己决定调哪个 REA 命令、把结果回喂给自己、最后给出带证据的结论。
这个功能我一度只做到「能跑」,因为我没法验证它------我的测试环境里没有一个可用的 API Key。直到有一次我发现,浏览器里其实配过。
配置存在浏览器 localStorage 里,而 Chrome 系内核把 localStorage 落在 LevelDB 文件里,value 是 snappy 压缩过的。我手写了一个 snappy 解压器把配置读了出来:一个国内推理服务商的 key,配了一个 DeepSeek 的轻量档模型。
于是第一次真实端到端测试。结果比「跑不通」更尴尬:它能跑完,但跑得很烂------同一个问题花了 7 到 8 轮工具调用、280 秒,最后一轮模型没有产出结论,界面把 21000 多字符的原始 JSON 直接糊在了聊天框里。
把这条链路拆开,找到五类问题:
- 轮数没有预算。 模型不知道自己还剩几轮,于是一直「再查一次」。现在轮数预算直接写进提示词,耗尽时强制模型基于已有证据作答,不许再调工具。
- 工具结果回喂时去重失败。 REA 的返回里
raw和normalized内容高度重叠,全部回喂会把上下文撑爆。现在只喂压缩后的版本。 - 压缩逻辑丢内容。 这个最隐蔽,后面单独说------反编译的伪代码是字符串不是数组,被当成无用数据扔了。
- 命令抽取在引号上翻车。 路径里有空格时,从模型输出里切 REA 命令会切错位置。现在六种引号都处理。
max_tokens没设。 推理型模型有时会沉默,补上上限并强制输出。
改完之后,同样的问题 3 轮、186 秒,结论、证据、不确定性都齐了:
它对 期货多周期趋势助手.exe 的结论里有这么一段,我觉得比结论本身更有价值:
需要明确说明的一点 :本次分析没有恢复出任何与「趋势/止损/周期」相关的业务函数名,所有业务函数都被冠以匿名
FUN_14000xxxx(例如FUN_140001000、FUN_140005990)......所以「无法仅从函数名判断其业务功能」------从导入表可以看出它是一个带 GUI 窗口的 Windows 桌面程序,但要弄清行情逻辑需要进一步反编译入口点或搜索字符串常量。
它知道自己不知道什么。 这比一个自信的错误答案值钱。
一类沉默的 bug:8 个命令一起坏,但只有 decompile 被人发现
上面第 3 条值得单独开一节,因为它代表一类很麻烦的问题。
decompile 命令报错,我修了两次才修对。
第一次 以为是参数没传对。翻了半天才发现:我的参数拼装逻辑只遍历 schema 里的 required 数组------address 这种可选的位置参数 直接被丢了。而 decompile 恰好要求传地址。受影响的不止它一个,把同类命令拉出来数了一下,8 个 :decompile、function、instructions、xrefs、search、trace、inspect-keyed-archive、compare-process-captures。
第二次 修完参数,decompile 还是报错。这次的问题在另一头:工具结果回喂给模型前会做一层压缩,原逻辑只保留数组类型 的字段------而 decompile 的伪代码是字符串。也就是说,命令执行成功了,伪代码也拿到了,然后在压缩这一步被当成空数据丢掉了。
这类 bug 的麻烦在于它不报错。数据在管道里静默消失,下游只看到「没有结果」,你会去怀疑命令、怀疑 schema、怀疑引擎,唯独不会怀疑那个看起来无害的压缩函数。
修完之后我做了一次全量回归:写了个脚本把 153 个命令的 --help 全部拉出来,和 catalog 里记录的参数逐项对------153 个命令,0 处不一致,并发跑完 45 秒。这种审计只值得在「参数是从 schema 生成」的架构下做,如果表单是手写的,153 个命令逐个点一遍就是一天。
批量反编译:把 1038 个函数压进 11 分钟
这个功能是后来加的。
我原本想用界面对着一个二进制逐个点反编译。点了 8 个之后我放弃了------这 8 个花了 20 分钟。
原因在 REA 的架构:它每次只回答一个问题,而每个问题都要把目标重新导入一次 Ghidra。按这个速度,1038 个函数要跑十几个小时。
但 Ghidra 本身有批量能力。绕过 REA 直接调 Ghidra headless,写了个脚本在一次会话内 遍历 FunctionManager,把每个函数依次过一遍反编译器:
java
DecompInterface decomp = new DecompInterface();
decomp.openProgram(currentProgram);
for (Function f : fm.getFunctions(true)) {
DecompileResults r = decomp.decompileFunction(f, 120, monitor);
// 写出 .c 和 .asm
}
把目标文件拖到 批量反编译.bat 上就行。结果:
| 阶段 | 函数数 | 耗时 |
|---|---|---|
| 本地定义 | 54 | 约 1 分钟 |
| 导入符号 | 984 | 约 11 分钟 |
1038 个函数,零失败。 相比逐个调用快了三个数量级。
顺手还发现个好东西:Ghidra 能自动读取目标同目录下的 .pdb。加载成功后,伪代码里的字段名会从裸偏移变成真实名字:
c
// 没有 PDB
return **(short **)this;
// 加载 PDB 后
return *(short *)this->mpVar;
mpVar 是从 PDB 恢复出来的。所以我特意警告:没有 PDB 的二进制,反编译结果里的字段名全是猜的。
技术选型的取舍
为什么用 SSE 而不是 WebSocket? 只需要服务端单向推进度,SSE 更轻,Node 原生支持,不用额外依赖。
为什么自己抓 stdout 而不是让 REA 输出到文件? 进度信息是 {"rea_progress":{...}} 这样的行分隔 JSON,边跑边解析才能实时显示。这里踩过一个坑:REA 写 stdout 不保证一次写一行,得自己缓冲到换行符,否则 JSON 会被截断成好几段(我第一次就踩了,表现为结果里出现 6 个截断的片段而不是 1 个完整对象)。
为什么命令面板还要保留命令行预览? 因为懂命令行的人可以直接复制走,在终端里跑。UI 不该绑架用户。
为什么进度用轮询而不是事件推送? Ghidra 不会主动报告「我处理到 37%」,它没有任何可供订阅的进度通道。进程内存增长是个粗糙但诚实的代理指标------粗糙到不值得为它建一条 WebSocket 长连接。
一些局限
说完好的,也说说不足:
- 对话面板需要你自己的 API Key。 这次我用真实模型把整条链路验证过了(上文),但它用的不是我的 key 替你跑------key 只存本机浏览器 localStorage,不经过任何第三方,也不会打进绿色包里。
- PDB 的作用被低估了。 上面说了,有 PDB 和没 PDB 的反编译质量是两个档次。如果你在分析自己编译的程序,一定记得把 PDB 放旁边。
- Hopper 在 Windows 上用不了。 REA 明确返回
unsupported_host。Windows 上唯一的原生分析引擎是 Ghidra。IDA 能用,但要商业版。 - 只覆盖了 REA 的能力边界。 REA 本身不做逆向,它接入你已有的工具。没装引擎的机器上,JS/Electron、.NET、SQLite、Go/Rust 二进制这些静态分析照常可用,原生二进制就得先装引擎。
- 没有 PDB 的结论要打折。 对话模式给出的函数名推断,在没有符号的情况下基本是猜的------它自己会这么说,但读的人容易忽略。
下载
绿色安装包(62 MB,解压即用)
百度网盘:https://pan.baidu.com/s/1YFZCNFWtR7iwk4Vs_mkZkQ?pwd=dyef
提取码:dyef
源码
GitHub:https://github.com/wyfirstname/rea-ui
MIT 许可。源码里包含全部构建脚本,你可以自己改界面、打包,或者照着改出一个自己顺手的版本。
REA 本身是 MIT 许可,morluto 维护。请仅在你拥有或已获授权分析的软件上使用它------逆向这事儿,技术上能做到和有权做到是两回事。