לדלג לתוכן

7.7 טכניקות אנטי הנדסה הפוכה פתרון

פתרון תרגיל 1 - ההשפעה של strip

  1. פלט nm:
  2. nm with_symbols מראה את כל הפונקציות: main, calculate, print_result, וגם פונקציות עזר של libc
  3. nm without_symbols מחזיר: nm: without_symbols: no symbols
  4. ה-strip הסיר את כל טבלת הסימבולים

  5. פלט objdump -t:

  6. with_symbols - מראה את כל הסימבולים כמו nm
  7. without_symbols - מראה רק את הסימבולים הדינמיים (DYNAMIC SYMBOL TABLE) - פונקציות של libc כמו printf, __libc_start_main וכו'. אלה נשארים כי הם הכרחיים לקישור דינמי.

  8. strings על שניהם: אין הבדל משמעותי. המחרוזת "Result: %d\n" מופיעה בשני הקבצים. strip מסיר סימבולים, לא מחרוזות.

  9. בגידרה:

  10. with_symbols - גידרה מראה את שמות הפונקציות המקוריים
  11. without_symbols - גידרה מראה שמות אוטומטיים (FUN_XXXXX), אבל ה-Decompiler מצליח לפענח את הלוגיקה בדיוק אותו דבר

  12. מסקנה: strip לא מגן באמת על הקוד. הוא מסיר שמות, אבל כל הלוגיקה, המחרוזות, ודפוסי הקוד נשארים. זה מאט את החוקר (צריך לשנות שמות ידנית) אבל לא עוצר אותו.


פתרון תרגיל 2 - זיהוי ועקיפת אנטי-דיבוג עם ptrace

  1. הרצה רגילה (בלי debugger):

    No debugger detected. Proceeding...
    The secret code is: 42-ALPHA-7
    

  2. הרצה עם GDB:

    Nice try! No debugging allowed.
    

    התוכנית מזהה את GDB ויוצאת.

  3. עקיפה:

דרך א - שינוי ערך ב-GDB:

(gdb) catch syscall ptrace
(gdb) run

GDB עוצר כשנכנסים ל-syscall ptrace. נמשיך:
(gdb) continue

GDB עוצר שוב כשיוצאים מה-syscall. עכשיו נשנה את ערך ההחזרה:
(gdb) set $rax = 0
(gdb) continue

הפלט:
No debugger detected. Proceeding...
The secret code is: 42-ALPHA-7

דרך ב - LD_PRELOAD:

gcc -shared -o fake_ptrace.so fake_ptrace.c
LD_PRELOAD=./fake_ptrace.so ./anti_debug

הפלט:
No debugger detected. Proceeding...
The secret code is: 42-ALPHA-7

  1. הסבר:
  2. דרך א: catch syscall ptrace גורם ל-GDB לעצור כשהתוכנית קוראת ל-syscall ptrace. אחרי שה-syscall חוזר, אנחנו משנים את ערך ההחזרה (שב-rax) ל-0 (הצלחה), במקום -1 (כישלון שנגרם כי GDB כבר עושה ptrace).
  3. דרך ב: LD_PRELOAD טוען את fake_ptrace.so לפני libc. כשהתוכנית קוראת ל-ptrace, היא מקבלת את הגרסה שלנו (שמחזירה 0 תמיד) במקום הגרסה האמיתית. התוכנה חושבת שהכל בסדר.

פתרון תרגיל 3 - זיהוי מחרוזות מוצפנות

  1. strings encrypted_strings - לא מוצא את הסיסמה. המחרוזות המוצפנות הן בתים לא קריאים, ולכן strings לא מזהה אותן כמחרוזות.

  2. בגידרה, main נראה בערך כך (ב-Decompiler):

    undefined8 main(void) {
        char local_28[28];
        // ... הבתים המוצפנים מועתקים ל-local_28 ...
        FUN_00401136(local_28, strlen(local_28), 0x42);
        printf("%s\n", local_28);
        return 0;
    }
    

  3. פונקציית 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 עם המפתח.

  4. המפתח הוא 0x42 (66 בעשרוני, 'B' כתו).

  5. שימוש ב-GDB:

    gdb ./encrypted_strings
    (gdb) break main
    (gdb) run
    (gdb) disass main
    

    מוצאים את הכתובת שאחרי הקריאה ל-decrypt (אחרי ה-call):
    (gdb) break *0x4011XX   # הכתובת שמצאנו
    (gdb) continue
    

    עכשיו המחרוזת כבר פוענחה. מוצאים את הכתובת שלה (למשל דרך rsp או דרך הDecompiler) ו:
    (gdb) x/s <address>
    

    הפלט: "Secret Password: opensesame"


פתרון תרגיל 4 - זיהוי packing עם UPX

  1. השוואת גדלים:

    original:  ~16KB (תלוי במערכת)
    packed:    ~8KB
    

    UPX דחס את הקובץ בכ-50%.

  2. strings:

  3. strings original - מוצא "Hello from packed binary!", "Count: %d\n" ועוד
  4. strings packed - לא מוצא את המחרוזות של התוכנית. רואים רק מחרוזות של UPX עצמו: UPX!, $Info: This file is packed with the UPX

  5. readelf -h:

  6. נקודת הכניסה שונה! ב-packed, הentry point מצביע על קוד ה-unpacker, לא על main המקורי.

  7. readelf -S:

  8. ב-original: סקשנים רגילים (.text, .data, .rodata, .bss וכו')
  9. ב-packed: סקשנים עם שמות שונים (UPX0, UPX1, UPX2). אלו סימנים ברורים לאריזת UPX.

  10. בגידרה:

  11. packed ייראה מוזר - ה-Decompiler יציג את קוד ה-unpacker (שמפרש את הקוד הדחוס) ולא את הקוד המקורי. הקוד המקורי מוסתר בתוך הנתונים הדחוסים.

  12. אחרי upx -d packed:

  13. strings packed מוצא את כל המחרוזות המקוריות
  14. הקובץ חזר לגודלו המקורי
  15. גידרה מראה את הקוד המקורי כרגיל

פתרון תרגיל 5 - ניתוח תוכנית עם מספר טכניקות אנטי-RE

  1. הרצה רגילה (בלי debugger):

    All checks passed!
    FLAG: Welcome back dark side
    

  2. הרצה עם GDB:

    Error: application integrity check failed.
    

    בדיקת ptrace מזהה את GDB.

  3. טכניקות אנטי-RE שזוהו:

  4. בדיקת ptrace: check_debugger() קוראת ל-ptrace(PTRACE_TRACEME)
  5. בדיקת זמן: check_timing() מודדת זמן ביצוע ובודקת אם הוא חשוד
  6. הצפנת מחרוזות: ה-flag מוצפן ב-XOR ומפוענח רק בזמן ריצה
  7. הסרת סימבולים: strip הסיר את שמות הפונקציות

  8. עקיפה צעד אחר צעד:

שלב א - עקיפת 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

אחרי עקיפת שתי הבדיקות:

All checks passed!
FLAG: Welcome back dark side

  1. בונוס - מציאת ה-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 על ידי ניתוח סטטי של פונקציית הפענוח.