‹ MobiSec

Mobile Hacking Labs - Strings

Jul 16, 2024

Check out my MHL Series which brings writeups of the Mobile Hacking Lab Android challenges, that are part of the CAPT course. The labs are well created and are good for preparation of the CAPT exam.

- - these writeups assume knowledge in basic android hacking methodolgy and familiarity with tools including adb, jadx(gui), Frida, etc

Challenge Overview

The Strings challenge gives a clear idea of how Intents and Intent filters work in Android and a hands-on experience using Frida APIs. The aim of the challenge is to get a flag with the format “MHL{…}”

Access the MHL Strings Challenge

Discovery and Recon

While we have a clue of what the challenge is – unsecured intents and components, the specifics of the vulnerability and later exploitation will be more apparent with a deeper analysis.

From the challenge instructions we have a clue of what to look out for:

  • Android Intents & Filters
  • Frida APIs & Memory Scanning
  • Shared Preferences
  • Android Native Libaries

Running the application

I run the application to see accesible activities and any other hints and features.

note: MainActivity displays Hello from C++ and nothing more.

Reverse Engineering & Code Analysis

I found a tool to speed up static analysis of the AndroidManifest.xml file called Deeper. However, I manually decompile the app using jadx-gui as I primarily use it for extensive code analyis. You could use deeper and other tools for automation in case of multiple or large applications.

An analysis of the manifest reveals an activity com.mobilehackinglab.challenge.Activity2 with the following attributes:

    <android:exported="true">
    <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data
        android:scheme="mhl"
        android:host="labs"/>
    </intent-filter>

note:

  • exported activities/components can be accessed by other applications and critical components should be protected by having the attribute as false
  • the activity handles a custom uri mhl://labs.

With that info, I start going through the Activity2 code satring with it’s onCreate() method.

In mind, is the goal of the lab –get the flag dumped into the memory. However there are four conditions to be met, before finally having the app call the getflag() method.

Getting the conditions true for all the if statements will get the getflag() method called. So let’s do that:

Statement 1: Has two conditions

If both conditions are true, the intent data is retrieved.

  • boolean isActionView = Intrinsics.areEqual(getIntent().getAction(), "android.intent.action.VIEW");

Checks that the action of the calling intent is "android.intent.action.VIEW". This is important to note, as it will be used in crafting the final adb command.

  • boolean isU1Matching = Intrinsics.areEqual(u_1, cd());

Checks if two strings (u_1 and string returned by cd()) are equal. And what are this two strings?

String u_1 = sharedPreferences.getString("UUU0133", null); -> u_1 gets the value of a sharedPreference record with the key “UUU0133”.

method cd()

    private final String cd() {
        String str;
        SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault());
        String format = sdf.format(new Date());
        Intrinsics.checkNotNullExpressionValue(format, "format(...)");
        Activity2Kt.cu_d = format;
        str = Activity2Kt.cu_d;
        if (str != null) {
            return str;
        }
        Intrinsics.throwUninitializedPropertyAccessException("cu_d");
        return null;
    }

Returns the date with the Format specified. This means that the string u_1 also has to be a date with the same format for the two to be equal.

Statement 2

    if (uri != null && Intrinsics.areEqual(uri.getScheme(), "mhl") && Intrinsics.areEqual(uri.getHost(), "labs")) {
        String base64Value = uri.getLastPathSegment();
        byte[] decodedValue = Base64.decode(base64Value, 0);

This checks if the scheme and host match “mhl” & “labs” respectively. If they do, it passes the segment/path (base64) of the uri and decodes the value to bytes.

Statement 3

if (decodedValue != null) {
     String ds = new String(decodedValue, Charsets.UTF_8);
     byte[] bytes = "your_secret_key_1234567890123456".getBytes(Charsets.UTF_8);
     Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
     String str = decrypt("AES/CBC/PKCS5Padding", "bqGrDKdQ8zo26HflRsGvVA==", new SecretKeySpec(bytes, "AES"));

If the decoded value is not null, the method decrypt() is called and returns a string str.

Okay, we’re getting somewhere. We have to understand the decrypt() method and find a way to decypt the encoded string.

Statement 4

if (str.equals(ds)) {
    System.loadLibrary("flag");
    String s = getflag();
    Toast.makeText(getApplicationContext(), s, 1).show();
    return;

Checks of the decrypted string str (from statement 3) matches the decode string(last segment/path) from the uri data. If true, the native library flag is loaded and the native method getflag() called.

With all the conditions known, two things remain. Ensuring the sharedPrefences “DAD4” with key ““UUU0133” has the date with the specified format and decrypting the encoded string to get the last segment of the uri.

Exploitation

After the recon and discovery of possible entry points we have a way to exploit this.

  • Get the dates returned by the sharedPreference and cd() method to match.
  • Decrypt the encoded string to get the last segment.
  • Use adb to start the exported activity.
  • Dump the memory of the app and look for string.

In the mainactivity we see a declaration of KLOW method that creates a SharePreference “DAD4”, gets the date in specified format and puts that in the sharedPrefence with key “UUU0133”. Well, we have to use Frida to reach and call this method.

public final void KLOW() {
    SharedPreferences sharedPreferences = getSharedPreferences("DAD4", 0);
    SharedPreferences.Editor editor = sharedPreferences.edit();
    Intrinsics.checkNotNullExpressionValue(editor, "edit(...)");
    SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault());
    String cu_d = sdf.format(new Date());
        editor.putString("UUU0133", cu_d);
        editor.apply();
    }
}

Jadx-gui has a very helpful feature to get Frida snippets. I used that and got the following to add to a klowing.js file.

Java.perform(function () {
    let MainActivity = Java.use("com.mobilehackinglab.challenge.MainActivity");
        MainActivity["KLOW"].implementation = function () {
        console.log(`MainActivity.KLOW is called`);
        this["KLOW"]();
};
});

With this script, frida hooks the MainActivity and calls the KLOW method.

frida -U -f com.mobilehackinglab.challenge -l klowing.js

And now if you check the shared prefs, we have the sharedPref with the date added.

Now, to decrypting the encoded text. The decrypt method has three parameters. The alogrithm, cipherText and key. Since we have all three, we can rewrite/have gpt rewrite for us the method in a way we can run locally to get decrypted string.

I used this script:

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class DecryptExample {

    // This is the IV used during encryption and decryption
    public static final String fixedIV = "1234567890123456";

    public static void main(String[] args) {
        try {
            // Define the key as byte array
            byte[] keyBytes = "your_secret_key_1234567890123456".getBytes(StandardCharsets.UTF_8);
            SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");

            // Encrypted text
            String cipherText = "bqGrDKdQ8zo26HflRsGvVA==";

            // Call the decrypt method
            String decryptedText = decrypt("AES/CBC/PKCS5Padding", cipherText, key);

            // Print the decrypted text
            System.out.println("Decrypted Text: " + decryptedText);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static String decrypt(String algorithm, String cipherText, SecretKeySpec key) {
        try {
            // Retrieve the IV
            byte[] ivBytes = fixedIV.getBytes(StandardCharsets.UTF_8);

            // Create Cipher instance
            Cipher cipher = Cipher.getInstance(algorithm);

            // Initialize the cipher with the key and IV
            IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
            cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);

            // Decode the Base64 encoded ciphertext
            byte[] decodedCipherText = Base64.getDecoder().decode(cipherText);

            // Decrypt the ciphertext
            byte[] decryptedBytes = cipher.doFinal(decodedCipherText);

            // Convert decrypted bytes to String
            return new String(decryptedBytes, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("Decryption failed", e);
        }
    }
}

Now that we have the last uri segment, and the date in the sharedpref, we can send the intent and see what happens.

Command:

adb shell am start -W -a android.intent.action.VIEW -d "mhl://labs/bWhsX3NlY3JldF8xMzM3" -n com.mobilehackinglab.challenge/.Activity2

Results:

Starting: Intent { act=android.intent.action.VIEW dat=mhl://labs/... cmp=com.mobilehackinglab.challenge/.Activity2 }
Status: ok
LaunchState: WARM
Activity: com.mobilehackinglab.challenge/.Activity2
WaitTime: 93
Complete

With that we can dump and examine the app memory. I use Fridump for this.