rea-reverse

用 REA 逆向分析 Electron 应用:ASAR、preload 与 IPC 全流程

基于 REA 6.4.0更新于 约 9 分钟读完
本页目录

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
  • 识别原生 .node addon,并交给原生分析工具

本文的示例目标是开源笔记应用 Joplin(AGPL-3.0-or-later,github.com/laurent22/joplin)。换成其他开源 Electron 应用或你自己编译的应用,步骤完全一样。

前置条件

  1. Node.js 和 npm。 REA 以 npm 包 rea-agents 发布。

  2. 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/。

  3. 静态分析不需要 Electron 运行时。 REA 只把 JavaScript 当作文本解析,从不执行目标代码。

  4. 可选: 分析原生 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()、CommonJS require、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 校验的候选位置

有两点要注意:

  1. REA 只报告源码里显式写出的 webPreferences,不会拿 Electron 各版本的默认值补位。源码里没写 contextIsolation,结论就是“代码中未设置”,而不是“true”。
  2. 看得到的 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,这些名字也不等于已验证的导出符号。

继续深入的做法:

  1. 用 { "kind": "native-export", "value": "<name>" } 追踪,看哪些功能会走到这个绑定。如果还没有对应的原生 Evidence,handoff 会给出 requires-provider-analysis,并写明 addon 的精确摘要。

  2. 用原生 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 上。

下一步