rea-reverse

AAB 转 APK(bundletool)与 APK 资源提取

基于 REA 6.7.0发布者 rea-reverse更新于 约 5 分钟读完
本页目录

结论先行:AAB 不能直接安装,但 bundletool 可以把它转成一个能装的 APK:java -jar bundletool.jar build-apks --bundle=app.aab --output=app.apks --mode=universal,再 unzip app.apks universal.apk。APK 本身就是 ZIP,unzip app.apk 'assets/*' -d out 就能取出 assets。只想看 AAB 里的 assets 的话,连 bundletool 都不用:它们就在包里的 base/assets/ 下面。

下面的输出全部来自我为本文构建的一个 hello-world 包(com.example.helloassets:两个 asset 文件,英、法、德三种语言的字符串,以及 arm64-v8a 和 x86_64 两种 ABI 的原生库)。bundletool 是从 GitHub Releases 下载的 1.18.3,运行在 Temurin JDK 17.0.18 上,安装步骤用的是 Android 14 模拟器。

AAB 里面有什么

AAB 也是 ZIP,但按模块组织,Manifest 和资源用的是 protobuf,而不是 APK 里的二进制 XML:

$ unzip -l hello-assets.aab
  Length      Date    Time    Name
---------  ---------- -----   ----
       10  01-01-2010 00:00   BundleConfig.pb
     5008  01-01-2010 00:00   base/lib/arm64-v8a/libhello.so
     4704  01-01-2010 00:00   base/lib/x86_64/libhello.so
     2616  01-01-2010 00:00   base/dex/classes.dex
       53  01-01-2010 00:00   base/assets/config.json
       64  01-01-2010 00:00   base/assets/www/index.html
     1433  01-01-2010 00:00   base/manifest/AndroidManifest.xml
       28  01-01-2010 00:00   base/assets.pb
       43  01-01-2010 00:00   base/native.pb
      390  01-01-2010 00:00   base/resources.pb
---------                     -------
    14349                     10 files

adb 根本不会把它发到设备上:

$ adb install hello-assets.aab
adb: filename doesn't end .apk or .apex: hello-assets.aab

第 1 步:准备 bundletool

从 Releases 页面 下载 bundletool-all-<版本>.jar(2026-11-02 时最新是 1.18.3),用 Java 运行(我用的是 JDK 17):

java -jar bundletool-all-1.18.3.jar version
1.18.3

下文把这个 JAR 简称为 bundletool.jar。

第 2 步:把 AAB 转成通用 APK

java -jar bundletool.jar build-apks --bundle=hello-assets.aab \
  --output=hello-universal.apks --mode=universal
INFO: The APKs will be signed with the debug keystore found at '/Users/longmon/.android/debug.keystore'.

输出的 .apks 是一个“APK 集”,本身也是 ZIP。universal 模式下里面只有一个目录文件和一个 APK:

$ unzip -l hello-universal.apks
  Length      Date    Time    Name
---------  ---------- -----   ----
       96  01-01-1981 01:01   toc.pb
    12827  01-01-1981 01:01   universal.apk
---------                     -------
    12923                     2 files
$ unzip -o hello-universal.apks universal.apk -d out
 extracting: out/universal.apk
$ adb install out/universal.apk
Performing Streamed Install
Success

通用 APK 把所有东西放进了一个文件:两种 ABI、三种语言和全部 assets。

$ unzip -l out/universal.apk
  Length      Date    Time    Name
---------  ---------- -----   ----
     2228  01-01-1981 01:01   AndroidManifest.xml
       53  01-01-1981 01:01   assets/config.json
       64  01-01-1981 01:01   assets/www/index.html
     2616  01-01-1981 01:01   classes.dex
     5008  01-01-1981 01:01   lib/arm64-v8a/libhello.so
     4704  01-01-1981 01:01   lib/x86_64/libhello.so
      556  01-01-1981 01:01   res/xml/splits0.xml
     1036  01-01-1981 01:01   resources.arsc
---------                     -------
    16265                     8 files

几个要注意的地方:

  • 签名。 不传 --ks 时,bundletool 会用 ~/.android/debug.keystore 签名,INFO 那一行说的就是这个。apksigner verify --print-certs out/universal.apk 显示的是 CN=Android Debug。测试和分析没问题;要把通用 APK 发给用户,就用发布密钥签名:加上 --ks=release.jks --ks-key-alias=release,密码通过 --ks-pass 和 --key-pass 传入。
  • 输出文件已存在。 同一条命令再跑一次,报错 [BT:1.18.3] Error: File 'hello.apks' already exists.,后面跟着一段 Java 堆栈。加 --overwrite 或先删掉旧文件。
  • 功能模块。 只有声明了 <dist:fusing dist:include="true"/> 的模块才会合进通用 APK。通用 APK 里少了某个功能,先查这个标记,别急着下结论说应用没有这个功能。
  • 字节已经不同。 你自己生成的通用 APK 和 Play 下发的 APK(用应用签名密钥签)不是同一组字节,两者的哈希没法比。

第 3 步:生成设备实际会拿到的拆分包

不加 --mode 时,bundletool 会生成 Play 那样的完整拆分包集合。我这个包一共 16 个文件:

$ java -jar bundletool.jar build-apks --bundle=hello-assets.aab --output=hello.apks
$ unzip -l hello.apks
     1057  01-01-1981 01:01   toc.pb
    28830  01-01-1981 01:01   splits/base-arm64_v8a.apk
    28830  01-01-1981 01:01   splits/base-arm64_v8a_2.apk
    28830  01-01-1981 01:01   splits/base-arm64_v8a_3.apk
     8339  01-01-1981 01:01   splits/base-de.apk
...
     8592  01-01-1981 01:01   splits/base-master.apk
    12688  01-01-1981 01:01   splits/base-master_2.apk
    12688  01-01-1981 01:01   splits/base-master_3.apk
    28827  01-01-1981 01:01   splits/base-x86_64.apk
...

也就是一个 master APK,加上每种 ABI、每种语言各一个拆分包,每个都有三个变体(无后缀、_2、_3)。不用自己挑:用 JSON 描述设备,交给 extract-apks 选。

cat device-spec.json
# {"supportedAbis":["arm64-v8a"],"supportedLocales":["fr-FR"],"screenDensity":420,"sdkVersion":34}
java -jar bundletool.jar extract-apks --apks=hello.apks \
  --output-dir=fr-arm64 --device-spec=device-spec.json
The APKs have been extracted in the directory: fr-arm64
$ ls fr-arm64
base-arm64_v8a_3.apk  base-fr_3.apk  base-master_3.apk

对 SDK 34 的设备,它选的是 _3 这一组:master、arm64 拆分包和法语拆分包。

java -jar bundletool.jar get-device-spec --output=emulator-spec.json 会为当前连接的设备生成这份 JSON。我的模拟器报告的是 "supportedAbis": ["arm64-v8a"]、"supportedLocales": ["zh-CN"]、密度 420、SDK 34。build-apks --connected-device 只生成这台设备需要的文件,这里是 base-master.apk 和 base-arm64_v8a.apk。

安装拆分包集合用 install-apks(或者用 adb install-multiple 把所有文件一起装)。单独装其中一个拆分包会失败,见 安装包似乎无效。

$ java -jar bundletool.jar install-apks --apks=hello.apks
$ adb shell pm path com.example.helloassets
package:/data/app/~~9-EMT_sEmxxD3fGvBiMVvw==/com.example.helloassets-2074h57ADobth13GxZbeZg==/base.apk
package:/data/app/~~9-EMT_sEmxxD3fGvBiMVvw==/com.example.helloassets-2074h57ADobth13GxZbeZg==/split_config.arm64_v8a.apk

没有装任何语言拆分包,因为模拟器只有 zh-CN 这一种语言,而这个包里没有中文字符串。

get-size total 按配置估算下载大小(单位是字节):

$ java -jar bundletool.jar get-size total --apks=hello.apks --dimensions=ABI,LANGUAGE
ABI,LANGUAGE,MIN,MAX
arm64-v8a,de,10685,10717
x86_64,de,10600,10629
x86_64,fr,10595,10626
arm64-v8a,fr,10680,10714

第 4 步:提取 APK 里的 assets

assets 是原样存放的普通文件,unzip 加个通配就行:

$ unzip -o out/universal.apk 'assets/*' -d universal-assets
  inflating: universal-assets/assets/config.json
  inflating: universal-assets/assets/www/index.html
$ cat universal-assets/assets/config.json
{"apiBase":"https://example.com/api","theme":"dark"}

直接从 AAB 里取,同样的文件不用任何转换就能拿到:

$ unzip -o hello-assets.aab 'base/assets/*' -d aab-assets
  inflating: aab-assets/base/assets/config.json
  inflating: aab-assets/base/assets/www/index.html

res/raw/、lib/<abi>/*.so、classes*.dex 也一样。unzip 给不了的是可读的 XML:Manifest 和 resources.arsc 都是编译过的。

$ unzip -p out/universal.apk AndroidManifest.xml | file -
/dev/stdin: Android binary XML

改用 SDK 工具读:

$ aapt2 dump badging out/universal.apk
package: name='com.example.helloassets' versionCode='3' versionName='1.2' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
minSdkVersion:'24'
targetSdkVersion:'35'
application-label:'Hello Assets'
...
launchable-activity: name='com.example.helloassets.MainActivity'  label='' icon=''
$ java -jar bundletool.jar dump manifest --bundle=hello-assets.aab --xpath=/manifest/@android:versionCode
3

bundletool dump manifest --bundle=... 不加 --xpath 会以 XML 打印完整 Manifest。要完整解码的 res/ 目录,就对 APK 的副本执行 apktool d app.apk。不想装任何东西、只想看 Manifest、签名证书、原生库和全部条目,可以把 APK 或 AAB 拖进本站的 APK/AAB 分析与上架体检,它在浏览器里运行。

macOS 的坑:只有大小写不同的文件名。 有些 release 包里有 res/2f.xml 和 res/2F.xml 这样的资源路径(F-Droid 2.0.2 客户端就有)。macOS 默认卷不区分大小写。我用一个同时包含这两个名字的测试 ZIP 试了一下:unzip -o 两个都显示 extracting,磁盘上却只剩 res/2F.xml;不加 -o 则停在 replace ...? 提示。需要完整文件时,请在 Linux 或区分大小写的卷上解压。

从 assets 能看出什么

文件名只是线索,文件头才是更可靠的证据(xxd -l 16 文件、file 文件)。用来确认自己的构建或开源应用是怎么打包的:

  • React Native。 Gradle 插件默认把 JavaScript bundle 打成 assets/index.android.bundle(见 ReactExtension.kt 里的 bundleAssetName)。lib/<abi>/ 里有 libhermes.so 说明引擎很可能是 Hermes。Hermes 字节码的魔数是 0x1F1903C103BC1FC6(Hermes 源码 BytecodeFileFormat.h 里的 MAGIC),按小端存放,xxd 显示为 c6 1f bc 03 c1 03 19 1f。纯 JavaScript 的 bundle 可以按 从 bundle 还原 JavaScript 源码 处理。
  • Flutter。 lib/<abi>/libapp.so 加 libflutter.so,见 Flutter 应用逆向。
  • Unity IL2CPP。 libil2cpp.so 加 global-metadata.dat。
  • 其他。 图片、字体、音频、JSON 配置和 WebView 用的 HTML,都以普通文件的形式放在 assets/ 和 res/raw/ 下。

只解包你自己写的应用、开源应用,或者你获得授权检查的应用。提取出来的文件仍然受原许可证约束。

可选:用 REA 做带哈希的清单

第三方开源命令行工具 REA 可以盘点并提取 APK 或 AAB,给每个条目算 SHA-256。在 6.7.0 里,rea inspect-artifact hello-assets.aab --json 把这个包识别为 zip 容器,两个 libhello.so 识别为 ELF dynamic-library。它没有 AAB 格式,Android 相关工具都要求独立的 APK,所以要先转换。之前的一次测试里,inspect-android-package 对 .aab 直接拒绝(“is zip; provide one standalone APK”),decode-android-resources 则不报错、但返回的字段全是空的。rea extract-artifact 会用清单里的哈希校验每个写出的文件;在不区分大小写的卷上遇到大小写冲突的文件名时,它会拒绝提取而不是覆盖,正好避开了上面 macOS 的问题。