7.1 פנימיות המקצה של glibc פתרון
פתרון - פנימיות המקצה של glibc¶
נעבור תרגיל אחר תרגיל. הכתובות שתראו אצלכם יהיו שונות (בגלל ASLR), אבל המבנה והיחסים בין הכתובות זהים. כל הפלטים כאן נלקחו מ-glibc 2.35 על x86-64 עם ASLR. אם ה-glibc שלכם 2.32 ומעלה, המצביעים ב-tcache יראו "מוזרים" בגלל safe-linking - נסביר את זה בתרגיל 2.
תזכורת להידור ולהרצה:
פתרון תרגיל 1 - צילום הערמה אחרי הקצאה¶
שמנו breakpoint על puts והרצנו עד ה"allocated":
הפלט (מקוצר) נראה כך:
Allocated chunk | PREV_INUSE
Addr: 0x555555559290
Size: 0x21
Allocated chunk | PREV_INUSE
Addr: 0x5555555592b0
Size: 0x21
... (7 more chunks of size 0x21) ...
Allocated chunk | PREV_INUSE
Addr: 0x555555559470
Size: 0x511 <- this is big
Allocated chunk | PREV_INUSE
Addr: 0x555555559980
Size: 0x21 <- this is guard
Top chunk | PREV_INUSE
Addr: 0x5555555599a0
Size: 0x20661
מה אנחנו רואים:
- תשעה נתחים רצופים בגודל
0x21, אחר כךbigבגודל0x511, אחר כךguard, ואז ה-top chunk הענק. - הגודל של
bigהוא0x511ולא0x510- כי הביט התחתון הוא PREV_INUSE שדלוק. הגודל האמיתי הוא0x511 & ~0x7 = 0x510, כמו שחישבנו:malloc(0x500)->(0x500 + 8 + 15) & ~15 = 0x510.
התשובה לשאלה בכתב: malloc(0x18) תפס 0x20 בתים (32), לא 0x18 (24). הסיבה כפולה: המקצה מוסיף 8 בתים לשדה ה-size, ומיישר את התוצאה ל-16 בתים. (0x18 + 8 + 15) & ~15 = 0x20. שימו לב שדווקא כאן 0x18 בתי משתמש כן נכנסים ב-0x20, כי אפשר "לשאול" את 8 הבתים של prev_size מהנתח הבא כל עוד הנתח הנוכחי בשימוש.
פתרון תרגיל 2 - מילוי ה-tcache וזליגה ל-fastbin¶
המשכנו עד אחרי כל ה-free-ים:
הפלט:
tcachebins
0x20 [ 7]: 0x555555559410 -> 0x5555555593f0 -> ... -> 0x555555559290
fastbins
0x20: 0x555555559430 -> 0x555555559450 -> 0x0
זה בדיוק מה שציפינו. שחררנו 9 נתחים בגודל 0x20:
- שבעת הראשונים (
chunks[0]עדchunks[6]) מילאו אתtcache[0x20]עד הגבול. - שני האחרונים (
chunks[7],chunks[8]) לא נכנסו ל-tcache שהתמלא, ולכן נפלו ל-fastbin של גודל0x20.
עכשיו לסדר. ה-tcache הוא LIFO, אז הראש שלו הוא הנתח האחרון שנכנס אליו. שחררנו לפי הסדר chunks[0] ... chunks[8], אבל רק 0..6 נכנסו ל-tcache, ואחרון מביניהם הוא chunks[6] בכתובת 0x555555559410. ואכן, זה ראש הרשימה. הזנב הוא chunks[0] בכתובת 0x555555559290.
ה-fastbin קיבל את chunks[7] ואז chunks[8], וגם הוא LIFO, אז הראש שלו הוא chunks[8] (0x555555559450), ואחריו chunks[7].
אימות עם malloc_chunk:
pwndbg> malloc_chunk 0x555555559410
0x555555559410 FD: 0x5555555593f0 (the next chunk in the tcache list)
הערה על safe-linking (glibc 2.32+): אם אתם על גרסה 2.32 ומעלה, ה-fd שתראו לא יהיה כתובת "נקייה" אלא ערך מעורבל: fd_real ^ (chunk_addr >> 12). pwndbg כבר יודע לפענח את זה ב-bins, ולכן שם תראו כתובות רגילות. אם תסתכלו על הבתים הגולמיים בזיכרון (x/2gx), תראו את הערך המעורבל. אל תיבהלו - זו הגנה, ונלמד לעקוף אותה בשיעור על tcache poisoning.
התשובה לשאלה בכתב: בראש ה-tcache יושב chunks[6] (האחרון שנכנס ל-tcache), ובראש ה-fastbin יושב chunks[8] (האחרון ששוחרר בכלל). הסיבה: שתי הרשימות הן LIFO, וה-tcache התמלא אחרי 7 שחרורים, כך שהשחרורים ה-8 וה-9 גלשו ל-fastbin.
פתרון תרגיל 3 - ה-unsorted bin וleak libc¶
באותה נקודה, בפלט של bins, נמצא גם את השורה של ה-unsorted bin:
הנתח big (בכתובת 0x555555559470) נחת ב-unsorted bin, כי הוא גדול מ-0x410 (לא tcache) וגם גדול מ-0x80 (לא fastbin). נסתכל על המצביעים שלו:
pwndbg> malloc_chunk 0x555555559470
0x555555559470 PREV_INUSE
Size: 0x511
fd: 0x7ffff7e19ce0
bk: 0x7ffff7e19ce0
הנה ההבדל המהותי מ-tcache/fastbin: ה-fd וה-bk כאן מצביעים ל-0x7ffff7e19ce0, שזו כתובת בטווח הגבוה (0x7f...) - בתוך libc, לא בערמה (0x55...). pwndbg אפילו מסמן לנו main_arena+96. כשיש נתח יחיד ב-unsorted bin, הרשימה המעגלית משרשרת אותו בחזרה אל ה-arena, וה-arena חיה בתוך glibc.
נאמת עם vmmap:
אכן, הכתובת יושבת בתוך המיפוי של libc.so.6.
התשובה לשאלה בכתב: לו יכולנו לקרוא את 8 הבתים של ה-fd (למשל דרך use-after-free שמדפיס נתח משוחרר), היינו מקבלים כתובת קבועה בתוך libc - main_arena+96. ה-offset של main_arena מתחילת libc קבוע לכל גרסת glibc נתונה, ולכן: libc_base = leaked - offset_of(main_arena+96). ברגע שיש לנו libc_base, כל פונקציה בו (למשל system) בכתובת ידועה, וה-ASLR של libc נשבר לגמרי. זו טכניקת leak קלאסית ב-heap.
פתרון תרגיל 4 (אתגר) - איחוד לתוך ה-top chunk¶
הסרנו את שורת ה-guard, הידרנו מחדש, והרצנו עד אחרי free(big):
ה-unsorted bin ריק. איפה big? הוא נבלע ל-top chunk. נסתכל על גודל ה-top:
לפני השחרור ה-top התחיל אחרי big. עכשיו, בלי ה-guard, big היה הנתח האחרון לפני ה-top - כלומר גבל בו ישירות. כשמשחררים נתח שגובל ב-top chunk, glibc לא שמה אותו בפח אלא מאחדת אותו לתוך ה-top. הכתובת של ה-top ירדה אחורה עד תחילת big, והגודל שלו גדל ב-0x510.
התשובה לשאלה בכתב: השומר guard היה נחוץ כדי למנוע מ-big לגבול ישירות ב-top chunk. בלעדיו, free(big) פשוט מרחיב את ה-top במקום להכניס את big ל-unsorted bin, וכך לא היינו יכולים לצפות בו שם.
איך להכליל¶
- חוק אצבע ראשון: כדי לדחוף נתחים ל-fastbin או ל-unsorted bin, קודם צריך "לעקוף" את ה-tcache. או שממלאים אותו (7 נתחים), או שמשחררים נתח גדול ממנו (מעל
0x410), או שמכבים אותו לגמרי עםexport GLIBC_TUNABLES=glibc.malloc.tcache_count=0. - חוק אצבע שני: כמעט בכל תרגיל heap צריך נתח שומר לפני ה-top chunk, אחרת נתחים שאתם משחררים נבלעים ל-top ונעלמים לכם מהעיניים.
- חוק אצבע שלישי: ה-unsorted bin הוא מקור הleak הקלאסי. אם אתם צריכים כתובת libc, החזקה של נתח משוחרר גדול ב-unsorted bin וקריאה של ה-
fdשלו זו הדרך הראשונה שכדאי לנסות.
שלושת הכלים heap, bins, vis_heap_chunks יהיו החברים הכי טובים שלכם בכל הפרק. כדאי להתרגל להריץ אותם אחרי כל malloc וכל free עד שהתמונה הופכת לאינטואיטיבית.