Mobile Hacking Lab - Strings

Intent Filter and Dumping the Memory: A Mobile Hacking Lab CTF Challenge

In cybersecurity, Capture The Flag (CTF) challenges serve as thrilling puzzles. They not only challenge our hacking skills but also improve our ability to solve problems. Today, I’m excited to share a detailed walkthrough of an Android application challenge from the Mobile Hacking Lab. This will take us through the art of reverse engineering, the magic of dynamic analysis with Frida, and the thrill of uncovering hidden secrets within mobile applications. Grab snacks because we’re going deep…

The Challenge Overview

The Mobile Hacking Lab presents a challenge designed to test participants’ proficiency in Android application security. This challenge involves an Android Package Kit (APK), which is the package file format used by the Android operating system for distribution and installation of mobile applications. We are tasked with a simple objective: to extract a flag formatted as "MHL{...}". However, as any seasoned hacker knows, the devil is in the details…

You can access the challenge here: Mobile Hacking Lab - Strings

Discovery Phase

To start the process, I extracted the APK from the device using the Android Debug Bridge (adb) command-line tool. Next, I ran my custom tool, Deeeeper, to decompile the APK. Deeeeper It also run a comprehensive search for key components such as Processing Activities, Aliases, Services, and Receivers.

During the analysis, I noticed that com.mobilehackinglab.challenge.Activity2 had its exported attribute set to true, indicating that the activity is accessible to other apps.

Also, this activity was associated with handling the custom URL scheme mhl://labs alongside the android.intent.action.VIEW intent action, as demonstrated below:

com.mobilehackinglab.challenge.Activity2 (exported=true)
    Intent Action: android.intent.action.VIEW
    Custom URL Scheme: mhl://labs

Next, I launched JADX-GUI to review the decompiled code, focusing my analysis on the Activity2 code. I began examining the initial onCreate method.

Basic Activity Setup:

  • super.onCreate(savedInstanceState); calls the superclass’s onCreate method to handle the creation of the activity.
  • setContentView(R.layout.activity_2); sets the UI layout for the activity from a predefined layout resource.

Shared Preferences Retrieval:

  • It retrieves a SharedPreferences object named "DAD4".
  • It attempts to fetch a string value with the key "UUU0133" from the shared preferences. If the key doesn’t exist, null is returned.

Intent and Action Verification:

  • Checks if the action of the incoming Intent that started the activity is android.intent.action.VIEW, which is commonly used to view a URI.
  • Calls a custom method cd() and compares its return value with the value obtained from shared preferences (u_1). The comparison result is stored in isU1Matching.

Custom URI Scheme Handling:

  • If the intent’s action is correct (isActionView) and the custom comparison (isU1Matching) holds true, it further checks the intent’s data (Uri):
  • Validates that the URI’s scheme is mhl and the host is labs.
  • Retrieves the last path segment of the URI, which is expected to be a Base64-encoded value.

Decoding and Validation:

  • Decodes the Base64-encoded value from the URI’s last path segment.
  • Encrypts a predefined secret key using AES/CBC/PKCS5Padding along with a specified initialization vector (IV), and compares the result with the decoded value (ds).

Dynamic Library Loading and Flag Retrieval:

  • If the comparison is successful, it loads a native library named flag and calls a native method getflag() to retrieve a flag or some sensitive information. The result is then shown to the user via a Toast.

Activity and Application Termination:

  • If any of the conditions fail or after successfully showing the flag, it calls finishAffinity() to finish the current task and all its affiliated activities, followed by finish() to finish the activity itself, and finally, System.exit(0); to terminate the process.

At this point, it is evident that certain aspects of the puzzle are yet to be uncovered. To acquire a comprehensive understanding of the application’s operational mechanisms, additional analysis is mandatory. Let us proceed with a more thorough examination…

Let’s go over the next method…

This Java method named decrypt is designed to decrypt a given piece of text (cipherText) using a specified encryption algorithm (algorithm) and a secret key (key). The method uses the Java Cryptography Architecture (JCA) for the decryption process, specifically the Cipher class.

Parameter Validation:

  • The method begins by validating its parameters (algorithm, cipherText, and key) using Intrinsics.checkNotNullParameter. This step ensures that none of the parameters are null, preventing NullPointerException errors during execution.

Cipher Initialization:

  • A Cipher instance is created for the specified algorithm. This object will perform the decryption.
  • The method retrieves a predefined initialization vector (IV) from Activity2Kt.fixedIV, converting it to a byte array using UTF-8 encoding. The IV is crucial for certain encryption modes like CBC (Cipher Block Chaining) to add randomness to the encryption process and prevent certain types of attacks.
  • An IvParameterSpec object is instantiated with the IV byte array, and the cipher is initialized in decryption mode (2, which is equivalent to Cipher.DECRYPT_MODE) with the provided key and IV.

Decryption Process:

  • The cipherText argument, which is expected to be a Base64-encoded string, is first decoded into its original byte array form.
  • The cipher then decrypts the decoded byte array using the doFinal method, which performs the decryption in one step for simplicity.
  • The decrypted byte array is converted back into a String using UTF-8 encoding, which is then returned as the output of the method.

Now onto the last method…

The method cd() generates a string representation of the current date in the format dd/MM/yyyy, assigns it to a static variable, and then returns this string.

Declaration and Initialization:

  • String str; declares a variable str of type String. Initially, it’s not assigned a value.

Date Formatting:

  • SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()); creates an instance of SimpleDateFormat, which is used for formatting dates. The format dd/MM/yyyy specifies that the date should be represented in a day/month/year format. Locale.getDefault() ensures that the formatting is consistent with the default locale of the device.
  • String format = sdf.format(new Date()); generates a formatted string representing the current date. new Date() obtains the current date and time, and sdf.format(...) formats this date according to the previously defined pattern (dd/MM/yyyy).

Assignment and Validation:

  • Intrinsics.checkNotNullExpressionValue(format, "format(...)"); is a safety check to ensure that the format variable is not null after the date formatting.
  • Activity2Kt.cu_d = format; assigns the formatted date string to a static variable cu_d within a class or file named Activity2Kt. This implies that cu_d is meant to be accessible across different parts of the application.
  • str = Activity2Kt.cu_d; assigns the value of cu_d back to the local variable str. This step seems redundant since str could have been directly assigned format, suggesting the code might be decompiled or automatically generated.

Return Value:

  • The method checks if str is null, which, under normal circumstances, it should never be due to the prior checks. If str is somehow null, it throws an UninitializedPropertyAccessException, indicating an unexpected state. Otherwise, it returns str, which holds the formatted current date string.

Method Conclusion:

  • The method concludes by returning the string str, which contains the current date formatted as dd/MM/yyyy.

Are we done yet?? Let’s keep analyzing…

I went over the Activity2Kt and was interested in the fixedIV

Static Field for Initialization Vector (IV):

  • public static final String fixedIV = "1234567890123456";declares a public, static, and final field named fixedIV. This field is initialized with a 16-character string, "1234567890123456", which is use as an Initialization Vector (IV) for cryptographic operations. The final keyword indicates that the value of fixedIV cannot be changed once initialized.

Finally, the last interesting part was the KLOW method. This was part of the MainActivity

The KLOW() method demonstrates how to programmatically store the current date, formatted as "dd/MM/yyyy", into a SharedPreferences file named DAD4 under the key UUU0133.

Very interesting method!

Retrieving SharedPreferences:

  • SharedPreferences sharedPreferences = getSharedPreferences("DAD4", 0); retrieves a SharedPreferences instance named “DAD4”.

Editing SharedPreferences:

  • SharedPreferences.Editor editor = sharedPreferences.edit(); creates an editor object for the SharedPreferences, which is used to make changes (put values) into the shared preferences file.

Formatting Current Date:

  • SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()); creates a SimpleDateFormat object to format dates in a day/month/year format, using the default system locale.
  • String cu_d = sdf.format(new Date()); formats the current date and time into a String following the specified pattern ("dd/MM/yyyy"). Storing Formatted Date:
  • editor.putString("UUU0133", cu_d); stores the formatted date string (cu_d) in the shared preferences under the key "UUU0133".
  • editor.apply(); applies the changes to the shared preferences file asynchronously, ensuring the new key-value pair is saved.

That was a lot of codes… but very helpful for the challenge.

So…what’s next?

The Exploitation Strategy

The main question: How can we retrieve the flag?

We first need to get the value of the key from the onCreate method because as we saw earlier, the url scheme would look something like this:

mhl://labs/[Base64EncodedEncryptedValue]

Remember the string bqGrDKdQ8zo26HflRsGvVA== from the onCreate method? We can build a short python script to decrypt the value… or we can use CyberChef.

Let’s go with the Python script:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from base64 import b64decode

# Base64-encoded ciphertext
encoded_cipher_text = "bqGrDKdQ8zo26HflRsGvVA=="

# Decoding the Base64 encoded cipher text
decoded_cipher_text = b64decode(encoded_cipher_text)

# Values for secret key and IV
secret_key = b"your_secret_key_1234567890123456" 
iv = b"1234567890123456"  # The IV must be 16 bytes for AES

# Initialize the AES cipher in CBC mode for decryption
cipher = AES.new(secret_key, AES.MODE_CBC, iv)

# Decrypt the cipher text and unpad it
try:
    decrypted_bytes = unpad(cipher.decrypt(decoded_cipher_text), AES.block_size)
    # Convert the decrypted bytes back to a string
    decrypted_string = decrypted_bytes.decode("UTF-8")
    print("Decrypted text:", decrypted_string)
except ValueError as e:
    print("Decryption failed:", e)

We use the cipher_text, secret_key and iv value from the onCreate method. We get the value after running the script:

We can encode the value mhl_secret_1337 to Base64 and send it as part of the URI

Encoded value: bWhsX3NlY3JldF8xMzM3

mhl://labs/bWhsX3NlY3JldF8xMzM3

Now, the game plan is this:

With the help of Frida, we remotely invoke the KLOW method in MainActivity to manipulate shared preferences, setting the stage for the crafted intent executed via ADB to breach the app’s defenses but…

First, we can use adb command to start Activity2 within the com.mobilehackinglab.challenge app, passing the custom URI mhl://labs/bWhsX3NlY3JldF8xMzM3 as data.

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

We can now build the Frida script to call the KLOW method.

Java.perform(function () {
    // Use Java.choose to dynamically find instances of MainActivity.
    // This function is wrapped in a setTimeout to allow the application some time to initialize,
    // ensuring that instances of MainActivity are likely to be instantiated by the time the script runs.
    setTimeout(function () {
        Java.choose("com.mobilehackinglab.challenge.MainActivity", {
            onMatch: function (instance) {
                // Invoke the KLOW method on the found instance of MainActivity.
                // The KLOW method is expected to set up shared preferences
                instance.KLOW();
                console.log("[*] KLOW method invoked to set shared preferences.");
            },
            onComplete: function () {
                // Once all instances have been processed, log a completion message.
                console.log("[*] Completed MainActivity enumeration.");
            }
        });
    }, 5000); // The delay here is set to 5 seconds (5000 milliseconds).

    // After setting the shared preferences via the KLOW method, another setTimeout is used
    // to delay further actions, giving the app sufficient time to react to the changes made by KLOW.
    setTimeout(function () {
        // Dynamically find instances of Activity2 to interact with.
        // This is where the main exploitation logic to obtain the flag is expected to be triggered.
        Java.choose("com.mobilehackinglab.challenge.Activity2", {
            onMatch: function (instance) {
                // Optionally, invoke methods or log information from Activity2.
                // Here, the cd() method is called, and its result is logged.
                var cdResult = instance.cd();
                console.log("[*] cd() method result: " + cdResult);

                // Attempt to directly invoke the native getflag method if accessible.
                // This method is expected to return the challenge's flag directly.
                try {
                    var flag = instance.getflag();
                    console.log("[+] Flag: " + flag);
                } catch (e) {
                    // If the attempt to invoke getflag fails, log an error message.
                    console.log("[-] Unable to directly invoke getflag: " + e.message);
                }
            },
            onComplete: function () {
                // Once all instances have been processed, log a completion message.
                console.log("[*] Completed Activity2 enumeration.");
            }
        });
    }, 10000); // This delay is set to 10 seconds (10000 milliseconds) to ensure Activity2 is ready for interaction.
});

Exploiting the Vulnerability

It’s time to exploit the vulnerability. Let’s fire the Frida script:

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

We can confirm that the DAD4.xml file was created in shared_prefs on the device.

Now time for adb magic:

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

A success message is now visible on the device!

Given that the challenge explicitly instructs us to scan the memory for the flag, we can utilize Fridump for this task.

I used USB Flux to simulate the device being connected via USB. I then took the application named “Strings” for my target.

python3 fridump.py -U -s Strings

We can finally read the strings.txt that Fridump created and grep for the MHL flag!

cat strings.txt | grep -a MHL

Final Thoughts

This exploration through the Mobile Hacking Lab Strings challenge showed the essential role of advanced security practices in the Android environment. It illustrates the necessity for a deep understanding of application security mechanisms.

Comprehensive Insights

  • In-depth Analysis: Using tools like JADX-GUI and bespoke solutions facilitated the dissection of the application’s architecture, unveiling pivotal components such as Activity2. This initial analysis laid the groundwork for targeted exploitation strategies.
  • Strategic Method Invocation: The application of Frida for dynamically invoking crucial methods, specifically KLOW within MainActivity, was instrumental. This manipulation altered the application’s state, setting the groundwork for subsequent exploitation efforts.
  • Intent Crafting and Execution: The creation and execution of a specialized intent through ADB allowed for the direct interaction with and exploitation of Activity2, propelling us towards our objective in an efficient manner.
  • Memory Examination: Employing Fridump for memory scanning highlighted the significance of such techniques in extracting concealed data, ultimately leading to the successful retrieval of the flag.
  • Successful Exploitation: The culmination of these efforts was the strategic extraction of the flag, highlighting the criticality of understanding and exploiting vulnerabilities within Android applications.

Continue to explore, to learn, and to conquer. Until our next adventure, happy hacking!