Bypassing Wi-Fi Check on a Flutter-Based iOS App That Uses connectivity_plus Plugin

Last month I had to do pentest on a couple of iOS apps developed using Flutter framework. All of them are financial related and have some security features in common. One of the features deployed on the apps is prohibiting users from using some functions when the app connects to internet using Wi-Fi e.g. transfer money, onboarding, or account recovery, etc. When performing pentest, our consultants need to intercept traffic between the app and its server and Wi-Fi is needed to intercept the traffic. The problem occurs when developer teams reject to disable Wi-Fi check and deploy a new app version for us to test.

I overcome this problem by writing a Frida script to bypass Wi-Fi check on an iOS app which uses connectivity_plus plugin to check connectivity type. This post explains some details about it.

Let’s build an app

For building an app that uses connectivity_plus Flutter plugin to check the connectivity type, I use the example codes available on its pub.dev, https://pub.dev/packages/connectivity_plus/example, here is how it looks like:

The app tries to detect and display the connectivity type in simple text format just like the following picture:

As you can see in the picture, the plugin tries to determine which network type it is using then displays a message indicating its connectivity type. Almost developers use this feature to detect if their apps connect to a network using Wi-fi or cellular especially for a banking app that requires their users to perform some actions by using only cellular network. We, as a penetration tester, need to bypass this check or somehow circumvent it to use Wi-fi during a test.

Let’s read connectivity_plus plugin’s Git repo.

It is being easier to analyze the plugin’s logics when the source code of the plugin is available on Github because it is an opensource library. You can find its source code at https://github.com/fluttercommunity/plus_plugins/tree/main/packages/connectivity_plus/connectivity_plus

Begin with ConnectivityProvider.swift file:

The Swift codes show 2 important variables, one is an enumeration of ConnectivityType which are 4 possible values, 0 – none, 1 – wireEternet, 2 – wifi, and 3 – cellular. The second one is currentConnectivityType which has its own getter method.

The next Swift file is “SwiftConnectivityPlusPlugin.swift” which contains the logic of the plugin and how does it return the current connectivity type to caller. The first few lines of SwiftConnectivityPlusPlugin.swift are just Swift-Flutter method calling convention. The main logics are highlighted in the following picture. The location “1” indicates declaration of the connectivityUpdateHandler method, an asynchronous method that updates the connectivityType variable during running. “2” is the method that registers the handler to event listener.

We can summarize the logic of the plugin as: it checks for current connectivity type then update the variable connectivityType. Because the ConnectivityType has its own getter method, the single point of logic we can use Frida script to hook it and change its return value.

Let’s reverse the connectivity_plus plugin

Decompress the IPA file built from the sample code and extract the connectivity_plus binary from its containing directory.

I use Ghidra to reverse the binary and find an exported function related to getter of currentConnectivityType. The following picture shows the method.

The offset of the method is at 0x6c7c.

Writing Frida Script

To hook at the getter method of currentConnectivity type, I write the following Frida script:

function bypass_connectivity_plus_byHookOffset() {
    var targetModule = 'connectivity_plus';
    var offset = ptr(0x6c7c);
    var moduleBase = Module.getBaseAddress(targetModule);
    var targetAddress = moduleBase.add(offset);
    Interceptor.attach(targetAddress, {
        onLeave: function(retval) {
            if (retval != 0x3) {
                retval.replace(0x3); 
                console.log("[+] Wi-Fi check bypassed");
            }
        }
    });
}

// usage examples
if (ObjC.available) {   
    bypass_connectivity_plus_byHookOffsets();
} else {
    send("error: Objective-C Runtime is not available!");
}

The following picture shows the app screen on iOS device indicates that the bypass is successful (app shows mobile network when connecting the network using Wi-fi).  

On problem I found during using this Frida script is I need to reverse engineer the connectivity_plus plugin every times I got a new app version because the offset of the getter method could have been changed during compiling. So I find a new way of hooking the getter method by using ApiResolver to find the method I want and hook it.

The following Frida scripts demonstrate that.

function bypass_connectivity_plus_byHookSearch(searchstring) {
    var type = "module";
    var res = new ApiResolver(type);
    var matches = res.enumerateMatchesSync(searchstring);
    var targets = uniqBy(matches, JSON.stringify);

    var target = targets[0];
    console.log("[!] Found at address: " + target.address + ", name: " + target.name);
    Interceptor.attach(target.address, {
        onEnter: function(args) {
            console.log("[!] Hook: " + target.address + ", name: " + target.name);
        },
        onLeave: function(retval) {
            console.log("\t[!]  retval: " + retval);
            if (retval == 0x3) {
                retval.replace(0x3); // 0x0 = none, 0x1 = ethernet, 0x2 = wifi, 0x3 = mobile
                console.log("\t\t[+] Wi-Fi check bypassed");
            }
        }
    });
}

// usage examples
if (ObjC.available) {   
    bypass_connectivity_plus_byHookSearch("exports:connectivity_plus!*current*Type*");
} else {
    send("error: Objective-C Runtime is not available!");
}

I publish the code on Frida codeshare at https://codeshare.frida.re/@zionspike/bypass-wi-fi-check-on-flutter-based-ios/

Reference and Thanks: