Flutter App Reversing Playbook
Flutter compiles Dart AOT to a native shared library (
libapp.so/Flutter.framework).jadx/apktoolshow only the thin Java/ObjC shell and are useless for app logic. Use this playbook when static Android or iOS skills yield nothing but an empty Java wrapper.
Prerequisites
- APK / IPA obtained (see
mobile/android/SKILL.mdPath 1 for APK pull). - Tools:
reflutter(pip install reflutter),uber-apk-signer(download JAR from GitHub releases),adb+ device or emulator,jadx(identification only), Burp Suite proxy. - For iOS:
reFlutteralso patchesFlutter.framework; useobjection patchipaor direct framework patching workflow.
Step 1: Identify a Flutter App
# Unzip APK, look for the telltale Flutter artifacts
unzip -o base.apk -d /tmp/apk-out/
ls /tmp/apk-out/lib/arm64-v8a/
# Flutter app: libflutter.so libapp.so
# Present: flutter_assets/ kernel_blob.bin (optional, debug)
# jadx shows only the Java bootstrap:
jadx -d /tmp/apk-java /tmp/base.apk
# Expect: ~1 class, LoadLibrary("app"), nothing useful
# Confirm with strings:
strings /tmp/apk-out/lib/arm64-v8a/libflutter.so | grep -i "flutter"
strings /tmp/apk-out/lib/arm64-v8a/libapp.so | grep -E "https?://"
Key tell: libapp.so size > 5 MB with no dex business logic; the
kernel_blob.bin in flutter_assets/ indicates a debug/JIT build
(rare in production; can extract Dart directly with dart_vm).
Step 2: reFlutter Workflow — Patch BoringSSL Pinning
reFlutter patches the ssl_crypto_x509_session_verify_cert_chain
function in libflutter.so to always return true, bypassing all
BoringSSL-based certificate validation (covers both custom pinning
and system trust).
Flutter <= 3.23.x (hardcoded proxy IP)
# Patch: supply your Burp listener IP
reflutter base.apk
# reFlutter prompts for IP — enter your Burp machine IP (e.g. 192.168.1.100)
# Output: release.RE.apk (patched, unsigned)
# Sign with uber-apk-signer
java -jar uber-apk-signer.jar --allowResign -a release.RE.apk -o /tmp/
# Install on device
adb install /tmp/release.RE-aligned-signed.apk
# Configure Burp proxy listener on 0.0.0.0:8083 (reFlutter default port is 8083)
# Launch app — HTTPS traffic appears in Burp
Flutter >= 3.24.0 (no hardcoded proxy IP — breaking change)
Starting Flutter 3.24.0 (August 2024), the hardcoded proxy IP was removed from the patched BoringSSL stub. Traffic is no longer redirected to a fixed IP. Two options:
Option A: Device-level proxy
# Patch as above (no IP prompt in 3.24+)
reflutter base.apk
java -jar uber-apk-signer.jar --allowResign -a release.RE.apk -o /tmp/
# Install and configure device proxy manually:
# Android Settings → Wi-Fi → [SSID] → Proxy → Manual
# Host: <Burp-IP> Port: 8080
# Or use TunProxy for non-proxy-aware processes:
adb install TunProxy.apk
# Launch TunProxy, set server to <Burp-IP>:8080, toggle VPN
# Then launch patched app
Option B: Per-app proxy injection via Frida
# Run patched APK + Frida script to redirect all socket connections
frida -U -f com.target.app -l proxy-redirect.js --no-pause
# proxy-redirect.js: hooks connect() syscall, redirects to Burp IP
iOS Flutter.framework patching
# Extract IPA
unzip target.ipa -d /tmp/ipa-out/
# Locate Flutter.framework
ls /tmp/ipa-out/Payload/TargetApp.app/Frameworks/Flutter.framework/Flutter
# reFlutter iOS (experimental — check reFlutter README for current support)
reflutter target.ipa
# Sign with codesign + adhoc or with valid mobileprovision
codesign --force --sign - /tmp/ipa-out/Payload/TargetApp.app/Frameworks/Flutter.framework/Flutter
# Repack + install with ios-deploy or Sideloadly
Step 3: BoringSSL Bypass Mechanism
reFlutter patches ssl_crypto_x509_session_verify_cert_chain to
unconditionally return 1 (success). This function is the central
chain verification entry point in BoringSSL (the TLS library embedded
in libflutter.so) — no root CA, no pinning config, and no custom
validator override this patch because the chain evaluation never runs.
Manual binary patch (if reFlutter fails on a specific version):
# Find the function offset in libflutter.so
r2 -A /tmp/apk-out/lib/arm64-v8a/libflutter.so
# In r2: afl~ssl_crypto_x509
# Or: /c ret in data section near ssl_crypto_x509_session_verify_cert_chain
# Patch: overwrite function prologue with MOV W0, #1 / RET (AArch64)
# MOV W0, 1 = 20 00 80 52
# RET = C0 03 5F D6
python3 -c "
import struct
with open('libflutter.so', 'r+b') as f:
f.seek(<offset>)
f.write(b'\x20\x00\x80\x52\xC0\x03\x5F\xD6')
"
Step 4: Static Native RE — libapp.so in Ghidra / radare2
# Load libapp.so in radare2 for quick triage
r2 -A /tmp/apk-out/lib/arm64-v8a/libapp.so
# Strings — API endpoints, Firebase config, hardcoded keys
r2 -qc 'iz~https' /tmp/apk-out/lib/arm64-v8a/libapp.so
r2 -qc 'iz~firebase' /tmp/apk-out/lib/arm64-v8a/libapp.so
r2 -qc 'iz~AIza' /tmp/apk-out/lib/arm64-v8a/libapp.so # Firebase API key prefix
# Imports — identify Flutter plugins with native bridges
r2 -qc 'ii' /tmp/apk-out/lib/arm64-v8a/libapp.so | head -40
In Ghidra (MCP-connected via ghidra server):
# connect_instance first, then batch analyze
# Load libapp.so → auto-analyze
# Search defined strings for URL patterns, credential patterns
# Dart AOT functions are not named — use string xrefs to find handlers
Dart AOT functions lack names (no symbol table in release builds) but
the Dart snapshot contains type metadata that Il2CppDumper-style
tools and dart_vm snapshot parsers can partially recover (see
snapshot dump section below).
Step 5: Dart Snapshot Dump (Hit-or-Miss)
# Option 1: runtime adb pull from memory-mapped snapshot
adb shell "run-as com.target.app cat /data/data/com.target.app/app_flutter/snapshot_blob.bin" \
> /tmp/snapshot_blob.bin 2>/dev/null
# Option 2: use Dart VM snapshot reader tools
# dart_snapshot_parser (community; partial support for Dart 2.x-3.x)
pip install dart-snapshot-parser 2>/dev/null || true
# Option 3: reFlutter's snapshot output
# After running reFlutter and launching the patched app once,
# reFlutter dumps snapshot_hash.txt which can seed symbol recovery tools
Caveats: Dart snapshot format changed in Dart 2.15, 3.0, and 3.4. Community parsers (snapshot_inspector, etc.) are often version-specific and may produce partial or no output on latest Flutter. Treat snapshot dump as a best-effort step; static string analysis is more reliable.
Step 6: Flutter Plugin Identification
Flutter plugins use platform channels with predictable naming:
# Find channel names in libapp.so strings
strings /tmp/apk-out/lib/arm64-v8a/libapp.so | grep -E "plugins\.|flutter\." | sort -u
# Channel names like:
# com.google.firebase.messaging → push tokens
# plugins.flutter.io/path_provider → storage paths
# flutter.baseflow.com/geolocator → GPS
# plugins.flutter.io/local_auth → biometric
# Cross-reference with flutter_assets/AssetManifest.json
cat /tmp/apk-out/flutter_assets/AssetManifest.json | python3 -m json.tool | head
Evidence
kg_add_node(
kind="finding",
label="Flutter BoringSSL pinning bypassed",
props={
"key": f"flutter-boringssl-bypass::{package_id}",
"severity": "high",
"cvss": 7.4,
"package": package_id,
"flutter_version": "<version-from-libflutter-strings>",
"bypass_method": "reFlutter",
"proxy_verified": True,
},
)
ZFP
- Burp HTTP history showing decrypted HTTPS from patched Flutter app.
strings libapp.so | grep httpsoutput showing extracted endpoints.- Screenshot of reFlutter build output + uber-apk-signer signing confirming the patched APK was installed.
OPSEC Notes
- reFlutter requires re-signing the APK; app integrity checks (Google Play Integrity API, SafetyNet) will flag the modified signature. Use emulator or device without Play Store for testing.
- The patched APK has a different certificate than the original; side-
load via
adb installoradb install --bypass-low-target-sdk-block. - For production devices, TunProxy routes traffic at the VPN layer without modifying the APK signature.
- libflutter.so binary patching is version-specific; wrong offset crashes the app. Always test on a throwaway device/emulator first.
Severity Table
| Bug | Severity |
|---|---|
| No certificate validation (BoringSSL patched trivially) | High 7.4 |
| Hardcoded API key / Firebase config in libapp.so strings | Critical 9.0 |
| Sensitive data in Dart snapshot / kernel_blob | High 7.5 |
| Flutter plugin channel unauthenticated method call | Medium-High |
References
- reFlutter: https://github.com/ptswarm/reFlutter
- Flutter 3.24.0 proxy change: https://github.com/ptswarm/reFlutter/issues/107
- TunProxy (Android): https://github.com/raise-isayan/TunProxy
- uber-apk-signer: https://github.com/patrickfav/uber-apk-signer
- kayssel "Breaking Flutter" guide: https://blog.nviso.eu/2022/08/18/intercept-flutter-traffic-on-ios-and-android-http-https-edition/
- Cross-ref:
mobile/android/SKILL.md(APK pull),mobile/ios/dynamic/SKILL.md(iOS BoringSSL path)