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 asfalse
- 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.