rea-reverse

Android APK Analysis with REA: JADX, Apktool, ADB and Flutter

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

REA, the open-source project by morluto, gives your terminal or coding agent a set of Android tools that return structured, artifact-bound Evidence instead of a folder of loose decompiler output. This guide walks through a complete static investigation of an open-source APK, then shows how to acquire an installed package from a device you control and how to triage a Flutter app.

Who this is for: developers and security researchers who want answers like “where is this value computed?” or “which Dart SDK built this app?” without clicking through a GUI decompiler.

What you will learn:

  • Inspect an APK’s manifest, permissions and class inventory with JADX through REA
  • Search classes, read decompiled methods and trace callers
  • Decode resources and localized strings with Apktool
  • List devices and pull an installed split APK set over ADB
  • Identify a Flutter build and its Dart AOT snapshot

Targets used: Aegis Authenticator (GPL-3.0, also distributed on F-Droid) and the open-source (BSD-3-Clause) Flutter Gallery app, whose v2.9.2 release page provides flutter_gallery_Android.apk. Both are open source, so you can compare every finding with the original code.

Android APK Analysis with REA: JADX, Apktool, ADB and FlutterSilent walkthrough with English captions.

Prerequisites

  1. REA 6.7.0: npm install --global [email protected], or prefix commands with npx rea-agents@latest. Connect it to your agent with npx rea-agents setup (/getting-started-rea-mcp/).
  2. A full JDK 17 or newer, including the jdk.compiler module. A JRE is not enough: REA compiles a small metadata bridge at startup. Check with java --list-modules | grep jdk.compiler.
  3. jadx-headless-mcp 0.7.1 (the -all.jar from its GitHub release). REA does not install it.
  4. Apktool 2.7.0 or newer for resource decoding.
  5. Android platform-tools adb if you want the device section, plus an emulator or a phone you own.

REA installs none of these. The Android provider is verified on macOS and Linux; on Windows the REA Aegis showcase recommends WSL.

We re-ran every command in this guide on macOS with REA 6.7.0 from npm, Temurin JDK 17.0.18, jadx-headless-mcp 0.7.1 (digest 6e5eacf5…ff75), Apktool 2.12.1 and adb 37.0.1, plus an API 36 emulator for Step 5.

Step 1: Get and pin the APK

Download Aegis v3.4.3 from its official GitHub release and check its digest:

shasum -a 256 aegis-v3.4.3.apk

The REA showcase recorded fa091c91912c0d56ec3553394fac4158f9a9d178e93bcbb35419601d788cb528 for this file (6,881,897 bytes).

Why pin the digest: every REA result embeds the APK’s SHA-256. If your hash matches, your results are comparable to the published case. The F-Droid build of Aegis is built and signed by F-Droid, so its bytes and digest differ; it is equally valid to analyze, just not byte-identical to the showcase.

Step 2: Point REA at JADX and inspect the package

export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
# Optional: choose a specific JDK instead of java on PATH
export JAVA_HOME=/absolute/path/existing-jdk

rea inspect-android-package /absolute/path/aegis-v3.4.3.apk --json

Why: package inspection is the map. It returns the decoded manifest XML, package and SDK declarations, permissions, class and resource counts, the engine identity, and the APK and engine JAR digests. For Aegis the showcase reports package com.beemdevelopment.aegis, version 3.4.3 (version code 82) and 3,077 classes. Our 6.7.0 run returned the same values in 3.8 seconds, trimmed from normalized_result:

"package_name": "com.beemdevelopment.aegis",
"version_name": "3.4.3",
"version_code": "82",
"min_sdk": "23",
"target_sdk": "36",
"class_count": 3077,
"resource_count": 1106,
"signature_verification": "not_performed"

Note that version_code and the SDK levels are strings, not numbers.

Two things this step does not do: verify the APK signature (reported as not_performed) or execute the app.

Over MCP, add REA_JADX_MCP_JAR to the REA server’s environment and reconnect, then call inspect_android_package with {"path": "/absolute/path/aegis-v3.4.3.apk"}. Since 6.5.0 the tool also takes detail: "summary", which keeps the declarations and counts but drops the decoded manifest text. For Aegis that cut the response from 26,991 to 4,634 characters. The summary carries a parent_evidence_id; pass it to inspect_analysis_view with {"source": {"kind": "retained-evidence", "evidence_id": "<parent_evidence_id>"}, "view": {"kind": "android", "facet": "manifest", "offset": 0, "limit": 4000}} to read the manifest in pages (offsets count UTF-16 code units; the Aegis manifest is 10,988). The ID only works in the MCP session that produced it: when we reused one from an earlier session, REA answered evidence_integrity_mismatch with “is not retained in this session”.

Step 3: Search, read and trace code

The showcase question was “how is the six-digit login code computed?”. The same pattern works for any feature.

Search class names:

rea search-android-classes /absolute/path/aegis-v3.4.3.apk Totp

The showcase got three matches for the case-sensitive query Totp: otp.TotpInfo, ui.views.TotpProgressBar and importers.TotpAuthenticatorImporter (package prefix shortened). Only the first looks like a calculation.

List the class’s members and overloads:

rea inspect-android-class /absolute/path/aegis-v3.4.3.apk com.beemdevelopment.aegis.otp.TotpInfo

This returns fields, methods and overload indices without generating source. For TotpInfo, getOtp() is overload 0 and getOtp(long) is overload 1.

Read one method:

rea inspect-android-method /absolute/path/aegis-v3.4.3.apk \
  com.beemdevelopment.aegis.otp.TotpInfo getOtp --overload-index 1

Why pass --overload-index: REA refuses an ambiguous method request instead of silently picking the first match. Overload indices belong to this artifact and engine; do not reuse them across builds.

Find callers:

rea trace-android-references /absolute/path/aegis-v3.4.3.apk \
  com.beemdevelopment.aegis.otp.OtpInfo --method-name getOtp

The showcase found ui.views.EntryHolder.getOtp and ui.MainActivity.copyEntryCode as callers: one displays the code, one copies it. 6.7.0 returned the same two, and each reference now carries a direct_invokes list (opcode such as INVOKE_VIRTUAL, DEX method index, code-unit offset) that 6.4.0 did not return.

OtpInfo.getOtp has a single overload, so no index is needed. TotpInfo.getOtp has two, and since 6.5.0 trace-android-references accepts --overload-index too. Without it, REA refuses with Select one of 2 overloads: 0: getOtp(): java.lang.String; 1: getOtp(long): java.lang.String.

How to read these results:

  • Decompiled Java is a derived representation. A complete text result is not proof of complete semantics.
  • References are static incoming relationships, not observed runtime calls. Reflection and dynamic loading stay unverified.
  • Static initializers (<clinit>) come back as smali, because Java decompilation can scatter their instructions into field declarations.
  • Release builds are often shrunk and renamed. In Aegis, the OTP generator surfaced under kotlin.ExceptionsKt, so follow calls from a known entry point rather than trusting class names.

Step 4: Decode resources with Apktool

JADX answers code questions; resource tables answer “what does the UI say?” and “which locales and versions ship?”.

export REA_APKTOOL_COMMAND=/absolute/path/apktool   # optional; otherwise 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
rea decode-android-resources /absolute/path/aegis-v3.4.3.apk --locales de --locales pt-BR

Why each one:

  • inspect-apktool-client runs apktool --version and reports which launcher was selected. Run it first so a missing tool fails fast.
  • The default decode returns apktool.yml metadata as Apktool printed it, the decoded manifest (bounded at 512 KiB) and the default strings table (up to 2000 entries), plus a locales list.
  • --locale de projects res/values-de/strings.xml instead. Regional locales such as pt-BR map to Android’s values-pt-rBR. Check the locales list before choosing: in Aegis, values-de holds only 86 AndroidX strings (“Nach oben”, “Fertig”), while the app’s own German text is in values-de-rDE. --locale de-DE returned 472 entries.
  • --no-strings skips string projection when you only need metadata and the manifest. The 6.7.0 help lists the flag as --strings (default true), but --no-strings still works: our run returned an empty strings array.
  • --locales (new in 6.7.0, repeatable, up to 20) adds a string_tables array next to the default table. For Aegis it held three tables: default 615 entries, de 86, pt-BR 557.

REA runs Apktool with --no-src, so no smali is produced here; code belongs to the JADX tools. Strings are projected verbatim: escaped entities stay escaped, and entries with nested markup are counted as unparsed rather than flattened.

Step 5: Acquire an installed app with ADB

Sometimes the APK you need is the one installed on a device: for example, the F-Droid build of Aegis on your own test phone, including any split APKs.

Authorization first. Only connect devices you own or are explicitly authorized to examine, and only pull apps and data you have the right to analyze. Several ADB operations change device state. Read REA’s DISCLAIMER.md before you start.

export REA_ADB_PATH=/absolute/path/to/adb   # optional; otherwise PATH
rea inspect-adb-client
rea list-adb-devices
rea inspect-adb-device emulator-5554

inspect-adb-client only probes the binary and never contacts the adb server. list-adb-devices returns each serial with its state, transport and an inferred emulator/physical kind together with the basis for that inference. Use the serial exactly as listed; emulator-5554 is just the common default emulator name.

Find and pull the package:

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

On our emulator (serial emulator-5560, Android 16) we installed the GitHub APK with install-adb-package, and the pulled base.apk had the same SHA-256 as the download, fa091c91…b528.

Why this order: resolve-adb-packages does a case-insensitive substring search so you get the exact package name; inspect-adb-package shows versions, installer and flags so you know which build you are about to pull; pull-adb-package collects the complete split set from pm path and digests every file. --output-directory may already exist, but the package-named subdirectory REA creates inside it (here /tmp/acq/com.beemdevelopment.aegis) must not, so a completed pull is never overwritten.

Breaking change in 6.7.0 (made in 6.6.0, which never reached npm): inspect-adb-package (MCP inspect_adb_package) now returns version_code as a decimal string, for example "version_code": "82", matching the schema pattern ^-?\d+$. Scripts that compared it as a number need updating. The same output reported installer_package_name as the string "null" for an app installed over adb. That is dumpsys text, not a JSON null.

Read-only vs. device-mutating. Commands such as list-adb-processes, read-adb-logcat, capture-adb-screen and list-adb-directory only read. These change the device and are never run implicitly:

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 removes the package and its data for user 0. Prefer an emulator for anything in this group.

REA builds every adb call from a fixed argument vector; no tool accepts an arbitrary shell command, and UI input (input, monkey) is deliberately not exposed.

Step 6: Triage a Flutter app

Flutter apps keep most logic in Dart, compiled ahead of time into lib/<abi>/libapp.so. JADX shows you little more than the Android embedding. REA’s Flutter tools parse the APK directly, with no engine, no execution and no network lookups.

These two commands shipped in npm 6.5.0, so they are in 6.7.0. For a full walkthrough on an APK you build yourself, see Flutter App Reverse Engineering with REA.

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

What you get:

  • flutter_detected, per-ABI libflutter.so digests, GNU build IDs and any labeled version strings
  • the Dart snapshot hash, read 20 bytes after the f5 f5 dc dc snapshot magic in libapp.so
  • from inspect-dart-aot: the four _kDart*Snapshot* sections and a string-pool projection listing package: URIs (a dependency inventory) and dart: SDK libraries

inspect-dart-aot defaults to arm64-v8a, then x86_64, x86 and armeabi-v7a. REA’s own verification record for the Flutter Gallery 2.9.2 release APK reports the agreeing snapshot hash 65817c30a78bb44c3dc3771876b6010a on every ABI, and no labeled Dart version, which is expected for a release engine. Our 6.7.0 run on the same 117.8 MB APK reproduced that hash for arm64-v8a, armeabi-v7a and x86_64 in about a second.

Why the snapshot hash matters: it is the key that maps to a Dart SDK release through an external dataset such as the one reFlutter maintains. REA deliberately performs no hash-to-SDK lookup. Once you know the SDK version, you can pick a snapshot parser for that layout, or continue with native analysis of libapp.so (/rea-ghidra-native-binary/).

The string pool is a byte-pattern projection, not deserialized classes. Treat class_like_tokens as hints that include incidental matches. The URI list is also capped: for Flutter Gallery, inspect-dart-aot reported at least 756 package: URIs but returned 500, so packages that sort late in the alphabet are missing. The Flutter guide shows what falls off.

Troubleshooting

Java starts but the bridge fails. You are probably on a JRE. Point JAVA_HOME at a full JDK and confirm jdk.compiler appears in java --list-modules.

“Ambiguous method” error. Run inspect-android-class to list overloads, then add --overload-index. trace-android-references takes the same --overload-index (6.5.0 and later).

Large APK times out or runs out of heap. Operations have a 120-second deadline and decompilation 90 seconds (jadx-headless-mcp reports decompile_timeout_ms: 90000). Raise the heap with export REA_JADX_HEAP_MIB=8192 or JAVA_TOOL_OPTIONS=-Xmx8g. In 6.7.0 you can also raise the operation deadline with REA_JADX_OPERATION_TIMEOUT_MS, a plain millisecond count: 300000 works, 5m is rejected.

You have an .aab. An Android App Bundle is not an APK. The JADX tools refuse it with “is zip; provide one standalone APK”. decode-android-resources does not refuse it: on our test bundle it exited 0 with every metadata field null and coverage: "complete", so don’t take an empty result there as an answer. Work from the APK the project publishes, or install the app on a test device through its normal channel and pull the installed split set with pull-adb-package.

Apktool reports capability_unavailable. Set REA_APKTOOL_COMMAND to the launcher’s absolute path and rerun rea inspect-apktool-client.

pull-adb-package refuses to run. The top-level 6.7.0 error is a generic unreadable_output (“Run rea doctor…”), but details.reason says what happened: the package subdirectory inside --output-directory already exists from an earlier pull. Choose a new output directory; REA does not overwrite earlier evidence.

No snapshot hash. If flutter_detected is false, coverage is complete and the app is not Flutter. If it is true but the hash is null, check snapshot_hash_candidates: disagreeing records are kept rather than resolved by guessing.

Next steps