Intercepting HTTPS Communication in Flutter: Going Full Hardcore Mode with Frida
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.
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
[...]