Browse by type

flutterdec is a static Flutter AOT decompiler research tool for Android ARM64 binaries.
It takes an APK or libapp.so and emits readable pseudo-Dart plus optional IR, asm, diff, startup, and symbol-reporting artifacts.
flutterdec is meant for people reversing Flutter apps who want a practical first pass that is more readable than raw disassembly, while still being easy to verify against lower-level artifacts.
If you are new to the project, the fastest way to think about it is:
info tells you what the target looks likedecompile gives you pseudocode plus reportsdiff compares two buildsmap-symbols improves naming when you have matched engine binarieslibapp.soIf you just want to try the tool, start with one of these paths:
nix runnix profile installRun directly from GitHub:
nix run github:caverav/flutterdec -- --help
nix run github:caverav/flutterdec -- info ./sample.apk --json
nix run github:caverav/flutterdec -- decompile ./sample.apk -o ./out
Run from a local checkout:
nix run . -- --help
nix run . -- info ./sample.apk --json
nix run . -- decompile ./sample.apk -o ./out
Install from GitHub:
nix profile install github:caverav/flutterdec
flutterdec --help
Install from a local checkout:
nix profile install .
flutterdec --help
Update later:
nix profile upgrade flutterdec
Current prerelease: v0.1.0-alpha.4
Linux x64:
curl -fLO https://github.com/caverav/flutterdec/releases/download/v0.1.0-alpha.4/flutterdec-v0.1.0-alpha.4-Linux-X64.tar.gz
tar -xzf flutterdec-v0.1.0-alpha.4-Linux-X64.tar.gz
sudo install -m 0755 flutterdec /usr/local/bin/flutterdec
flutterdec --help
macOS arm64:
curl -fLO https://github.com/caverav/flutterdec/releases/download/v0.1.0-alpha.4/flutterdec-v0.1.0-alpha.4-macOS-ARM64.tar.gz
tar -xzf flutterdec-v0.1.0-alpha.4-macOS-ARM64.tar.gz
sudo install -m 0755 flutterdec /usr/local/bin/flutterdec
flutterdec --help
Other platforms and future tags:
Install into the user Cargo bin:
nix develop -c cargo install --path crates/flutterdec-cli
~/.cargo/bin/flutterdec --help
Run from source without installing:
nix develop -c cargo run -p flutterdec-cli -- --help
nix develop -c cargo run -p flutterdec-cli -- info ./sample.apk --json
nix develop -c cargo run -p flutterdec-cli -- decompile ./sample.apk -o ./out
Build a local release binary:
nix develop -c cargo build -p flutterdec-cli --release
./target/release/flutterdec --help
If this is your first run, this is the shortest useful path.
flutterdec info ./sample.apk --json
info resolves the Dart SDK version straight from the snapshot hash, with no adapter
installed and no disassembly:
dart_version (for example 3.9.2)dart_tag_style (CID_INT32, CID_SHIFT1, or OBJECT_HEADER)Both are null for snapshot hashes not in the bundled table (data/dart-profiles.json).
For APK inputs, info reports Android startup summary fields such as:
android_startup_presentandroid_startup_confidenceandroid_startup_entrypoint_countandroid_startup_flutter_activity_countIf adapter metadata is available, info also reports package and compatibility signals such as:
app_package_counts_topadapter_kindadapter_snapshot_hash_matchcompatibility_warnings
Install the adapter for the detected Dart hash:
flutterdec adapter install --dart-hash <HASH>
flutterdec decompile ./sample.apk -o ./out
Expect a non-zero exit on a real app, and expect your artifacts anyway. The strict quality gate is on by
default with --max-placeholder-ifs 0, and every real Flutter app has placeholder ifs. So the command above
prints reasons: placeholder if-count exceeded threshold, exits 1, and still writes every artifact
listed in step 4. Nothing is missing - the gate is reporting a quality measurement, not a failure to
decompile. Measured on LocalSend 1.17: 501 at the default scope, 5,800 pseudocode files written, exit 1.
Read your own number rather than guessing one. It is placeholder_ifs in out/quality.json, and it grows
with scope - the same app reports 1,178 under --function-scope all --split-records:
flutterdec decompile ./sample.apk -o ./out # exits 1, writes everything
python3 -c "import json;print(json.load(open('out/quality.json'))['placeholder_ifs'])"
flutterdec decompile ./sample.apk -o ./out --max-placeholder-ifs <that number>
Setting the threshold from the measurement is the point: a huge round number silences the gate permanently,
whereas a real one still fails when the count rises, which is the only thing it is useful for. Keep the
strict default in CI. See docs/cli-reference.md for the other gate flags.
Open the main outputs first:
out/pseudocode/*.dartpseudo
out/report.jsonout/quality.jsonThat is enough to start working on most APKs.
Inspect target metadata:
flutterdec info ./sample.apk --json
Install and list adapters:
flutterdec adapter install --dart-hash <HASH>
flutterdec adapter list
Decompile with the default app-focused scope:
flutterdec decompile ./sample.apk -o ./out
Emit asm and IR too:
flutterdec decompile ./sample.apk -o ./out --emit-asm --emit-ir
Compare two builds:
flutterdec diff --old ./old.apk --new ./new.apk -o ./out-diff --json
Generate Ghidra and IDA import scripts:
flutterdec decompile ./sample.apk -o ./out \
--emit-ghidra-script \
--emit-ida-script
By default, decompile focuses app reversing with --function-scope app-unknown and excludes known Flutter/Dart framework internals.
Available scopes:
app-unknown (default): app (package:*) plus unknown ownership functionsapp: only app (package:*) functionsall: app plus Flutter, Dart runtime, and framework internalsInclude everything:
flutterdec decompile ./sample.apk -o ./out --function-scope all
Focus only specific Dart packages (repeatable):
flutterdec decompile ./sample.apk -o ./out \
--function-scope app-unknown \
--app-package my_app
If package names are unknown, inspect report.json under function_scope.app_package_counts_top.
When --app-package is not provided, capped prioritization also uses manifest-derived package hints under function_scope.priority_package_hints to favor app-owned code.
Target a specific function by id:
flutterdec decompile ./sample.apk -o ./out \
--target id:42 \
--emit-asm
Target by entry address:
flutterdec decompile ./sample.apk -o ./out \
--target va:0x613468 \
--emit-asm
--target accepts:
id:<N>va:0x<ADDR>0x<ADDR><N>If <N> is ambiguous, flutterdec requires explicit id: or va:. Selection details are emitted in report.json.target_selection.
If you have a stripped/unstripped libflutter.so pair, generate a symbol map:
flutterdec map-symbols \
--stripped ./libflutter.stripped.so \
--unstripped ./libflutter.unstripped.so \
-o ./out/symbol-map \
--register-local-cache
Then use that in later decompile runs:
flutterdec decompile ./sample.apk -o ./out \
--extra-symbol-elf ./libflutter.unstripped.so
If the cached engine build id matches the APK's embedded libflutter.so, decompile auto-loads the cached symbol_target_summary.json and reports it under report.json.engine_symbol_ingestion.
flutterdec diff --old ./old.apk --new ./new.apk -o ./out-diff --json
diff_report.json includes added, removed, and common function summaries plus added_packages_top and removed_packages_top churn summaries.
| If you want to... | Start with... | Why |
|---|---|---|
| Read recovered logic | pseudocode/*.dartpseudo |
Best first pass for branches, loops, returns, and named callsites |
| Validate the decompiler | asm/*.s and ir/*.json |
Lets you confirm control flow and pool-backed calls |
| Understand startup | report.json.android_startup |
Shows manifest anchors, startup stages, and DartEntrypoint evidence |
| Check analysis health | quality.json and report.json |
Shows coverage, compatibility, target selection, and symbol-ingestion diagnostics |
| Review version-to-version changes | diff_report.json |
Shows recovered function churn and package-level change summaries |
decompile exposes analysis-engine profiles so you can trade detail for speed.
Default profile:
balanced (recommended)Available profiles:
balanced: full semantic naming, hints, and reportinglight: lower-overhead analysis for faster large-scale runsExample:
flutterdec decompile ./sample.apk -o ./out --analysis-profile light
Adapter backend selection:
--adapter-backend auto (default): try r2flutter, then Blutter, then fall back to the internal adapter--adapter-backend internal: force the internal adapter only--adapter-backend blutter: require the Blutter backend and fail if unavailable--adapter-backend r2-flutter: require the r2flutter backend and fail if unavailable--require-snapshot-hash-match: fail when the adapter-reported snapshot hash does not match the loader snapshot hashWhat the backends actually recover:
| Backend | Function names | Classes | ObjectPool |
|---|---|---|---|
internal |
none (sub_<addr> placeholders) |
none | carved strings, no real index space |
blutter |
exact, from Blutter dumps | yes | Blutter pp.txt entries |
r2flutter |
exact, from the AOT instruction table | yes, with fields and methods | real slots, resolvable from x27 displacements |
Only backends that recover the real ObjectPool layout report pool_geometry. Without
it flutterdec leaves pool references unresolved rather than attaching a value from an
unrelated index space, and says so in report.json.pool_metadata.hints_suppressed_reason.
r2flutter backend environment knobs:
FLUTTERDEC_R2FLUTTER_BIN: path to the r2flutter binaryFLUTTERDEC_R2FLUTTER_CMD: full command to launch it, when a wrapper is neededFLUTTERDEC_R2FLUTTER_TIMEOUT: per-invocation timeout in seconds (default 900)r2flutter is taken from PATHr2flutter is an external MIT tool (radareorg/r2flutter)
that parses Dart AOT snapshots directly. It needs radare2 available at build time.
Blutter backend environment knobs:
FLUTTERDEC_BLUTTER_CMD: full command to launch Blutter, for example python3 /path/to/blutter.pyFLUTTERDEC_BLUTTER_PY: path to blutter.py when you want the current Python interpreter to run itNix integration:
nix develop provides flutterdec-blutter and auto-exports FLUTTERDEC_BLUTTER_CMD to that wrappernix run .#blutter-bridge -- --helpPer-feature engine toggles:
--with-canonical-model-symbols / --no-canonical-model-symbols--with-pool-value-hints / --no-pool-value-hints--with-pool-semantic-hints / --no-pool-semantic-hints--with-semantic-reporting / --no-semantic-reporting--with-bootflow-category-seeds / --no-bootflow-category-seeds--with-apk-startup-analysis / --no-apk-startup-analysisMain outputs under -o <OUT_DIR>:
pseudocode/*.dartpseudoquality.jsonreport.jsondiff_report.json (if flutterdec diff)asm/*.s (if --emit-asm)--emit-asm --emit-asm-opcodes)ghidra_apply_symbols.py (if --emit-ghidra-script)ida_apply_symbols.py (if --emit-ida-script)ir/*.json (if --emit-ir)report.json also includes:
compatibility for schema, hash, and manifest alignment diagnosticsandroid_manifest for manifest-derived launcher, deeplink, and activity signalsandroid_startup for APK bytecode startup evidence such as embedding calls, JNI bootstrap stages, and recovered DartEntrypoint callsites when presentandroid_startup.dart_entrypoints entries can carry function_name, library_uri, and app_bundle_path when those values are directly recoverable from APK bytecode or simple helper return propagationandroid_startup.bootstrap_chain summarizes observed Android embedder startup stages per source method, including ownership, stage ordering, completeness, and missing stepsengine_symbol_ingestion for auto-loaded local engine symbol cache matches keyed by libflutter.so build idbootflow_discovery entries tagged by source (adapter, manifest, apk_startup)The goal of these examples is simple: show original public source first, then show what flutterdec recovers from the shi
$ claude mcp add flutterdec \
-- python -m otcore.mcp_server <graph>