用 REA 分析 Android APK:JADX、Apktool、ADB 与 Flutter 快照
本页目录
REA(morluto 的开源项目)为终端和 coding agent 提供了一组 Android 工具。它返回的是绑定到具体文件的结构化 Evidence,而不是一堆零散的反编译输出。本文先对一个开源 APK 做完整的静态分析,再演示如何从你自己掌控的设备上拉取已安装的包,最后讲 Flutter 应用的初步判定。
适合谁读: 想直接回答“这个值是在哪里算出来的”“这个应用是哪个 Dart SDK 编译的”这类问题,又不想在 GUI 反编译器里一层层点的开发者和安全研究人员。
你会学到:
- 通过 REA 调用 JADX,查看 APK 的 manifest、权限和类清单
- 搜索类、阅读反编译后的方法、追踪调用者
- 用 Apktool 解码资源和多语言字符串
- 列出设备,通过 ADB 拉取已安装应用的完整 split APK
- 识别 Flutter 构建及其 Dart AOT 快照
示例目标: Aegis Authenticator(GPL-3.0,F-Droid 上也有发布)和开源的 Flutter Gallery(BSD-3-Clause,v2.9.2 发布页提供 flutter_gallery_Android.apk)。两者都开源,每个结论都可以和原始代码对照。
前置条件
- REA 6.4.0 或更新版本:
npm install --global rea-agents,或在命令前加npx rea-agents@latest。接入 agent 用npx rea-agents setup,详见 /zh/getting-started-rea-mcp/。 - 完整的 JDK 17 或更新版本,必须包含
jdk.compiler模块。只有 JRE 不行,因为 REA 启动时要编译一个小的元数据桥接程序。可以用java --list-modules | grep jdk.compiler检查。 - jadx-headless-mcp 0.7.1(GitHub release 里的
-all.jar)。REA 不会替你安装。 - Apktool 2.7.0 或更新版本,用于资源解码。
- 如果要做设备部分,还需要 Android platform-tools 里的
adb,以及一台模拟器或你自己的手机。
以上工具 REA 都不负责安装。Android provider 在 macOS 和 Linux 上验证过;Windows 用户可参考 REA 的 Aegis 案例,使用 WSL。
第 1 步:下载并固定 APK
从 Aegis 的 GitHub 正式发布页下载 v3.4.3,并校验摘要:
shasum -a 256 aegis-v3.4.3.apk
REA 的案例记录这个文件为 6,881,897 字节,SHA-256 为 fa091c91912c0d56ec3553394fac4158f9a9d178e93bcbb35419601d788cb528。
为什么要固定摘要: REA 的每个结果都会写入 APK 的 SHA-256。摘要一致,你的结果就能和公开案例逐项对比。F-Droid 版 Aegis 由 F-Droid 自行构建和签名,字节和摘要都不同;它一样可以分析,只是和案例不是同一个文件。
第 2 步:配置 JADX 并检查包
export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
# 可选:指定某个 JDK,而不是 PATH 上的 java
export JAVA_HOME=/absolute/path/existing-jdk
rea inspect-android-package /absolute/path/aegis-v3.4.3.apk --json
为什么: 包检查相当于地图。它返回解码后的 manifest XML、包名和 SDK 声明、权限、类和资源数量、引擎身份,以及 APK 和引擎 JAR 的摘要。案例中 Aegis 的结果是:包名 com.beemdevelopment.aegis,版本 3.4.3(version code 82),共 3,077 个类。
这一步不会做两件事:验证 APK 签名(结果中标为 not_performed),以及运行应用。
通过 MCP 使用时,把 REA_JADX_MCP_JAR 加到 REA 服务器的环境变量里并重新连接,然后调用 inspect_android_package,参数为 {"path": "/absolute/path/aegis-v3.4.3.apk"}。npm 6.4.0 里这个工具只接受 path,返回完整结果。仓库 main 分支新增了可选的 detail: "summary",以及通过 inspect_analysis_view 分页读取 manifest 的视图;这些不在 6.4.0 里,使用前先看已连接服务器的工具 schema(见 android-analysis.md)。
第 3 步:搜索、阅读、追踪代码
案例要回答的问题是“六位登录验证码是怎么算出来的”。同样的套路适用于任何功能。
搜索类名:
rea search-android-classes /absolute/path/aegis-v3.4.3.apk Totp
案例中,区分大小写的查询 Totp 命中三个类:otp.TotpInfo、ui.views.TotpProgressBar、importers.TotpAuthenticatorImporter(省略了包名前缀)。只有第一个像是计算逻辑。
列出类成员和重载:
rea inspect-android-class /absolute/path/aegis-v3.4.3.apk com.beemdevelopment.aegis.otp.TotpInfo
这一步不生成源码,直接返回字段、方法和重载序号。TotpInfo 里,getOtp() 是重载 0,getOtp(long) 是重载 1。
阅读单个方法:
rea inspect-android-method /absolute/path/aegis-v3.4.3.apk \
com.beemdevelopment.aegis.otp.TotpInfo getOtp --overload-index 1
为什么要加 --overload-index: 方法有歧义时,REA 会直接拒绝,而不是默默选第一个。重载序号只对当前文件和引擎有效,换了构建不要复用。
查找调用者:
rea trace-android-references /absolute/path/aegis-v3.4.3.apk \
com.beemdevelopment.aegis.otp.OtpInfo --method-name getOtp
案例中找到的调用者是 ui.views.EntryHolder.getOtp 和 ui.MainActivity.copyEntryCode:一个负责显示验证码,一个负责复制。
解读结果时注意:
- 反编译出的 Java 是派生表示。文本完整不等于语义完整。
- 引用关系是静态的入向关系,不是运行时观测到的调用。反射和动态加载仍属未验证。
- 静态初始化块(
<clinit>)会以 smali 形式返回,因为 Java 反编译可能把它的指令拆散到字段声明里。 - 正式版通常经过压缩和重命名。在 Aegis 里,OTP 生成函数出现在
kotlin.ExceptionsKt下,所以要从已知入口顺着调用走,别轻信类名。
第 4 步:用 Apktool 解码资源
JADX 回答代码层面的问题;资源表回答“界面上写了什么”“带了哪些语言、版本信息是什么”。
export REA_APKTOOL_COMMAND=/absolute/path/apktool # 可选;不设则从 PATH 查找
rea inspect-apktool-client
rea decode-android-resources /absolute/path/aegis-v3.4.3.apk
rea decode-android-resources /absolute/path/aegis-v3.4.3.apk --locale de
rea decode-android-resources /absolute/path/aegis-v3.4.3.apk --no-strings
每条命令的作用:
inspect-apktool-client执行apktool --version,报告选中的是哪个启动器。先跑它,工具缺失时能尽早发现。- 默认解码会返回 Apktool 原样打印的
apktool.yml元数据、解码后的 manifest(上限 512 KiB)、默认字符串表(最多 2000 条),以及locales列表。 --locale de改为读取res/values-de/strings.xml。pt-BR这样的地区代码会映射到 Android 的values-pt-rBR。--no-strings跳过字符串,只要元数据和 manifest 时用。
REA 以 --no-src 方式运行 Apktool,这里不会产出 smali,代码交给 JADX 工具处理。字符串按原文输出:转义实体保持转义,含嵌套标记的条目计为未解析,不会被压平。
第 5 步:通过 ADB 获取已安装的应用
有时你要分析的正是设备上已经安装的那一份,比如你自己测试机上的 F-Droid 版 Aegis,可能还带有 split APK。
先取得授权。 只连接你自己的设备或明确获得授权的设备,只拉取你有权分析的应用和数据。部分 ADB 操作会改变设备状态。开始前请阅读 REA 的 DISCLAIMER.md。
export REA_ADB_PATH=/absolute/path/to/adb # 可选;不设则从 PATH 查找
rea inspect-adb-client
rea list-adb-devices
rea inspect-adb-device emulator-5554
inspect-adb-client 只探测 adb 二进制,不连接 adb server。list-adb-devices 返回每台设备的 serial、状态、传输方式,以及推断出的模拟器/真机类型和推断依据。serial 必须原样使用;emulator-5554 只是模拟器常见的默认名。
查找并拉取应用:
rea resolve-adb-packages emulator-5554 aegis
rea inspect-adb-package emulator-5554 com.beemdevelopment.aegis
rea pull-adb-package emulator-5554 com.beemdevelopment.aegis --output-directory /tmp/acq
rea inspect-android-package /tmp/acq/com.beemdevelopment.aegis/base.apk
为什么按这个顺序: resolve-adb-packages 做不区分大小写的子串搜索,帮你拿到准确包名;inspect-adb-package 显示版本、安装来源和标志位,让你确认要拉的是哪个构建;pull-adb-package 根据 pm path 拉取完整的 split 集合,并对每个文件计算摘要。--output-directory 本身可以已存在,但 REA 在其中创建的以包名命名的子目录(这里是 /tmp/acq/com.beemdevelopment.aegis)必须事先不存在,所以已完成的拉取结果不会被覆盖。
只读操作与会改变设备的操作。 list-adb-processes、read-adb-logcat、capture-adb-screen、list-adb-directory 等只读。下面这些会改变设备状态,REA 也绝不会隐式执行:
rea install-adb-package emulator-5554 /tmp/app.apk --replace
rea start-adb-app emulator-5554 com.example.app
rea stop-adb-app emulator-5554 com.example.app
rea uninstall-adb-package emulator-5554 com.example.app
uninstall-adb-package 会删除 user 0 下的应用及其数据。这一组操作尽量在模拟器上做。
REA 的每次 adb 调用都使用固定的参数向量,没有任何工具接受任意 shell 命令;UI 输入(input、monkey)也刻意没有开放。
第 6 步:初判 Flutter 应用
Flutter 应用的大部分逻辑写在 Dart 里,预编译成 lib/<abi>/libapp.so。用 JADX 看,基本只能看到 Android 嵌入层。REA 的 Flutter 工具直接解析 APK:不需要引擎、不执行代码、不联网查询。
版本说明。
identify-flutter-build和inspect-dart-aot(MCP 中为identify_flutter_build/inspect_dart_aot)目前只在仓库main分支上,npm 6.4.0 发布版的命令列表里没有它们。在后续版本发布之前,需要从main源码构建才能使用;先用npm view rea-agents dist-tags.latest查看最新版本,并参考 released package and main。
rea identify-flutter-build /absolute/path/flutter-gallery.apk
rea inspect-dart-aot /absolute/path/flutter-gallery.apk
rea inspect-dart-aot /absolute/path/flutter-gallery.apk --abi armeabi-v7a
能拿到什么:
flutter_detected、各 ABI 的libflutter.so摘要、GNU build ID,以及带标签的版本字符串- Dart 快照哈希:位于
libapp.so中快照魔数f5 f5 dc dc之后 20 字节处 inspect-dart-aot还会给出四个_kDart*Snapshot*段,以及字符串池投影,其中列出package:URI(相当于依赖清单)和dart:SDK 库
inspect-dart-aot 默认依次优先 arm64-v8a、x86_64、x86、armeabi-v7a。REA 自己对 Flutter Gallery 2.9.2 正式版 APK 的验证记录显示:所有 ABI 的快照哈希一致,为 65817c30a78bb44c3dc3771876b6010a,并且没有带标签的 Dart 版本字符串,这对 release 引擎来说是正常的。
为什么快照哈希很关键: 它可以借助外部数据集(例如 reFlutter 维护的那份)对应到具体的 Dart SDK 版本。REA 刻意不做哈希到 SDK 的查询。知道 SDK 版本后,才能选用对应布局的快照解析器,或者继续用原生分析去看 libapp.so(见 /zh/rea-ghidra-native-binary/)。
字符串池是按字节模式提取的投影,不是反序列化出的类结构。class_like_tokens 只能当线索,其中会混有偶然匹配。
常见问题与排错
Java 能启动,但桥接程序失败。 多半用的是 JRE。把 JAVA_HOME 指向完整 JDK,并确认 java --list-modules 里有 jdk.compiler。
提示方法有歧义。 先用 inspect-android-class 列出重载,再加 --overload-index。对于有重载的方法,trace-android-references 无法指定具体重载,会把这一点报告为不支持。
大 APK 超时或堆内存不足。 单次操作的期限是 120 秒,反编译是 90 秒。可以用 export REA_JADX_HEAP_MIB=8192 或 JAVA_TOOL_OPTIONS=-Xmx8g 调大堆。
手里是 .aab。 Android App Bundle 不是 APK,这些工具也不会把它当 APK 处理。改用项目发布的 APK,或者在测试设备上通过正常渠道安装应用,再用 pull-adb-package 拉取已安装的 split 集合。
Apktool 报 capability_unavailable。 把 REA_APKTOOL_COMMAND 设为启动器的绝对路径,再跑一次 rea inspect-apktool-client。
pull-adb-package 拒绝执行。 --output-directory 里同名的包子目录已经由之前的拉取创建。换一个新的输出目录;REA 不会覆盖之前的证据。
拿不到快照哈希。 如果 flutter_detected 为 false,覆盖率是 complete,说明它不是 Flutter 应用。如果为 true 但哈希为 null,看 snapshot_hash_candidates:多条记录不一致时,REA 会全部保留,不会靠猜选一个。
下一步
- 安装 REA 并接入 agent:/zh/getting-started-rea-mcp/
- 用 Ghidra 继续分析
libapp.so或 JNI 库:/zh/rea-ghidra-native-binary/ - 用 Evidence 对比两个版本:/zh/rea-evidence-snapshots-compare/
- 拿 CTF 题练手:/zh/ctf-reverse-engineering-ai-agent/
- 参考文档:android-analysis.md、apktool-resource-analysis.md、adb-device-analysis.md、flutter-analysis.md,以及 Aegis 案例