Flutter

Flutter is Google’s cross-platform UI toolkit that lets developers write a single Dart code-base which the Engine (native C/C++) turns into platform-specific machine code for Android & iOS.
The Engine bundles a Dart VM, BoringSSL, Skia, etc., and ships as the shared library libflutter.so (Android) or Flutter.framework (iOS). All actual networking (DNS, sockets, TLS) happens inside this library, not in the usual Java/Kotlin Swift/Obj-C layers. That siloed design is why the usual Java-level Frida hooks fail on Flutter apps.

Intercepting HTTPS traffic in Flutter

This is a summary of this blog post.

Why HTTPS interception is tricky in Flutter

  • SSL/TLS verification lives two layers down in BoringSSL, so Java SSL‐pinning bypasses don’t touch it.
  • BoringSSL uses its own CA store inside libflutter.so; importing your Burp/ZAP CA into Android’s system store changes nothing.
  • Symbols in libflutter.so are stripped & mangled, hiding the certificate-verification function from dynamic tools.

Fingerprint the exact Flutter stack

Knowing the version lets you re-build or pattern-match the right binaries.

Step Command / File Outcome
Get snapshot hash python3 get_snapshot_hash.py libapp.so adb4292f3ec25…
Map hash → Engine enginehash list in reFlutter Flutter 3 · 7 · 12 + engine commit 1a65d409…
Pull dependent commits DEPS file in that engine commit dart_revision → Dart v2 · 19 · 6
dart_boringssl_rev → BoringSSL 87f316d7…

Find get_snapshot_hash.py here.

Target: ssl_crypto_x509_session_verify_cert_chain()

  • Located in ssl_x509.cc inside BoringSSL.
  • Returns bool – a single true is enough to bypass the whole certificate chain check.
  • Same function exists on every CPU arch; only the opcodes differ.

Option A – Binary patching with reFlutter

  1. Clone the exact Engine & Dart sources for the app’s Flutter version.
  2. Regex-patch two hotspots:
  3. In ssl_x509.cc, force return 1;
  4. (Optional) In socket_android.cc, hard-code a proxy ("10.0.2.2:8080").
  5. Re-compile libflutter.so, drop it back into the APK/IPA, sign, install.
  6. Pre-patched builds for common versions are shipped in the reFlutter GitHub releases to save hours of build time.

Option B – Live hooking with Frida (the “hard-core” path)

Because the symbol is stripped, you pattern-scan the loaded module for its first bytes, then change the return value on the fly.

// attach & locate libflutter.so
var flutter = Process.getModuleByName("libflutter.so");

// x86-64 pattern of the first 16 bytes of ssl_crypto_x509_session_verify_cert_chain
var sig = "55 41 57 41 56 41 55 41 54 53 48 83 EC 38 C6 02";

Memory.scan(flutter.base, flutter.size, sig, {
  onMatch: function (addr) {
    console.log("[+] found verifier at " + addr);
    Interceptor.attach(addr, {
      onLeave: function (retval) { retval.replace(0x1); }  // always 'true'
    });
  },
  onComplete: function () { console.log("scan done"); }
});

Run it:

frida -U -f com.example.app -l bypass.js

Porting tips
For arm64-v8a or armv7, grab the first ~32 bytes of the function from Ghidra, convert to a space-separated hex string, and replace sig.
Keep one pattern per Flutter release, store them in a cheat-sheet for fast reuse.

Forcing traffic through your proxy

Flutter itself ignores device proxy settings. Easiest options:
Android Studio emulator: Settings ▶ Proxy → manual.
Physical device: evil Wi-Fi AP + DNS spoofing, or Magisk module editing /etc/hosts.

Quick Flutter TLS bypass workflow (Frida Codeshare + system CA)

When you only need to observe a pinned Flutter API, combining a rooted/writable AVD, a system-trusted proxy CA, and a drop-in Frida script is often faster than reverse-engineering libflutter.so:

  1. Install your proxy CA in the system store. Follow Install Burp Certificate to hash/rename Burp's DER certificate and push it into /system/etc/security/cacerts/ (writable /system required).

  2. Drop a matching frida-server binary and run it as root so it can attach to the Flutter process:

adb push frida-server-17.0.5-android-x86_64 /data/local/tmp/frida-server
adb shell "su -c 'chmod 755 /data/local/tmp/frida-server && /data/local/tmp/frida-server &'"
  1. Install the host-side tooling and enumerate the target package.
pip3 install frida-tools --break-system-packages
adb shell pm list packages -f | grep target
  1. Spawn the Flutter app with the Codeshare hook that neuters BoringSSL pin checks.
frida -U -f com.example.target --codeshare TheDauntless/disable-flutter-tls-v1 --no-pause

The Codeshare script overrides the Flutter TLS verifier so every certificate (including Burp's dynamically generated ones) is accepted, side-stepping public-key pin comparisons.

  1. Route traffic through your proxy. Configure the emulator Wi-Fi proxy GUI or enforce it via adb shell settings put global http_proxy 10.0.2.2:8080; if direct routing fails, fall back to adb reverse tcp:8080 tcp:8080 or a host-only VPN.

  2. If the app ignores OS proxy settings, redirect sockets with a Frida shim. Tools like frida4burp hook dart:io/BoringSSL socket creation to force outbound TCP sessions to your proxy, even with hardcoded HttpClient.findProxyFromEnvironment or Wi‑Fi bypasses. Set the proxy host/port in the script and run it alongside the TLS bypass:

frida -U -f com.example.target --no-pause \
  --codeshare TheDauntless/disable-flutter-tls-v1 \
  -l frida4burp.js

Works on iOS via a Frida gadget or USB frida-server; chaining the socket redirect with the TLS bypass restores both routing and certificate acceptance for Burp/mitmproxy.

Once the CA is trusted at the OS layer and Frida quashes Flutter's pinning logic (plus socket redirection if needed), Burp/mitmproxy regains full visibility for API fuzzing (BOLA, token tampering, etc.) without repacking the APK.

Offset-based hook of BoringSSL verification (no signature scan)

When pattern-based scripts fail across architectures (e.g., x86_64 vs ARM), directly hook the BoringSSL chain verifier by absolute address within libflutter.so. Workflow:

  • Extract the right-ABI library from the APK: unzip -j app.apk "lib/*/libflutter.so" -d libs/ and pick the one matching the device (e.g., lib/x86_64/libflutter.so).
  • Analyze in Ghidra/IDA and locate the verifier:
  • Source: BoringSSL ssl_x509.cc function ssl_crypto_x509_session_verify_cert_chain (3 args, returns bool).
  • In stripped builds, use Search → For Strings → ssl_client → XREFs, then open each referenced FUN_... and pick the one with 3 pointer-like args and a boolean return.
  • Compute the runtime offset: take the function address shown by Ghidra and subtract the image base (e.g., Ghidra often shows 0x00100000 for PIE Android ELFs). Example: 0x02184644 - 0x00100000 = 0x02084644.
  • Hook at runtime by base + offset and force success:
// frida -U -f com.target.app -l bypass.js --no-pause
const base = Module.findBaseAddress('libflutter.so');
// Example offset from analysis. Recompute per build/arch.
const off  = ptr('0x02084644');
const addr = base.add(off);

// ssl_crypto_x509_session_verify_cert_chain: 3 args, bool return
Interceptor.replace(addr, new NativeCallback(function (a, b, c) {
  return 1; // true
}, 'int', ['pointer', 'pointer', 'pointer']));

console.log('[+] Hooked BoringSSL verify_cert_chain at', addr);

Notes - Signature scans can succeed on ARM but miss on x86_64 because the opcode layout changes; this offset method is architecture-agnostic as long as you recalc the RVA. - This bypass causes BoringSSL to accept any chain, enabling HTTPS MITM regardless of pins/CA trust inside Flutter. - If you force-route traffic during debugging to confirm TLS blocking, e.g.:

iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination <Burp_IP>:<Burp_Port>

…you will still need the hook above, since verification happens inside libflutter.so, not Android’s system trust store.

Reversing the Dart payload (libapp.so)

Quick static triage

Before rebuilding libflutter.so, spend two minutes identifying what is actually packaged inside the APK:

unzip -l target.apk | grep -E "(libflutter|libapp|flutter_assets|kernel_blob)"
unzip -j target.apk "lib/arm64-v8a/libapp.so" "lib/arm64-v8a/libflutter.so" -d extracted_libs/
strings extracted_libs/libflutter.so | grep -E "^[0-9]+\.[0-9]+\.[0-9]+" | head
  • libapp.so is the AOT-compiled Dart payload.
  • assets/flutter_assets/ usually contains manifests, JSON/config files, and sometimes secrets or hidden feature flags.
  • kernel_blob.bin is a high-value indicator of a debug/non-release build; if present, prioritize it before spending time on AOT reversing.

A quick first pass that regularly pays off:

apktool d target.apk -o apktool-out
rg -n -a "(https?://|api[_-]?key|token|secret|BEGIN (RSA|EC|PRIVATE)|supabase|stripe|aws)" \
  apktool-out/assets/flutter_assets extracted_libs -S

Recovering symbols, strings and hook points with blutter

For ARM64 targets, blutter is currently the most practical first step because it parses the Dart AOT snapshot, labels functions, dumps the object pool, and generates Frida hook stubs. For generic ELF work after that, check Reversing Native Libraries.

python3 blutter.py extracted_libs/ blutter-out
rg -n -a "(https?://|api[_-]?key|token|secret|BEGIN (RSA|EC|PRIVATE)|supabase|stripe|aws)" \
  blutter-out/pp.txt blutter-out/asm -S
gitleaks detect -s blutter-out/pp.txt --no-git

Useful outputs: - asm/: annotated assembly with recovered Dart/library names - pp.txt: object-pool dump; often the fastest place to find endpoints, JWTs, keys, IVs, feature flags, and C2 paths - blutter_frida.js: starter hooks you can adapt to print arguments or return values from recovered functions

If you want a fully static workflow or a fast diff between two releases, newer tools like unflutter and flutterdec are worth trying, but blutter remains the quickest route from APK → named functions → Frida hook points.

Platform / MethodChannel reconnaissance

A lot of Flutter-specific attack surface is not in TLS at all but in the bridge between Dart and the Android host. The most valuable strings are often MethodChannel / EventChannel names because they point to privileged native actions (contacts, SMS, files, biometrics, device identifiers, push tokens, custom crypto helpers, etc.).

jadx -r target.apk -d jadx-out
rg -n "MethodChannel|EventChannel|BasicMessageChannel|plugins\\.flutter\\.io|setMethodCallHandler|invokeMethod" jadx-out -S

Frida can log channel creation names very early during startup:

Java.perform(function () {
  var MC = Java.use('io.flutter.plugin.common.MethodChannel');
  MC.$init.overload('io.flutter.plugin.common.BinaryMessenger', 'java.lang.String')
    .implementation = function (messenger, name) {
      console.log('[+] MethodChannel: ' + name);
      return this.$init(messenger, name);
    };
});

Once you know the channel name, pivot into the matching Java/Kotlin handler and the Dart caller in blutter-out/asm/ to trace how sensitive data moves across the bridge.

Obfuscation notes for pentesters

Flutter obfuscation (--obfuscate --split-debug-info) only renames symbols and stores the symbol map externally so developers can later run flutter symbolize. It does not encrypt flutter_assets, prevent native disassembly of libapp.so, or stop runtime hooks.

If your assessment includes CI/CD artifacts, crash-upload buckets, update infrastructure, or mobile build pipelines, search for accidentally exposed: - app.*.symbols - SYMBOLS - obfuscation-map JSON files - directories used as --split-debug-info output

Those artifacts can turn a “hard to read” AOT binary back into a much friendlier target.

References