7.3 GDB מתקדם הרצאה
הקדמה¶
בפרק 3.9 למדנו את הבסיס של GDB - breakpoints, step, next, print, watch. השתמשנו בו בעיקר לדיבוג קוד C שלנו, עם קוד מקור וsymbols.
עכשיו נלמד להשתמש ב-GDB ככלי הנדסה הפוכה - דיבוג של תוכניות בלי קוד מקור. זו רמה אחרת לגמרי. במקום break main ו-print x, נעבוד עם כתובות בזיכרון, נקרא אסמבלי בזמן אמת, ונשנה את ריצת התוכנית.
הגדרות ראשוניות - intel syntax¶
הדבר הראשון שכדאי לעשות כשפותחים GDB לצורך RE:
זה משנה את תחביר האסמבלי מAT&T (שזה ברירת המחדל ב-GDB) ל-Intel. תחביר Intel הרבה יותר קריא - הוא כותב mov eax, 5 במקום movl $5, %eax.
כדי שזה יקרה אוטומטית בכל פעם, הוסיפו את השורה הבאה לקובץ ~/.gdbinit:
פתיחת בינארי בלי קוד מקור¶
כשפותחים בינארי בלי symbols (בלי -g), אין שורות C, אין שמות משתנים מקומיים. אם הבינארי לא עבר strip, עדיין יהיו שמות פונקציות. אם הוא stripped - אפילו שמות פונקציות לא יהיו.
דיסאסמבלי בתוך GDB¶
דיסאסמבלי של פונקציה¶
או בקיצור:
אם הבינארי stripped ואין שם לmain, נצטרך למצוא את הכתובת של main בדרכים אחרות (למשל, דרך entry point ועקיבה אחרי הקריאות).
דיסאסמבלי בכתובת מסוימת¶
או נוסחה יחסית:
צפייה בהוראות סביב המיקום הנוכחי¶
זה מציג 10 הוראות אסמבלי מהמיקום הנוכחי (instruction pointer). שימושי מאוד כשעוצרים ב-breakpoint ורוצים לראות מה הולך לקרות.
אפשר גם לראות הוראות לפני המיקום הנוכחי:
נקודות עצירה - breakpoints על כתובות¶
בפרק 3.9 למדנו break main ו-break 42 (שורה 42). בלי קוד מקור, עובדים עם כתובות:
עצירה בכתובת מסוימת¶
הכוכבית (*) אומרת ל-GDB שזו כתובת ולא שם של פונקציה.
עצירה ב-offset מתחילת פונקציה¶
עוצר 42 בתים אחרי תחילת main. שימושי כשרוצים לעצור באמצע פונקציה.
צפייה ב-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) print $rax
(gdb) print/x $rdi ; בפורמט הקסדצימלי
(gdb) print/d $esi ; בפורמט עשרוני
(gdb) print/t $eax ; בפורמט בינארי
הדגלים - flags register¶
מראה את הדגלים הפעילים, למשל: [ CF ZF SF ].
בדיקת זיכרון - examine (פקודת x)¶
כבר למדנו את x בפרק 3.9, אבל נרחיב. התחביר המלא:
כאשר:
- 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 בתים מהמחסנית בהקסדצימלי:
הצגת 8 מילים (words) מהמחסנית:
הצגת מחרוזת שמצביע rdi מצביע עליה:
הצגת 15 הוראות אסמבלי מהמיקום הנוכחי:
הצגת תוכן של כתובת ספציפית:
נקודות צפייה - watchpoints¶
watchpoint עוצר את התוכנית כשערך בכתובת מסוימת משתנה:
זה שימושי מאוד ב-RE כשאתה רוצה לדעת מי משנה ערך מסוים. למשל, אם ראית משתנה גלובלי שמכיל flag (0 או 1), תוכל לשים עליו watch ולגלות איזו פונקציה משנה אותו.
סוגי watchpoints¶
watch *addr- עוצר כשכותבים לכתובת (write watchpoint)rwatch *addr- עוצר כשקוראים מהכתובת (read watchpoint)awatch *addr- עוצר בכל גישה - קריאה או כתיבה (access watchpoint)
נקודות עצירה מותנות - conditional breakpoints¶
עוצר רק אם rdi שווה 5 כשמגיעים לכתובת. שימושי כשיש לולאה ואתה רוצה לעצור רק באיטרציה ספציפית:
אפשר גם תנאים מורכבים:
תפיסת קריאות מערכת - catch syscall¶
עוצר כל פעם שהתוכנית מבצעת את ה-syscall write. שימושי ל-RE כשרוצים לדעת מתי תוכנית כותבת לקובץ או לרשת.
עוצר על open ו-openat - שימושי לדעת אילו קבצים התוכנית פותחת.
סקריפטים ב-GDB - commands¶
אפשר להגדיר פעולות אוטומטיות שירוצו כשנעצרים ב-breakpoint:
עכשיו, כל פעם שהתוכנית מגיעה לכתובת 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 יעקוב אחרי תהליך הבן אחרי fork.
חוזר לברירת המחדל - עקיבה אחרי האב.
בדיקת המחסנית - stack analysis¶
מסגרת נוכחית¶
מציג מידע על הstack frame הנוכחי - כתובת ה-frame, כתובת ההחזרה, ה-saved rbp.
רשימת קריאות¶
גם בלי symbols, backtrace יציג את כתובות ההחזרה ואת שמות הפונקציות (אם יש). אם הבינארי stripped, תראו רק כתובות:
עדיין שימושי - אתה יכול לעשות disas 0x401180 כדי לראות את הפונקציה שקראה לפונקציה הנוכחית.
שינוי ריצת התוכנית - patching on the fly¶
אחת היכולות החזקות ביותר של GDB ב-RE: שינוי ערכים בזמן ריצה.
שינוי ערך באוגר¶
שינוי ערך בזיכרון¶
קפיצה לכתובת - דילוג על קוד¶
או שינוי ה-instruction pointer ישירות:
שימוש מעשי: עקיפת בדיקת סיסמה¶
נניח שבדיסאסמבלי ראינו:
0x401234: call check_password
0x401239: test eax, eax ; eax == 0?
0x40123b: je 0x401260 ; אם 0 -> "access denied"
0x40123d: ... ; "access granted"
אפשרות 1 - לשנות את ערך ההחזרה:
עכשיו eax הוא 1, אז test eax, eax לא יגדיר ZF, ו-je לא יקפוץ - והתוכנית תמשיך ל-"access granted".
אפשרות 2 - לדלג על הבדיקה:
דילגנו ישירות על ה-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: ריקונסנס (סיור)¶
מגלים שיש פונקציות main ו-verify. נראה מחרוזות כמו "Enter password:", "Correct!", "Wrong!".
שלב 2: ניתוח סטטי¶
נסתכל על 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¶
עכשיו אנחנו בתוך verify. נמצא את המערך הקבוע (secret):
הסיסמה היא: כל בית 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 מתקדמות - עיון מהיר¶
| פקודה | תיאור |
|---|---|
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, יש לכם את הבסיס כדי להתחיל לפרק בינאריים אמיתיים.