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.
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 :
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.
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:
ID | HASH |
---|---|
1 | d8531a519b3d4dfebece0259f90b466a23efc57b |
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
.
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.
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
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.
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.
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
:
DatabaseUtilities
. To do so we need the instance of MainActivity
.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=
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