writeup

Strings - Mobile Hacking Lab Writeup

This is a challenge about exported activities and memory analysis. The goal is to find a flag hidden somewhere in the application. Let's dive in!

#android

Introduction

This is a challenge about exported activities and memory analysis. The goal is to find a flag hidden somewhere in the application. Let's dive in!

Reconnaissance

Since this CTF is about exported activities, the first thing I did was open the app in jadx-gui and check the AndroidManifest.xml.

I found two activities:

 <activity  
    android:name="com.mobilehackinglab.challenge.Activity2"  
    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>  
</activity>  
<activity  
    android:name="com.mobilehackinglab.challenge.MainActivity"  
    android:exported="true">  
    <intent-filter>  
        <action android:name="android.intent.action.MAIN"/>  
        <category android:name="android.intent.category.LAUNCHER"/>  
    </intent-filter>  
</activity>

One of them is the MainActivity, and the other is Activity2. Both are exported. However, looking at Activity2, we can see in the intent-filter an action VIEW, category DEFAULT and BROWSABLE (typical deep-link setup), and right below, the data with scheme mhl and host labs. So the deep-link structure is: mhl://labs.

Reverse Engineering

MainActivity Analysis

Going into the MainActivity source code, there's an onCreate method full of stuff I was too lazy to read (and honestly didn't understand yet - still learning Android reverse engineering).

But there's an interesting function called KLOW():

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();
}

This function initializes a SharedPreferences with the name "DAD4" and another parameter 0 (which I don't remember what it is).

Then it creates a SharedPreferences.Editor to be able to modify the shared preferences, and tests if it's not null.

Below that, there's SimpleDateFormat, which basically sets the value to the current system date (at the moment the function is called) in the format dd/MM/yyyy. Then it assigns this date to a variable cu_d, and puts this variable as the value of the key "UUU0133" in the SharedPreferences.

Activity2 Analysis

Going to Activity2, in the onCreate we have a giant function with several if-else statements, but I focused on the part of the code that verifies and loads the flag and a native library:

protected void onCreate(Bundle savedInstanceState) throws BadPaddingException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_2);
    SharedPreferences sharedPreferences = getSharedPreferences("DAD4", 0);
    String u_1 = sharedPreferences.getString("UUU0133", null);
    boolean isActionView = Intrinsics.areEqual(getIntent().getAction(), "android.intent.action.VIEW");
    boolean isU1Matching = Intrinsics.areEqual(u_1, cd());
    if (isActionView && isU1Matching) {
        Uri uri = getIntent().getData();
        if (uri != null && Intrinsics.areEqual(uri.getScheme(), "mhl") && Intrinsics.areEqual(uri.getHost(), "labs")) {
            String base64Value = uri.getLastPathSegment();
            byte[] decodedValue = Base64.decode(base64Value, 0);
            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 (str.equals(ds)) {
                    System.loadLibrary("flag");
                    String s = getflag();
                    Toast.makeText(getApplicationContext(), s, 1).show();
                    return;
                    ...

To understand which if-else mattered, I went backwards from the crypto part and found the crucial one:

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

This is the key point! If the decrypted string (str) equals the decoded value from the deep link (ds), then it loads the native library libflag.so, calls getflag(), and shows the result in a Toast.

Understanding the Validation Flow

Reading in the correct order this time, here's what I understood:

In getSharedPreferences("DAD4", 0), it gets that value from the DAD4 file (but that function hasn't been called yet, so it doesn't exist). It assigns to a variable u_1 the value of the key "UUU0133" from the SharedPreferences DAD4, and then does some checks, one of them verifying if the value of this variable u_1 is the same as the return of the cd() function. So I went to check out cd():

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

In this function, we can see a similar process to KLOW: it creates a SimpleDateFormat with the date from when the function is called and puts it in a variable str. Then it creates a variable str2 with the value of cu_d (which is the date created in KLOW), and tests if it's null in that weird Kotlin-recompiled-to-Java format.

After that, there's a line Intrinsics.throwUninitializedPropertyAccessException("cu_d"); which I don't know what it is. Maybe part of the null check? idk.

Anyway, both KLOW and cd create dates.

Back to the onCreate validation, we have the boolean that now makes sense: it checks if both dates are equal, and the value will be true or false.

Going down to the first if:

  • It verifies if we have the intent in the correct pattern, being scheme mhl and host labs
  • If yes, it creates a variable base64Value that gets the lastPathSegment. Basically what's in the path of the deep-link: mhl://labs/PATH-HERE
  • Then it does a Base64.decode of this path value and puts it in decodedValue
  • After that, it tests if the decoded value is null

Going inside this if, we have:

  • A hardcoded key: your_secret_key_1234567890123456
  • The decrypt method: AES/CBC/PKCS5Padding
  • A hash (Base64-like): bqGrDKdQ8zo26HflRsGvVA==

And a validation: if the decoded value of this hash with this key and method equals the decoded value from the path in the deep-link, it loads the flag library.

Finding the IV

One important observation: going back to where cu_d is called (Activity2Kt), we also have a hardcoded fixedIV:

public final class Activity2Kt {
    private static String cu_d = null;
    public static final String fixedIV = "1234567890123456";
}

This is crucial to be able to decrypt that Base64 we had!

Decryption Process

Using CyberChef with:

  • FromBase64 → AES Decrypt
  • Mode: CBC
  • Key: your_secret_key_1234567890123456
  • IV: 1234567890123456
  • Input: bqGrDKdQ8zo26HflRsGvVA== cyber-chef.png We get the result: mhl_secret_1337

And remembering the code, the validation is between their hash and ours, but for ours they only decode the standard Base64 value. So I just encoded the value mhl_secret_1337 and got: bWhsX3NlY3JldF8xMzM3

Now I had everything to build the correct deep link: mhl://labs/bWhsX3NlY3JldF8xMzM3

Exploitation

Step 1: Calling KLOW()

But it's no use trying to use the deep link now - we won't even reach the verification because KLOW hasn't been called yet!

So I used Frida to first call this function so the file would be created:

Java.perform(function () {
    setTimeout(function () {
        Java.choose("com.mobilehackinglab.challenge.MainActivity", {
            onMatch: function(instance){
                instance.KLOW();
            },
            onComplete: function(){}
        });
    }, 1000);
});

Right after activating KLOW(), I triggered the intent:

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

Only a "Success" message appeared, nothing else. So I reasoned: this loads the flag library, but it doesn't necessarily mean the lib will print the flag in my face.

Step 3: Memory Scanning

MHL gives a hint about memory dump, and I'm not crazy (yet) to open Ghidra to analyze this library. So I used Objection to read the memory searching for their flag pattern.

I started Objection and used:

memory search "MHL{" --string

objection.png And there it was the flag!

Conclusion

This challenge was a great introduction to:

  • Android exported activities and deep links
  • (overview) SharedPreferences manipulation
  • AES-CBC decryption
  • (maybe) Native library analysis
  • Memory scanning techniques