December 6, 2020

Explore & Reverse iOS Flutter App

Explore & Reversing iOS Flutter App

In this article we will perform a static and partly dynamic analysis on the app “Human Firewall”. The purpose of the analysis is to understand the behavior of the app, draw conclusions about the technology, collect information and finally see if an attack vector is available. The information in this article is mostly taken from a paper I published internally. Here (made with love from d lab_)you can read more about the activities and the authors of the app.

Preparation

Hardware

All examples in this article are based on the following hardware:

  • iPhone7 (iOS 12.4.1)
  • MacBookPro 2019 (Catalina 10.15.5)

Software

To be able to follow all examples and code snippets, please make sure that you have the following programs/libraries installed:

python

We are going to use all lot of tools that requires an installed python runtime and environment. All examples in this article based on python3.8.3.

If you struggle to install python have a look here.
We are also using pythons pip to install additional packages. Make sure that pip is installed. All examples in this book bases on pip 20.0.2. If you struggle to install python pip have a look here.

frida

Please make sure that you have the latest version installed. Installation instructions can be found here.

mitm proxy

For a deep traffic analysis we’re going to use mitm proxy. See installation instructions here.

checkra1n

To be able to work relatively relaxed we are going to jailbreak the iphone with checkra1n. checkra1in installation guide can be found here.

iphone

In this guide we’re going to inspect the app Human Firewall. Search in the app store for human firewall and install the app.

JailBreak your iphone

Please start checkra1n and follow the instructions to jailbreak your iphone. You can either run in GUI mode or terminal mode. checkra1n tells you exactly what to do.

Start in terminal mode:

./Applications/checkra1n.app/Contents/MacOS/checkra1n

After successfully jailbreak your iphone you should see Cydia and checkra1n on your home screen.

Installing and preparing frida

From offical frida: It’s a dynamic code instrumentation toolkit. It lets you inject snippets of JavaScript or your own library into native apps on Windows, macOS, GNU/Linux, iOS, Android, and QNX. Frida also provides you with some simple tools built on top of the Frida API.

Start Cydia and add Frida’s repository by going to Manage -> Sources -> Edit -> Add and enter https://build.frida.re

Open cydia and search for frida. Then install frida for pre-A12-devices:

After the frida installation is finished connect your iphone via usb to your macbook. Open a terminal an enter:

frida-ps -Uai

Great, frida is prepared and we can continue.

Prepare objection

For deeper inspection and information gathering we are going to use objection. You can install objection via pythons pip:

pip install objection

You can read more about objection on the objection official GitHub repository here.

Information gathering

Information Gathering is the act of gathering different kinds of information against the targeted victim or system. The more the information gathered about the target, the more the probability to obtain relevant results.

Bundle investigation

Now let’s see what information we can extract from the app. For this we want to use objection. We proceed as follows:

frida-ps -Uai

The bundle identifier of our app is: com.innogy.humanFirewall.public. Now we want to look deeper into the app and find out what technology we have here. Please connect objection against the target app. We can do this with the following command:

objection --gadget "com.innogy.humanFirewall.public" explore

If all goes fine you should see that objection is now connected to app and give us a REPL(Read-Eval-Print-Loop) for further inspection.

Get more information about the binary.

ios info binary

This information gives us a good indicator that the app has been programmed using Flutter. Let’s try to get more information about possible frameworks:

ios bundle list_frameworks

After a first look at the framework list we can assume that the app interacts with a sqlite database. Interesting is also the secure storage framework. We will investigate this further in the course of the keychain analysis.

At this point you should start the app, create an account and log in to the app. Request your access token, login, and you should see the human firewall dashboard. After successful login you should be able to see the dashboard of the app. Should it be the case that you can no longer register, you can still see the next steps. The shown procedures let you transfer directly to other apps.

Dump device keychain

In objection run:

ios keychain dump

Ok. App uses keychain to store an access token and a refresh token.

Dump NSURLCredentialStorage

Here you can store password-based credentials permanently. Read more about it here.

ios nsurlcredentialstorage dump

Ok, there are no information inside. Maybe we have more luck when we going to inspect the NSUSerDefaults:

ios nsuserdefaults get

Dump app plist file

ios plist cat Info.plist

Summary information gathering

In this section, we’ve learned how we can use frida together with objection, to get some interesting static information (bundle, keychain, user defaults, frameworks) from the app.

Memory dump

The next step is to dump the memory of the app and search for information that are maybe helpful for further use (passwords, static links, etc.). To easy find our memory dump on our host, we’re going to create a folder called “objection_out”.

mkdir objection_out && cd objection_out && pwd

Then run objection against your iPhone from the fresh created folder:

objection --gadget "com.innogy.humanFirewall.public" explore

Then dump the whole memory. This command creates a file “memory_dump” on our host:

memory dump all memory_dump

First thing we can do is search for printable characters by using the string command.

strings memory_dump > strings_memory_dump

Then grep for example “http*” or other parts that are interesting for you. If have found the following interesting parts:

  • user-agent: Human Firewall App
  • Base url: https://human-firewall.cfapps.mila.external.ap.innogy.com/api/v1/profile/de
  • authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleH…
  • GET /api/v1/profile/de
  • GET /api/v1/badge/de
  • GET /api/v1/profile/de
  • POST /oauth/token#

Traffic Analysis

Let’s switch to a more dynamic approach by trying to intercept the live http
communication from the app. We are going to user our macbook as gateway and route all the iphone traffic over it. To use our macbook as a gateway you must allow IP forwarding by entering the following command:

sudo sysctl -w net.inet.ip.forwarding=1

Then we need to create a virtual bridge network interface. Go to “System Preferences -> Network” and create a new bridge for your WiFi adapter:

Accept and save the new network settings and go back to your terminal. We will now check if the new virtual interface is available.

networksetup -listallhardwareports

Last step is to enter your macbook ip address as gateway address on you iphone and test if you can reach the internet.

man-in-the-middle proxy

You can simply install mitm via brew:

brew install mitmproxy

After installation is complete, start the mitm web interface by entering:

mitmweb

port forwarding

On mac we need to set and enable a port forwarding rule:

echo "rdr pass on bridge1 inet proto tcp to any port {80, 443} -> 127.0.0.1 port 8080" > pf.conf
sudo pfctl -e -f pf.conf

recording traffic

At this time we should be able to record the iphone traffic, which is routed over our mac. Start the proxy with the following command:

sudo mitmweb --anticomp --mode transparent --anticache --showhost

On your iphone open any website. You should see the http traffic in the mitm web interface.

It is also sufficient to simply restart the app and see which communication goes through the network. For a specific analysis of the traffic at login, you have to re-login to the app. The best way to do this is to use a new token.

To work with ssl traffic, please follow the instructions from mitm proxy here.

With mitm you now have a powerful tool to analyze and manipulate data traffic.

Dynamic analysis

Our first static analysis has already provided some very interesting information. We now want to take a dynamic approach and see what information we can get by using frida directly in the app.

The whole javascript frida code that we going to inject can be found here.

connect with frida

frida -U "human firewall"

After connecting frida give us a repl inside the app. We can know start exploring the app. A short example of what we can do with frida. We can easily call ObjectivC routines via Javascript to start Safari.

var w = ObjC.classes.LSApplicationWorkspace.defaultWorkspace();
var toOpen = ObjC.classes.NSURL.URLWithString_("https://www.google.de");
w.openSensitiveURL_withOptions_(toOpen, null);

Or play some sounds on your iphone:

var address = Module.findExportByName('AudioToolbox', 'AudioServicesPlaySystemSound');
var playSound = new NativeFunction(address, 'void', ['int']);
playSound(1000);

FlutterMethodCall and FlutterMethodChannel tracing

At this point it should be said that I have absolutely no idea about Flutter or Dart. But the methods “MethodCall” and “MethodChannel” seem to play a central role in the internal app communication. Therefore these two methods want to take a closer look. Of course you can use other methods as you like. Trace script can be found here.

/**
 * run the script to a running app: frida -U "appName" -l flutter_ios.js --no-pause
 * start app direct with the script:  frida -Uf bundleIdentifier -l flutter_ios.js --no-pause
 */
// #############################################
// HELPER SECTION START
var colors = {
    "resetColor": "\x1b[0m",
    "green": "\x1b[32m",
    "yellow": "\x1b[33m",
    "red": "\x1b[31m"
}

function logSection(message) {
    console.log(colors.green, "#################################################", colors.resetColor);
    console.log(colors.green, message, colors.resetColor);
    console.log(colors.green, "#################################################", colors.resetColor);
}

function logMessage(message) {
    console.log(colors.yellow, "---> " + message, colors.resetColor);
}

function logError(message) {
    console.log(colors.red, "---> ERRROR: " + message, colors.resetColor);
}

function getAllClasses() {
    var classes = [];
    for (var cl in ObjC.classes) {
        classes.push(cl);
    }
    return classes;
}

function filterFlutterClass() {
    var matchClasses = [];
    var classes = getAllClasses();

    for (var i = 0; i < classes.length; i++) {
        if (classes[i].toString().toLowerCase().includes('flu')) {
            matchClasses.push(classes[i]);
        }
    }

    return matchClasses;
}


function getAllMethodsFromClass(cl) {
    return ObjC.classes[cl].$ownMethods;
}

function listAllMethodsFromClasses(classes) {
    for (var i = 0; i < classes.length; i++) {
        var methods = getAllMethodsFromClass(classes[i]);
        for (var a = 0; a < methods.length; a++) {
            logMessage("class: " + classes[i] + " --> method: " + methods[a]);
        }
    }
}

function blindCallDetection(classes) {
    for (var i = 0; i < classes.length; i++) {
        var methods = getAllMethodsFromClass(classes[i]);
        for (var a = 0; a < methods.length; a++) {
            var hook = ObjC.classes[classes[i]][methods[a]];
            try {
                Interceptor.attach(hook.implementation, {
                    onEnter: function (args) {
                        this.className = ObjC.Object(args[0]).toString();
                        this.methodName = ObjC.selectorAsString(args[1]);
                        logMessage("detect call to: " + this.className + ":" + this.methodName);
                    }
                })
            } catch (err) {
                logError("error in trace blindCallDetection");
                logError(err);
            }
        }
    }
}

function singleBlindTracer(className, methodName) {
    try {
        var hook = ObjC.classes[className][methodName];
        Interceptor.attach(hook.implementation, {
            onEnter: function (args) {
                this.className = ObjC.Object(args[0]).toString();
                this.methodName = ObjC.selectorAsString(args[1]);
                logMessage("detect call to: " + this.className + ":" + this.methodName);
            }
        })
    } catch (err) {
        logError("error in trace singleBlindTracer");
        logError(err);
    }
}

// #############################################
//HELPER SECTION END
// #############################################
// BEGIN FLUTTER SECTION
// #############################################
function listAllFlutterClassesAndMethods() {
    var flutterClasses = filterFlutterClass();
    for (var i = 0; i < flutterClasses.length; i++) {
        var methods = getAllMethodsFromClass(flutterClasses[i]);
        for (var a = 0; a < methods.length; a++) {
            logMessage("class: " + flutterClasses[i] + " --> method: " + methods[a]);
        }
    }
}
// https://api.flutter.dev/objcdoc/Classes/FlutterMethodCall.html#/c:objc(cs)FlutterMethodCall(cm)methodCallWithMethodName:arguments:
function traceFlutterMethodCall() {
    var className = "FlutterMethodCall"
    var methodName = "+ methodCallWithMethodName:arguments:"
    var hook = ObjC.classes[className][methodName];

    try {
        Interceptor.attach(hook.implementation, {
            onEnter: function (args) {

                this.className = ObjC.Object(args[0]).toString();
                this.methodName = ObjC.selectorAsString(args[1]);
                logMessage(this.className + ":" + this.methodName);
                logMessage("method: " + ObjC.Object(args[2]).toString());
                logMessage("args: " + ObjC.Object(args[3]).toString());
            }
        })
    } catch (err) {
        logError("error in trace FlutterMethodCall");
        logError(err);
    }
}

// https://api.flutter.dev/objcdoc/Classes/FlutterMethodChannel.html#/c:objc(cs)FlutterMethodChannel(im)invokeMethod:arguments:
function traceFlutterMethodChannel() {
    var className = "FlutterMethodChannel"
    var methodName = "- setMethodCallHandler:"
    var hook = ObjC.classes[className][methodName];

    try {
        Interceptor.attach(hook.implementation, {
            onEnter: function (args) {
                this.className = ObjC.Object(args[0]).toString();
                this.methodName = ObjC.selectorAsString(args[1]);
                logMessage(this.className + ":" + this.methodName);
                logMessage("method: " + ObjC.Object(args[2]).toString());
            }
        })
    } catch (err) {
        logError("error in trace FlutterMethodChannel");
        logError(err);
    }
}

// enum function from defined classes
function inspectInteresingFlutterClasses(classes) {
    logSection("START BLIND TRACE FOR SPECIFIED METHODS");
    for (var i = 0; i < classes.length; i++) {
        logMessage("inspect all methods from: " + classes[i]);
        var methods = getAllMethodsFromClass(classes[i]);
        for (var a = 0; a < methods.length; a++) {
            logMessage("method --> " + methods[a]);
            blindTraceWithPayload(classes[i], methods[a]);
        }
    }
}

function blindTraceWithPayload(className, methodName) {
    try {
        var hook = ObjC.classes[className][methodName];
        Interceptor.attach(hook.implementation, {
            onEnter: function (args) {
                this.className = ObjC.Object(args[0]).toString();
                this.methodName = ObjC.selectorAsString(args[1]);
                logMessage(this.className + ":" + this.methodName);
                logMessage("payload: " + ObjC.Object(args[2]).toString());
            },
        })
    } catch (err) {
        logError("error in blind trace");
        logError(err);
    }
}

// #############################################
// END FLUTTER SECTION
// #############################################
/**
 * check if a method in the specified class get called
 */
logSection("BLIND TRACE NATIVE FUNCTION");
var blindCallClasses = [
    "FlutterStringCodec",
]
blindCallDetection(blindCallClasses);

/**
 * List found flutter classes and there methods
 */
logSection("SEARCH ALL FLUTTER CLASSES AND METHODS");
listAllFlutterClassesAndMethods();


/**
 * define custom class for further investigation. be careful: it calls blindTraceWithPayload logMessage("payload: " + ObjC.Object(args[2]).toString());
 * If you are not sure if the arg[2] is present read the function docs or do some try catch
 */
var interestingFlutterClasses = [
    //https://api.flutter.dev/objcdoc/Protocols/FlutterMessageCodec.html#/c:objc(pl)FlutterMessageCodec(im)encode:
    "FlutterJSONMessageCodec",
    //https://api.flutter.dev/objcdoc/Protocols/FlutterMethodCodec.html
    "FlutterJSONMethodCodec",
    //"FlutterStandardReader",
    //https://api.flutter.dev/objcdoc/Classes/FlutterEventChannel.html
    "FlutterEventChannel",
    //https://api.flutter.dev/objcdoc/Classes/FlutterViewController.html
    //"FlutterViewController",
    //https://api.flutter.dev/objcdoc/Classes/FlutterBasicMessageChannel.html
    "FlutterBasicMessageChannel",
]

inspectInteresingFlutterClasses(interestingFlutterClasses)

/**
 * trace implementation for
 * https://api.flutter.dev/objcdoc/Classes/FlutterMethodCall.html
 * https://api.flutter.dev/objcdoc/Classes/FlutterMethodChannel.html
 */
logSection("TRACING FLUTTER BEHAVIOUR");
traceFlutterMethodCall();
traceFlutterMethodChannel();


logSection("SINGLE BLIND TRACING");
singleBlindTracer("FlutterObservatoryPublisher","- url")

Create a file “test_frida.js” and paste script. Then, back in our terminal, call frida with our script by entering the following command:

frida -U "human firewall" -l test_frida.js --no-pause

After connecting to the script via frida against the app, use it for a while. Then you can follow the scripts out:

Summary

I hope I could give you a rough overview how to extract information and behavior from an app. Furthermore we can also manipulate behavior with tools like frida. We can use these techniques excellently to detect possible weaknesses in our apps.

Hope you had fun.

Cheers

close

Leave a Reply

Your email address will not be published. Required fields are marked *