Library / SDK
Impact-I/reFlutter avatar
Impact-I/reFlutter

reFlutter patches Flutter's own certificate checks so a proxy sees everything: only for apps you are authorized to test

Flutter Reverse Engineering Framework

2,765 stars294 forksPythonGPL-3.0

At a glance

What is it?
reFlutter reverse engineers Flutter apps by swapping the engine for a prebuilt patched build or by instrumenting the loaded engine at runtime, and its traffic path makes BoringSSL certificate verification succeed unconditionally. Nothing in the repository checks who owns the app you point it at, so the authorization question sits with the operator. What the code does document is unusually precise: coverage keyed to snapshot hashes, a proxy scheme that changed at Flutter 3.27, and a runtime route that only works on unstripped engines.
Who is it for?
reFlutter earns its keep in the two situations it is actually built for: you own the app, or you have written authorization and a scope that covers the target. Outside those, the same mechanics that make it useful make it a liability, since the traffic path turns certificate verification into a no-op and the dump hands you the offsets where an app's interesting functions live.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 4, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two modes, and the first question is who owns the app

The framework works in one of two ways. Repack mode replaces the app's engine with a prebuilt patched one, which is why a repacked Android package has to be signed again afterwards and why the tool prints a reminder about signing an IPA. Attach mode leaves the package alone and instruments the app's own engine while it runs. What you get from either is traffic interception: BoringSSL certificate verification is patched to succeed unconditionally, so a proxy sees the traffic, and several Flutter certificate pinning implementations are bypassed in the process. On Android no root and no certificate installation is needed. The first run looks like this:

console
$ reflutter main.apk

Please enter your Burp Suite IP: <input_ip>

SnapshotHash: 8ee4ef7a67df9845fba331734198a953
The resulting apk file: ./release.RE.apk
Please sign the apk file

Note what is absent from that interaction. The tool asks for a proxy address and reports a hash, and there is no ownership check, no license validation and no scope record anywhere in the repository. A tool that unconditionally disables certificate verification in someone else's app is a traffic interceptor pointed at whichever package you hand it, so the permission question is entirely the operator's to answer before the first run, not something the framework enforces. Two further scope notes sit beside it. On iOS the interception depends on a per-app proxy tool configured on the device, and on Android it is the device's own proxy setting that carries the traffic, which means the interception setup lives on hardware you control rather than in the tool. The page also names an external write-up on the technique, hosted on a security research site, as further reading.

Coverage is keyed to a snapshot hash, kept in three spreadsheets

What reFlutter can do to a given app depends on the exact Flutter engine that built it, and that dependency is tracked as data rather than logic. Three CSV files sit at the repository root: enginehash.csv for release builds, enginehash_profile.csv for profile builds, and enginehash_sb.csv for Shorebird builds, alongside a SNAPSHOT_HASH file. The quick start output shows the hash being read out of the app so you know which engine you are dealing with. Prebuilt patched engines are then published per platform and per hash as release tags, and the three most recent are ios-v3, ios-v2 and android-v3, all built for the same engine hash 0451907c2eaa8467e848c0067bfe8ed4 on 22 September 2026. The last commit to the default branch is dated 24 September 2026. The practical consequence is that an engine missing from those spreadsheets has no prebuilt counterpart, and the coverage you get depends on someone having built and published that hash.

Architecture coverage is narrower than the tag names suggest

The supported engine list is short and specific. On Android the architectures are arm64, arm32 and x64, with one caveat attached to x64: those assets require Flutter 3.41 or later. On iOS it is arm64 only. Both release and profile builds are covered, across the stable and beta channels, which is why a second hash spreadsheet exists for profile builds. Engines from before the monorepo merge, meaning 3.27 and earlier, are built from the archived flutter/engine repository instead of the current monorepo, and the 3.24.x assets ship through that same pipeline. This is where the era notes start to matter. Engines up to and including 3.24.x, identified by the snapshot hash 80a49c7111088100a233b2ae788e1f48, carry a hardcoded proxy address inside the engine that reFlutter can patch in place. From 3.27.x onward that hardcoded address is gone, so the interception point moves from the engine binary to the device's own network configuration, and the tool no longer has anything to rewrite there.

Dump mode writes JSONL with offsets into the Dart instructions image

Dump mode is the second half of the tool, and it is the half that turns a binary into something you can navigate. Started with the patch-dump flag, the repacked engine emits dump.dart on start, one JSON object per function, and the tool writes a ready made frida.js next to the output instead of printing proxy instructions instead. One line looks like this:

json
{"method_name":"_handleRequest","offset":"0x00000000000a8740","library_url":"package:anyapp/api/client.dart","class_name":"ApiClient","is_static":"false","parameter_count":"2"}

The offset is the interesting field and it has a precise meaning: it is relative to the start of the Dart instructions image, not to the ELF base, so an address from this file cannot be dropped into a disassembler without that adjustment. Two symbol names are involved depending on the Dart vintage, with newer engines exporting _kDartSnapshotText and older ones exporting _kDartIsolateSnapshotInstructions, and the generated frida.js resolves whichever is present. For static work there is a companion script, scripts/dump2disasm.py, which turns the same JSONL into naming scripts for IDA and Ghidra. Shorebird applications are excluded from this path, and the exclusion is explained on the page rather than left as a mystery.

Stripped engines cannot take the runtime route, and that is a symbol table problem

The runtime route skips repacking entirely. frida-ssl.js patches BoringSSL inside the already loaded libflutter.so, and to find the function to patch it resolves an internal linkage symbol out of .symtab, which means the engine has to still carry a symbol table. Shorebird engines qualify because they ship unstripped at roughly 150 MB, and debug and profile builds qualify for the same reason. Stock release APKs do not: their engines are stripped, and the documentation is explicit that the symbols archive published alongside them is a separate download whose addresses do not transfer into your process. For those, repack mode is the route, because it swaps in an engine with the bypass compiled in. The same script handles arm, arm64 and x64 separately, per instruction set, and aborts without writing anything on an architecture it does not recognise. The Frida side has its own version floor: the script is described as working across Frida 14 to 17, but 16.x servers predate Android 15 and cannot inject there, 16.x is noted as killing system_server on Android 15 and 16, and 17.9.9 is the version recorded as verified on API 36.

The Shorebird path fetches someone else's engine and rewrites it in Python

Applications built with Shorebird use a patched engine whose snapshot hash differs from the vanilla release they are based on, and reFlutter recognises them from the hash table alone, so detection still works when the shorebird.yaml file is absent from the asset bundle. Traffic interception works for them like any other app, but with no engine build behind it. Shorebird's own engine artifact is fetched from their public bucket, its BoringSSL certificate chain verification is patched to succeed unconditionally, and the app is repacked with the result. The patch itself is described precisely enough to be interesting: it is a pure Python walk over the ELF or Mach-O file that goes from the symbol table to a file offset and then to a per instruction set patch. After patching, the roughly 150 MB of shipped symbols are dropped, because Android's linker only reads program headers. Dump mode cannot follow this route at all. Patched engines cannot run Shorebird snapshots, because that code lives in regions only the private Dart fork's loader understands, which the page says was verified empirically, so those apps are dumped at runtime instead with scripts/frida-dump.js.

The package is a setup.py script labelled production stable at version 0.9.8

There is no pyproject.toml in the tree, only setup.py, and it describes a package named reflutter at version 0.9.8, licensed GPLv3+, with python_requires of 3.10 or later and classifiers for 3.10 through 3.13. Two details in that file are worth pausing on. The classifier list includes Development Status 5 - Production/Stable on a tool whose version is still below 1.0, which is a packaging claim rather than a statement about readiness. And the keywords field still holds the default distutils placeholder, distutils setuptools egg pip requirements, which suggests the metadata was assembled once and not revisited. The package itself is a single reflutter package with frida.js and a patches directory shipped as package data, and the console script entry point calls reflutter.__init__:main. Note that frida.js and frida-ssl.js are also committed at the repository root, so the same hook script exists both as package data and as a loose file in the tree.

Building an engine means cloning depot_tools and hard resetting flutter

The Dockerfile is where the prebuilt engines come from, and it is a full engine build rather than a wrapper. It starts from ubuntu:22.04, installs a JDK, sudo, git-svn, python3-pip and the usual build tools, then clones depot_tools and the flutter repository itself into temporary paths, sets a global git identity of [email protected], fetches the commit named by a build argument and hard resets to it. The reFlutter source patches are applied with the build-engine flag inside that checkout, a .gclient file is written pointing at the local flutter clone, gclient sync runs, and the engine is built per instruction set with gn and ninja in release runtime mode. The three architectures are build arguments defaulting to arm64-v8a, armeabi-v7a and x86_64, each skipped when its argument is set to 0, and a WAIT variable of 4 sits between the patch steps. This is the manual engine patching route too, for anyone who wants changes the published prebuilts do not carry.

Editorial conclusion

reFlutter earns its keep in the two situations it is actually built for: you own the app, or you have written authorization and a scope that covers the target. Outside those, the same mechanics that make it useful make it a liability, since the traffic path turns certificate verification into a no-op and the dump hands you the offsets where an app's interesting functions live. Three things to settle before running it. Confirm the target is in scope in writing, because the tool asks for a proxy address on the first run and enforces no ownership check of its own. Check the hash coverage first, since an engine that is not in the spreadsheets gets no prebuilt engine and the runtime route needs an unstripped build. And read the era notes before configuring anything, because the hardcoded proxy address this tool used to rewrite no longer exists in current engines, which changes where the interception has to happen.

Frequently asked questions

what is reflutter

It is a Flutter reverse engineering framework that works either by swapping an app's engine for a prebuilt patched one, called repack mode, or by instrumenting the app's own engine at runtime, called attach mode. It patches BoringSSL certificate verification to succeed unconditionally and can emit a JSONL dump of Dart function names and code offsets.

how to install reflutter

The documented install is pip3 install reflutter on Linux, Windows or MacOS. The setup script requires Python 3.10 or later, publishes version 0.9.8 under the GPLv3+ license, and installs a console script called reflutter. Prebuilt patched engines are published as release tags per platform and per snapshot hash.

Which Flutter engines does reFlutter cover?

Android arm64, arm32 and x64, where the x64 assets require Flutter 3.41 or later, and iOS arm64. Release and profile builds are covered across the stable and beta channels, with coverage keyed by snapshot hash in enginehash.csv and enginehash_profile.csv, and engines at 3.27 and earlier are built from the archived flutter/engine repository.

Why does the reFlutter runtime route fail on a stock release APK?

The runtime script resolves an internal linkage verification function out of .symtab, so it needs an unstripped engine. Shorebird engines ship unstripped at about 150 MB and debug or profile builds qualify, but stock release APKs are stripped, and the separately published symbols archive does not transfer into the process. Those builds need repack mode.

Can reFlutter dump a Shorebird app with its patch-dump mode?

No. Patched engines cannot run Shorebird snapshots, because that code lives in regions only the private Dart fork's loader understands, so those apps are dumped at runtime with scripts/frida-dump.js instead. Traffic interception still works for them, by fetching Shorebird's engine artifact and patching it in a pure Python walk.

Official sources

  1. Impact-I/reFlutter on GitHub
  2. Issues
  3. License: GPL-3.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/impact-i-reflutter.svg)](https://hysenlabs.com/projects/impact-i-reflutter)