Pinstore - BSides San Francisco CTF 2017

17 August 2019

Challenge - Pinstore.apk

First contact

After downloading the APK from the github repository we install it on our phone with:

adb install pinstore.apk

When we start the application we get textfield where we can enter a PIN and a button to validate it.

Static analysis with JADX

We start our reverse engineering with a static analysis of the application. We use JADX to do this.

We can see that there are the following classes :

  • CryptoUtilities
  • DatabaseUtilities
  • MainActivity
  • SecretDisplay

Let's look at the MainActivity class to see how the PIN is verified. When we click on the validate button, the hash of the PIN is retrieved from a database:

pinFromDB = new DatabaseUtilities(MainActivity.this.getApplicationContext())
            .fetchPin();

Then the PIN entered by the user is hashed and compared to the one in the database:

if (pinFromDB.equalsIgnoreCase(hashOfEnteredPin)) {
    Intent intent = new Intent(MainActivity.this, SecretDisplay.class);
    intent.putExtra("pin", enteredPin);
    MainActivity.this.startActivity(intent);
    return;
}

If we got the correct PIN the SecretDisplay activity is started.

Cracking the PIN

In this exploit we retrieve the database to extract and crack the hash of the PIN. To do so we use adb to retrieve the database.

The path to the database can be found in DatabaseUtilities:

public class DatabaseUtilities extends SQLiteOpenHelper {
    private static String dbName = "pinlock.db";
    private static String pathToDB = "/data/data/pinlock.ctf.pinlock.com.pinstore/databases/";

We start adb as root to be able to do a pull of the database and then we retrieve the file:

$ adb root
restarting adbd as root
$ adb pull /data/data/pinlock.ctf.pinlock.com.pinstore/databases/pinlock.db

/data/data/pinlock.ctf.pinlock.com.pinstore/databases/pinlock.db: 1 file pulled. 5.0 MB/s (5120 bytes in 0.001s)

Note that this method might be a bit overkill. We could also have extracted the database directly from the apk in assets/pinlock.db, but it is always nice to discover new tools.

From there we use sqlitebrowser to explore the database. The PIN is stored in the table pinDB which only has one entry:

IDHASH
1d8531a519b3d4dfebece0259f90b466a23efc57b

From the source code we know that this hash is produced from SHA-256 we can try to crack it on our machine but since it is a PIN code it is most likely composed of 4 numbers and we can find rainbow tables on the internet.

We use crackstation to crack the hash, the result is 7498.

Decrypting the database

We enter the PIN in the application and we now have access to a new activity SecretDisplay.

This activity displays the secrets stored in the database.

try {
    tv.setText(new CryptoUtilities("v1", pin)
    .decrypt(new DatabaseUtilities(
        getApplicationContext()).fetchSecret()));
} catch (Exception e) {
    Log.e("Pinlock", "exception", e);
}
Toast makeText = Toast.makeText(context, pin, 1);

In the current version of the application, the activity displays the secret stored in the table secretDBv1. To do so the class CryptoUtilities is used. This class will fetch the proper key according to the table targetted:

public CryptoUtilities(String version, String pin2) throws Exception {
    this.pin = pin2;
    this.key = getKey(version);
}

public SecretKeySpec getKey(String version) throws Exception {
    if (version.equalsIgnoreCase("v1")) {
        Log.d("Version", version);
        return new SecretKeySpec(
            Arrays.copyOf(MessageDigest.getInstance("SHA-1")
            .digest("t0ps3kr3tk3y".getBytes("UTF-8")),
            16), "AES");
    }
    Log.d("Version", version);
    byte[] salt = "SampleSalt".getBytes();
    return new SecretKeySpec(
        SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
        .generateSecret(new PBEKeySpec(this.pin.toCharArray(), salt, 1000, 128))
        .getEncoded(), "AES");
}

Since the key is generated from the PIN we should be able to generate it ourselves. We just have to copy the class CryptoUtilities and reuse it.

I still had to apply some minor modifications to avoid using the android Base64 dependency.

In encrypt():

return Base64.getEncoder().encodeToString(ciphertext);

And decrypt():

byte[] ciphertextBytes = Base64.getDecoder().decode(ciphertext.getBytes());

To verify that everything works we reuse the secret from the table v1 and try to decrypt it. Here is some Kotlin code that does just that:

fun main() {
    val cryptoUtilities = CryptoUtilities("v1", "7498")
    println(cryptoUtilities.decrypt("hcsvUnln5jMdw3GeI4o/txB5vaEf1PFAnKQ3kPsRW2o5rR0a1JE54d0BLkzXPtqB"))
}

Since the text is decrypted properly (the same as the one displayed in SecretDisplay) we can now change the version to v2 and replace the ciphertext to the one displayed in the table v2:

fun main() {
    val cryptoUtilities = CryptoUtilities("v2", "7498")
    println(cryptoUtilities.decrypt("Bi528nDlNBcX9BcCC+ZqGQo1Oz01+GOWSmvxRj7jg1g="))
}

The result is the following Flag:OnlyAsStrongAsWeakestLink.

Hooking functions with Frida

Starting Frida

In this alternative I will hook functions with Frida to modify the application flow and get to the flag.

We start our android emulator with Android Studio and in a terminal we restart adb to have root access on the emulator.

adb root

Then we push the frida server to the andrdoid phone with adb push.

adb push ~/Resources/SecurityTools/frida/frida-server-12.6.16-android-x86_64 /data/local/tmp

We can check that the file was pushed properly with adb shell ls.

adb shell ls /data/local/tmp/

Finally we start frida server by executing the binary we just pushed on the phone.

adb shell "/data/local/tmp/frida-server-12.6.16-android-x86_64 &"

Now we are ready to play with Frida. To verify that everything is working properly we can execute frida-ps -U in another terminal. We should be getting something like this:

  PID  Name
-----  --------------------------------------------------
14748  adbd
 1564  android.process.media
 1473  audioserver
 1474  cameraserver
32037  com.android.chrome
 1860  com.android.inputmethod.latin
 2619  com.android.launcher3
 2023  com.android.phone
 1927  com.android.systemui
16321  com.google.android.apps.maps

Getting the PIN

Using Frida does not exempt us from doing a static analysis if possible. Now we know that the MainActivity retrieves the PIN code from the database to check if the PIN entered by the user is correct. To do so the method DatabaseUtilities.fetchPin() is used.

pinFromDB = new DatabaseUtilities(MainActivity.this.getApplicationContext())
            .fetchPin();

We would like to intercept the PIN fetched from the database. To do so we will create a frida script dump_pin.js in javascript. The script will get the class DatabaseUtilities and modify the implementation of the method fetch_pin() to add debug log and display the value that was fetched.

console.log("Script loaded successfully ");
Java.perform(function x() {

    console.log("Inside java perform function");
    //get a wrapper for our class
    var activity = Java.use("pinlock.ctf.pinlock.com.pinstore.DatabaseUtilities");
    //replace the original implmenetation of the function `fun` with our custom function
    activity.fetchPin.implementation = function () {
        var ret_value = this.fetchPin();
        console.log("fetchPin result = " + ret_value);
        return ret_value;
    }
});

To use this frida script we need the following python script exploit.py. This script starts the targetted application and initialize the hook from the javascript file created earlier:

import frida

def my_message_handler(message, payload):
    print(message)
    print(payload)

device = frida.get_usb_device()
pid = device.spawn(["pinlock.ctf.pinlock.com.pinstore"])
device.resume(pid)
session = device.attach(pid)
with open("dump_pin.js") as f:
    script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()

# prevent the python script from terminating
input()

Now we can start the script with python3 exploit.py and enter a random PIN in the emulator to trigger the call to fetchPin(). We should see this in our terminal:

Script loaded successfully
Inside java perform function
fetchPin result = d8531a519b3d4dfebece0259f90b466a23efc57b

We just successfully hooked the fetchPin() function and retrieved the hash of the PIN code.

Bypassing the PIN

We can go further and bypass the PIN check. To do so we modify the implementation of fetchPin() to always return 39dfa55283318d31afe5a3ff4a0e3253e2045e43 : the SHA1 sum for the PIN 0000.

We just have to replace the line return ret_value; in dump_pin.js by:

return "39dfa55283318d31afe5a3ff4a0e3253e2045e43";

Now we create the hook by running exploit.py and we enter the PIN 0000 in the application. We now have access to the next activity SecretDisplay.

Finding the second secret

Now let's say that we managed to crack the PIN code with hashcat or something else as seen in the first part of this write-up.

Now we would like to decrypt the secret in the table secretsDBv2 using the PIN we just obtained. First we will need to obtain the secret by calling fetchSecret() on the table we are interested in.

We create a script fetch_secret.js that modify the implementation of SQLiteDatabase.rawQuery to always query for SELECT entry FROM secretsDBv2. This way when fetchSecret() is called, we are really querying from the table secretsDBv2. This function is called when the SecretDisplay activity is instanciated so we could trigger it by entering the correct PIN; however for educational purposes we will see another way of calling it. The script is the following:

console.log("Script loaded successfully ");
Java.perform(function x() {

    console.log("Inside java perform function");
    //get a wrapper for our class
    // Modify rawQuery implementation to always query secretsDBv2
    var sqlitedb = Java.use("android.database.sqlite.SQLiteDatabase")
    sqlitedb.rawQuery.overload("java.lang.String", "[Ljava.lang.String;").implementation = function (query, other) {
        console.log("Modifying SQL query to dump secretsDBv2")
        return this.rawQuery("SELECT entry FROM secretsDBv2", null)
    }

    // Retrieve the instance of MainActivity
    var activity = Java.choose("pinlock.ctf.pinlock.com.pinstore.MainActivity", {
        onMatch: function(instance) {
            console.log("Found instance of MainActivity: " + instance)
            console.log("\tInstanciating a new DatabaseUtilities object...")
            var dbUtils = Java.use("pinlock.ctf.pinlock.com.pinstore.DatabaseUtilities")
            var dbInstance = dbUtils.$new(instance.getApplicationContext())
            console.log("\tDatabaseUtilities instance: "+dbInstance)
            var secretsDBv2 = dbInstance.fetchSecret()
            console.log("\tSecret from DB v2: " + secretsDBv2)
        },
        onComplete:function(){}
    })
});

As we can see, in the second part of the script we retrieve the existing instance of MainActivity. When we have found an instance of MainActivity, we execute the code in onMatch:

  1. We create an instance of DatabaseUtilities. To do so we need the instance of MainActivity.
  2. We call fetchSecret() on the instance we just created.

That's it! Now we just have to start our script. When frida has found the instance of MainActivity our code in onMatch executes and we get the following display:

Script loaded successfully
Inside java perform function
Found instance of MainActivity: pinlock.ctf.pinlock.com.pinstore.MainActivity@c474849
        Instanciating a new DatabaseUtilities object...
        DatabaseUtilities instance: pinlock.ctf.pinlock.com.pinstore.DatabaseUtilities@ec6a66f
Modifying SQL query to dump secretsDBv2
        Secret from DB v2: Bi528nDlNBcX9BcCC+ZqGQo1Oz01+GOWSmvxRj7jg1g=

Decrypting the secret

Now that we have found the secret, we might want to decrypt it. To do so we will use the class CryptoUtilities instanciated with the proper PIN code. We add the following code at the end of the onMatch function:

var cryptoUtils = Java.use("pinlock.ctf.pinlock.com.pinstore.CryptoUtilities")
var cryptoInstance = cryptoUtils.$new("v2", "7498")
var plaintext = cryptoInstance.decrypt(secretsDBv2)
console.log("\tDecrypted secret: " + plaintext)

When we execute our exploit we finally retrieve the flag !

Script loaded successfully
Inside java perform function
Found instance of MainActivity: pinlock.ctf.pinlock.com.pinstore.MainActivity@c474849
        Instanciating a new DatabaseUtilities object...
        DatabaseUtilities instance: pinlock.ctf.pinlock.com.pinstore.DatabaseUtilities@ec6a66f
Modifying SQL query to dump secretsDBv2
        Secret from DB v2: Bi528nDlNBcX9BcCC+ZqGQo1Oz01+GOWSmvxRj7jg1g=
        Decrypted secret: Flag:OnlyAsStrongAsWeakestLink