rea-reverse

AAB to APK with bundletool, and How to Extract APK Assets

Written for REA 6.7.0Published by rea-reverseUpdated 5 min read
On this page

Short answer: an AAB can’t be installed, but bundletool turns it into one installable APK: java -jar bundletool.jar build-apks --bundle=app.aab --output=app.apks --mode=universal, then unzip app.apks universal.apk. An APK is a ZIP file, so unzip app.apk 'assets/*' -d out extracts its assets. You don’t even need bundletool to read an AAB’s assets: they sit under base/assets/ in the bundle.

All output below comes from a hello-world bundle I built for this guide (com.example.helloassets: two asset files, English, French and German strings, and a native library for arm64-v8a and x86_64), run with bundletool 1.18.3 from its GitHub releases on Temurin JDK 17.0.18, plus an Android 14 emulator for the install steps.

AAB to APK and APK Unpack: Extract Assets and Scripts with REASilent walkthrough with English captions.

What is inside an AAB

An AAB is a ZIP too, but laid out per module, with protobuf manifests and resources instead of the binary XML an APK uses:

$ 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 refuses it before it reaches the device:

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

Step 1: Get bundletool

Download bundletool-all-<version>.jar from the releases page (1.18.3 was the latest on 2026-11-02) and run it with Java (I used JDK 17):

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

The examples below call the JAR bundletool.jar.

Step 2: Convert the AAB to a universal 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'.

The output is an .apks file, an “APK set” that is itself a ZIP. In universal mode it holds a table of contents and one 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

The universal APK has everything in one file: both ABIs, all three languages and the 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

Things to know:

  • Signing. Without --ks, bundletool signs with ~/.android/debug.keystore, as the INFO line says. apksigner verify --print-certs out/universal.apk showed CN=Android Debug. That is fine for testing and analysis. To give a universal APK to users, sign it with your release key: add --ks=release.jks --ks-key-alias=release (passwords go in --ks-pass and --key-pass).
  • Existing output. Running the same command again failed with [BT:1.18.3] Error: File 'hello.apks' already exists. followed by a Java stack trace. Add --overwrite, or delete the file.
  • Feature modules. Only modules that declare <dist:fusing dist:include="true"/> go into the universal APK. If a feature is missing from it, check that flag before concluding the app doesn’t have the feature.
  • Re-signed bytes. A universal APK you build is not byte-identical to what Play delivers, which is signed with the app signing key. Don’t compare hashes across the two.

Step 3: Build the split APKs a device would get

Without --mode, bundletool builds the full split set Play would serve. For my bundle that was 16 files:

$ 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
...

That is one master APK plus one split per ABI and per language, each in three variants (no suffix, _2, _3). You don’t pick among them by hand: describe the device in JSON and let extract-apks choose.

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

For an SDK 34 device it took the _3 variants: master, the arm64 split and the French split.

java -jar bundletool.jar get-device-spec --output=emulator-spec.json writes that JSON for the connected device. For my emulator it reported "supportedAbis": ["arm64-v8a"], "supportedLocales": ["zh-CN"], density 420 and SDK 34. build-apks --connected-device builds only what that device needs, which here was base-master.apk and base-arm64_v8a.apk.

To install a split set, use install-apks (or adb install-multiple with every file). Installing one split on its own fails, as shown in package appears to be invalid.

$ 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

No language split was installed, because the emulator’s only locale was zh-CN and the bundle has no Chinese strings.

get-size total estimates the download size per configuration, in bytes:

$ 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

Step 4: Extract the APK’s assets

Assets are stored as ordinary files, so unzip with a pattern is enough:

$ 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"}

From the AAB, the same files come out without converting anything:

$ 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

The same works for res/raw/, lib/<abi>/*.so and classes*.dex. What unzip does not give you is readable XML. The manifest and resources.arsc are compiled:

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

Read them with the SDK tools instead:

$ 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=... without --xpath prints the whole manifest as XML. For a fully decoded res/ tree, run apktool d app.apk on a copy of the APK. To browse the manifest, signing certificate, native libraries and every entry without installing anything, drop the APK or AAB into this site’s APK & AAB analyzer, which runs in your browser.

macOS gotcha: names that differ only in case. Some release builds contain resource paths like res/2f.xml and res/2F.xml (the F-Droid 2.0.2 client does). The default macOS volume is case-insensitive, and with a test ZIP holding both names, unzip -o printed extracting for both but left only res/2F.xml on disk. Without -o it stopped at a replace ...? prompt. Extract on Linux or on a case-sensitive volume if you need every file.

What the assets tell you

File names are hints and headers are better evidence (xxd -l 16 file, file file). This is useful for checking how your own build, or an open-source app, is packaged:

  • React Native. The Gradle plugin packs the JavaScript bundle as assets/index.android.bundle by default (bundleAssetName in ReactExtension.kt). libhermes.so in lib/<abi>/ hints that Hermes is the engine. Hermes bytecode starts with the magic 0x1F1903C103BC1FC6 (MAGIC in Hermes’s BytecodeFileFormat.h), which xxd shows little-endian as c6 1f bc 03 c1 03 19 1f. A plain-JavaScript bundle can be worked on as in recovering JavaScript source from a bundle.
  • Flutter. lib/<abi>/libapp.so plus libflutter.so. See Flutter app reverse engineering.
  • Unity IL2CPP. libil2cpp.so together with global-metadata.dat.
  • Everything else. Images, fonts, audio, JSON and WebView HTML sit under assets/ and res/raw/ as ordinary files.

Only unpack apps you wrote, open-source apps, or apps you have permission to examine. The licenses of the extracted files still apply.

Optional: REA for an inventory with hashes

REA, a third-party open-source CLI, can inventory and extract an APK or AAB with a SHA-256 for every entry. In 6.7.0, rea inspect-artifact hello-assets.aab --json classified the bundle as a zip container and the two libhello.so files as ELF dynamic-library entries. There is no AAB format: its Android tools expect a standalone APK, so convert first. In an earlier run, inspect-android-package refused an .aab with “is zip; provide one standalone APK”, and decode-android-resources returned empty fields for one without failing. rea extract-artifact checks every written file against the inventory hash and refuses archives with case-colliding names on a case-insensitive volume instead of overwriting, which avoids the macOS problem above.