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’sonCreate
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 isandroid.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 inisU1Matching
.
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 islabs
. - 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 methodgetflag()
to retrieve a flag or some sensitive information. The result is then shown to the user via aToast
.
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 byfinish()
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
, andkey
) usingIntrinsics.checkNotNullParameter
. This step ensures that none of the parameters arenull
, preventingNullPointerException
errors during execution.
Cipher Initialization:
- A
Cipher
instance is created for the specifiedalgorithm
. This object will perform the decryption. - The method retrieves a predefined initialization vector (IV) from
Activity2Kt.fixedIV
, converting it to a byte array usingUTF-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 toCipher.DECRYPT_MODE
) with the providedkey
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
usingUTF-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 variablestr
of typeString
. Initially, it’s not assigned a value.
Date Formatting:
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault());
creates an instance ofSimpleDateFormat
, which is used for formatting dates. The formatdd/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, andsdf.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 theformat
variable is not null after the date formatting.Activity2Kt.cu_d = format;
assigns the formatted date string to a static variablecu_d
within a class or file namedActivity2Kt
. This implies thatcu_d
is meant to be accessible across different parts of the application.str = Activity2Kt.cu_d;
assigns the value ofcu_d
back to the local variablestr
. This step seems redundant sincestr
could have been directly assignedformat
, 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. Ifstr
is somehow null, it throws anUninitializedPropertyAccessException
, indicating an unexpected state. Otherwise, it returnsstr
, which holds the formatted current date string.
Method Conclusion:
- The method concludes by returning the string
str
, which contains the current date formatted asdd/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 namedfixedIV
. This field is initialized with a 16-character string,"1234567890123456"
, which is use as an Initialization Vector (IV) for cryptographic operations. Thefinal
keyword indicates that the value offixedIV
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 aSharedPreferences
instance named “DAD4”.
Editing SharedPreferences:
SharedPreferences.Editor editor = sharedPreferences.edit();
creates an editor object for theSharedPreferences
, 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 aSimpleDateFormat
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 aString
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
withinMainActivity
, 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!