AAB 转 APK 与 APK 解包:用 REA 盘点并提取资源和脚本
本页目录
APK 本质上是 ZIP,unzip 就能打开。但 unzip 不会告诉你每个条目是什么、写到磁盘上的字节是否和包里一致,也不会告诉你应用真正的逻辑放在哪里:DEX、JavaScript bundle、Unity 元数据,还是 Lua 脚本。REA(morluto 的开源项目)补上了这一层:按内容寻址的清单、逐字节校验的安全提取器,以及封装好的 Apktool 资源解码。
本文只讲拆包和盘点,不讲 Java 反编译。想读类和方法,看完本文后去看 用 REA 分析 Android APK 或 JADX MCP 配置指南。
适合谁读: 手上有 .apk 或 .aab,想在挑选更深入的工具之前先弄清楚里面有什么的开发者和研究人员。
你会学到:
- 用 bundletool 把 Android App Bundle(AAB)转成 universal APK
- 用
inspect-artifact盘点 APK,以及这份清单看不到什么 - 用
extract-artifact提取全部文件,以及它为什么会拒绝某些包 - 用
decode-android-resources解码 manifest 和字符串 - 通过文件名和文件头识别 React Native、Hermes、Lua、Unity IL2CPP,并清楚 REA 6.4.0 的能力边界
示例目标: F-Droid 客户端 APK(GPL-3.0,源码在 gitlab.com/fdroid/fdroidclient),以及你自己从开源项目构建出的 .aab。不要用这些步骤从商业应用里提取付费内容。
先确认授权。 只解包你自己写的应用、开源应用,或你明确获准分析的应用。提取出来的文件依然受许可证和服务条款约束。开始之前请阅读 REA 的 DISCLAIMER.md。
前置条件
- REA 6.4.0 或更新版本。
npm install --global rea-agents,或在命令前加npx rea-agents@latest。如果rea --version的输出不对劲,先按 如何确认装的是真正的 REA 检查。 - bundletool,从 GitHub releases 下载,另需 Java 来运行 JAR。只有输入是 AAB 时才需要。
- Apktool 2.7.0 或更新版本,用于资源解码。REA 自己的文档是用 2.7.0 验证的(apktool-resource-analysis.md)。
- 可选:用
xxd或file查看文件头;如果关心签名,再装 Android SDK build-tools 里的apksigner。
这些外部工具 REA 一个都不会帮你装。清单和提取命令是 REA 内置的,不需要额外的 provider。
APK 和 AAB:REA 实际看到的是什么
两者都以 ZIP 签名 PK\x03\x04 开头,但不是一回事:
- APK 是可安装包:二进制 XML 格式的
AndroidManifest.xml、一个或多个classes*.dex、resources.arsc、res/、assets/、lib/<abi>/*.so和META-INF/。 - AAB 是发布格式,由 Google Play 生成面向具体设备的 APK。它按模块组织(
base/manifest/、base/dex/、base/res/等),不能直接安装。
REA 6.4.0 先检查 ZIP 魔数,再按路径后缀判断包格式。在 dist/domain/zipPackageFormat.js 里,.apk 对应格式 apk,代码注释写着 “An Android App Bundle is a ZIP archive, not a standalone APK.” 没有 aab 这个格式:ZIP 魔数正确的 .aab 会被归为普通 zip。
实际影响有两点:
inspect-artifact和extract-artifact能处理.aab,你会得到 bundle 目录结构的 ZIP 清单。- Android 专用工具(
decode-android-resources、inspect-android-package和 JADX 系列)要的是 APK。REA 的 android-analysis.md 明确写了这些操作不处理 split APK/AAB。所以要先转换。
第 1 步:用 bundletool 把 AAB 转成 APK
已经有 APK 的话跳过这一步。
先从你能掌控的源码构建 bundle,例如在某个开源 Android 项目里执行 ./gradlew bundleRelease,然后让 bundletool 生成一个 universal APK:
java -jar bundletool.jar build-apks \
--bundle=/absolute/path/app-release.aab \
--output=/absolute/path/app-universal.apks \
--mode=universal
为什么用 --mode=universal: 默认情况下,bundletool 会生成 Play 会下发的整套 split APK。bundletool 官方文档 把 universal 模式描述为一个包含应用全部代码和资源、兼容所有受支持设备配置的单一 APK。一个文件比一组 split 好盘点得多。
文档和 bundletool 源码里有三个细节:
- 签名。 不传
--ks时,bundletool 会尝试用 debug key 签名。源码(DebugKeystoreUtils.java)依次在ANDROID_SDK_HOME、用户主目录、HOME下找.android/debug.keystore,别名为AndroidDebugKey。装过 Android Studio 的机器通常都有。debug 签名的 APK 用来分析没问题,但它不是开发者的正式签名。 - 功能模块。 只有在 manifest 里声明了
<dist:fusing dist:include="true"/>的模块才会进 universal APK。没声明的模块不在里面,不能据此断定应用没有这个功能。 - 输出已存在。 如果
--output指定的文件已经存在,bundletool 会报错,除非加--overwrite。
输出是 .apks 文件,它本身也是 ZIP(即 “APK set”)。universal 模式下 bundletool 把 APK 命名为 universal.apk,放在包的根目录(见 bundletool 源码 ApkPathManager.java)。用普通的 unzip 取出来:
mkdir /absolute/path/apks-out
unzip /absolute/path/app-universal.apks universal.apk -d /absolute/path/apks-out
也可以直接对 .apks 跑 REA:它会被归为 zip,extract-artifact 会把 universal.apk 当作一个文件写出来。REA 不会打开 APK set 里嵌套的 APK,清单会注明这个 .apk 后缀“提示是 APK,但没有读取这些字节”。两种方式都行,后面都用取出来的 universal.apk。
第 2 步:用 inspect-artifact 盘点 APK
从 f-droid.org 下载 F-Droid 客户端(或者用你的 universal.apk),先记下摘要,再做盘点:
shasum -a 256 /absolute/path/F-Droid.apk
set -o pipefail
rea inspect-artifact /absolute/path/F-Droid.apk --json | jq . > fdroid-inventory.json
为什么先盘点再提取: 一次调用就能拿到完整的、按内容寻址的 artifact 图:中央目录里每个条目的逻辑路径、条目类型、声明大小和压缩大小、是否加密、SHA-256,再加上完整性观察、限制说明和建议的下一步探测。你可以先用 jq 检索,决定要取哪些文件,再往磁盘上写。pipefail 保证后面接 jq 时不丢 REA 的退出码。
优先看这几样:
classes*.dex的数量,判断是不是 multidexlib/<abi>/*.so,REA 把它们归为dynamic-libraryassets/下的全部内容,脚本和引擎数据通常放在这里res/和resources.arsc,即编译后的资源(第 4 步解码)
--integrity-policy 可选 fail(默认)或 record-and-continue。它对 ASAR 这类自带哈希声明的格式最重要;APK 一般保持默认,除非 REA 报出了你想作为不可信数据保留下来的矛盾。
清单看不到什么。 REA 把这条限制原样写进结果(常量 ZIP_NON_ENTRY_TAIL_LIMITATION):“ZIP inventory lists central-directory entries only. Bytes after the central directory, including an APK Signing Block or a ZIP comment, are not occurrences.” 严格说 APK Signing Block 位于中央目录之前,但结论不变:v2 及以上签名不是 ZIP 条目,不会出现在清单里,inspect-artifact 也不校验它们。关心签名就运行 apksigner verify --print-certs /absolute/path/F-Droid.apk。只有 v1(JAR)签名文件会以普通条目的形式出现在 META-INF/ 下。
通过 MCP 使用: 先用 open_binary 把 APK 设为当前目标,再让 agent 调用 inspect_artifact。6.4.0 里它唯一的参数是 integrity_policy。仓库 mcp-contracts.md 里提到的 detail: "summary" 只在 main 分支,6.4.0 没有。
第 3 步:用 extract-artifact 提取 APK 资源
rea extract-artifact /absolute/path/F-Droid.apk --json > fdroid-extract.json
没有指定输出目录的选项。REA 会在系统临时目录下新建一个 rea-extracted-<uuid> 目录,结果里的 output_root 就是它。每个提取出的文件都会列出 relative_path、sha256 和 bytes_written。
为什么不直接用 unzip: dist/artifacts/extraction/ArtifactExtraction.js 和 SafeOutputTree.js 里的提取器是按“包不可信”来设计的:
- 条目路径必须是规范化的相对路径。绝对路径和
..段会被拒绝,构造出来的条目没法写到输出目录外面(也就是常说的 “zip slip”)。 - 不跟随符号链接条目,输出根目录以
0700权限创建。 - 每个文件都以独占方式(
O_EXCL)创建,绝不覆盖;输出根目录已存在会直接报错。 - 写入的字节会和清单里的大小、SHA-256 比对,不一致就失败。
- 如果目标文件系统不区分大小写(比如 macOS 默认卷),只有大小写不同的两个文件名会被检测出来。
- 遇到加密条目就整体停止。REA 从不解密包内条目,所以它会把这个包报告为不支持,而不是给你一棵残缺的目录树。
嵌套的压缩包会作为文件写出,不会展开。这是有意为之:如果 assets/ 里还有个 ZIP,就对提取出的副本单独跑一次 inspect-artifact,让每一层都有自己的摘要。
需要的文件要尽快复制出来。 输出在 $TMPDIR 下,系统可能会清理。
第 4 步:用 Apktool 解码 manifest 和字符串
AndroidManifest.xml 和 resources.arsc 都是二进制格式,直接提取只能拿到字节,读不到 XML。Apktool 负责解码:
export REA_APKTOOL_COMMAND=/absolute/path/apktool # 可选;不设就从 PATH 找
rea inspect-apktool-client
rea decode-android-resources /absolute/path/F-Droid.apk --json
rea decode-android-resources /absolute/path/F-Droid.apk --locale de --json
rea decode-android-resources /absolute/path/F-Droid.apk --no-strings --json
REA 怎么调用 Apktool(见 dist/apktool/ApktoolProvider.js):
- 先把 APK 复制到私有临时工作区,复制过程中计算哈希;如果文件在此期间被改动,就拒绝这个输入。
- 运行
apktool d --no-src -f -o <workspace> --frame-path <private dir> <copy>。--no-src表示不生成 smali,代码交给 JADX 工具。私有的 framework 路径避免 Apktool 改动你全局的 framework 目录。 - 返回
apktool.yml元数据(版本名、版本号、最低和目标 SDK)、解码后的 manifest、locales列表和一张字符串表,然后删除工作区。磁盘上不会留下解码后的res/目录。
6.4.0 的上限是写死的:解码 600 秒超时,manifest 最多读 512 KiB,strings.xml 最多 8 MiB,最多返回 2,000 条字符串,解码目录的枚举到 100,000 个条目为止。触发任何一个上限都会写进 limitations,结果被截断时一定看得到。
REA_APKTOOL_COMMAND 必须是绝对路径,相对路径会以 not_absolute 被拒绝。inspect-apktool-client 只运行 apktool --version,先跑它,找不到启动器时几秒就会报错,而不是解码到一半才失败。
需要完整的 res/ 目录树时,自己对 APK 副本运行 Apktool。REA 在这里给的是一份可复现、绑定 APK SHA-256 的投影结果。
第 5 步:找出脚本和引擎数据
很多应用的真正逻辑不在 DEX 里。用第 2 步的清单定位,再用文件头确认。文件名只是线索,文件头才是更可靠的证据。
xxd -l 16 /path/to/rea-extracted-xxxx/assets/some-file
file /path/to/rea-extracted-xxxx/assets/some-file
React Native 与 Hermes
React Native 的 Gradle 插件默认把 JavaScript bundle 打包成名为 index.android.bundle 的 asset(ReactExtension.kt 里的 bundleAssetName)。lib/<abi>/ 下出现 libhermes.so 或 libhermes_executor.so,说明引擎很可能是 Hermes。
- 如果 bundle 是普通 JavaScript,接着看 从 bundle 恢复 JavaScript 源码。
- 如果是 Hermes 字节码,文件头魔数是 64 位常量
0x1F1903C103BC1FC6(Hermes 源码 BytecodeFileFormat.h 中的MAGIC)。按小端存储,xxd显示为c6 1f bc 03 c1 03 19 1f。REA 6.4.0 不支持 Hermes 字节码:只能盘点、提取和计算哈希。
Lua 脚本
游戏和部分应用会把 Lua 放在 assets/ 下。预编译的 chunk 以转义字节开头:
- 标准 Lua 字节码以
\x1bLua开头(Lua 源码 lua.h 中的LUA_SIGNATURE),十六进制为1b 4c 75 61。 - LuaJIT 字节码以
\x1bLJ开头(LuaJIT 源码 lj_bcdump.h 中BCDUMP_HEAD1..3=0x1b 0x4c 0x4a)。
没有这些字节的 .lua 文件多半是明文源码,可以直接当文本读。REA 6.4.0 不解码 Lua 或 LuaJIT 字节码。
Unity IL2CPP
Unity 的 IL2CPP 构建把原生代码放在 lib/<abi>/libil2cpp.so(和 libunity.so 在一起),并在 assets/ 下放一个名为 global-metadata.dat 的元数据文件。未经改动的元数据以 32 位值 0xFAB11BAF 开头;广泛使用的开源工具 Il2CppDumper 在解析前检查的正是这个 “sanity” 值。按小端存储,xxd 显示为 af 1b b1 fa。
REA 6.4.0 不解码 IL2CPP 元数据(managed-code-analysis.md 对 managed 工具有明确说明)。你能做的是把 libil2cpp.so 当成普通原生库,通过 REA 交给 Ghidra 分析(/zh/rea-ghidra-native-binary/)。没有元数据时,看到的会是去掉符号、没有名字的函数。
Flutter
出现 lib/<abi>/libapp.so 和 libflutter.so 就是 Flutter。Dart 快照相关命令(identify-flutter-build、inspect-dart-aot)只在仓库 main 分支,npm 6.4.0 里没有。变通办法见 用 REA 分析 Android APK 的 Flutter 一节。
其他资源
图片、字体、音频、JSON 配置、WebView 用的 HTML 都以普通文件的形式放在 assets/ 和 res/raw/ 下。第 3 步之后它们已经在磁盘上,并且哈希经过校验。
常见问题和排错
decode-android-resources 处理 .aab 失败。 正常现象:Apktool 解码的是 APK。先做第 1 步。
extract-artifact 提示条目已加密。 REA 从不解密 ZIP 条目,所以整个包都会被拒绝。这是格式边界,不是 bug。不要试图绕过不属于你的应用的加密。
“Extraction output root already exists”。 目录名带随机 UUID,几乎不会发生;真遇到就重跑。REA 绝不会往已存在的目录里写。
Apktool 报 capability_unavailable。 找不到启动器,或者它没有报告版本号。把 REA_APKTOOL_COMMAND 设成启动器的绝对路径,再跑一次 rea inspect-apktool-client。
字符串只有 2,000 条。 用 --locale 一次读一个语言,或者直接运行 Apktool 拿完整的表。
universal APK 缺了某个功能。 对应模块没有标记 fusing。去掉 --mode=universal 从 .apks 生成该模块的 APK,或者直接对 .aab 跑 inspect-artifact。
同一个应用两次结果的摘要不一样。 F-Droid 构建和开发者自己的构建用的签名密钥不同,bundletool 生成的 universal APK 又是用你的 debug key 重新签名的。字节不同,哈希自然不同。记下你分析的是哪个文件。
顺带一提:ASAR
同样的流程(盘点、校验后提取、再交给专门工具)也适用于 Electron 的 app.asar。完整演示见 用 REA 分析 Electron 应用,其中包括 integrity policy 真正起作用的情况。
下一步
- 用 JADX 阅读类和方法:/zh/android-apk-analysis-rea/
- 把 headless JADX 接入 Claude Code 或 Codex:/zh/jadx-mcp-server-claude-code/
- 用 Ghidra 分析
libil2cpp.so等 JNI 库:/zh/rea-ghidra-native-binary/ - 对比两个应用版本:/zh/rea-evidence-snapshots-compare/
- 确认你装的 REA 是真的:/zh/verify-real-rea-install/
- 参考文档:android-analysis.md、apktool-resource-analysis.md、bundletool