Brute Forcing PINs with Frida: Mobile Penetration Testing

Brute Forcing PINs with Frida: Mobile Penetration Testing

I recently performed a mobile penetration test on a 2FA application that resulted in the creation of a Frida script to brute force hardcoded values. This was all done on the Corellium platform. In this blog I will share my journey. This was a pentest with live accounts. As a result, some of the screenshots are redacted.

For some context, the use case for this app was to be a 2FA for transactions such as money transfers, bill payments, etc. For example, a user would log into their bank account and request a money transfer from one account to another. A notification would then show up on the Token app, the user enters an already setup PIN, taps accept, and the transfer would be authorized. 

Once authorized to the application, the user never had to sign in again. The only thing standing between authorizing a bank transfer and myself was a six-digit PIN (and a way to kick off a transfer, of course). 

Mobile Pentest: Discovery 

After exercising the app, I was able to narrow down the class and method that was used during the change PIN functionally and found it didn’t make a request to the server. This means that the PIN is stored somewhere on the device and is likely recoverable by brute forcing. So, I fired up Objection and started hooking the suspect method. 

Objection makes it very simple to hook classes and methods and print out what values are passed as arguments and return values. Something I’m looking for in this situation is for the return value to be different when I pass an incorrect PIN than when I pass a correct PIN. 

Passing an incorrect PIN, as in the screenshot above, gives us a return value of ‘-1’, while passing the correct PIN, as in the screenshot below, gives us a long, Base64 encoded blob of data. 

This slight difference in return values is all we need to be able to brute force the correct PIN. 

Building the Script Using Frida 

The method for changing the PIN is unsurprisingly called ‘pin’, and the class is ‘ops’. So our starting point is to hook ‘ops.pin()’ and print out arguments to verify we are indeed hooking the correct method. But before we can hook the method and print out arguments, we need to know what arguments it takes. Hooking the method with Objection will show us the arguments for the method. 

android hooking watch class_method com.example.app.ops.pin --dump-args 

This will produce the following output once the method is run by the app:

A screenshot of a code snippet.

As we can see above, the ‘pin’ method takes seven arguments: an Android context, two PINs, which are our initial guess that gets compared against the true PIN, and four string values that are encrypted and Base64 encoded. All the arguments other than the context are strings, and the function returns a string. 

On line 4, we use the API ‘Java.use()’, which creates a JavaScript wrapper for the class that was passed to it. The redacted part is the package name dot class name, i.e., ‘com.example.app.ops’. We put the return value into a variable also named ‘ops’, which allows us to later create an instance of that class to call the pin method in order to iterate over PIN’s. 

On line 6 we instantiate an instance of the class by calling ‘ops.$new()’ and capturing the return value in a variable named ‘instance’. This will come into play later. 

On line 8 we hook the method ‘pin’, passing its arguments in our function call and returning a result later on line 10. 

We then print out the argument ‘pin1’ to the console to ensure we are hooking the method properly. This part isn’t strictly necessary, but it’s nice to know that everything works as intended. 

A screenshot of the Corellium Frida Tools.

Perfect! We successfully hooked ‘ops.pin()’ and printed out an argument to verify. 

At this point I should mention that this way of hooking requires interaction on the device. That is, the app must be exercised to run the hook. In later steps I will show how to run the hook without interaction. 

A screenshot of a code snippet.

The next step is to iterate over all possible PIN values by calling the ‘pin()’ function ourselves and passing in our guesses. This is where ‘ops.$new()’ comes into play. 

Inside our hook of ‘ops.pin()’ we start by creating a for loop to iterate over PIN values. Since the PIN can only be six-digits we start by setting our counter ‘i’ to ‘0’ and count until ‘i’ is ‘999999’. 

Inside of our for loop, on line 14, we create a variable named ‘guess’ and give it the value of ‘i.toString()’. If you recall from earlier, all the arguments passed to the ‘pin()’ function are strings except for the first one so we must change our integer value of ‘i’ to a string, which is what we are doing here. We also must use the function ‘padStart(6, “0”)’ if we want to start at ‘000000’. This function will add up to 6 zero’s to the beginning of our guess. If we didn’t do this, we would get an error stating that ‘Octal literals are not allowed in strict mode’. Basically, if an integer starts with a zero, it will be treated as Octal (base 8), and we don’t want that here. 

On line 16, we call the ‘pin()’ method ourselves by using our instance of ‘ops’ that we created earlier. The ‘pin()’ method expects seven arguments and since we have hooked it already, the arguments ‘context’, ‘string1’, ‘string2’, ‘string3’, ‘string4’ can all be passed directly to our function while we pass our ‘guess’ variable into argument slots 2 and 3. We then capture the return of that function into a variable named ‘result’. 

On line 18, we create an ‘if’ statement to check whether our result from our method call is incorrect or not. If it is incorrect, ‘result’ will contain a ‘-1’, otherwise it will contain a blob of Base64 encoded data, which means our guessed PIN was correct. 

At this point, when we have guessed the correct PIN, we want to print out ‘result’ and our PIN guess, which happens on line 19. We then break from the loop on line 20 because there is no need to continue guessing since we got our result. 

Running the Script

Now that the script is fully built, we can attach to the Token app with Frida in a terminal and see if we get the correct PIN. 

To launch the app with our script attached, we use the following command: 

frida -l pin.js -f com.example.app 

This command launches Frida and tells it to use our script (-l, this is a lower case ‘L’), and launch our application via package name (-f).  

Note that adding a -U to any Frida command will tell it to connect to a device over USB. Since I am using Corellium, Frida is installed directly on the device, which means I don’t need to use that flag. 

“Hey, dude!”, you exclaim.  

“Where do I find the package name?” 

I’m glad you asked! You can find the package (Identifier) by running the following command: 

frida-ps -ai 

A screenshot of the Corellium console.

frida-ps is a utility for interacting with processes. 

-ai tells frida-ps to list applications (-a) installed (-i). 

Pro tip: you can also launch the application with Frida by using the application name with the   -n option like so: 

frida -l pin.js -n Token 

Or by using the -F option if the application is launched and in the foreground: 

frida -l pin.js -F 

For our script to run, we need to exercise the function that we are hooking. In the Token app, we head to the change PIN screen and enter some false values to kick off the script. We don’t need any specific values here since our script will be iterating through all possible six-digit values. Once we tap continue in the app, the PIN method will be called, and our script will hook it and hopefully print out the correct PIN. 

This can take some time depending on your Android device so be patient. 

A screenshot of the Corellium platform.

That’s it! We successfully hooked the PIN method and brute-forced the correct PIN for the app.  

Running without Interaction

But what if we wanted to run the script without any interaction? Well, we can do that too with just a few tweaks! 

Below is an updated script of the entire code base. 

 A screenshot of a code snippet.

There are a few things to note here: 

  1. To run this automatically, we don’t need to hook the ‘pin’ method anymore. We just need to call it with our own values. So, we can comment out lines 16 and 33. 
  2. The first argument for the method is an Android context. I won’t go into detail about what exactly context is, but just know that to pass context to a function, you can do so exactly how I do it on line 8. 
  3. Remember those four random strings I talked about earlier? In this case, they would need to be hardcoded into our script to pass them to our function.

There are other ways to get those strings other than hardcoding them, but this was a time-boxed assessment, and, well, I was running out of it.

Hooking the Token app with this script should run it automatically when the class loads. If nothing happens, try again by hitting ctrl-s to save the Frida script. 

If you didn’t know already, when a script is attached to an app and you change it, the changes will take effect when the file is saved. 

Full Script 

Java.perform(() => {

 

     //get class

     var ops = Java.use("com.exmaple.app.ops");

    //create instance

    var instance = ops.$new();

    //create Android context

    /*var context =

Java.use("android.app.ActivityThread").currentApplication().getApplicationContext();

    //create string variables

    string1 = "d2hhdCBhcmU=";

    string2 = "eW91IGxvb2tpbmcgZm9y";

    string3 = "aHVoPw==";

    string4 =

"aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj14dkZaam81UGdHMA==";*/ 
     

 

    //hook PIN method

    ops.pin.implementation = function(context, pin1, pin2, string1, string2, string3, string4) {

         //console.log("Test --> " + pin1)

        //iterate over pin values

        for(var i = 111000; i <= 999999; i++) {

            //convert pin to a string

            var guess = i.toString().padStart(6, "0");

            console.log("Guess --> " + guess);

            //call the 'pin' method using our instance of ops.$new() created earlier

            var result = instance.changePin(context, guess, guess, string1, string2, string3, string4);

            //if result is -1, our guess is incorrect

            if(result != "-1") {

                console.log("\nReturn Value --> " + result + "\nPIN --> " + guess);

                break;

            }

         }

        //return the result for the app to continue its flow

        return result;

    }

}); 

 

Happy Hunting! 

If this caught your attention, learn more about using Frida to find hooks with Corellium.