UnCrackable Level 2 - Mobile Security Writeup
מאת שי מרדכי | 30 באפריל 2026
איתור מכשולים ב-Java
קובץ ה-Manifest מראה שה-Launcher מתחיל מ-MainActivity[cite: 1]. שם מצאתי פונקציית Native בשם bar המקבלת מערך בייטים[cite: 1].
שאלה: מדוע פונקציות NATIVE תמיד ישלחו BYTES לקוד ב-JAVA, ולא STRING או INT? בגלל הדרך בה עובד JNI - הוא מסיר את ההגנות על משתנים בזיכרון ומבחינתו זה רק בייטים רציפים[cite: 1].
טעינת הספרייה (LoadLibrary) וה-Linker
הקוד System.loadLibrary("foo") מסמן ל-Linker להפנות לקובץ libfoo.so[cite: 1]. בניגוד לשפת C שם הקישור (Static/Dynamic) קורה ב-Compile-time או Load-time, ב-JNI הקישור קורה ב-Runtime (רק כשמפעילים את הפונקציה בפועל)[cite: 1]. לכן, הצהרה על פונקציית Native לפני טעינת הספרייה לא תקריס את האפליקציה[cite: 1].
ה-init function ומנגנוני רקע
הפונקציה init() מפעילה AsyncTask (Worker Thread) שבודק האם מישהו מאזין (Debug.isDebuggerConnected()) בכל 100 מילי-שניות[cite: 1]. ברגע שמישהו מבצע דיבאגינג, מתחיל onPostExecute()[cite: 1]. ב-Java Hook אפשר לדרוס את ה-doInBackground שיחזיר מיד FALSE, או לדרוס את onPostExecute[cite: 1].
חקירת ה-Native ב-IDA Pro
בטבלת ה-Exports ב-IDA, מצאתי את Java_sg_vantagepoint_uncrackable2_CodeCheck_bar[cite: 1]. לפני ה-JNI, מתבצעת בדיקה ידנית של משתנה byte_400C (ה-Flag) שחייב להיות שווה ל-1[cite: 1]. ראיתי ש-INIT אחראית להכניס אליו 1, מיד אחרי שהיא מבצעת fork()[cite: 1].
מנגנון Anti-Debugging דרך Ptrace
מבט ב-Disassembly של הפונקציה מגלה מנגנון לינוקסי מרתק[cite: 1]:
- ה-
forkיוצר תהליך בן (Child Process)[cite: 1]. - תהליך הבן נועל את דיבוג תהליך האב באמצעות
ptrace(PTRACE_ATTACH, parent_pid)[cite: 1]. - הבן נכנס ללולאה אינסופית ששומרת על האבא "נעול", בעוד שהאבא ממשיך בתוכנית ומשנה את הדגל ל-1[cite: 1].
הפתרון: Inline Hooking עם Frida
במקום לפצ'פץ' ב-IDA או לעצור את הבן ב-Kernel, בחרתי לשתול Hook בזמן ריצה בעזרת Frida כדי לא לשנות את חתימת האפליקציה[cite: 1]. זיהיתי שהפונקציה משתמשת באופסטים של JNIEnv כדי לשלוף את הקלט (באמצעות GetByteArrayElements) ולהשוות אותו לסוד בעזרת strncmp[cite: 1].
var strncmpPtr = Module.findExportByName("libfoo.so", "strncmp");
Interceptor.attach(strncmpPtr, {
onEnter: function(args) {
var arg0 = Memory.readUtf8String(args[0]); // User Input
var arg1 = Memory.readUtf8String(args[1]); // Secret
if (arg1.indexOf("Thanks for all the fish") !== -1) {
console.log("\n[!] Bridge Intercepted!");
console.log("[+] User Input: " + arg0);
console.log("[+] Secret Key: " + arg1);
}
}
});