[Writeup] Mobile Hacking Lab - Strings Challenge
I. Write-up: Mobile Hacking Lab - Strings Challenge

Trong lập trình Android, Intent là cơ chế truyền tin giúp các thành phần trong cùng ứng dụng hoặc giữa các ứng dụng khác nhau giao tiếp với nhau. Intent gồm 2 loại chính:
- Explicit Intent: Chỉ định rõ thành phần mục tiêu (Activity, Service) sẽ nhận và xử lý.
-
Implicit Intent: Hệ thống tự xác định thành phần phù hợp dựa trên Action, Category và Data URI được cung cấp.
- Mục tiêu: Trích xuất cờ (Flag) định dạng
MHL{...}từ ứng dụng Android. - Kỹ thuật áp dụng: Dịch ngược mã nguồn (Reverse Engineering), Hooking & Quét bộ nhớ động (Dynamic Instrumentation & Memory Scanning) bằng Frida, Patch file Smali.
- Cơ chế bảo vệ: Mã nguồn Native C/C++ (
libflag.so), kiểm tra SharedPreferences, và Intent Data Filtering (Deep Link).

Sau khi chạy ứng dụng, bạn sẽ thấy nội dung màn hình tĩnh với tiêu đề “Strings” và văn bản “Hello from C++”.
Vì không có mã nguồn nên mình reverse mã nguồn APK của app bằng JADX.
Đầu tiên mình muốn tìm dòng lệnh exported=”true”
Trong hệ điều hành Android, thuộc tính android:exported = “true” được dùng để cấp phép cho các ứng dụng khác hoặc hệ điều hành Android có quyền tương tác với các thành phần (Activity, Service, BroadcastReceiver, ContentProvider) bên trong ứng dụng của bạn
Rủi ro bảo mật: Đặt true không kiểm soát cho phép các ứng dụng độc hại trên thiết bị có thể thao túng dữ liệu hoặc đánh cắp thông tin. Hãy đảm bảo bạn chỉ đặt true cho các thành phần thực sự cần thiết và sử dụng thêm quyền (permission) nếu cần
Đây là màn hình MainActivity trong JADX:

Ứng dụng này bao gồm hai màn hình:
- MainActivity: Được mở mặc định từ Launcher, hiển thị chuỗi chào mừng lấy từ thư viện Native C++.
- Activity2: Chỉ có thể khởi chạy thông qua một Browsable Intent (Deep Link) với schema, host và path cụ thể.
Phân tích mã nguồn (Static Analysis)
1. Quản lý trạng thái tại MainActivity

Hàm KLOW() trong MainActivity làm nhiệm vụ khởi tạo dữ liệu:
- Lấy ngày hiện tại của hệ thống (định dạng
dd/MM/yyyy). - Ghi giá trị này vào SharedPreferences
DAD4.xmlvới key làUUU0133. - Lưu ý: Nếu không mở qua
MainActivity, hàmKLOW()sẽ không chạy, khiến fileDAD4.xmlrỗng hoặc không có dữ liệu ngày.
2. Kiểm tra điều kiện tại Activity2

public final class Activity2 extends AppCompatActivity {
private final native String getflag();
@Override
protected void onCreate(Bundle savedInstanceState) throws NoSuchPaddingException, NoSuchAlgorithmException {
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);
}
}
Để Activity2 chạy thành công và nạp thư viện libflag.so, chúng ta cần thỏa mãn các điều kiện sau:
1. Kiểm tra ngày tháng (isU1Matching):
Hàm cd() lấy ngày hôm nay và so sánh với giá trị UUU0133 được lưu từ hàm KLOW() trong MainActivity. Vì vậy, cần đảm bảo KLOW() đã được gọi trước.
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;
}
2. Kiểm tra Intent Action:
Ứng dụng ép buộc tiến trình gọi phải có Action là android.intent.action.VIEW. Khi gọi qua ADB, ta phải thêm cờ -a android.intent.action.VIEW.
adb shell am start -a android.intent.action.VIEW -d <link>
3. Định dạng cấu trúc URI (Scheme & Host):
(uri != null && Intrinsics.areEqual(uri.getScheme(), "mhl") && Intrinsics.areEqual(uri.getHost(), "labs"))
URI truyền vào bắt buộc phải có dạng mhl://labs/<payload>.
4. Xác thực Payload bằng AES:
- Lấy phần cuối URI (
getLastPathSegment()) và Decode Base64 để thu được chuỗids. - Giải mã đoạn CipherText
bqGrDKdQ8zo26HflRsGvVA==bằngAES/CBC/PKCS5Paddingvới Key"your_secret_key_1234567890123456"và IV tĩnh (fixedIV). - So sánh kết quả giải mã với
ds.

Bạn có thể dùng CyberChef hoặc tool giải mã AES online để lấy Plaintext:
- Plaintext thu được:
mhl_secret_1337 - Cần Encode Base64 chuỗi này trước khi đưa vào URI:
Base64("mhl_secret_1337")=bWhsX3NlY3JldF8xMzM3
=> Deep Link hoàn chỉnh: mhl://labs/bWhsX3NlY3JldF8xMzM3
III. Quá trình giải bài và các nguyên nhân lỗi
Trong quá trình làm bài, việc thử nghiệm trực tiếp sẽ gặp một số lỗi:
Lỗi 1: Bỏ qua MainActivity và gọi thẳng Activity2
- Hành động: Chạy ADB gọi trực tiếp
Activity2(adb shell am start -n .../.Activity2). - Kết quả: Hàm trả về
"Success"hoặc app bị crashSystem.exit(0). - Nguyên nhân: Do chưa chạy
MainActivity, hàmKLOW()chưa được gọi khiến fileDAD4.xmltrống. Lớp Native nhận đầu vào sai nên không tạo Flag thật.
Lỗi 2: Lỗi Mismatch Type (SIGABRT) khi hook hàm Native con
- Hành động: Dùng
frida-tracetheo dõi hàm C++ nội bộ_Z6flag13vvà đọc bằng JNIgetStringUtfChars. - Kết quả: App bị sập (Crash - SIGABRT).
- Nguyên nhân:
_Z6flag13vlà hàm C++ trả về con trỏchar*, không phải đối tượngjstringcủa Java. Ép kiểu đọc JNI sẽ gây lỗi bộ nhớ.
Lỗi 3: Lỗi Null Byte khi quét RAM (Error: can't decode byte 0x00)
- Hành động: Dùng
readUtf8String(64)quét vùng nhớ RAMrw-tìm chuỗiMHL{. - Kết quả: Script báo lỗi
Error: can't decode byte 0x00. - Nguyên nhân: Chuỗi trong C/C++ kết thúc bằng Null Byte (
\0). Đọc UTF-8 với độ dài cố định va phải Null Byte gây lỗi decode. Cần đổi sang dùngreadCString().
IV. Khai thác chi tiết
Cách 1: Sử dụng Frida Script (exploit.js)
Mã Frida script hoàn chỉnh giúp:
- Chặn
System.exitvàfinishđể giữ app luôn sống. - Tìm instance
MainActivitytrên Heap để gọi hàmKLOW(). - Quét bộ nhớ
rw-bằngreadCString()để lấy Flag từlibflag.so.
// Hàm tìm MainActivity trên Heap và ép chạy KLOW()
globalThis.runKlowAndPrepare = function() {
Java.perform(function () {
console.log("[*] Đang truy vết MainActivity trên Heap để ép chạy KLOW()...");
var SystemClass = Java.use("java.lang.System");
var Activity = Java.use("android.app.Activity");
SystemClass.exit.implementation = function (code) {};
Activity.finish.overload().implementation = function () {};
Activity.finishAffinity.overload().implementation = function () {};
Java.choose("com.mobilehackinglab.challenge.MainActivity", {
onMatch: function(instance) {
console.log("[+] Đã tóm được instance của MainActivity. Đang gọi KLOW()...");
instance.KLOW();
console.log("[🔥] ĐỒNG BỘ CẤU HÌNH THÀNH CÔNG! File DAD4.xml đã được ghi nhận.");
},
onComplete: function() {
console.log("[*] Quá trình quét Heap hoàn tất.");
}
});
});
};
// Hàm quét bộ nhớ an toàn với chuẩn C-String
globalThis.memorySearchFlag = function() {
console.log("[*] Bắt đầu quét bộ nhớ RAM tìm kiếm chuỗi 'MHL{'...");
var pattern = "4d 48 4c 7b"; // Hex của "MHL{"
Process.enumerateRanges('rw-').forEach(function (range) {
try {
Memory.scan(range.base, range.size, pattern, {
onMatch: function (address, size) {
try {
// Sử dụng readCString() để tránh lỗi 0x00 Null Byte của C/C++
var content = address.readCString();
if (content && content.indexOf("MHL{") !== -1) {
console.log("\n==================================================");
console.log("[🎉] KHAI THÁC THÀNH CÔNG!");
console.log("👉 FLAG THỰC TẾ: " + content.trim());
console.log("==================================================\n");
}
} catch (err) {}
},
onError: function (reason) {},
onComplete: function () {}
});
} catch (e) {}
});
};

Các bước thực hiện:
- Khởi chạy app và attach Frida:
frida -U -l exploit.js com.mobilehackinglab.challenge. ChạyrunKlowAndPrepare()để ghi ngày tháng vào file XML. - Gửi Intent qua ADB:
adb shell am start -n com.mobilehackinglab.challenge/.Activity2 -a android.intent.action.VIEW -d "mhl://labs/bWhsX3NlY3JldF8xMzM3" - Gọi
memorySearchFlag()từ Frida console để trích xuất flag trong RAM.
Cách 2: Patch file Smali & Build lại APK
Ngoài việc dùng Frida, chúng ta có thể patch file Smali để bypass các bước check trong Activity2.
- Decompile APK bằng
apktool:apktool d com.mobilehackinglab.strings.apk -o strings_decompiled -
Chỉnh sửa file
Activity2.smali: Mở filesmali/com/mobilehackinglab/challenge/Activity2.smali:- Sửa điều kiện check Intent & Date từ
AND(&&) thànhOR(||):.line 37 .local v4, "isU1Matching":Z if-nez v3, :doBlock if-nez v4, :doBlock goto :cond_3 :doBlock - Sửa lệnh kiểm tra chuỗi giải mã AES (
equals) tại line 47:.line 47 .local v10, "str":Ljava/lang/String; invoke-virtual {v10, v8}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z move-result v11 if-nez v11, :cond_0Thay đổi điều kiện
if-eqzthànhif-nezđể mọi giá trị nhập vào đều được chấp nhận là hợp lệ.
- Sửa điều kiện check Intent & Date từ
- Rebuild và ký lại APK bằng
uber-apk-signer:# Rebuild APK apktool b strings_decompiled -o dist/modified.apk # Ký APK java -jar .\uber-apk-signer-1.3.0.jar --apk .\dist\modified.apk
Sau khi cài đặt APK đã patch, app sẽ nhận cả các giá trị không hợp lệ và nạp libflag.so. Cuối cùng, chạy script Frida scan memory để lấy flag.
V. Kết quả
Kịch bản khai thác thành công, flag đã được giải mã và trích xuất từ RAM: MHL{IN_THE_MEMORY}
Và đây là certificate sau khi hoàn thành bài lab. Cảm ơn các bạn đã theo dõi bài viết!
