rea-reverse

AAB to APK and APK Unpack: Extract Assets and Scripts with REA

Written for REA 6.4.0Updated 11 min read
On this page

An APK is a ZIP file, so unzip will open it. What unzip will not tell you is what each entry is, whether the bytes you wrote to disk match the archive, or where the app’s real logic lives: DEX, a JavaScript bundle, a Unity metadata file or a Lua script. REA, the open-source project by morluto, adds that layer: a content-addressed inventory, a safe extractor that verifies every byte it writes, and an Apktool wrapper that decodes resources.

This guide is about unpacking and taking stock, not decompiling Java. If you want to read classes and methods, go to Android APK Analysis with REA or the JADX MCP setup guide after you finish here.

Who this is for: developers and researchers who have an .apk or .aab and want to know what is inside before choosing a deeper tool.

What you will learn:

  • Convert an Android App Bundle (AAB) to a universal APK with bundletool
  • Inventory an APK with inspect-artifact, and what that inventory cannot see
  • Extract every file with extract-artifact, and why it refuses some archives
  • Decode the manifest and strings with decode-android-resources
  • Recognize React Native, Hermes, Lua and Unity IL2CPP payloads by file name and header, and know where REA 6.4.0 stops

Targets used: the F-Droid client APK (GPL-3.0, source at gitlab.com/fdroid/fdroidclient) and an .aab you build yourself from an open-source project. Do not use these steps to pull paid content out of commercial apps.

Authorization first. Only unpack apps you wrote, open-source apps, or apps you have explicit permission to examine. Licenses and terms of service still apply to the extracted files. Read REA’s DISCLAIMER.md before you start.

Prerequisites

  1. REA 6.4.0 or newer. npm install --global rea-agents, or prefix commands with npx rea-agents@latest. If rea --version prints something unexpected, check you are running the real package with How to verify a real REA install.
  2. bundletool from its GitHub releases, plus Java to run the JAR. Only needed for AAB input.
  3. Apktool 2.7.0 or newer for resource decoding. REA’s own doc was verified with 2.7.0 (apktool-resource-analysis.md).
  4. Optional: xxd or file to look at file headers, and apksigner from the Android SDK build-tools if you care about signatures.

REA installs none of the external tools. The artifact inventory and extraction commands are built in and need no extra provider.

APK vs AAB: what REA actually sees

Both formats start with the ZIP signature PK\x03\x04, but they are different things:

  • An APK is an installable package: AndroidManifest.xml in binary XML, one or more classes*.dex, resources.arsc, res/, assets/, lib/<abi>/*.so and META-INF/.
  • An AAB is a publishing format. Google Play generates device-specific APKs from it. Its layout is per module (base/manifest/, base/dex/, base/res/ and so on), and you cannot install it directly.

REA 6.4.0 decides the package format by path suffix after checking the ZIP magic. In dist/domain/zipPackageFormat.js, a .apk name maps to the format apk, and the code carries the comment “An Android App Bundle is a ZIP archive, not a standalone APK.” There is no aab format: an .aab with valid ZIP magic is classified as plain zip.

That has two practical consequences:

  • inspect-artifact and extract-artifact work on an .aab. You get a ZIP inventory of the bundle layout.
  • The Android tools (decode-android-resources, inspect-android-package and the JADX family) expect an APK. REA’s android-analysis.md states that these operations do not provide split APK/AAB handling. Convert the AAB first.

Step 1: Convert an AAB to an APK with bundletool

Skip this step if you already have an APK.

Build the bundle from source you control, for example with ./gradlew bundleRelease in an open-source Android project, then ask bundletool for one universal APK:

java -jar bundletool.jar build-apks \
  --bundle=/absolute/path/app-release.aab \
  --output=/absolute/path/app-universal.apks \
  --mode=universal

Why --mode=universal: by default bundletool generates the full set of split APKs Play would serve. The official bundletool documentation describes universal mode as a single APK that “includes all of your app’s code and resources” for every device configuration your app supports. One file is much easier to inventory than a split set.

Three details from the docs and the bundletool source:

  • Signing. If you pass no --ks, bundletool “attempts to sign your APKs with a debug key.” In the source (DebugKeystoreUtils.java) it looks for .android/debug.keystore under ANDROID_SDK_HOME, the user home directory or HOME, with alias AndroidDebugKey. A machine with Android Studio usually has one. A debug-signed APK is fine for analysis but is not the developer’s release signature.
  • Feature modules. Only modules that declare <dist:fusing dist:include="true"/> in their manifest go into the universal APK. Anything else is missing from it, so do not conclude a feature is absent from the app.
  • Existing output. bundletool fails if the --output file exists, unless you add --overwrite.

The output is an .apks file, which is itself a ZIP archive (an “APK set”). In universal mode bundletool names the APK universal.apk at the archive root (ApkPathManager.java in the bundletool source). Take it out with plain unzip:

mkdir /absolute/path/apks-out
unzip /absolute/path/app-universal.apks universal.apk -d /absolute/path/apks-out

You could also run REA on the .apks: it is classified as zip, and extract-artifact writes universal.apk out as one file. REA does not open a nested APK inside the set; the inventory notes that the .apk suffix “suggests APK, but these bytes were not read”. Either way, continue with the extracted universal.apk.

Step 2: Inventory the APK with inspect-artifact

Download the F-Droid client from f-droid.org (or use your universal.apk), record its digest, then inspect it:

shasum -a 256 /absolute/path/F-Droid.apk

set -o pipefail
rea inspect-artifact /absolute/path/F-Droid.apk --json | jq . > fdroid-inventory.json

Why inventory before extracting: the result is the complete content-addressed artifact graph in one call: every central-directory entry with its logical path, entry kind, declared and compressed size, encryption flag, SHA-256, plus integrity observations, limitations and suggested next probes. You can search it with jq and decide what to pull out before writing anything to disk. pipefail keeps REA’s exit status if jq follows it.

What to look for first:

  • classes*.dex count, which tells you whether the app is multidex
  • lib/<abi>/*.so, which REA classifies as dynamic-library
  • everything under assets/, where scripts and engine data usually live
  • res/ and resources.arsc, which hold compiled resources (decoded in Step 4)

--integrity-policy takes fail (default) or record-and-continue. It matters most for formats that declare their own hashes, such as ASAR; leave it at the default for APKs unless REA reports a contradiction you want to keep as untrusted data.

What the inventory cannot see. REA records this limitation verbatim (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.” Strictly, the APK Signing Block sits just before the central directory, but the point stands: v2+ signatures are not ZIP entries, so you will not see them in the list, and inspect-artifact does not validate them. If signatures matter, run apksigner verify --print-certs /absolute/path/F-Droid.apk. Only v1 (JAR) signature files under META-INF/ appear as normal entries.

Over MCP: open the APK as the active target (open_binary), then ask your agent to call inspect_artifact. In 6.4.0 its only parameter is integrity_policy. The detail: "summary" option described in the repository’s mcp-contracts.md is on main, not in 6.4.0.

Step 3: Extract APK assets with extract-artifact

rea extract-artifact /absolute/path/F-Droid.apk --json > fdroid-extract.json

There is no output-directory option. REA creates a fresh directory named rea-extracted-<uuid> under the system temp directory, and the result reports it as output_root. Each extracted file is listed with its relative_path, sha256 and bytes_written.

Why use this instead of unzip: the extractor in dist/artifacts/extraction/ArtifactExtraction.js and SafeOutputTree.js is built for untrusted archives:

  • Entry paths must be relative and normalized. Absolute paths and .. segments are rejected, so a crafted entry cannot write outside the output root (the classic “zip slip”).
  • Symlink entries are not followed, and the output root is created with mode 0700.
  • Every file is created exclusively (O_EXCL). Nothing is ever overwritten, and an existing output root is an error.
  • Bytes written are checked against the inventory’s size and SHA-256. A mismatch fails the extraction.
  • Names that differ only in case are detected when the destination filesystem cannot store both, as on default macOS volumes.
  • Encrypted entries stop the whole extraction. REA never decrypts archive entries, so it reports the archive as unsupported rather than producing a partial tree.

Nested archives are written as files, not expanded. That is deliberate: if assets/ contains a ZIP, run inspect-artifact on the extracted copy as a separate step, so each layer keeps its own digest.

Copy what you need out of the temp directory. It is in $TMPDIR, which your OS may clean up.

Step 4: Decode the manifest and strings with Apktool

AndroidManifest.xml and resources.arsc are binary formats. Raw extraction gives you the bytes, not readable XML. Apktool decodes them:

export REA_APKTOOL_COMMAND=/absolute/path/apktool   # optional; otherwise 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

What REA does with Apktool (from dist/apktool/ApktoolProvider.js):

  • It copies the APK into a private temporary workspace, hashes it during the copy and refuses the input if the file changes meanwhile.
  • It runs apktool d --no-src -f -o <workspace> --frame-path <private dir> <copy>. --no-src means no smali is produced; code belongs to the JADX tools. The private framework path keeps Apktool from touching your global framework directory.
  • It returns apktool.yml metadata (version name and code, min and target SDK), the decoded manifest, a locales list and one strings table, then deletes the workspace. You do not get a decoded res/ tree on disk.

The bounds are fixed in 6.4.0: decoding times out after 600 seconds, the manifest is read up to 512 KiB, strings.xml up to 8 MiB, at most 2,000 string entries are returned, and the decoded tree enumeration stops after 100,000 entries. Each cap that is hit appears in limitations, so a truncated answer is never silent.

REA_APKTOOL_COMMAND must be an absolute path. A relative value is rejected as not_absolute. inspect-apktool-client only runs apktool --version, so run it first and a missing launcher fails in seconds instead of mid-decode.

If you need the full decoded res/ tree, run Apktool yourself on a copy of the APK. REA’s job here is a reproducible projection tied to the APK’s SHA-256.

Step 5: Find the scripts and engine data

Many apps keep their real logic outside DEX. Use the inventory from Step 2 to find it, then confirm with the file header. File names are hints; headers are better evidence.

xxd -l 16 /path/to/rea-extracted-xxxx/assets/some-file
file /path/to/rea-extracted-xxxx/assets/some-file

React Native and Hermes

React Native’s Gradle plugin packs the JavaScript bundle as an asset named index.android.bundle by default (bundleAssetName in ReactExtension.kt). The presence of libhermes.so or libhermes_executor.so in lib/<abi>/ is a hint that Hermes is the engine.

  • If the bundle is plain JavaScript, continue with recovering JavaScript source from a bundle.
  • If it is Hermes bytecode, the header magic is the 64-bit constant 0x1F1903C103BC1FC6 (MAGIC in Hermes’s BytecodeFileFormat.h). Stored little-endian, xxd shows it as c6 1f bc 03 c1 03 19 1f. REA 6.4.0 has no Hermes bytecode support: it can inventory and extract the file and hash it, nothing more.

Lua scripts

Games and some apps embed Lua under assets/. Precompiled chunks start with an escape byte:

  • Standard Lua bytecode starts with \x1bLua (LUA_SIGNATURE in Lua’s lua.h), hex 1b 4c 75 61.
  • LuaJIT bytecode starts with \x1bLJ (BCDUMP_HEAD1..3 = 0x1b 0x4c 0x4a in LuaJIT’s lj_bcdump.h).

A .lua file without those bytes is probably plain source and readable as text. REA 6.4.0 does not decode Lua or LuaJIT bytecode.

Unity IL2CPP

A Unity IL2CPP build ships native code as lib/<abi>/libil2cpp.so (next to libunity.so) and a metadata file named global-metadata.dat under assets/. Unmodified metadata begins with the 32-bit value 0xFAB11BAF; the widely used open-source Il2CppDumper checks exactly this “sanity” value before parsing. Little-endian, that is af 1b b1 fa in xxd.

REA 6.4.0 does not decode IL2CPP metadata (managed-code-analysis.md says so for the managed tools). What you can do is treat libil2cpp.so as an ordinary native library and analyze it with Ghidra through REA (/en/rea-ghidra-native-binary/). Without metadata, expect stripped, unnamed functions.

Flutter

lib/<abi>/libapp.so and libflutter.so mean Flutter. The Dart snapshot commands (identify-flutter-build, inspect-dart-aot) are on repository main only, not in npm 6.4.0. The Flutter section of Android APK Analysis with REA explains the workaround.

Other assets

Images, fonts, audio, JSON configuration and HTML for WebViews sit under assets/ and res/raw/ as ordinary files. After Step 3 they are already on disk with verified hashes.

Troubleshooting

decode-android-resources fails on an .aab. Expected: Apktool decodes APKs. Run Step 1 first.

extract-artifact says an entry is encrypted. REA never decrypts ZIP entries, so it refuses the whole archive. This is a format boundary, not a bug. Do not try to work around encryption in apps you do not own.

“Extraction output root already exists”. Extremely unlikely with the generated UUID name; if it happens, rerun. REA never writes into an existing directory.

Apktool reports capability_unavailable. The launcher was not found or did not report a version. Set REA_APKTOOL_COMMAND to the launcher’s absolute path and rerun rea inspect-apktool-client.

Strings are cut off at 2,000. Use --locale to read one locale at a time, or run Apktool directly for the full table.

The universal APK lacks a feature. The module was not marked for fusing. Build that module’s APKs from the .apks without --mode=universal, or inspect the .aab itself with inspect-artifact.

Two runs report different digests for the “same” app. The F-Droid build and the developer’s own build are signed by different keys, and a bundletool universal APK is re-signed with your debug key. Different bytes, different hashes. Pin the file you analyzed.

A quick note on ASAR

The same pattern (inventory, verified extraction, then a specialized tool) applies to Electron’s app.asar. See Reverse Engineer an Electron App with REA for that walkthrough, including the integrity-policy case where it really matters.

Next steps