Read more on the blog: https://swarm.ptsecurity.com/fork-bomb-for-flutter/
reFlutter reverse-engineers Flutter apps by swapping the app's engine for a prebuilt patched one (repack mode) or by instrumenting the app's own engine at runtime (attach mode). What you get:
- Traffic interception — boringssl certificate verification patched to succeed unconditionally, so Burp (or any proxy) sees the traffic; bypasses several Flutter certificate-pinning implementations. No root or certificate installation needed on Android.
- Dump mode — a
dump.dartJSONL with every function's name, class, library, static-ness, parameter count and code offset into the Dart instructions image, ready for Frida hooking or IDA/Ghidra naming (scripts/dump2disasm.py). - Runtime routes where repacking can't reach: a runtime SSL bypass for engines that ship unstripped, and a runtime dumper for Shorebird apps (whose private Dart fork makes patched-engine builds impossible).
- Manual engine patching via a crafted
Dockerfileif you want your own changes.
- Android: arm64, arm32, x64 (x64 assets: Flutter >= 3.41);
- iOS: arm64;
- Release and profile builds, Stable and Beta channels — coverage is keyed by snapshot hash in enginehash.csv;
- Pre-monorepo-merge engines (<= 3.27) are built from the archived flutter/engine repo — the 3.24.x assets ship with the same pipeline.
# Linux, Windows, MacOS
pip3 install reflutter
$ reflutter main.apk
Please enter your Burp Suite IP: <input_ip>
SnapshotHash: 8ee4ef7a67df9845fba331734198a953
The resulting apk file: ./release.RE.apk
Please sign the apk fileSign and align the APK — uber-apk-signer works well:
java -jar uber-apk-signer.jar --allowResign -a release.RE.apkFor an IPA, sign and install the result as usual; the tool prints a reminder.
Options:
-p, --patch-dump— dump mode: the repacked engine emitsdump.dart(JSONL: classes/methods/offsets) on start, and the tool writes a readyfrida.jsnext to the output instead of the proxy instructions. Shorebird apps cannot use-p(their private Dart fork's snapshots don't run in patched engines) — dump those at runtime withscripts/frida-dump.jsinstead (see Shorebird builds).-n, --no-interact— never prompt for a Burp IP (implies127.0.0.1); useful for old engines in CI.-b <Snapshot_Hash>, --build-engine— engine build mode: print the engine commit for a snapshot hash and (when run inside an engine checkout) apply the reFlutter source patches. See Build Engine.
Point the app at a proxy on the same network. Configure the Burp listener:
- Add port:
8083, bind toAll interfaces; - Request handling: Support invisible proxying =
True.
Android — set the device proxy, then install the repacked APK:
adb -s <device> shell "settings put global http_proxy <proxy_ip:port>"Optionally route everything through Burp with TunProxy.
iOS — install the signed IPA and configure Potatso (or any per-app proxy tool) to use your Burp listener.
⚠️ Proxy routing by era: engines up to and including Flutter 3.24.x (snapshot hash80a49c7111088100a233b2ae788e1f48) carry a hardcoded proxy IP that gets patched in place. From 3.27.x on, the hardcoded IP is gone — configure the proxy directly on the device as shown above.
Run with -p, start the app, and pull the JSONL dump:
reflutter -p main.apk && java -jar uber-apk-signer.jar --allowResign -a release.RE.apk
adb shell "cat /data/data/<PACKAGE_NAME>/dump.dart" > dump.dartOn iOS the dump lands in the app container; the console log prints the exact path.
file contents (one JSON object per function)
{"method_name":"_handleRequest","offset":"0x00000000000a8740","library_url":"package:anyapp/api/client.dart","class_name":"ApiClient","is_static":"false","parameter_count":"2"}Offsets are relative to the Dart instructions image start
(_kDartIsolateSnapshotInstructions / _kDartSnapshotText), not the ELF
base. Two ways to use them:
-
Frida — the auto-written
frida.jsresolves the instructions symbol automatically (newer Dart exports_kDartSnapshotText, older_kDartIsolateSnapshotInstructions) and hooks an offset you fill in:frida -U -f <package> -l frida.js
Works across Frida 14–17; prefer recent frida-server (16.x servers predate Android 15+ and cannot inject there — use 17.x).
-
Static analysis — generate naming scripts for IDA and Ghidra:
python3 scripts/dump2disasm.py dump.dart
frida-ssl.js disables TLS verification without repacking at all by
patching boringssl inside the loaded libflutter.so:
frida -U -f <package> -l frida-ssl.jsIt resolves the internal-linkage verification function from .symtab, so it
needs an engine that still carries a symbol table: Shorebird engines
(ship unstripped, ~150 MB) and debug/profile builds qualify. Stock
release APKs ship stripped engines — the bucket's symbols.zip is a
separate link whose addresses do not transfer — so for those use reFlutter
repack mode, which swaps in a prebuilt engine with the bypass compiled in.
arm, arm64 and x64 are handled per-ISA; other arches abort without writing
anything. Requires frida-server 17.x on Android 15/16 (16.x kills
system_server there; 17.9.9 verified on API 36).
Apps built with Shorebird use a patched Flutter
engine whose snapshot hash differs from the vanilla release it is based on.
reFlutter identifies these automatically (a hash found only in
enginehash_sb.csv
identifies a Shorebird app even when flutter_assets/shorebird.yaml is
absent).
Traffic interception works like any other app — reflutter app.apk/ipa
— but with no engine build behind it: Shorebird's own engine artifact is
fetched from their public bucket, boringssl's certificate-chain verification
is patched to succeed unconditionally (a pure-Python ELF/Mach-O walk —
symbol table to file offset to a per-ISA patch — after which the ~150 MB of
shipped symbols are dropped, since Android's linker only reads program
headers), and the app is repacked with it.
Dump mode runs at runtime instead — our patched engines cannot run
Shorebird snapshots (their code lives in patchable regions understood only
by their private Dart fork's loader — verified empirically), but the engine
Shorebird ships inside the APK is unstripped. scripts/frida-dump.js
hooks the app's own libflutter.so at FunctionDeserializationCluster::PostLoad
— the exact splice point of the engine patch — and replays its JSONL dump
by reading raw Dart object layouts, deriving the instructions-image base at
runtime:
frida -D <device> -f <package> -l scripts/frida-dump.js
adb pull /data/data/<package>/dump.dart .
python3 scripts/dump2disasm.py dump.dartRequires an arm64 AOT app and the stock, unrepacked engine (repacked
copies are stripped — the script fails loudly there). Offsets use the same
instructions-image convention as engine dumps; names carry the raw
Class@12345 form (strictly more information than the engine patch's
scrubbed names); parameter_count is unreliable on Shorebird snapshots.
Validated live: 6,957 functions, offsets arm64-spot-checked against the
APK's instructions image, consumed downstream by dump2disasm.
Notes: Android requires the engine artifact to carry a symbol table — recent revisions (Aug 2025+) ship with one; some older revisions are stripped and fail loudly. iOS requires a dSYM from the same build; dSYM availability varies by engine revision. The resulting IPA must be re-signed, as the tool's output already instructs.
Profile apps carry the same snapshot hash as their release counterparts
(verified across engines — the hash covers the VM sources, not the runtime
mode), so they work with the regular release engine assets;
scripts/gen_enginehash.py --profile re-verifies this as new engines ship.
Debug builds are genuinely different (JIT kernel snapshots, no AOT
libapp.so, debug artifacts embed the engine commit rather than a snapshot
hash) — there is nothing to repack, and nothing to dump. For traffic
analysis use frida-ssl.js, which runs on any engine that keeps its symbol
table, debug builds included.
Engines are built with scripts/local-release (macOS; builds the traffic
and dump variants for iOS + Android arm64/arm/x64, verifies every patch
landed, and uploads the release assets), keyed by the snapshot hashes in
enginehash.csv.
The engine commit for a snapshot hash resolves via reflutter -b <Snapshot_Hash>. Pre-monorepo-merge engines (<= 3.27) are fetched from the
archived flutter/engine repo and built with the matching gclient layout —
the era needs a handful of toolchain compatibility patches (dead mirror
pins, a libcxx roll against newer macOS SDKs), all applied automatically by
the build. Backfill past hashes with scripts/backfill '<hash> ...'.
scripts/update-enginehash refreshes the three CSVs — run it manually
whenever you want the lists current; it is incremental and idempotent
(seconds when nothing changed). On an always-on machine you can install a
weekly launchd agent instead:
scripts/install-enginehash-agent # Mondays 04:23; removal command printedManual Flutter code patching is supported using Docker:
git clone https://github.com/Impact-I/reFlutter && cd reFlutter
docker build -t reflutter -f Dockerfile .Run with:
docker run -it -v "$(pwd):/t" -e HASH_PATCH=<Snapshot_Hash> -e COMMIT=<Engine_commit> reflutterFlags:
-e x64=0/-e arm64=0/-e arm=0: disable that arch's build-e WAIT=300: time in seconds to modify source before build-e HASH_PATCH: snapshot hash fromenginehash.csv-e COMMIT: engine commit hash