用 REA 逆向分析 Electron 应用:ASAR、preload 与 IPC 全流程
本页目录
Electron 应用把业务逻辑打包成 app.asar 里的 JavaScript,分散在主进程、preload 脚本和渲染进程页面三层。只盯着一个压缩过的 bundle 看,往往看不出什么;真正有价值的问题(比如“点了导出按钮之后发生了什么”)一定会穿过这三层。REA(morluto 的开源项目)以静态方式读取归档,构建一张包含入口、模块、preload 桥接和 IPC 通道的应用图,并且把没能解析的部分明确标为 unknown,而不是猜一个答案。
适合谁读: 想弄清 Electron 应用内部结构的开发者,用途可以是调试、审计、互操作或学习。需要熟悉终端和 Electron 的基本概念。
你会学到:
- 不运行应用,就能清点并解出
app.asar - 从静态图里看模块、import 关系和 source map
- 找到
contextBridge暴露的 API、BrowserWindow 的webPreferences和 preload 路径 - 把一个 IPC 通道从渲染进程一路追到主进程的 handler
- 识别原生
.nodeaddon,并交给原生分析工具
本文的示例目标是开源笔记应用 Joplin(AGPL-3.0-or-later,github.com/laurent22/joplin)。换成其他开源 Electron 应用或你自己编译的应用,步骤完全一样。
前置条件
-
Node.js 和 npm。 REA 以 npm 包
rea-agents发布。 -
REA 6.4.0 或更新版本。 可以用
npx -y rea-agents@latest <command>临时运行,也可以全局安装:npm install --global rea-agents如果要在 Claude Code、Cursor 或 Codex 里调用 REA,执行
npx rea-agents setup后重启 agent,详见 /zh/getting-started-rea-mcp/。 -
静态分析不需要 Electron 运行时。 REA 只把 JavaScript 当作文本解析,从不执行目标代码。
-
可选: 分析原生 addon 需要 Ghidra 或 Hopper(见安装文档);处理大结果建议装
jq;如果要拆解压缩过的 bundle,需要 Linux x64 上的 Wakaru(后文会讲)。
下文命令统一写成 rea;没有全局安装的话,在前面换成 npx -y rea-agents@latest。
第 1 步:先拿 REA 的 notes 示例练手
在分析真实应用之前,先跑一遍 REA JavaScript 指南附带的六文件示例。它足够小,你可以逐个文件核对 REA 的结论。示例里 renderer.js 调用 window.notes.exportCsv(),preload.js 通过 contextBridge 暴露这个方法并 invoke notes:export,main.js 用 ipcMain.handle 处理这个通道。
rea analyze-javascript-application /absolute/path/to/notes-example --json
为什么先做这一步: 建立预期。指南记录了 REA 4.1.0 在该示例上输出的 normalized_result.summary.ipc 片段:
{
"literal_channels": 1,
"renderer_transmissions": 1,
"main_handlers": 1,
"paired_renderer_transmissions": 1,
"ambiguous_renderer_transmissions": 0
}
一个字面量通道、一次渲染进程调用、一个 handler、一次配对。真实应用的数字会大得多,也会出现动态通道,但字段含义不变。
第 2 步:找到 ASAR
打包后的 Electron 应用一般把代码放在 resources/app.asar。以 Joplin 为例:
- macOS:
/Applications/Joplin.app/Contents/Resources/app.asar - Linux AppImage: 用
--appimage-extract参数运行 AppImage,再到squashfs-root/resources/下找 - Windows: 安装目录下的
resources\文件夹
旁边可能还有 app.asar.unpacked/。打包工具会把必须落在真实文件系统上的文件放进去,最常见的就是原生 .node 二进制。两者要一起保留,REA 也会校验这些 unpacked 文件。
建议把归档和 .unpacked 目录一起复制到单独的工作目录,避免已安装的副本在分析过程中被更新。
第 3 步:清点并解出归档
set -o pipefail
rea inspect-artifact /absolute/path/to/work/app.asar --json | jq . > inspection.json
为什么: inspect-artifact 返回按内容寻址的清单(路径、大小、SHA-256、ASAR 包含关系),还会给出下一步建议。pipefail 保证管道后面接 jq 时不会吞掉 REA 的退出码。
如果想把文件落到磁盘上对照阅读:
rea extract-artifact /absolute/path/to/work/app.asar --json
结果里会告诉你文件写到了哪里。
关于 ASAR 完整性校验。 Electron 归档会为条目声明 SHA-256。REA 默认在哈希不一致时直接失败。很多打包工具会在写完 header 之后再对 .asar.unpacked 里的原生文件做签名或 strip,所以即使是完全正常的正式版,哈希也可能对不上。遇到这种情况,改用:
rea extract-artifact /absolute/path/to/work/app.asar \
--integrity-policy record-and-continue --json
这样矛盾会连同声明值和实际值一起记录下来,而不是被悄悄忽略。
第 4 步:构建应用图
rea analyze-javascript-application /absolute/path/to/work/app.asar \
--integrity-policy record-and-continue --json > app-evidence.json
不带 --provider 和 --snapshot 时,rea analyze /path/app.asar 也会走同一个静态分析流程。
为什么要存成文件: 真实应用的完整 Evidence 很大。REA 的 CLI 文档提到,Obsidian 的 Evidence 文件有 334 MB,而摘要视图只有约 10 KB。所以别整个读,先投影出摘要:
jq -c '{source: {kind: "inline", evidence: .}, view: {kind: "summary"}}' \
app-evidence.json > app-view.json
rea inspect-analysis-view app-view.json
摘要包含归档身份、统计、Electron 相关数量、覆盖率和限制说明。先看 limitations,它会告诉你哪些范围是 partial、原因是什么。
分析过程中,清点、解析、建图等阶段的进度(rea_progress)写在 stderr,stdout 只有干净的 JSON。
分析太慢或内存不够时, 专用命令支持 --max-heap-mb 和 --analysis-timeout-ms(默认每个源文件 1024 MiB V8 old-space、300000 ms)。超出预算的文件会变成明确的 unknown,其余部分照常分析。
第 5 步:查看模块和 import
用同样的视图机制分页查看模块:
jq -c '{source: {kind: "inline", evidence: .},
view: {kind: "page", collection: "modules", offset: 0, limit: 32}}' \
app-evidence.json > modules-view.json
rea inspect-analysis-view modules-view.json
按 coverage.next_offset 翻下一页。每个模块都带有路径、摘要值和源码位置。
REA 在这一层能从静态语法里恢复:
package.json元数据和声明的main入口- 静态
import、动态import()、CommonJSrequire、worker 和 service worker - 以 AST 字面量形式存在的 Webpack / Rspack chunk 注册和模块工厂
- 路由、网络端点、存储和第三方库特征
已经知道要看哪里时,缩小范围。 比如只关心主进程和 preload,就指定精确的归档内相对路径。注意:--source-path 目前只在仓库 main 分支上,npm 6.4.0 发布版没有这个参数,会直接拒绝;需要源码构建或等后续版本(见 released package and main):
rea analyze /absolute/path/to/work/app.asar \
--source-path package.json --source-path main.js --json
REA 仍会清点并校验整个归档,但只解析选中的文件。import 不会自动扩展选择范围,未选中的文件标记为 outside-analysis-scope,覆盖率如实显示为 partial。上面的路径只是占位,真实入口请从清单里的 package.json 读取。
第 6 步:应用带了 source map 就用上
有些应用会附带 .map 文件或内联的 sourceMappingURL 注释。REA 会解析本地 source map 声明,把原始源文件名和内容摘要作为 maps_to 边记录在图中,但不会把原始 sourcesContent 文本存进图里。
为什么重要: src/services/sync/Synchronizer.ts 这样的原始路径比 chunk-4f2a.js 好理解得多。在模块分页里找 source map 节点;一个都没有,就说明应用没带 map。
没有 map、代码又被深度打包? REA 可以调用你自行提供的 Wakaru 从 bundle 中拆出可读模块。这个适配器目前只支持 Linux x64:
export REA_WAKARU_COMMAND="$PWD/wakaru"
rea recover-javascript-sources /tmp/work/main.js /tmp/recovered-main --json
rea analyze-javascript-application /tmp/recovered-main/modules --json
输出目录必须事先不存在。恢复出的模块属于 derived 证据,原始变量名和运行时等价性仍是未知。详见 javascript-recovery.md。
第 7 步:找到 preload 桥接
preload 脚本是沙箱页面与 Node.js 之间的契约。REA 会报告:
- 显式写出的
BrowserWindow选项和webPreferences,包括能静态解析的preload路径 contextBridge.exposeInMainWorld与exposeInIsolatedWorld的 API 名称和字面量成员路径- sender、frame、URL、origin 校验的候选位置
有两点要注意:
- REA 只报告源码里显式写出的
webPreferences,不会拿 Electron 各版本的默认值补位。源码里没写contextIsolation,结论就是“代码中未设置”,而不是“true”。 - 看得到的 origin 检查只是候选,REA 不声称它构成了完整的安全策略。
真实应用经常动态拼装桥接对象。rea.tools 上的 Notion 案例可以作为参考:它的 preload 暴露了 __electronApi,api_status: "dynamic",没有字面量成员,invoke 的通道表达式是个变量(channel: null、resolution: "dynamic")。分析者随后去读打包后的封装代码,才找到字面量通道。把它当成“如何阅读动态桥接”的思路示例即可。
第 8 步:追踪 IPC 通道
REA 的 IPC 配对是推断,规则很严格:
- 渲染进程的
invoke只会和同一个字面量通道上唯一的ipcMain.handle/handleOnce配对 - 渲染进程的
send只会和该通道上唯一的ipcMain.on/once/addListener配对 - 动态通道一律不配对;有多个候选 handler 时标记为 ambiguous
要追踪一个功能,先从保存的 Evidence 生成追踪输入。下面的写法来自 REA 的 workflow 文档,把 seed 换成你在摘要里找到的通道:
node --input-type=module -e '
import { readFileSync, writeFileSync } from "node:fs";
const application = JSON.parse(readFileSync("app-evidence.json", "utf8"));
writeFileSync("trace-input.json", JSON.stringify({
application, seed: { kind: "channel", value: "your:channel-name" }, direction: "both"
}));'
rea trace-application-feature ./trace-input.json --json
seed 类型有 node-id、route、string、api、channel、module、native-export。匹配是字面量匹配:string 默认按子串匹配,其他默认精确匹配,不支持正则。
结果包含命中的 seed、完整可达子图、终端路径和原生交接(handoff)。匹配到通道只能证明静态上的连接,不能证明 handler 在运行时一定注册、或某次会话一定走到。
很多时候从 api seed 开始最省事:比如示例里的 notes.exportCsv,一次追踪就能穿过 preload 走到 handler。
第 9 步:原生 addon
当 JavaScript 调用 require("./build/Release/something.node") 或加载原生依赖时,REA 会记录一个原生 addon 节点,以及 JavaScript 语法里请求的成员名。它既不解析也不执行 addon,这些名字也不等于已验证的导出符号。
继续深入的做法:
-
用
{ "kind": "native-export", "value": "<name>" }追踪,看哪些功能会走到这个绑定。如果还没有对应的原生 Evidence,handoff 会给出requires-provider-analysis,并写明 addon 的精确摘要。 -
用原生 provider 分析
app.asar.unpacked/里的.node文件:rea analyze /absolute/path/to/work/app.asar.unpacked/path/to/addon.node --provider ghidra --json rea search /absolute/path/to/work/app.asar.unpacked/path/to/addon.node "Init" --provider ghidra --json
如果没有提供 .unpacked 目录,对应条目会保持 unavailable,不会被当成不存在。Ghidra 的用法见 /zh/rea-ghidra-native-binary/。
第 10 步(可选):在运行时核对静态结论
静态分析回答的是代码可能做什么。想知道页面实际加载了什么,REA 可以被动连接本机回环地址上的 Electron CDP 端点。只在你有权分析的应用和机器上这样做(参见 REA 的 DISCLAIMER.md)。
用远程调试端口启动应用,例如 --remote-debugging-port=9223。这是 Electron 自带的 Chromium 开关(见 Electron command-line switches),不是 REA 的参数;REA 文档只要求一个字面量回环地址的 CDP 端点,有些应用会禁用或忽略这个开关。然后:
rea list-electron-targets http://127.0.0.1:9223 --json
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID --observation-ms 100 --json
检查过程不会执行 JavaScript、不点击、不调用 IPC,也不关闭应用。如果需要启动应用并记录 IPC 通道和参数形状元数据(从不保存载荷值)的主动场景,参见 electron-observation.md 里的 rea capture-electron-scenario。注意主动场景以你当前用户的权限运行应用,它不是沙箱。
常见问题与排错
.asar.unpacked 里的文件完整性校验失败。 对很多签过名的正式版来说这是正常现象。加 --integrity-policy record-and-continue 重跑,覆盖率会变成 partial,并列出矛盾项。
用 --source-path 时提示路径未知(该参数仅 main 分支有,6.4.0 没有)。路径必须与清单中的文件完全一致,包括嵌套 ASAR 前缀;通配符和别名会被拒绝。直接从清单里复制路径。
IPC 摘要里配对数为 0。 大概率是通道在运行时拼出来的(变量、模板字符串或封装函数)。到图里找动态通道的位置,去读那个封装函数,思路同 Notion 案例。
MCP 下请求 summary 被拒,提示 capability_unavailable。 当前连接的服务器不保留 Evidence。改用第 4 步的 CLI 文件视图,或用 npx rea-agents@latest setup 更新 REA 注册。
输出太大,agent 读不下。 使用摘要和分页视图、--source-path 缩小范围(仅 main 分支),或用 --format json --filter-output evidence_id,normalized_result.statistics 只取需要的字段。
Wakaru 恢复无法启动。 确认 REA_WAKARU_COMMAND 是平台二进制的绝对路径,而不是 node_modules/.bin 下的 Node.js shim,并且你在 Linux x64 上。
下一步
- 安装 REA 并接入 agent:/zh/getting-started-rea-mcp/
- 继续分析原生 addon:/zh/rea-ghidra-native-binary/
- 对比同一应用的两个版本:/zh/rea-evidence-snapshots-compare/
- 追踪网页前端的 API 调用:/zh/trace-website-api-calls/
- 参考文档:JavaScript artifact reconstruction、application workflows