7 minute read

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:

  1. 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++.
  2. 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.xml với key là UUU0133.
  • Lưu ý: Nếu không mở qua MainActivity, hàm KLOW() sẽ không chạy, khiến file DAD4.xml rỗ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ỗi ds.
  • Giải mã đoạn CipherText bqGrDKdQ8zo26HflRsGvVA== bằng AES/CBC/PKCS5Padding vớ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ị crash System.exit(0).
  • Nguyên nhân: Do chưa chạy MainActivity, hàm KLOW() chưa được gọi khiến file DAD4.xml trố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-trace theo dõi hàm C++ nội bộ _Z6flag13v và đọc bằng JNI getStringUtfChars.
  • Kết quả: App bị sập (Crash - SIGABRT).
  • Nguyên nhân: _Z6flag13v là hàm C++ trả về con trỏ char*, không phải đối tượng jstring củ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ớ RAM rw- tìm chuỗi MHL{.
  • 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ùng readCString().

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:

  1. Chặn System.exitfinish để giữ app luôn sống.
  2. Tìm instance MainActivity trên Heap để gọi hàm KLOW().
  3. Quét bộ nhớ rw- bằng readCString() để 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:

  1. Khởi chạy app và attach Frida: frida -U -l exploit.js com.mobilehackinglab.challenge. Chạy runKlowAndPrepare() để ghi ngày tháng vào file XML.
  2. Gửi Intent qua ADB:
    adb shell am start -n com.mobilehackinglab.challenge/.Activity2 -a android.intent.action.VIEW -d "mhl://labs/bWhsX3NlY3JldF8xMzM3"
    
  3. 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.

  1. Decompile APK bằng apktool:
    apktool d com.mobilehackinglab.strings.apk -o strings_decompiled
    
  2. Chỉnh sửa file Activity2.smali: Mở file smali/com/mobilehackinglab/challenge/Activity2.smali:

    • Sửa điều kiện check Intent & Date từ AND (&&) thành OR (||):
      .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_0
      

      Thay đổi điều kiện if-eqz thành if-nez để mọi giá trị nhập vào đều được chấp nhận là hợp lệ.

  3. 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!

Updated: