Strings - Mobile Hacking Lab
Introduction
Welcome to the Strings Challenge! In this lab,your goal is to find the flag. The flag’s format should be “MHL{…}”. The challenge will give you a clear idea of how intents and intent filters work on android also you will get a hands-on experience using Frida APIs.
Objective
Exploit the application to obtain the flag.
When the application launches, it displays the message “Hello from C++”.
Analyzing the application using JADX
From: AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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>
This manifest entry defines an exported activity named com.mobilehackinglab.challenge.Activity2
, meaning it can be launched by components outside the app, such as other apps or browsers. The intent filter attached to it allows the activity to respond to android.intent.action.VIEW
intents, which are commonly used for opening URLs. It also includes the DEFAULT
and BROWSABLE
categories, enabling it to be triggered from web links. The <data>
element specifies that this activity handles deep links with the custom URI scheme mhl
and host labs
, so a URI like mhl://labs
would open this activity.
Start Activity2 with adb
1
adb shell am start -a android.intent.action.VIEW -n com.mobilehackinglab.challenge/.Activity2 -d "mhl://labs"
it didn’t navigate to Activity2, and instead, the application crashed.
From: com.mobilehackinglab.challenge.MainActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
public final native String stringFromJNI();
static {
System.loadLibrary("challenge");
}
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();
}
}
The KLOW()
method is responsible for storing the current date into the app’s SharedPreferences
under a specific key. It begins by accessing the SharedPreferences
file named "DAD4"
in private mode, which means the data is accessible only to the app. Then, it initializes an editor to modify the stored data. A SimpleDateFormat
object is used to format the current date into the "dd/MM/yyyy"
format based on the device’s default locale. The formatted date is then stored using the key "UUU0133"
, and the changes are saved using the apply()
method, which commits the update asynchronously. This setup is often used to persistently track the date of a particular event or user interaction within the app.
In general, the KLOW method:
- Opens or creates a SharedPreferences file named DAD4.
- Retrieves the current date in the dd/MM/yyyy format.
- Saves this date under a key named UUU0133.
- Applies the saved changes.
From: com.mobilehackinglab.challenge.Activity2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public final class Activity2 extends AppCompatActivity {
private final native String getflag();
protected void onCreate(Bundle savedInstanceState) {
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;
} else {
finishAffinity();
finish();
System.exit(0);
return;
}
}
finishAffinity();
finish();
System.exit(0);
return;
}
finishAffinity();
finish();
System.exit(0);
return;
}
finishAffinity();
finish();
System.exit(0);
}
public final String decrypt(String algorithm, String cipherText, SecretKeySpec key) {
Intrinsics.checkNotNullParameter(algorithm, "algorithm");
Intrinsics.checkNotNullParameter(cipherText, "cipherText");
Intrinsics.checkNotNullParameter(key, "key");
Cipher cipher = Cipher.getInstance(algorithm);
try {
byte[] bytes = Activity2Kt.fixedIV.getBytes(Charsets.UTF_8);
Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
IvParameterSpec ivSpec = new IvParameterSpec(bytes);
cipher.init(2, key, ivSpec);
byte[] decodedCipherText = Base64.decode(cipherText, 0);
byte[] decrypted = cipher.doFinal(decodedCipherText);
Intrinsics.checkNotNull(decrypted);
return new String(decrypted, Charsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Decryption failed", e);
}
}
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;
}
}
This code performs a validation check when the activity is launched via a deep link. It first retrieves a stored date (UUU0133
) from SharedPreferences
and compares it with the current date using a cd()
function. If the intent action is VIEW
and the dates match, it checks if the URI scheme is mhl
and the host is labs
. Then it extracts the last segment of the URI, decodes it from Base64, and compares it with a decrypted AES value using a predefined key and IV. If the comparison succeeds, it loads the native library flag
and shows the flag using a toast. If any of the checks fail, the app terminates.
The decrypt
method takes an encryption algorithm (e.g., "AES/CBC/PKCS5Padding"
), a Base64-encoded ciphertext, and a SecretKeySpec
key. It initializes a cipher with the specified algorithm and a fixed IV (Initialization Vector) from Activity2Kt.fixedIV
. It then decodes the ciphertext from Base64 and decrypts it using the cipher. The resulting bytes are converted to a UTF-8 string and returned. If decryption fails for any reason, it throws a RuntimeException
.
This function cd()
generates the current date formatted as dd/MM/yyyy
using the device’s default locale. It stores this formatted date in a static variable cu_d
from Activity2Kt
and returns it. If cu_d
is unexpectedly null, it throws an exception indicating that the property was accessed before being properly initialized.
getflag()
It is a native method, used to invoke a function in a native (C/C++) library, and it returns a String.
Note: Although the KLOW()
function is implemented, it is never actually invoked in the code. As a result, the DAD4.xml
file will not be created in the shared preferences by default. To work around this, we can either manually create the file and push it into the shared preferences directory or use Frida to dynamically invoke the KLOW()
function at runtime.
frida script to call KLOW() function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Java.perform(function () {
setTimeout(function () {
Java.choose("com.mobilehackinglab.challenge.MainActivity" , {
onMatch : function(instance){
console.log("Found instance: "+instance);
console.log("calling KLOW function");
instance.KLOW();
},
onComplete:function(){}
});
}, 1000);
setTimeout(function () {
Java.choose("com.mobilehackinglab.challenge.Activity2" , {
onMatch : function(instance){
console.log("Found instance: "+instance);
console.log("cd function: " + instance.cd());
console.log("native function: " + instance.getflag());
},
onComplete:function(){}
});
}, 10000);
});
Alternatively, you can manually create the file and place it in the shared preferences directory with the filename DAD4.xml
.
1
2
3
4
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="UUU0133">18/04/2025</string>
</map>
1
2
adb root
adb push DAD4.xml
Also, you can retrieve the UUU0133 value stored in SharedPreferences by the KLOW method in the MainActivity class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.perform(function () {
var sharedPreferencesClass = Java.use("android.app.SharedPreferencesImpl");
sharedPreferencesClass.getString.overload('java.lang.String', 'java.lang.String').implementation = function (key, defValue) {
if (key === "UUU0133") {
var value = this.getString(key, defValue);
console.log("UUU0133 value: " + value);
return value;
}
return this.getString(key, defValue);
};
});
1
frida -U -f com.mobilehackinglab.challenge -l frida.js
Hooking AES Encryption
I used this frida script to hook the AES encryption functions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Java.perform(function x() {
var secret_key_spec = Java.use("javax.crypto.spec.SecretKeySpec");
//SecretKeySpec is inistantiated with the bytes of the key, so we hook the constructor and get the bytes of the key from it
//We will get the key but we won't know what data is decrypted/encrypted with it
secret_key_spec.$init.overload("[B", "java.lang.String").implementation = function (x, y) {
send('{"my_type" : "KEY"}', new Uint8Array(x));
//console.log(xx.join(" "))
return this.$init(x, y);
}
//hooking IvParameterSpec's constructor to get the IV as we got the key above.
var iv_parameter_spec = Java.use("javax.crypto.spec.IvParameterSpec");
iv_parameter_spec.$init.overload("[B").implementation = function (x) {
send('{"my_type" : "IV"}', new Uint8Array(x));
return this.$init(x);
}
//now we will hook init function in class Cipher, we will be able to tie keys,IVs with Cipher objects
var cipher = Java.use("javax.crypto.Cipher");
cipher.init.overload("int", "java.security.Key", "java.security.spec.AlgorithmParameterSpec").implementation = function (x, y, z) {
//console.log(z.getClass());
if (x == 1) // 1 means Cipher.MODE_ENCRYPT
send('{"my_type" : "hashcode_enc", "hashcode" :"' + this.hashCode().toString() + '" }');
else // In this android app it is either 1 (Cipher.MODE_ENCRYPT) or 2 (Cipher.MODE_DECRYPT)
send('{"my_type" : "hashcode_dec", "hashcode" :"' + this.hashCode().toString() + '" }');
//We will have two lists in the python code, which keep track of the Cipher objects and their modes.
//Also we can obtain the key,iv from the args passed to init call
send('{"my_type" : "Key from call to cipher init"}', new Uint8Array(y.getEncoded()));
//arg z is of type AlgorithmParameterSpec, we need to cast it to IvParameterSpec first to be able to call getIV function
send('{"my_type" : "IV from call to cipher init"}', new Uint8Array(Java.cast(z, iv_parameter_spec).getIV()));
//init must be called this way to work properly
return cipher.init.overload("int", "java.security.Key", "java.security.spec.AlgorithmParameterSpec").call(this, x, y, z);
}
//now hooking the doFinal method to intercept the enc/dec process
//the mode specified in the previous init call specifies whether this Cipher object will decrypt or encrypt, there is no functions like cipher.getopmode() that we can use to get the operation mode of the object (enc or dec)
//so we will send the data before and after the call to the python code, where we will decide which one of them is cleartext data
//if the object will encrypt, so the cleartext data is availabe in the argument before the call, else if the object will decrypt, we need to send the data returned from the doFinal call and discard the data sent before the call
cipher.doFinal.overload("[B").implementation = function (x) {
send('{"my_type" : "before_doFinal" , "hashcode" :"' + this.hashCode().toString() + '" }', new Uint8Array(x));
var ret = cipher.doFinal.overload("[B").call(this, x);
send('{"my_type" : "after_doFinal" , "hashcode" :"' + this.hashCode().toString() + '" }', new Uint8Array(ret));
return ret;
}
});
Output:
1
2
3
4
5
6
7
[Android Emulator 5554::com.mobilehackinglab.challenge ]-> message: {'type': 'send', 'payload': '{"my_type" : "KEY"}'} data: b'your_secret_key_1234567890123456'
message: {'type': 'send', 'payload': '{"my_type" : "IV"}'} data: b'1234567890123456'
message: {'type': 'send', 'payload': '{"my_type" : "hashcode_dec", "hashcode" :"238648933" }'} data: None
message: {'type': 'send', 'payload': '{"my_type" : "Key from call to cipher init"}'} data: b'your_secret_key_1234567890123456'
message: {'type': 'send', 'payload': '{"my_type" : "IV from call to cipher init"}'} data: b'1234567890123456'
message: {'type': 'send', 'payload': '{"my_type" : "before_doFinal" , "hashcode" :"238648933" }'} data: b'n\xa1\xab\x0c\xa7P\xf3:6\xe8w\xe5F\xc1\xafT'
message: {'type': 'send', 'payload': '{"my_type" : "after_doFinal" , "hashcode" :"238648933" }'} data: b'mhl_secret_1337'
The decrypted string is “mhl_secret_1337”.
Therefore, we need to base64 encode this string and append it to the path section of the deeplink URL to trigger the activity.
1
adb shell am start -a android.intent.action.VIEW -n com.mobilehackinglab.challenge/com.mobilehackinglab.challenge.Activity2 -d "mhl://labs/bWhsX3NlY3JldF8xMzM3"
Hooking getflag() method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Java.perform(function () {
setTimeout(function () {
Java.choose("com.mobilehackinglab.challenge.MainActivity" , {
onMatch : function(instance){
console.log("Found instance: "+instance);
console.log("calling KLOW function");
instance.KLOW();
},
onComplete:function(){}
});
}, 1000);
setTimeout(function () {
Java.choose("com.mobilehackinglab.challenge.Activity2" , {
onMatch : function(instance){
console.log("Found instance: "+instance);
console.log("cd function: " + instance.cd());
console.log("native function: " + instance.getflag());
},
onComplete:function(){}
});
}, 10000);
});
1
frida -U -f com.mobilehackinglab.challenge -l .\frida.js
1
adb shell am start -a android.intent.action.VIEW -n com.mobilehackinglab.challenge/com.mobilehackinglab.challenge.Activity2 -d "mhl://labs/bWhsX3NlY3JldF8xMzM3"
After running the script and executing the intent command, we received a Success toast message.
Method 1 : Get the flag with frida script
However, we were only able to get the Success message. To retrieve the flag, we need to run a Frida script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Java.perform(function() {
var lib = Process.getModuleByName("libflag.so");
console.log("libflag.so =>", JSON.stringify(lib));
//"MHL{"=4D 48 4C 7B(HEX)
var pattern = "4D 48 4C 7B";
Memory.scan(lib.base, lib.size, pattern, {
onMatch: function(address, size) {
console.log(" Match found at address:", address, "size:", size);
console.log(hexdump(address, { length: 64 }));
var flagString = Memory.readCString(address);
console.log("Flag:", flagString);
},
onComplete: function() {
console.log("Memory scan complete");
}
});
});
Method 2: Get the Flag with Fridump
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
python fridump.py -U -s Strings
fridump\fridump.py:11: SyntaxWarning: invalid escape sequence '\|'
logo = """
______ _ _
| ___| (_) | |
| |_ _ __ _ __| |_ _ _ __ ___ _ __
| _| '__| |/ _` | | | | '_ ` _ \| '_ \
| | | | | | (_| | |_| | | | | | | |_) |
\_| |_| |_|\__,_|\__,_|_| |_| |_| .__/
| |
|_|
Current Directory: fridump
Output directory is set to: fridump\dump
Starting Memory dump...
fridump\fridump.py:119: DeprecationWarning: Script.exports will become asynchronous in the future, use the explicit Script.exports_sync instead
agent = script.exports
Oops, memory access violation!-------------------------------] 27.37% Complete
Oops, memory access violation!-------------------------------] 27.51% Complete
Progress: [##################################################] 99.72% Complete
Running strings on all files:
Progress: [##################################################] 100.0% Complete
Finished!
Flag: MHL{IN_THE_MEMORY}