夜雨聆风学习资料网

ARTICLE · 1197732

REA 拆解:AI 是怎么把一个 App 拆开读懂的

REA 拆解:AI 是怎么把一个 App 拆开读懂的

摘要:REA 不是反编译器,是给 AI 装的一层接口。它把 Ghidra、IDA、Hopper 这些引擎接到你的编程 agent 上,让「这个功能到底怎么实现的」变成一句能问出口的话。这篇从反汇编原理讲到 Evidence 机制,把它的技术底细一层层拆开。


上个月我想弄明白一个 Electron 应用把配置存在哪。

按老办法,解开 asar,翻几万行 webpack 产物,再装个 Ghidra 去啃它自带的原生模块。一个下午过去,我只弄清楚了「配置文件叫 config.json」,至于它什么时候写、写之前做了什么校验,没头绪。

后来换了个路子:把目录丢给 agent,让它自己去读、去问、去查。它跑了十几分钟,回来给我一条链:renderer 点了保存 → 一个叫 notes:save 的 IPC 通道 → 主进程写文件。

中间没装任何新软件,因为真正干活的东西早就在我机器上了。缺的只是一层把它和模型接起来的胶水。

那层胶水叫 REA。

先把「逆向」这件事讲明白

不弄清楚这一步,后面全是黑话。

你写的 TypeScript 经过编译,最后变成本地机器能直接执行的指令序列。这个过程中丢掉了三类信息:变量名和函数名、类型、以及代码的结构(哪里是 if,哪里是循环)。

发布版本还会再剥一层,把符号表也去掉。所以拿到一个二进制,你手里只有地址和字节。

逆向干的事,就是从这个状态往回推。标准流程是三步:

第一步切函数。哪些字节属于同一个函数,入口在哪,谁调用了谁。

第二步认指令。MOV EAX, [R13+0x18] 是把内存里某个值搬进寄存器,CMP EAX, 0x5c 是拿它跟 92 比。这一步是确定性的,反汇编器不会猜错指令编码。

第三步猜数据流。为什么要跟 92 比?比完之后跳去干嘛?这一步开始有不确定性,因为中间值叫什么、代表什么,编译器没留下任何提示。

反汇编器的核心算法有两种。线性扫描是从头到尾一段段解码,简单但会把数据段误认成指令。递归下降是从入口出发,遇到 call 就进去,遇到 jmp 就跟过去,只走「真实执行可能到达」的路径。后者准确,代价是碰到间接跳转就断链,因为目标地址是运行时才算出来的。这也是混淆器和虚拟化保护能生效的原因:它们专门制造这种断链。

反编译再往上走一层。以 Ghidra 为例,它先把每条机器指令提升成一种叫 P-code 的中间表示,在中间表示上做控制流图、数据流分析和 SSA 化,识别出 if/while/switch 的结构,最后吐出一段 C 伪代码。

伪代码看着像源码,实际上不是。变量是 v1、v2,类型是推出来的,函数边界可能是错的,宏和模板全部展开成了最原始的样子。

所以行业里一直有句话:反编译给你 80% 的骨架,剩下 20% 靠人读。那 20% 恰恰是最费时间的部分。

REA 想接的,就是这 20%。

它不是引擎,是接线员

刚看到 REA 的时候我以为是又一个反编译器,用下来发现它自己不负责把机器码翻译成汇编。

它的定位是中间层,三层结构很清楚:

底层是引擎,Ghidra、IDA Pro、Hopper,负责真正把字节变成汇编和伪代码。

中间是 REA,负责调用引擎、编排流程、把结果整理成一份带证据的报告。

上层是你的 agent,负责推理:这段代码在算什么,跟业务对不对得上。

连接方式是 MCP,Model Context Protocol。说人话就是一套让模型调用外部工具的协议,跑在标准输入输出上,一次请求一次响应。装了 REA 之后,agent 的工具列表里会多出 open_binary、analyze_function、get_call_graph、trace_call_path 这类东西。

对模型来说,它不需要知道 Hopper 的 Python API 长什么样,也不需要拼 Ghidra 的命令行,它只需要决定「现在该看函数清单,还是该看这个函数的调用者」。

这个分工是整件事成立的原因。Ghidra 的伪代码质量早就够了,卡住人的从来不是能力,是操作成本。

一条命令,装了什么

官方给的起步命令是 npx rea-agents@latest setup。

它做的事比看起来多。先探测你机器上装了哪些 agent,Claude Code、Cursor、Codex、Gemini CLI、Windsurf、VS Code 都在支持列表里,然后生成一份变更计划让你确认:要改哪几个配置文件、要写什么内容。确认之后才动手,改之前会备份原文件。

写进去的是两样东西。

一样是 MCP 配置,让 agent 知道有这么个服务可以调,并且把包版本钉住,避免下次跑出不一样的结果。

另一样是一个 investigation skill,一份给 agent 看的工作流说明书:先看清单再定位函数,先读伪代码再核对指令,跨引擎的结论要交叉验证。这东西容易被忽略,但没有它,模型拿到一堆工具也只会乱点,像给新人一整套 IDE 却不给规范。

原生二进制分析还得自己准备引擎:Ghidra 免费,但要 JDK 21;Hopper 是商业软件,macOS 上体验最好;IDA 最贵也最全。JS 和 .NET 的静态分析不需要引擎,Node 就够。

Node 版本要求是 22.19 以上、24.11 以上或者 26 以上,这个门槛不算低,但也说明它用了不少新特性。

一次调用,经过哪几层

这一段我想讲细一点,因为它能解释很多「为什么它有时快有时慢」的现象。

入口是一个分发脚本,判断这次是走 MCP 常驻服务,还是走一次性命令行。两条路后面接的是同一套业务逻辑,所以你手动跑 CLI 得到的结果和 agent 拿到的一致,不会出现两套行为。

MCP 服务启动后注册工具、等请求。模型发起一次工具调用,请求进到服务层被翻译成内部指令,交给应用层的工作流。工作流里有一个路由组件,负责给这个目标绑定一个分析引擎。

这里有个设计我觉得挺关键:绑定是不可变的,选定之后整个会话都用它,而且选择过程是确定性的排序匹配,不做 fallback。

意思是不会出现「Ghidra 挂了就悄悄换 Hopper 顶上」这种事。因为两个引擎的反编译结果本来就不一样,混着用会得到看似连贯、实际无法比较的结论。宁可报错,不可污染。

再往下就是各引擎的适配器,每个的处理方式都不同:

Hopper 是个 GUI 程序,REA 会把它启动起来,通过本地 socket 跟一个跑在 Hopper 进程内部的 Python 桥通信。桥脚本用 Hopper 自己的 API 取反汇编、伪代码、交叉引用,等于借 Hopper 的手干活。

Ghidra 走的是无头模式,REA 带了一个 Java 桥脚本,注册成 Ghidra 的 HeadlessScript,做函数清单、反编译、引用和调用图。导入之前它会先把目标复制成一份私有副本,你原来的文件不会被改。

IDA 的情况特殊,它复用了社区已有的 ida-pro-mcp 契约,可以连你手动打开的 GUI,也可以自己起一个无头数据库。

底层还有一层统一管理:所有子进程归一个模块管,独立进程组、私有工作目录、超时、读取上限、退出清理。这层不出现在任何演示里,但它决定了这东西能不能在你自己的电脑上跑一个几 GB 的程序而不把系统拖死。

Evidence:最值钱的设计

结果回来的时候,不是一段自然语言,是一条结构化的 Evidence 记录。

里面有几样东西:normalized_result 是整理后的结果,raw_result 是引擎的原始输出,analysis_profile 是这次分析所用配置的指纹,coverage 声明这次观测覆盖到什么程度,取值为 complete 或 unknown,还有 limitations 和 unknowns 两个字段专门记录没看到的东西。

为什么这个设计重要?

因为大模型最擅长的就是在证据不足时把话说圆。你把 Ghidra 输出粘进对话框,问它「这个函数是不是在做加密」,它会给你一个很像样的答案,而那个答案可能是编的。

Evidence 机制把「不知道」变成一等公民。分析器没看到的东西,必须显式声明 unknown;比对两次分析结果时,只要有一边的 coverage 是 unknown,工具就只允许你得出「无法判定」,不许你说「一致」。

官方文档里甚至规定了:函数证据要按维度声明可用性,伪代码、汇编、调用者、被调用者、引用、字符串、控制流图,每一项都要标是「看到了」还是「没拿到」。某个维度 unavailable,比对结果里那个维度就只能是 unknown。

这套东西啰嗦得有点过分,但它把「可信」从一句口号变成了数据结构。

顺带解决了一个工程问题:大结果不塞进单次响应。超出传输预算的记录留在会话里,agent 拿到一个引用,需要细节时分页去取。所以你不会看到 agent 因为一个函数的伪代码太长而崩掉。

它对不同软件,分别用了什么招

这是最多人关心的部分。REA 覆盖的目标比想象中宽,但每一类背后的技术完全是两套东西,混在一起讲容易晕。

目标
用的技术
原理一句话
原生可执行文件、动态库
Ghidra / IDA / Hopper
反汇编 + 反编译 + 调用图
JavaScript、Electron 应用
自研静态分析器
解归档、还原模块图、读 source map
网站、浏览器页面
CDP / Playwright
挂上调试协议,拿已加载脚本和请求
Android APK
JADX + apktool + adb
DEX 反编译、资源解包、从设备取包
.NET 程序集
PE/CLI 元数据解析
直接读 IL,等于半份源码
Flutter 应用
Dart AOT 相关适配
从快照里恢复结构
固件镜像
Binwalk / Unblob
按签名切分嵌套镜像并解压
WebAssembly
WABT
解析模块段与导出表
以太坊合约
EVMole
从字节码恢复函数选择器与参数
抓包文件
TShark、HAR、mitmproxy
解析协议与请求响应
崩溃转储
pwntools / pwndbg / GDB
还原调用栈与寄存器状态
逻辑信号
sigrok-cli
分析硬件总线时序

挑几个展开。

APK 本质是个 zip。里面最关键的是 classes.dex,Dalvik 字节码。JADX 的做法是先把 dex 转成 smali 这种可读的汇编,再把 smali 的控制流还原成 Java 代码。资源走 apktool,清单、布局、字符串都在那。如果 App 装在你手机上,adb 直接 pull 下来就行,不用去网上找安装包。

Electron 是另一条路,压根不碰机器码。打包产物 .asar 是个带索引的归档,解开就是 webpack 或 vite 的输出。REA 在这一层做三件事:建模块图,弄清谁 import 谁;读 source map,把压缩后的行列号映射回原始文件,映射表用的是 VLQ 编码;找 IPC 通道,也就是 ipcRenderer.invoke 和 ipcMain.handle 里那一对字符串。新版还能读 Electron fuses,那些编译进二进制的开关位,决定能不能开调试端口。

.NET 是最舒服的目标。PE 文件里有完整的 CLI 元数据,方法体是 IL 而不是机器码,类型名、方法名、参数名全在。读它更像读文档,不像逆向。

网站和浏览器页面属于运行时观测。CDP 是 Chromium 自己开的调试协议,Playwright 在上层封装。挂上去之后能看到已加载的脚本原文、网络请求、以及运行时的一些状态。这部分得到的信息和静态读文件完全不同,因为它反映的是「此刻真的发生了什么」。

固件是另一套思路。binwalk 和 unblob 会扫描文件头签名,找出里面嵌着的压缩包、文件系统镜像,层层切出来再解压。很多路由器和摄像头的固件是这么拆开的。

静态和运行时,得分清楚

这一点必须说透,否则很容易对工具有错误期待。

REA 的主体是静态分析。它读文件,不运行程序。你给它一个 exe,它分析这个 exe 的字节,不会替你点开界面。

所以「这个按钮点下去会发生什么」这类问题,静态分析只能回答一半:它能告诉你代码里有哪些分支、调用了哪些函数,但哪条分支真的会走,取决于运行时状态。

要观测运行时,得你自己先启动一个场景,让它挂上去看。浏览器有 CDP,进程有 PTY 捕获,macOS 上还能用 LLDB 跟踪函数和方法调用。这些是另外一条线,需要显式发起。

第三方做过一次实测,对象是一个真实的 Electron 应用。459 个文件、24.2 MB,52 秒解析完,文件和行号定位 30 项全对。它找到 18 个 IPC 通道,而应用实际有 17 个,多出来那个是误报。但 8 处窗口实例化和 2 个 preload 入口全部漏掉,因为那些行为只在运行时发生。

这个结果挺诚实:静态分析在「读代码」这件事上已经很强,在「还原行为」上仍有明确边界。

有意思的是,REA 自己的输出会告诉你它漏了。coverage 标成 unknown,limitations 里写着哪里没覆盖到。这比给你一个看起来很完整的错误答案要好得多。

法律这条线

工具越强,边界越要说清楚。

REA 不做的事:不还原原始源码,不做加壳脱壳,不绕过 DRM。它读的是没被加密的字节,遇到加密的二进制就没办法。

法律上有两条线要分开。一条是合同,很多商业软件的 EULA 明确禁止逆向,违反它是违约,不是侵权,但依然是麻烦。另一条是版权法里的访问控制条款,比如美国 DMCA 1201,禁止绕过保护措施,哪怕逆向本身是合法的。

而为了互操作性去逆向,在司法实践里长期被当作合理使用。EFF 整理过一份 Coders' Rights FAQ,讲得比大多数法律科普清楚。

落到实操上:拿它学习别人的实现思路、排查第三方 SDK 的行为、维护没有源码的老项目,都是正常用法。拿它把别人的闭源功能抄成自己的产品,那是另一回事。

对做移动和前端的人,它解决什么

我在移动开发里遇到的三类问题,它都能省时间。

接手一个没有源码的老项目。Electron 或 Flutter 打包出来的东西,前一个人走了,仓库里只剩构建产物。以前只能靠行为猜,现在可以问结构。

核查第三方 SDK。有些 SDK 会偷偷读设备信息、偷偷连某个域名。静态看一遍它的代码路径,比看它的隐私政策靠谱。

学一个功能的实现。比如某个编辑器的富文本复制为什么能保留格式,某个游戏的加速度曲线是怎么算的。看清规则,然后用自己的方式重写。

最后这个用法,官方给的例子挺典型。Windows 计算器里 200 + 10% 得 220,而 200 × 10% 得 20。为什么?

REA 把 % 处理函数的汇编取出来,看到两处比较:CMP EAX, 0x5c 和 CMP EAX, 0x5b,都跳向同一个地址。对照微软公开的源码,0x5c 是乘、0x5b 是除。

规则就出来了:乘除时把 10% 当成 0.1;加减时取第一个数的 10%,也就是 200 的 10% 等于 20,再加回去得 220。

注意这里谁做了什么。REA 提供指令、伪代码和引用关系,判断是 agent 做的。工具负责让证据到位,模型负责解释证据。这个分工,是这类工具和「一键脱壳破解」最本质的区别。

想自己试,从这三步开始

第一步,装。Node 版本够的话一条命令:

npx rea-agents@latest setup

按提示选你的 agent,看完变更计划再确认,然后重启客户端。重启这步别省,MCP 配置是启动时读的。

第二步,问对问题。有效的提问有两个要素:绝对路径,和一个具体行为。

用 REA 分析 /Users/me/apps/notes,回答一个问题:点「导出」之后,数据是怎么写到磁盘的?给出对应的函数、反编译代码和调用链,标注哪些环节没观测到。

路径必须是绝对的,这是 6.0 之后的破坏性变更,相对路径会直接报错。问题必须具体,「帮我分析这个 App」这种问法只会得到一堆泛泛而谈。

第三步,看 coverage。结果里标 unknown 的地方,就是这次没看到的地方。别把没看到当成没有。

最后说句实在的

这类工具刚出来的时候,讨论区里最常见的两个极端:一个说软件行业要完了,一个说这就是个套壳。

都不对。

它没有让逆向变得不需要理解。你仍然要知道什么是函数边界、什么是调用约定、什么是控制流,否则 agent 给你一段伪代码,你也判断不了它说得对不对。

但它确实搬走了一块很重的石头:过去你要在四个工具之间来回切换、导出、粘贴,现在这些动作被压缩成一次提问。

门槛降低不等于知识作废。相反,懂原理的人用它会更快,因为知道该问什么,也知道什么时候该怀疑它。


你觉得最想拆开看看的是哪个 App 的哪个功能?留言说说,我挑一个下次拿 REA 跑一遍。

💡 移动APP开发 | 前端 · 跨端 · AI 应用📱 关注我们,获取更多移动开发与 AI 工程实战内容💬 回复「逆向」加入技术交流群,一起做工具选型与排障❤️ 觉得有用?点个「在看」分享给更多人

相关学习资料