7.3 שחרור כפול וtcache poisoning תרגול
תרגול - שחרור כפול וtcache poisoning¶
בתרגול הזה תבנו exploit מלא של הרעלת tcache: leaks libc, דרך הרעלת שדה ה-next, ועד shell - קודם על glibc 2.31 (המקרה הנקי), ואז שוב על glibc 2.35 שבו כבר יש safe-linking וה-hooks נעלמו. המטרה היא שתרגישו על העור את ההבדל בין הגרסאות: מתי אפשר לכתוב כתובת גולמית, מתי חייבים לערבל, ומה עושים כשאין __free_hook. אל תעתיקו מהפתרון. תריצו, תיפלו על בדיקת המפתח, תראו את double free detected in tcache 2 בעיניים, ותתקנו.
סביבה והנחות¶
כל התרגילים רצים מקומית על לינוקס. הכי נקי לעבוד בתוך Docker כדי לשלוט בגרסת ה-glibc:
# glibc 2.31
docker run -it --rm -v "$PWD":/w -w /w ubuntu:20.04 bash
# glibc 2.35
docker run -it --rm -v "$PWD":/w -w /w ubuntu:22.04 bash
הכלים שתצטרכו: python3 עם pwntools, gdb עם pwndbg (במיוחד הפקודות tcachebins, bins, vis_heap_chunks), one_gadget, ו-ROPgadget. חשוב: תעבדו מול עותק מקומי של ה-libc.so.6 שהבינארי טוען - תמצאו אותו עם ldd ./tc, והעתיקו אותו לתיקיית העבודה.
ההנחות: ASLR פעיל (הדרך הרגילה), NX פעיל, בלי canary, והבינארי no-pie (כדי לבודד את חולשת ה-heap). אנחנו נדליף libc בעצמנו בתוך ה-exploit.
הבינארי לדוגמה¶
שמרו בשם tc.c. זה ממשק תפריט קלאסי של heap: alloc/free/edit/view. שימו לב לחולשה - free לא מאפס את המצביע, אז נשארת לנו גישת UAF גם ל-edit וגם ל-view:
// tc.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define N 16
char *slots[N];
size_t sizes[N];
long num(const char *prompt) {
char buf[32];
printf("%s", prompt);
if (!fgets(buf, sizeof buf, stdin)) exit(0);
return strtol(buf, 0, 10);
}
int main() {
setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stdin, 0, _IONBF, 0);
while (1) {
long c = num("\n1)alloc 2)free 3)edit 4)view 5)exit\n> ");
if (c == 1) {
long i = num("idx: "), s = num("size: ");
if (i < 0 || i >= N) continue;
slots[i] = malloc(s);
sizes[i] = s;
printf("data: ");
read(0, slots[i], s);
} else if (c == 2) {
long i = num("idx: ");
if (i < 0 || i >= N) continue;
free(slots[i]); // UAF: the pointer is not reset
} else if (c == 3) {
long i = num("idx: ");
if (i < 0 || i >= N) continue;
printf("data: ");
read(0, slots[i], sizes[i]); // UAF write
} else if (c == 4) {
long i = num("idx: ");
if (i < 0 || i >= N) continue;
write(1, slots[i], sizes[i]); // UAF read (leak)
} else {
break;
}
}
return 0;
}
קמפול (אותו מקור, שתי גרסאות glibc לפי הקונטיינר):
ודאו: NX enabled, No canary, No PIE. העתיקו את ה-libc: cp "$(ldd ./tc | awk '/libc/{print $3}')" ./libc.so.6.
תרגיל 1 - מיפוי ה-tcache ב-gdb¶
לפני שתוקפים, תראו את המבנה בעיניים.
המטרה:
- הריצו את
tcתחת gdb+pwndbg. הקצו שני צ'אנקים בגודל0x50(idx 0 ו-1), ושחררו את שניהם. - הריצו
tcachebins. כמה צ'אנקים ב-bin של0x60? מי בראש - הראשון ששוחרר או האחרון? רשמו לעצמכם את הכתובות. - הריצו
vis_heap_chunks. אתרו את שדה ה-nextואת שדה ה-key בכל צ'אנק חופשי. ודאו ש-keyמצביע לתחילת ה-tcache (tcache_perthread_struct).
רמז: בקשה של 0x50 בתים ממופה לצ'אנק 0x60 (ראש 0x10 + נתונים aligned). זה tc_idx שמתאים ל-tcachebins תחת 0x60.
תרגיל 2 - להיכשל בכוונה: בדיקת המפתח של 2.29¶
עכשיו תפגשו את ההגנה.
המטרה:
- הקצו צ'אנק בגודל
0x50(idx 0), שחררו אותו, ואז שחררו אותו שוב בלי לגעת בכלום. מה קורה? - תעדו את הודעת הקריסה המדויקת. איזה מחרוזת glibc מדפיס?
- עכשיו עקפו: שחררו את idx 0 פעם אחת, השתמשו ב-
editכדי לדרוס את שדה ה-key (offset 8) לאפס תוך שמירת ה-next, ואז שחררו שוב. הפעם זה עובר?
רמז: ההודעה היא free(): double free detected in tcache 2. היא באה מהשוואת e->key == tcache.
רמז: ב-edit אתם כותבים מ-offset 0. כדי לשמור את ה-next (8 בתים ראשונים) ולאפס רק את ה-key, שלחו 8 בתים כלשהם ואז p64(0). אחרי צ'אנק בודד ששוחרר, ה-next הוא ממילא NULL, אז b'\x00'*16 עושה את העבודה.
תרגיל 3 - leak libc דרך ה-unsorted bin¶
הרעלה דורשת כתובת יעד ב-libc, אז קודם צריך את הבסיס.
המטרה:
- הקצו צ'אנק גדול מדי בשביל tcache (בקשה של
0x500, idx 0), ואחריו צ'אנק שמירה קטן (idx 1, גודל0x20) כדי שהצ'אנק הגדול לא יתמזג עם ה-top chunk כשמשחררים. - שחררו את הצ'אנק הגדול (idx 0). הוא הולך ל-unsorted bin, ושדות ה-
fd/bkשלו מצביעים לתוךmain_arenaשבתוך libc. - עשו
viewל-idx 0, קראו 8 בתים, וזה leak libc. חשבו אתlibc.addressבעזרת ה-offset של הערך הleaked מבסיס libc.
רמז: למה 0x500? כי מעל בקשה של 0x408 הצ'אנק לא נכנס ל-tcache, ובלי צ'אנק שמירה הוא היה מתמזג ל-top. הצ'אנק הקטן חוסם את המיזוג.
רמז: את ה-offset של הערך הleaked מדדו בעצמכם ב-gdb: אחרי השחרור, bins יראה לכם את כתובת ה-fd הleaked; חשבו הפרש = leak - libc_base פעם אחת, וקבעו אותו קבוע. אל תעתיקו offset עיוור בין גרסאות.
תרגיל 4 - הרעלה אל __free_hook והשגת shell (glibc 2.31)¶
המטרה המרכזית. הרכיבו את הכל.
המטרה:
- אחרי שהדלפתם libc (תרגיל 3), חשבו את
__free_hookואתsystem. - הקצו שני צ'אנקים בגודל
0x50, שחררו את שניהם (מונה 2 באותו bin), ובעזרתeditדרסו את ה-nextשל הראש לכתובת__free_hook. - שתי הקצאות של
0x50: הראשונה מחזירה את הצ'אנק המקורי, השנייה נוחתת על__free_hook. כתבו לשםp64(system). - הקצו צ'אנק שהתוכן שלו
"/bin/sh\x00"ושחררו אותו. קבלו shell, הריצוid.
רמז: זכרו למה שני צ'אנקים ולא אחד - עניין המונה מההרצאה. אם תשחררו רק אחד, ההקצאה השנייה תדלג על ה-tcache.
רמז: על 2.31 אין safe-linking, אז כותבים את __free_hook גולמי ל-next, בלי ערבול. אם משהו קורס, פתחו tcachebins ב-gdb אחרי ה-edit - צריך לראות את __free_hook כ-next של הראש.
תרגיל 5 - אותה התקפה על glibc 2.35 (safe-linking, בלי hooks)¶
עכשיו קמפלו את אותו tc.c בתוך ubuntu:22.04 והתמודדו עם העולם המודרני.
המטרה:
- הריצו את ה-exploit מתרגיל 4 כמו שהוא. איפה הוא נשבר? (שני דברים ישתנו.)
- תקנו את החלק של ה-
next: כדי לכתובnextשמפוענח ל-TARGET, חשבו(chunk_addr >> 12) ^ TARGET. אתchunk_addrהשיגו מleak heap - שחררו צ'אנק בודד וקראו את ה-nextהמעורבל שלו (זהpos >> 12), ואזheap << 12. - תקנו את החלק של היעד:
__free_hookלא קיים ב-2.34+. בחרו יעד אחר. הקל ביותר כאן: כיוון שהבינאריno-pieעם Partial RELRO, הקצו על כניסת GOT של פונקציה שהבינארי באמת קורא (למשלprintf,freeאוstrtol), דרסו אותה ל-one_gadget או ל-system, והפעילו את הפונקציה. - קבלו shell גם על 2.35.
רמז: את leak ה-heap עשו לפני ההרעלה: free(0) על צ'אנק בודד, view(0), heap = u64(...) << 12. אחר כך chunk_addr שאתם מרעילים הוא כתובת ה-mem של הצ'אנק הרלוונטי - חשבו אותו מבסיס ה-heap ב-gdb.
רמז: יעד GOT דורש Partial RELRO. ודאו עם checksec. אם תבחרו לדרוס free@got, שימו לב שאותה הקצאה נוחתת על ה-GOT, ושהפונקציה שתפעילו (למשל free הבא) תזנק ליעד. one_gadget נוח כי אין צורך לבנות ארגומנטים.
רמז: זכרו את בדיקת הalignment של safe-linking - הצ'אנק המורעל חייב להיות aligned ל-16. כתובת GOT היא לרוב aligned ל-8 בלבד; אם תיתקלו ב-unaligned tcache chunk detected, כוונו לכתובת aligned ל-16 בתוך אותו אזור, או השתמשו בטריק ה-0x??f0.
תרגיל 6 (אתגר) - שחרור כפול טהור בלי UAF edit¶
בתרגילים הקודמים השתמשנו ב-edit כדי לדרוס את ה-next. עכשיו דמיינו בינארי שאין בו edit בכלל - רק alloc/free - אבל malloc נותן לכם לכתוב לצ'אנק חדש.
המטרה:
- חשבו: איך משיגים הרעלה כשהדרך היחידה לכתוב ל-
nextהיא דרךmallocשמחזיר צ'אנק ששוחרר קודם? (רמז: זה בדיוק מה שהשחרור הכפול נותן - מצביע חי לצ'אנק שעדיין ברשימה.) - ממשו: שחרור כפול (עם עקיפת המפתח לפי 2.29), ואז
mallocשמחזיר את הצ'אנק ובו זמנית משאיר אותו ברשימה, וכתיבת ה-nextדרך ה-dataשל אותה הקצאה. - השוו: כמה פעולות תפס המסלול הזה לעומת המסלול עם
edit? מתי כל אחד עדיף?
רמז: הרצף הוא free(a); (עקיפת key); free(a); a2 = alloc(...) - וה-data של ההקצאה a2 נכתב לתוך אותו זיכרון שעדיין ראש ה-bin. שם אתם שותלים את ה-next המורעל.
רמז: אם אתם על 2.32+, אל תשכחו לערבל את ה-next גם כאן. הleak ה-heap עדיין נחוץ.