tl;dr In this blog post, I will share insights I learned while researching the Flutter framework and the reFlutter tool. It will dive deep into Flutter’s architecture, some of its inner workings and dependencies, and finally, drill down into the SSL verification logic. The post will end by exploring what the reFlutter tool actually does and my attempts at replicating the same behaviour with Frida.
Note: If you are in a pinch on a mobile assessment where the application uses Flutter, the reFlutter tool is a great option. This blog post does not advocate that you need to use Frida logic. It is simply an exercise in seeing whether a Frida equivalent may exist.
How it started
During a recent assessment, a colleague and I encountered a problem on a tablet-only mobile assessment. In particular, we were trying to intercept HTTPS communication, but the client application was not co-operating. In search of a solution, I spent some time loading my Burp CA into the tablet’s system store. Turned out to be quite the exercise!
Even with the Burp CA loaded into the system store (eventually), the mobile application still did not allow its HTTPS traffic to be intercepted. I tried using various Frida scripts, manually routing traffic via a Wi-Fi access point and some Magisk modules, but nothing worked. I then returned to the application’s architecture and realised it was based on the Flutter framework.
This was not our first rodeo with Flutter. Another colleague (hi Joe) had encountered and fought with Flutter a few years back. A major struggle in pentesting mobile applications using Flutter is that it uses native libraries to handle communication and other security measures. Hence, it does not depend on the traditional Android logic or libraries (e.g. OkHttp3) we are used to seeing. This also means it is not easily susceptible to the tampering techniques we usually apply with Frida and Objection. For example, SSL pinning and verification are done several layers deep; Frida/Objection will not easily pinpoint the functions responsible for it.
From our colleague’s prior experience, we knew about the reFlutter tool. I struggled a bit getting it to run since the exact Flutter version our current application used was not yet built into the tool. After some head-scratching and a few Docker containers later, I had it running. The tool worked perfectly to patch the application and allowed its HTTPS communication to be intercepted. But it felt like magic, and I felt like a script kiddy. So I wondered what the tool was actually doing and if it could be replicated using Frida logic? Essentially, I wanted to dive deeper and demystify the magic that reFlutter was applying.
What is Flutter: A 101 Introduction
A major struggle for application developers is dealing with the different mobile ecosystems, Android vs iOS. Flutter aims to abstract the differences away from developers by providing a single unified API which developers can depend on.
At its core, Flutter is an SDK. This SDK exposes UI elements and other common elements (e.g. HTTP/S clients) that map to native equivalents in the Android and iOS spheres behind the scenes. The SDK achieves this through a combination of Dart and C/C++ integrations:

From a Flutter development perspective, developers write code that interact with what is commonly known as the Framework. The framework is a cross-platform layer written in Dart. It includes a rich set of platforms, layouts and foundational libraries. Many higher-level features that developers might use are implemented as packages, including platform plugins that can; make use of a device’s camera, display WebViews, engage in HTTP communication, display animations, and other functions.
Since Flutter is cross-platform, it needs a unified layer that can translate to Android and iOS. In Flutter, this unified layer is known as the Engine. The Engine is a portable runtime for hosting Flutter applications that contain the required SDK for Android and iOS. It is mostly written in C++ and provides primitives to support all Flutter applications.
Using Android as an example, the Flutter application would be written in Dart. Since Android does not natively run Dart, the Framework instructions will get forwarded to the Engine. The Engine then contains a Dart VM, which converts those instructions to Java, which finally gets executed by Android underneath.

There is, however, one extra aspect to keep in mind. Flutter is not only a UI framework. A developer can extend the logic in the Engine with their own code. So when compiling a Flutter application, the Engine is created anew each time – better known as an AOT (Ahead-of-Time) App Snapshot. The snapshot contains the machine code of both the Flutter framework and the developer’s source code.
With the above in mind, when one reviews the structure of a Flutter mobile application, you will find it very different to native Android and iOS applications.
Here is the structure of an example Android application:

Notice in the output above how we have a classes.dex
file and then a lib folder containing several Flutter shared libraries. In a regular Android application, our focus would be on the classes.dex
file. Using Jadx, one could reverse the Dalvik instructions. In Flutter, this is still possible; but the Dalvik instructions do little more than invoke the embedded Flutter shared libraries.
Given the above, our attention shifts to the shared library files. A Flutter application typically has a lib
folder; within it, you will find a directory specific to the architecture(s) the application was built for. In Android cases, you will find ARM64 and/or ARM (i.e. arm64-v8a). Inside that directory, you will then find two files: libapp.so
and libflutter.so
.
The libflutter.so
file contains the required functionality for using the OS (network, file system, etc.) and a stripped version of the DartVM. From a mobile application assessment perspective, this file becomes a major focus; since it contains the SSL certificate pinning and verification logic, among other things!
The libflutter.so
file does not contain execution logic and is incapable of loading the Dart source code itself. Instead, the libapp.so
file is a wrapper (or, more aptly, a loader around it).
Diving Deeper into the Shared Libraries
As seen in the section above, Android Flutter-based applications are largely driven by the two shared libraries: libapp.so and libflutter.so.
While not technically required for assessments, it does help to know which version of Flutter was used by the mobile developer. At least since it might allow cross-referencing to specific functions or logic we might want to alter.
The libflutter.so and libapp.so files both contain an MD5 hash (known as the snapshot_hash) that corresponds with the build version. reFlutter contains a script that can be used on the libapp.so file to retrieve this hash:
python3 get_snapshot_hash.py libapp.so adb4292f3ec25074ca70abcd2d5c7251
In our application’s case, the script returned the above MD5 hash. From this, we could now narrow down the exact Flutter version using reFlutter’s enginehash file:

The first column gives the Flutter version, while the second gives the engine hash. The engine hash is tied to a GitHub commit that contains all the dependencies and resources used to build that specific version of Flutter.
In our example, the commit and its dependencies would be found at:
https://github.com/flutter/engine/blob/1a65d409c7a1438a34d21b60bf30a6fd5db59314/DEPS
You might be wondering why the reference to the above commit is useful. To tamper with the Flutter logic, we need access to the specific Dart SDK on which the mobile application depends. After all, the Dart SDK is responsible for network communication – or in our case, SSL certificate verification.
Following the link above, we can discern the exact Dart SDK commit that the Flutter Engine depended on during the build process for Flutter 3.7.12:
# Dart is: https://github.com/dart-lang/sdk/blob/main/DEPS # You can use //tools/dart/create_updated_flutter_deps.py to produce # updated revision list of existing dependencies. 'dart_revision': '5b8936496673a285bf8f37472119bc2afb38a295',
Once again, we can search for that revision in the Dart SDK GitHub repo:
https://github.com/dart-lang/sdk/tree/5b8936496673a285bf8f37472119bc2afb38a295
Looking at the commit history, we find that the link refers to Dart SDK v2.19.6. The SDK can be rather complex, but the main area of interest is the /runtime/bin
directory. Inside, you will find many C/C++ files containing the logic Flutter uses underneath.
If we wanted to tamper with the proxy configuration for the Flutter application, you will narrow your search to the socket_android.cc
file:
Narrowing Down the X509 Verification Logic
So we can narrow down specific files and sections that we might want to tamper with, or alter, to change the behaviour of a Flutter application using the previous section’s steps.
Unfortunately, when I went down the above route, I found references to X.509 in the Dart SDK, but I could not identify how the SSL certificate verification was done. Then I noticed some headers at the top of a few files:
#include <openssl/ssl.h> #include <openssl/x509.h>
Online searches revealed that Flutter does not perform SSL certificate verification within the Dart SDK. Instead, it depends on a third-party SSL library. Returning to the earlier Flutter dependency links, I narrowed it down to the BoringSSL library:
'dart_boringssl_rev': '87f316d7748268eb56f2dc147bd593254ae93198',
The versioning here is perhaps less important since the BoringSSL library does not change that frequently. But one could use the commit hash above (originally from the Flutter dependency page) to narrow down the specific library version.
While scouring the BoringSSL library, I managed to identify that the SSL certificate verification logic resided in the /ssl/ssl_x509.cc
file and the contained ssl_crypto_x509_session_verify_cert_chain
function:

The function returns a boolean that indicates whether the presented SSL certificate is valid or not. To bypass the verification logic, one needs to modify the return value to always be ‘true
‘.
Note: The BoringSSL library within the context of Android Flutter applications does not utilise the device’s CA stores. Instead, it uses its own store and, in some cases, may even depend on Google’s own online verification routines.
However, the above logic resides several layers deep in the Flutter Engine file (libflutter.so
). Since it is in native C/C++, there are no immediate references to it when using Frida or Objection. To further complicate things, the BoringSSL library and Dart SDK’s symbols are stripped when the libflutter.so
file gets created.
This was when I realised why tooling like reFlutter existed. You would have to modify the logic at build time in the Dart SDK even before the libflutter.so
file has been created. At least, that was my understanding at the time!
Patching with the reFlutter Tool
As we have seen in the previous sections, if one wants to modify the logic embedded within a Flutter application, it requires diving deeply into the dependency tree of the Flutter framework. This is a time-consuming exercise you would not want to repeat regularly.
The reFlutter tool was developed to simplify the process of tracking down specific security measures in the Dart SDK and patching them such that testing of Flutter-based applications is easier to perform.
The author (Philip Nikiforov) of the tool realised that due to the compression and stripping of symbols in the libflutter.so file; patching or modification on the file itself would be extremely difficult. One would basically have to reverse engineer and apply custom logic each time. As such, it would not be an easily repeatable process. Not to mention, each version of Flutter and the Dart SDK would change this process.
Instead, one can focus on the raw source code of the Engine being built into the libflutter.so file. Since Flutter and the Dart SDK is open-source, the build process to generate the file is publicly known (explained here and here). As such, one could start with the source code, modify it and then compile a custom version of the libflutter.so file – the caveat being you would need to know the exact Flutter version that the mobile application depends on, which we saw earlier is achievable.
Retracing the steps of the official Flutter build process, the reFlutter tool pulls down the Flutter SDK from GitHub and all its dependencies. It then creates a folder structure as follows:

Here, all the files needed to create a Flutter snapshot can be found: artifacts
, flutter_patched_sdk_product
, and gen_snapshot
. The lib.stripped
folder contains the libflutter.so
engine, which will be copied into a folder with libapp.so
during the compilation of the APK.
Inside the lib.stripped
folder, you will find the source code for all the third-party dependencies. For example:
src/third_party/dart/runtime/bin/socket.cc <--- Proxy capability src/third_party/boringssl/src/ssl/ssl_x509.cc <--- SSL cert verify
To patch the BoringSSL certificate verification logic, the reFlutter tool uses regular Python text replacement to adjust the ssl_x509.cc
file such that the verification logic is bypassed. The patching logic can be seen here:
https://github.com/Impact-I/reFlutter/blob/main/src/init.py#L325
Tracing the replacement logic, the earlier ssl_crypto_x509_session_verify_cert_chain
function would be adjusted as follows:

Since the Flutter framework does not respect the Android device’s proxy settings, the reFlutter tool also holds logic to adjust the Dart SDK’s Socket creation logic (socket.cc
) to force a proxy:
https://github.com/Impact-I/reFlutter/blob/main/src/init.py#L322
In the above case, the socket.cc
file would be adjusted to include a hardcoded proxy IP address and port:

Once the patching is completed, the libflutter.so is built from the adjusted source code. It can then be embedded into the original mobile application. After which, one can use an intercepting proxy, load a custom CA onto the mobile device and proxy network traffic.
Note: To avoid repeating this process for every APK and IPA, the reFlutter author pre-patched several versions of Flutter. These pre-patched libflutter.so files can be found in the GitHub repository’s releases. When using the reFlutter tool, it first checks whether a pre-patched file already exists for the targeted APK or IPA. If so, then it just downloads the necessary file. If not, you would have to manually perform the above steps – a Dockerfile helps with this process!
Going Hardcore: The Search for a Frida Equivalent
The patching that the reFlutter tool performs is done at source code level. It works fantastically well and gets the job done! But, I started wondering while helping my fellow colleague; whether one could achieve similar results using Frida. After all, it did not seem like an impossible task to achieve. At least, not for the certificate verification logic. Changing a boolean return type is simple – most of the time!
My search started by investigating the classes and class methods Frida and Objection could pick up at runtime within the target mobile application:
com.org.package on (google: 12) [usb] # android hooking list classes
...
[Landroid.animation.Keyframe$FloatKeyframe;
[Landroid.animation.PropertyValuesHolder;
[Landroid.app.AppOpsManager$RestrictionBypass;
[Landroid.app.LoaderManagerImpl;
[Landroid.app.VoiceInteractor$Request;
[Landroid.app.admin.PasswordMetrics$ComplexityBucket;
[Landroid.app.appsearch.AppSearchSchema$PropertyConfig;
...
I immediately encountered problems tracking down the classes from the libflutter.so file. Objection’s ‘hooking list classes’ functionality only shows Java classes and not native C++ classes. As such, it would not show the classes from the libflutter.so file.
The second problem was even more complicated. One could possibly still hook the functions in the libflutter.so file provided that it exports those functions. However, when I reverse-engineered the libflutter.so file with Ghidra, I noticed that the file exported very few functions, and they were all stripped and mangled:

Chatting with @leonjza (the creator of Objection), he mentioned that it might be fruitful to start by seeing whether I could locate the libflutter.so module in memory while the mobile application was running. Since that will possibly give entry to the functions hosted inside it.
I fired up Frida and attached it to the running application. Then used Process.enumerateModules() to see whether the libflutter.so file was loaded:
[Android Emulator 5554::com.org.package ]-> var modules = Process.enumerateModules()
[Android Emulator 5554::com.org.package ]-> modules
[
...
{
"base": "0x795181600000",
"name": "libflutter.so",
"path": "/data/app/~~QXyGrDRGt5-Lj2Jd_tmopw==/com.org.package-TZxV9QSv5kQ5gUNcaBv5LQ==/base.apk!/lib/x86_64/libflutter.so",
"size": 11165696
},
...
]
As seen above, the libflutter.so file was indeed loaded into the running process. At a minimum, that allows us to get a handle on the module and inspect it:
[Android Emulator 5554::com.org.package ]-> var flutter = Process.getModuleByName("libflutter.so")
[Android Emulator 5554::com.org.package ]-> flutter
{
"base": "0x795181600000",
"name": "libflutter.so",
"path": "/data/app/~~QXyGrDRGt5-Lj2Jd_tmopw==/com.org.package-TZxV9QSv5kQ5gUNcaBv5LQ==/base.apk!/lib/x86_64/libflutter.so",
"size": 11165696
}
Using the handle above, I started to explore the imports and exports of the module but quickly realised that it did not hold much success – as Ghidra confirmed as well. For example, the exports just showed this:
[Android Emulator 5554::com.org.package ]-> var exports = flutter.enumerateExports()
[Android Emulator 5554::com.org.package ]-> exports
[
{
"address": "0x79518196cd99",
"name": "JNI_OnLoad",
"type": "function"
}
]
Out of ideas, I revisited my earlier Google searches. In doing so, I came across references to two public blog posts from 2019 and 2020 where Jeroen Beckers had bypassed the SSL certificate pinning in Flutter using Frida. Reviewing those blog posts, I realised that Jeroen was using memory pattern matching to grab the specific functions (i.e. ssl_verify_result
) from within the libflutter.so module:

I retraced the steps he published but had no success replicating the result. Naturally, the Flutter and Dart SDKs had changed since 2019, which meant the function(s) he was hooking did not exist anymore – or were not the same. That said, the blog posts did narrow down which function(s) might be useful.
Continuing my research online, I noticed that Jeroen had released an update in 2022:
Although not exactly the same as our current application, he mentioned briefly in the update that Flutter had shifted some logic. In the latest iterations, the target function to bypass the SSL certificate verification logic is ssl_crypto_x509_session_verify_cert_chain
.
Righto, so it is the same one reFlutter uses and the one we identified earlier. I had the target function at this stage, but I still needed to track down a way of hooking it in memory using Frida. To achieve this, I had to find the assembly instructions that denoted the start of the function – similar to Jeroen’s script above.
Back in Ghidra, I searched for string reference to the ssl_x509.cc
file:

From the search, I managed to learn that the libflutter.so file (at least, the one present in our current application) held nine references to the file:

These are cross-references to anything Flutter and the Dart SDK depends on within the BoringSSL library. As such, within the nine references, I had to track down which was the session_verify_cert_chain
function.
Unfortunately, identifying the function turned into quite the exercise. Remember the libflutter.so file is stripped, which means no symbols are available. For example, this is the output Ghidra showed when navigating to a cross-reference of the ssl_x509.cc
file:

At first, I was lost in how one would make sense of these cross-references. My search online continued, but I mainly found other blog posts referencing Jeroen’s research. That is, until I managed to track down this Vietnamese post:
https://sec.vnpt.vn/2022/04/frida-hooking-va-bypass-ssl-pinning-cho-flutter-aplication/
In the blog post, the author had the following note:
The definition of the session_verify_cert_chain function is in the file ssl_x509.cc.
Okay, now the goal is to change the above function’s return to true. And also note that the above function has 3 arguments passed (will help us find the correct function when decompiled with Ghidra).
In a flash of realisation, I understood what the author had meant with his note. The arguments of the session_verify_cert_chain
function remains intact even when stripped. Or simply put, the function still needs to receive three arguments:

As such, I could narrow my search to a cross-reference having a signature with three arguments. Fortunately, it turns out that only one of the nine cross-references satisfied this requirement:

I do not know how this code maps back to the original logic. Perhaps it comes down to Ghidra’s reversing of the library, but without the Vietnamese blog post, I would never have known this was the function.
Afterwards, with the function identified, I could use Ghidra to capture the first few assembly instructions that (hopefully) uniquely identified the function in memory:

Note: The assembly shown above is taken from the x86_64 libflutter.so file; since I was using an Android Studio emulator.
With the assembly instructions in hand, I revisited Jeroen’s blog posts and took inspiration from the Frida scripts he had previously written to write my own:
function hook_ssl_verify_result(address) { Interceptor.attach(address, { onEnter: function (args) { console.log('Disabling SSL validation'); }, onLeave: function (retval) { console.log('Retval: ' + retval); retval.replace(0x1); }, }); } function disablePinning() { var m = Process.findModuleByName('libflutter.so'); var pattern = '55 41 57 41 56 41 55 41 54 53 48 83 ec 38 c6 02 50 48 8b af a8 00 00 00'; var res = Memory.scan(m.base, m.size, pattern, { onMatch: function (address, size) { console.log('[+] ssl_verify_result found at: ' + address.toString()); hook_ssl_verify_result(address.add(0x00)); }, onError: function (reason) { console.log('[!] There was an error scanning memory'); }, onComplete: function () { console.log('All done'); }, }); } setTimeout(disablePinning, 1000);
Showcasing the Frida Script
To close out the blog post, I will first demonstrate the failure when an intercepting proxy (Burp Suite) is introduced to proxy the Flutter application without any bypass:

The screenshot above shows that the BoringSSL library rejects the SSL handshake since the endpoint’s certificate is now self-signed. Remember that this occurs even when the Burp CA is loaded in the system CA store!
The issue above stems from the fact that the session_verify_cert_chain
function returns an invalid response, which then causes the SSL handshake to be aborted.

If the Frida script above works, we can change that response allowing the SSL handshake to complete. At that point, we can intercept the network traffic and go on our way!
So let us exit out of the running mobile application and then run the Frida script while spawning the application afresh:
frida -U -l bypass.js -f com.org.package
Shortly after the application spawns, the Frida script hooks the intended function based on the assembly instructions in memory:

Then the moment of truth! Can we raise communication from the Flutter application while proxying through Burp Suite? Absolutely, yes!

Back in the Frida console, we notice that the hooked function originally would have returned a 0x1
response, but our script modified it such that it returned 0x0
instead:

As such, we bypassed the SSL certificate verification step, and the SSL handshake was completed with Burp Suite without interference. Naturally, at this point, the mobile assessment would start by focusing on the network traffic and any vulnerabilities it may contain.
Sidebar: Proxying the Traffic
If you have a keen eye, you might recall earlier in the blog post, I said Flutter is not proxy aware. So you might be asking yourself, how did I intercept the traffic above?
Since I am using an Android Studio emulator, I control networking at the hardware/simulation level. Within Android Studio, you can set a global proxy for an emulated device by navigating to the device’s Settings -> Proxy:

Note: More recent versions of Android Studio have changed where this setting is located. However, it is still possible to configure.
The setting above forces all traffic to be routed to the proxy regardless of whether the application is proxy-aware. This a very nifty trick when dealing with Flutter applications.
As an aside, if you are using a regular Android device, you could achieve the same result by setting up a custom /etc/hosts file on the device or using a custom Wi-Fi access point + DNS server and pointing specific hostnames to your Burp Suite instance’s IP address.
Conclusion & Summary
You’ve made it to the end of this blog post. An achievement in itself!
In this blog post, I shared insights into how the Flutter framework is structured and its different elements. We learned that underneath the hood, it employs the Dart SDK alongside several other dependencies (i.e. BoringSSL).
Through exploration of the reFlutter tool, we learned that one can patch the raw source code of the Flutter Engine before it is compiled and embedded into an APK or IPA. To this extent, the reFlutter tool does much of the heavy lifting, making the patching process significantly easier. More so, since the associated GitHub repository contains several pre-patched libflutter.so files for Android and iOS.
With a clear understanding of the inner workings of the reFlutter tool, attention shifted to a Frida equivalent. Using several public blog posts, the libflutter.so file and stumbling around Ghidra, I wrote a Frida script to hook the session_verify_cert_chain
function within the BoringSSL library and bypass Flutter’s SSL certificate verification logic.
To be clear, the Frida script only caters for the x86_64 architecture and the libflutter.so file found within an application I helped a colleague with. That said, this blog post’s steps and the Frida script could serve as a template for the steps to be repeated for ARM and ARM64; if somebody finds themselves dealing with Flutter on a real device soon.
I hope that the content of the blog post was interesting. Before this research, I knew very little of how Flutter operated and why it was so difficult to pentest mobile applications based on it. Having explored it, it is a strange beast to encounter and requires quite a bit of insight into Dart, C/C++ and reverse engineering with IDA or Ghidra.
Until next time, stay curious and happy hacking!
References
- https://swarm.ptsecurity.com/fork-bomb-for-flutter/
- https://blog.nviso.eu/2019/08/13/intercepting-traffic-from-android-flutter-applications/
- https://blog.nviso.eu/2020/06/12/intercepting-flutter-traffic-on-ios/
- https://blog.nviso.eu/2022/08/18/intercept-flutter-traffic-on-ios-and-android-http-https-dio-pinning/
- https://github.com/Impact-I/reFlutter/
- https://docs.flutter.dev/
- https://github.com/frida/frida/issues/434
- https://github.com/frida/frida/issues/2265
- https://frida.re/docs/javascript-api/
- https://raphaeldenipotti.medium.com/bypassing-ssl-pinning-on-android-flutter-apps-with-ghidra-77b6e86b9476
- https://www.horangi.com/blog/a-pentesting-guide-to-intercepting-traffic-from-flutter-apps
- https://github.com/google/boringssl/blob/master/ssl/ssl_x509.cc
- https://sec.vnpt.vn/2022/04/frida-hooking-va-bypass-ssl-pinning-cho-flutter-aplication/
- https://github.com/horangi-cyops/flutter-ssl-pinning-bypass/blob/main/flutter-bypass-sslpinning.js
- https://blog.nviso.eu/2020/11/19/proxying-android-app-traffic-common-issues-checklist
- https://medium.com/hackernoon/intercept-https-traffic-on-a-android-emulator-46023f17f6b3