7.7 טכניקות אנטי הנדסה הפוכה פתרון
פתרון תרגיל 1 - ההשפעה של strip¶
- פלט
nm: nm with_symbolsמראה את כל הפונקציות: main, calculate, print_result, וגם פונקציות עזר של libcnm without_symbolsמחזיר:nm: without_symbols: no symbols-
ה-strip הסיר את כל טבלת הסימבולים
-
פלט
objdump -t: with_symbols- מראה את כל הסימבולים כמו nm-
without_symbols- מראה רק את הסימבולים הדינמיים (DYNAMIC SYMBOL TABLE) - פונקציות של libc כמו printf,__libc_start_mainוכו'. אלה נשארים כי הם הכרחיים לקישור דינמי. -
stringsעל שניהם: אין הבדל משמעותי. המחרוזת "Result: %d\n" מופיעה בשני הקבצים. strip מסיר סימבולים, לא מחרוזות. -
בגידרה:
with_symbols- גידרה מראה את שמות הפונקציות המקוריים-
without_symbols- גידרה מראה שמות אוטומטיים (FUN_XXXXX), אבל ה-Decompiler מצליח לפענח את הלוגיקה בדיוק אותו דבר -
מסקנה: strip לא מגן באמת על הקוד. הוא מסיר שמות, אבל כל הלוגיקה, המחרוזות, ודפוסי הקוד נשארים. זה מאט את החוקר (צריך לשנות שמות ידנית) אבל לא עוצר אותו.
פתרון תרגיל 2 - זיהוי ועקיפת אנטי-דיבוג עם ptrace¶
-
הרצה רגילה (בלי debugger):
-
הרצה עם GDB:
התוכנית מזהה את GDB ויוצאת. -
עקיפה:
דרך א - שינוי ערך ב-GDB:
GDB עוצר כשנכנסים ל-syscall ptrace. נמשיך:
GDB עוצר שוב כשיוצאים מה-syscall. עכשיו נשנה את ערך ההחזרה:
הפלט:
דרך ב - LD_PRELOAD:
הפלט:
- הסבר:
- דרך א: catch syscall ptrace גורם ל-GDB לעצור כשהתוכנית קוראת ל-syscall ptrace. אחרי שה-syscall חוזר, אנחנו משנים את ערך ההחזרה (שב-rax) ל-0 (הצלחה), במקום -1 (כישלון שנגרם כי GDB כבר עושה ptrace).
- דרך ב: LD_PRELOAD טוען את fake_ptrace.so לפני libc. כשהתוכנית קוראת ל-ptrace, היא מקבלת את הגרסה שלנו (שמחזירה 0 תמיד) במקום הגרסה האמיתית. התוכנה חושבת שהכל בסדר.
פתרון תרגיל 3 - זיהוי מחרוזות מוצפנות¶
-
strings encrypted_strings- לא מוצא את הסיסמה. המחרוזות המוצפנות הן בתים לא קריאים, ולכן strings לא מזהה אותן כמחרוזות. -
בגידרה, main נראה בערך כך (ב-Decompiler):
-
פונקציית decrypt ב-Decompiler:
void FUN_00401136(char *param_1, int param_2, char param_3) { for (int i = 0; i < param_2; i++) { param_1[i] = param_1[i] ^ param_3; } }
הפונקציה עוברת על כל בית במערך ומבצעת XOR עם המפתח. -
המפתח הוא 0x42 (66 בעשרוני, 'B' כתו).
-
שימוש ב-GDB:
מוצאים את הכתובת שאחרי הקריאה ל-decrypt (אחרי ה-call):
עכשיו המחרוזת כבר פוענחה. מוצאים את הכתובת שלה (למשל דרך rsp או דרך הDecompiler) ו:
הפלט:"Secret Password: opensesame"
פתרון תרגיל 4 - זיהוי packing עם UPX¶
-
השוואת גדלים:
UPX דחס את הקובץ בכ-50%. -
strings: strings original- מוצא "Hello from packed binary!", "Count: %d\n" ועוד-
strings packed- לא מוצא את המחרוזות של התוכנית. רואים רק מחרוזות של UPX עצמו:UPX!,$Info: This file is packed with the UPX -
readelf -h: -
נקודת הכניסה שונה! ב-packed, הentry point מצביע על קוד ה-unpacker, לא על main המקורי.
-
readelf -S: - ב-original: סקשנים רגילים (.text, .data, .rodata, .bss וכו')
-
ב-packed: סקשנים עם שמות שונים (UPX0, UPX1, UPX2). אלו סימנים ברורים לאריזת UPX.
-
בגידרה:
-
packed ייראה מוזר - ה-Decompiler יציג את קוד ה-unpacker (שמפרש את הקוד הדחוס) ולא את הקוד המקורי. הקוד המקורי מוסתר בתוך הנתונים הדחוסים.
-
אחרי
upx -d packed: strings packedמוצא את כל המחרוזות המקוריות- הקובץ חזר לגודלו המקורי
- גידרה מראה את הקוד המקורי כרגיל
פתרון תרגיל 5 - ניתוח תוכנית עם מספר טכניקות אנטי-RE¶
-
הרצה רגילה (בלי debugger):
-
הרצה עם GDB:
בדיקת ptrace מזהה את GDB. -
טכניקות אנטי-RE שזוהו:
- בדיקת ptrace:
check_debugger()קוראת ל-ptrace(PTRACE_TRACEME) - בדיקת זמן:
check_timing()מודדת זמן ביצוע ובודקת אם הוא חשוד - הצפנת מחרוזות: ה-flag מוצפן ב-XOR ומפוענח רק בזמן ריצה
-
הסרת סימבולים: strip הסיר את שמות הפונקציות
-
עקיפה צעד אחר צעד:
שלב א - עקיפת ptrace:
gdb ./challenge
(gdb) catch syscall ptrace
(gdb) run
(gdb) continue
(gdb) set $rax = 0
(gdb) continue
שלב ב - עקיפת בדיקת זמן:
אם עוצרים ב-breakpoints, בדיקת הזמן תיכשל. יש כמה אפשרויות:
אפשרות 1 - מצאו את הכתובת של ההשוואה בcheck_timing ושנו:
(gdb) # מצאו את check_timing בגידרה
(gdb) break *<address_of_timing_comparison>
(gdb) continue
(gdb) set $rax = 0 # או שנו את דגל הZF
(gdb) continue
אפשרות 2 - שנו את ערך diff:
מצאו את המשתנה diff ושנו אותו לערך קטן.
אפשרות 3 - דלגו על הפונקציה:
(gdb) break main
(gdb) run
# מצאו את call check_timing
# שימו break אחרי ה-call
(gdb) break *<after_timing_call>
(gdb) continue
(gdb) set $rax = 0
(gdb) continue
אחרי עקיפת שתי הבדיקות:
- בונוס - מציאת ה-flag סטטית:
בגידרה, מצאו את print_flag. הDecompiler יראה מערך של בתים מוצפנים ולולאת XOR עם מפתח 0x7A.
חישוב ידני (כל בית XOR 0x7A):
- 0x36 ^ 0x7A = 0x4C = 'L' (אבל הflag מתחיל ב-F...)
בעצם, אפשר לכתוב סקריפט פייתון:
encrypted = [0x36, 0x2c, 0x23, 0x29, 0x44, 0x19, 0x0d,
0x1e, 0x0a, 0x1b, 0x16, 0x44, 0x13, 0x0e,
0x1b, 0x0c, 0x44, 0x19, 0x0a, 0x1b, 0x17,
0x44, 0x1b, 0x0e]
result = ''.join(chr(b ^ 0x7A) for b in encrypted)
print(result)
התוצאה: "FLAG: Welcome back dark side" (הערה: הערכים בתרגיל הם לדוגמה - התוצאה בפועל תלויה בערכים המדויקים שבקוד)
המסקנה: גם בלי להריץ את התוכנית, אפשר למצוא את ה-flag על ידי ניתוח סטטי של פונקציית הפענוח.