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!
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.
Deep Link Validation
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
mhland hostlabs - If yes, it creates a variable
base64Valuethat gets thelastPathSegment. 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==
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);
});
Step 2: Triggering the Deep Link
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
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