In my previous post Bypassing MTLS in Flutter, I discussed how MTLS is generaly implemented in the Flutter framework. Depending on how an application is developed, exposing certificates can become a security issue. If an attacker gains access to the certificate, MTLS is rendered useless. This reinforces the analogy: “It’s pointless to protect the front of your house with lasers, cameras, guard dogs, and motion detectors while leaving the windows and doors wide open.”

In this blog post, I will explain how to bypass scenarios where the application encrypts the request body (RSA) before sending it to the server. This technique is commonly seen in banking applications or other apps with mature security practices in mobile development.

Understanding RSA Encryption

The most commonly used encryption method in such cases is RSA. Let’s break it down in simple terms:

In mobile applications, RSA is generally used for communication security and authentication (and, in some cases, even authorization). TL;DR:

  1. Secure Key Exchange: RSA is often used to exchange symmetric encryption keys (like AES). Since RSA is slow for large data volumes, it primarily secures the transmission of a faster encryption key.
  2. Authentication and Digital Signatures: Apps use RSA to ensure messages or transactions originate from a trusted source. This is achieved by signing content with a private key and verifying it with a public key.
  3. Secure Communication (HTTPS/TLS): When an app connects to a secure server, RSA is used to establish a TLS/SSL connection. The server sends its public key, the app encrypts a session key with it, and subsequent communication is encrypted with a more efficient algorithm.
  4. Sensitive Data Protection: Some apps store encrypted information locally, using RSA to safeguard credentials or critical data.

RSA plays a crucial role in app security but is often combined with faster methods (like AES) to ensure both security and performance. The following image illustrates how RSA works:

Implementing RSA in Flutter

Now that we understand how RSA works, let’s explore how Flutter apps implement it. Based on empirical research, most Flutter applications use the following libraries:

  • dio - A powerful HTTP networking package.
  • pointycastle - A Dart library for encryption and decryption.

In general, there are two common implementation approaches:

  1. The application requests the public key from the backend, which is then encrypted and stored in SharedPreferences on Android using another widely used Flutter package: flutter_secure_storage.
  2. The public key is hardcoded in the Flutter code, meaning you would need to reverse-engineer the app or identify the exact function call that utilizes the key.

To simulate this scenario, I developed a Flutter app with a Node.js backend. The backend holds the private key, while the Flutter app contains the public key to encrypt the request body. This is a common setup in REST API or GraphQL applications.

However, to make it more realistic, I compiled the Flutter app in release mode rather than debug mode (More easy to reverse). When a Flutter app is built for production (e.g., for Google Play Store), several optimizations are applied to the binary (libflutter.so and libapp.so), including:

  • Dart Ahead-Of-Time (AOT) compilation.
  • Execution inside a Dart Virtual Machine (VM).
  • Serialization of code and data into a binary snapshot.
  • Stripping of symbols in release mode.
  • Full optimization.

For more details, check out the talk by Worawit: B(l)utter Reversing Flutter Application Using Dart Runtime.

Analyzing the Android APK

In a real scenario, obtaining the APK file is the first step. Once you have the APK, you can use tools like APKTool with the command apktool d foo.apk to decompile it.

Since I built the app myself, I simply compiled it in release mode, signed it, and installed it on a physical device:

  • Build and sign:

  • Install with ADB:

With the APK in hand, we can use Worawit’s Blutter tool. Blutter locally initializes the Dart app and inspects objects like the Object Pool and Thread Pool, helping us understand its structure.

The next step is to analyze Blutter’s output and identify function addresses for hooking. Each application is different and may require a unique approach (shoutout to @edunovella 🍻 for this reminder!).

Since we aim to intercept the request before encryption, we first attempt to capture it using a proxy. Flutter has some quirks in this area; I recommend reading this blog for insights into Flutter and HTTP proxying.

A good reversing approach is to search for the app’s entry points. Using Blutter, we can inspect the main package (flutter_body_encrypt_rsa) and locate the relevant class:

In this case, we find rsa_helper.dart, which appears to handle encryption:

Hooking Flutter’s Encryption Function

To proceed, I analyze the loaded libraries and locate the libapp.so and libflutter.so addresses using Frida:

Java.perform(function() {
    console.log("[*] Waiting for libraries to load...");

    setTimeout(function() {
        console.warn("[*] Loaded libraries:");
        var modules = Process.enumerateModules();

        modules.forEach(function(m) {
            if (m.name.toLowerCase() === "libflutter.so") {
                console.warn("[*] libflutter found!", m.name);
            }
            console.log("[*] " + m.name + " -> " + m.base);
        });
    }, 1000);
});

From Frida’s output, we can verify that we have the address of libflutter.so and can also retrieve addresses for other libs. The most interesting one for us is libapp.so, where the application’s core logic is usually located.

By analyzing the code more deeply 👀, we can understand that before calling the encrypt function, the app performs a JsonEncode, which is likely where the Dio package uses Flutter’s convert package before encrypting the request:

I usually use the following Frida script to interact with lower-level functions within libapp.so. The idea is to get the base address of libapp.so, find the jsonEncode() function, and perform a hexdump. You can search for all BL (Branch with Link) instructions in ARM. The BL instruction performs a jump to a function/subroutine and stores the return address in the LR (Link Register). You can do this for all functions.

The hook I generally use is as follows:

setTimeout(function() {

var flutterBase = Module.findBaseAddress("libapp.so");

    if (flutterBase) {

        var encryptFnAddr = flutterBase.add(0x337694);

            console.warn("[*] Function found at:", encryptFnAddr);

    Interceptor.attach(encryptFnAddr, {

            onEnter: function(args) {

            console.log("[*] Entering encrypt function...");

  

    try {

            var size = 1024;

            var buffer = Memory.readByteArray(args[0], size);

            console.log("[*] Hexdump (function) args[0]:\n" +

            hexdump(buffer, { offset: 0, length: size, header: true, ansi: true }));

    } catch (e) {

            console.error("[!] Error reading input (args[0]):", e);

    }

},

onLeave: function(retval) {

        try {

            var size = 1024;

            var buffer = Memory.readByteArray(retval, size);

            console.log("[*] Hexdump return value (encrypted):\n" +

            hexdump(buffer, { offset: 0, length: size, header: true, ansi: true }));

        } catch (e) {

            console.error("[!] Error reading return value:", e);
        }

    }

});

} else {

        console.log("[!] libapp.so not found!");
}

}, 1000);

Finally, we successfully intercept the data before encryption:

If you enjoyed this post, send feedback on X (@incogbyte) or email me at incogbyte@protonmail.com. Let me know if you’d like a part 2!

References: