לדלג לתוכן

7.3 GDB מתקדם הרצאה

הקדמה

בפרק 3.9 למדנו את הבסיס של GDB - breakpoints, step, next, print, watch. השתמשנו בו בעיקר לדיבוג קוד C שלנו, עם קוד מקור וsymbols.

עכשיו נלמד להשתמש ב-GDB ככלי הנדסה הפוכה - דיבוג של תוכניות בלי קוד מקור. זו רמה אחרת לגמרי. במקום break main ו-print x, נעבוד עם כתובות בזיכרון, נקרא אסמבלי בזמן אמת, ונשנה את ריצת התוכנית.


הגדרות ראשוניות - intel syntax

הדבר הראשון שכדאי לעשות כשפותחים GDB לצורך RE:

(gdb) set disassembly-flavor intel

זה משנה את תחביר האסמבלי מAT&T (שזה ברירת המחדל ב-GDB) ל-Intel. תחביר Intel הרבה יותר קריא - הוא כותב mov eax, 5 במקום movl $5, %eax.

כדי שזה יקרה אוטומטית בכל פעם, הוסיפו את השורה הבאה לקובץ ~/.gdbinit:

set disassembly-flavor intel


פתיחת בינארי בלי קוד מקור

gdb ./binary

כשפותחים בינארי בלי symbols (בלי -g), אין שורות C, אין שמות משתנים מקומיים. אם הבינארי לא עבר strip, עדיין יהיו שמות פונקציות. אם הוא stripped - אפילו שמות פונקציות לא יהיו.


דיסאסמבלי בתוך GDB

דיסאסמבלי של פונקציה

(gdb) disassemble main

או בקיצור:

(gdb) disas main

אם הבינארי stripped ואין שם לmain, נצטרך למצוא את הכתובת של main בדרכים אחרות (למשל, דרך entry point ועקיבה אחרי הקריאות).

דיסאסמבלי בכתובת מסוימת

(gdb) disas 0x401149

או נוסחה יחסית:

(gdb) disas main+20

צפייה בהוראות סביב המיקום הנוכחי

(gdb) x/10i $rip

זה מציג 10 הוראות אסמבלי מהמיקום הנוכחי (instruction pointer). שימושי מאוד כשעוצרים ב-breakpoint ורוצים לראות מה הולך לקרות.

אפשר גם לראות הוראות לפני המיקום הנוכחי:

(gdb) x/5i $rip-20


נקודות עצירה - breakpoints על כתובות

בפרק 3.9 למדנו break main ו-break 42 (שורה 42). בלי קוד מקור, עובדים עם כתובות:

עצירה בכתובת מסוימת

(gdb) break *0x401234

הכוכבית (*) אומרת ל-GDB שזו כתובת ולא שם של פונקציה.

עצירה ב-offset מתחילת פונקציה

(gdb) break *main+42

עוצר 42 בתים אחרי תחילת main. שימושי כשרוצים לעצור באמצע פונקציה.

צפייה ב-breakpoints

(gdb) info breakpoints

צעידה ברמת אסמבלי

בפרק 3.9 למדנו step ו-next שזוזים שורת C אחת. בלי קוד מקור, נשתמש בגרסאות האסמבלי:

צעידה הוראה-הוראה

  • stepi (או si) - מבצע הוראת אסמבלי אחת. אם ההוראה היא call, נכנס לתוך הפונקציה
  • nexti (או ni) - מבצע הוראת אסמבלי אחת. אם ההוראה היא call, מריץ את כל הפונקציה ועוצר אחרי שהיא חוזרת

ההבדל קריטי: אם אתה ב-call printf@plt ועושה si, תיכנס לתוך הPLT stub ומשם לdynamic linker ומשם לprintf - מסע ארוך. עם ni אתה פשוט מדלג על כל זה ועוצר בהוראה הבאה אחרי ה-call.

כלל אצבע: השתמש ב-ni כשלא מעניין אותך מה קורה בתוך הפונקציה הנקראת, ו-si כשכן.


בדיקת אוגרים - registers

כל האוגרים

(gdb) info registers

או בקיצור:

(gdb) i r

אוגר ספציפי

(gdb) print $rax
(gdb) print/x $rdi         ; בפורמט הקסדצימלי
(gdb) print/d $esi          ; בפורמט עשרוני
(gdb) print/t $eax          ; בפורמט בינארי

הדגלים - flags register

(gdb) print $eflags

מראה את הדגלים הפעילים, למשל: [ CF ZF SF ].


בדיקת זיכרון - examine (פקודת x)

כבר למדנו את x בפרק 3.9, אבל נרחיב. התחביר המלא:

x/NFU address

כאשר:
- N - כמה יחידות להציג
- F - פורמט: x (hex), d (decimal), s (string), i (instruction), c (char)
- U - גודל יחידה: b (byte), h (halfword - 2 bytes), w (word - 4 bytes), g (giant - 8 bytes)

דוגמאות שימושיות ל-RE

הצגת 20 בתים מהמחסנית בהקסדצימלי:

(gdb) x/20xb $rsp

הצגת 8 מילים (words) מהמחסנית:

(gdb) x/8xg $rsp

הצגת מחרוזת שמצביע rdi מצביע עליה:

(gdb) x/s $rdi

הצגת 15 הוראות אסמבלי מהמיקום הנוכחי:

(gdb) x/15i $rip

הצגת תוכן של כתובת ספציפית:

(gdb) x/4xw 0x404040


נקודות צפייה - watchpoints

watchpoint עוצר את התוכנית כשערך בכתובת מסוימת משתנה:

(gdb) watch *0x601040

זה שימושי מאוד ב-RE כשאתה רוצה לדעת מי משנה ערך מסוים. למשל, אם ראית משתנה גלובלי שמכיל flag (0 או 1), תוכל לשים עליו watch ולגלות איזו פונקציה משנה אותו.

סוגי watchpoints

  • watch *addr - עוצר כשכותבים לכתובת (write watchpoint)
  • rwatch *addr - עוצר כשקוראים מהכתובת (read watchpoint)
  • awatch *addr - עוצר בכל גישה - קריאה או כתיבה (access watchpoint)

נקודות עצירה מותנות - conditional breakpoints

(gdb) break *0x401234 if $rdi == 5

עוצר רק אם rdi שווה 5 כשמגיעים לכתובת. שימושי כשיש לולאה ואתה רוצה לעצור רק באיטרציה ספציפית:

(gdb) break *0x40125a if $ecx == 100

אפשר גם תנאים מורכבים:

(gdb) break *0x401234 if $rdi > 10 && $rsi != 0


תפיסת קריאות מערכת - catch syscall

(gdb) catch syscall write

עוצר כל פעם שהתוכנית מבצעת את ה-syscall write. שימושי ל-RE כשרוצים לדעת מתי תוכנית כותבת לקובץ או לרשת.

(gdb) catch syscall open openat

עוצר על open ו-openat - שימושי לדעת אילו קבצים התוכנית פותחת.


סקריפטים ב-GDB - commands

אפשר להגדיר פעולות אוטומטיות שירוצו כשנעצרים ב-breakpoint:

(gdb) break *0x401234
(gdb) commands
> printf "rdi = %d, rsi = %d\n", $rdi, $rsi
> continue
> end

עכשיו, כל פעם שהתוכנית מגיעה לכתובת 0x401234, GDB ידפיס את ערכי rdi ו-rsi וימשיך אוטומטית. זה שימושי מאוד כשרוצים לעקוב אחרי ערכים בלולאה בלי לעצור בכל איטרציה.

דוגמה יותר מתקדמת - מעקב אחרי קריאות לפונקציה:

(gdb) break strcmp@plt
(gdb) commands
> silent
> printf "strcmp(\"%s\", \"%s\")\n", (char*)$rdi, (char*)$rsi
> continue
> end

זה ידפיס כל קריאה ל-strcmp עם הארגומנטים שלה, בלי לעצור. שימושי מאוד כשמחפשים איפה תוכנית משווה סיסמאות.


מעקב אחרי forks

תוכניות שעושות fork יוצרות תהליך חדש. כברירת מחדל, GDB עוקב אחרי ה-parent:

(gdb) set follow-fork-mode child

עכשיו GDB יעקוב אחרי תהליך הבן אחרי fork.

(gdb) set follow-fork-mode parent

חוזר לברירת המחדל - עקיבה אחרי האב.


בדיקת המחסנית - stack analysis

מסגרת נוכחית

(gdb) info frame

מציג מידע על הstack frame הנוכחי - כתובת ה-frame, כתובת ההחזרה, ה-saved rbp.

רשימת קריאות

(gdb) backtrace

גם בלי symbols, backtrace יציג את כתובות ההחזרה ואת שמות הפונקציות (אם יש). אם הבינארי stripped, תראו רק כתובות:

#0  0x0000000000401230 in ?? ()
#1  0x0000000000401180 in ?? ()
#2  0x00000000004011a0 in ?? ()

עדיין שימושי - אתה יכול לעשות disas 0x401180 כדי לראות את הפונקציה שקראה לפונקציה הנוכחית.


שינוי ריצת התוכנית - patching on the fly

אחת היכולות החזקות ביותר של GDB ב-RE: שינוי ערכים בזמן ריצה.

שינוי ערך באוגר

(gdb) set $rax = 1
(gdb) set $rdi = 0x41414141

שינוי ערך בזיכרון

(gdb) set *(int*)0x601040 = 42
(gdb) set *(char*)0x601040 = 'A'

קפיצה לכתובת - דילוג על קוד

(gdb) jump *0x401250

או שינוי ה-instruction pointer ישירות:

(gdb) set $rip = 0x401250

שימוש מעשי: עקיפת בדיקת סיסמה

נניח שבדיסאסמבלי ראינו:

0x401234:  call    check_password
0x401239:  test    eax, eax          ; eax == 0?
0x40123b:  je      0x401260          ; אם 0 -> "access denied"
0x40123d:  ...                       ; "access granted"

אפשרות 1 - לשנות את ערך ההחזרה:

(gdb) break *0x401239
(gdb) run
Enter password: wrong_password
(gdb) set $eax = 1
(gdb) continue

עכשיו eax הוא 1, אז test eax, eax לא יגדיר ZF, ו-je לא יקפוץ - והתוכנית תמשיך ל-"access granted".

אפשרות 2 - לדלג על הבדיקה:

(gdb) break *0x401234
(gdb) run
(gdb) set $rip = 0x40123d
(gdb) continue

דילגנו ישירות על ה-call ועל הבדיקה.


תוספים ל-GDB: כלי pwndbg/GEF/PEDA

כפי שלמדנו בפרק 3.9, GDB תומך בתוספים שמשפרים משמעותית את חוויית השימוש. לצורך RE, שלושת התוספים הנפוצים:

כלי pwndbg - הכי פופולרי ב-RE ופיתוח exploit-ים. מספק:
- תצוגת context אוטומטית עם אוגרים, קוד, ומחסנית בכל עצירה
- פקודת vmmap לצפייה במיפוי הזיכרון
- פקודת telescope להצצה לתוך שרשרת מצביעים
- פקודת search לחיפוש בזיכרון
- ועוד מאות פקודות

כלי GEF (GDB Enhanced Features) - ממשק נקי ופשוט יותר מpwndbg, מתאים גם הוא ל-RE.

כלי PEDA (Python Exploit Development Assistance) - תוסף ותיק עם תכונות בסיסיות טובות.

לכל התוספים האלה יש יתרון מרכזי אחד: context אוטומטי. בכל פעם שעוצרים (breakpoint, step, וכו'), מוצגת תצוגה מלאה של מצב האוגרים, ההוראות הבאות, ותוכן המחסנית. זה חוסך המון פקודות info registers ו-x/i $rip.


דוגמה מעשית: הנדסה הפוכה של בדיקת סיסמה

בואו נעבור על תהליך RE מלא של תוכנית פשוטה שבודקת סיסמה.

שלב 1: ריקונסנס (סיור)

file password_check
strings password_check
objdump -d -M intel password_check | grep "<.*>:"

מגלים שיש פונקציות main ו-verify. נראה מחרוזות כמו "Enter password:", "Correct!", "Wrong!".

שלב 2: ניתוח סטטי

objdump -d -M intel password_check

נסתכל על verify:

verify:
        push    rbp
        mov     rbp, rsp
        mov     qword [rbp-8], rdi     ; שמירת הארגומנט (המחרוזת)
        mov     dword [rbp-12], 0      ; i = 0
        jmp     .check
.loop:
        mov     eax, [rbp-12]
        movsxd  rdx, eax
        mov     rax, [rbp-8]
        add     rax, rdx
        movzx   eax, byte [rax]        ; eax = input[i]
        xor     eax, 0x42              ; eax ^= 0x42
        movsxd  rdx, dword [rbp-12]
        lea     rcx, [rip+0x...]       ; rcx = כתובת של מערך קבוע
        movzx   edx, byte [rcx+rdx]    ; edx = secret[i]
        cmp     al, dl                 ; input[i] ^ 0x42 == secret[i]?
        jne     .wrong
        add     dword [rbp-12], 1      ; i++
.check:
        mov     eax, [rbp-12]
        cmp     eax, 5                 ; i < 5?
        jl      .loop
        mov     eax, 1                 ; return 1 (correct)
        jmp     .end
.wrong:
        mov     eax, 0                 ; return 0 (wrong)
.end:
        pop     rbp
        ret

שלב 3: הבנת הלוגיקה

מהניתוח הסטטי מבינים:
- הפונקציה לוקחת כל תו מהקלט, עושה עליו XOR עם 0x42, ומשווה לערך קבוע
- אורך הסיסמה הוא 5 תווים
- הסיסמה הנכונה היא כזו שאחרי XOR 0x42 נותנת את הערכים הקבועים

שלב 4: מציאת הסיסמה עם GDB

gdb ./password_check
(gdb) break verify
(gdb) run
Enter password: AAAAA

עכשיו אנחנו בתוך verify. נמצא את המערך הקבוע (secret):

(gdb) x/5xb <address_of_secret>
0x402010:  0x27  0x23  0x30  0x30  0x21

הסיסמה היא: כל בית XOR 0x42:
- 0x27 ^ 0x42 = 0x65 = 'e'
- 0x23 ^ 0x42 = 0x61 = 'a'
- 0x30 ^ 0x42 = 0x72 = 'r'
- 0x30 ^ 0x42 = 0x72 = 'r'
- 0x21 ^ 0x42 = 0x63 = 'c'

הסיסמה היא "earrc".

שלב 5: אימות

(gdb) run
Enter password: earrc
Correct!

טבלת פקודות GDB מתקדמות - עיון מהיר

פקודה תיאור
disas func דיסאסמבלי של פונקציה
x/Ni $rip הצגת N הוראות מהמיקום הנוכחי
x/Nxg $rsp הצגת N ערכים מהמחסנית (8 בתים כל אחד)
x/s $rdi הצגת מחרוזת שrdi מצביע עליה
break *0xaddr עצירה בכתובת
si / ni צעד אסמבלי אחד (עם/בלי כניסה ל-call)
i r הצגת כל האוגרים
p/x $rax הדפסת אוגר בהקס
watch *0xaddr עצירה כשכתובת משתנה
break *0xaddr if $reg == val עצירה מותנית
catch syscall name עצירה על syscall
set $reg = val שינוי ערך אוגר
set *(type*)addr = val שינוי ערך בזיכרון
jump *0xaddr קפיצה לכתובת
set follow-fork-mode child מעקב אחרי תהליך בן
commands הגדרת פעולות אוטומטיות על breakpoint
info frame מידע על ה-stack frame
bt רשימת קריאות (backtrace)

סיכום

בהרצאה הזו למדנו להשתמש ב-GDB ככלי RE מתקדם:
- דיסאסמבלי בתוך GDB עם disas ו-x/i
- עבודה עם כתובות: breakpoints, stepping, וexamine
- בדיקת אוגרים וזיכרון לעומק
- watchpoints למעקב אחרי שינויים בזיכרון
- breakpoints מותנים ותפיסת syscalls
- סקריפטים של GDB לאוטומציה
- שינוי ריצת התוכנית: שינוי אוגרים, זיכרון, וקפיצות
- תוספי GDB (pwndbg, GEF, PEDA) לשיפור חוויית RE
- דוגמה מעשית של הנדסה הפוכה מלאה של בדיקת סיסמה

עם הכלים האלה, בשילוב עם ידע דפוסי האסמבלי מהרצאה 7.2, יש לכם את הבסיס כדי להתחיל לפרק בינאריים אמיתיים.