7.6 תקיפות tcache וfastbin מודרניות תרגול
תרגול - הרצת קוד עם tcache poisoning בעולם שאחרי ההוקים¶
בתרגול הזה נבנה binary vulnerable משלנו, נריץ אותו מול glibc מודרני (2.31 ומעלה), ונexploit אותו עד shell בלי לגעת ב-__free_hook - בדיוק כמו שנצטרך לעשות על 2.34+. המטרה: לתרגל את המסלול המלא, מleak, דרך tcache poisoning, ועד אחת המטרות המודרניות מההרצאה.
זה לא אתגר pwnable.kr קלאסי אלא סביבת אימון מקומית, וזה בכוונה - כך תשלטו בכל משתני הסביבה (גרסת glibc, ASLR, offsets) ותוכלו לבדוק את הפתרון שלכם צעד אחר צעד.
הכנת הסביבה¶
צרו את הקובץ notes.c:
// notes.c - a playground for the heap
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX 16
char *notes[MAX];
size_t sizes[MAX];
int get_int(void) { char b[16]; read(0, b, sizeof b); return atoi(b); }
void add(void) {
printf("idx: "); int i = get_int();
printf("size: "); int sz = get_int();
if (i < 0 || i >= MAX) return;
notes[i] = malloc(sz);
sizes[i] = sz;
printf("data: ");
read(0, notes[i], sz);
}
void edit(void) {
printf("idx: "); int i = get_int();
if (i < 0 || i >= MAX || !notes[i]) return;
printf("data: ");
read(0, notes[i], sizes[i]);
}
void view(void) {
printf("idx: "); int i = get_int();
if (i < 0 || i >= MAX || !notes[i]) return;
write(1, notes[i], sizes[i]);
}
void del(void) {
printf("idx: "); int i = get_int();
if (i < 0 || i >= MAX) return;
free(notes[i]); // bug: no notes[i] = NULL
}
int main(void) {
setvbuf(stdout, 0, _IONBF, 0);
while (1) {
printf("\n1) add 2) edit 3) view 4) delete 5) exit\n> ");
switch (get_int()) {
case 1: add(); break;
case 2: edit(); break;
case 3: view(); break;
case 4: del(); break;
case 5: return 0;
}
}
}
קמפלו בלי canary ובלי PIE כדי לפשט את הצעדים הראשונים:
הנחות ההגנה שתראו ב-checksec: NX enabled, No canary, No PIE, Partial RELRO. ASLR של המערכת דולק - אל תכבו אותו, כל הרעיון הוא לעקוף אותו עם leaks.
כדי לשלוט בגרסת glibc, הריצו את הבינארי מול libc ספציפי. הכי נוח עם pwninit או patchelf:
# download the libc for the desired version (e.g. 2.31 from Ubuntu 20.04, or 2.35 from 22.04)
pwninit --bin ./notes --libc ./libc.so.6 # creates notes_patched and ld
# or manually:
patchelf --set-interpreter ./ld-linux-x86-64.so.2 --replace-needed libc.so.6 ./libc.so.6 ./notes
הבאג: del משחרר בלי לאפס את המצביע. זה נותן לכם Use-After-Free מלא - אתם יכולים לצפות ב-chunk ששוחרר (leak), לערוך אותו (הרעלת fd), ואפילו לשחרר אותו פעמיים.
תרגיל 1 - מיפוי החולשה והגנות¶
לפני שכותבים exploit, הבינו מה יש לכם ביד.
- הריצו
checksecוכתבו במפורש אילו הגנות פעילות. - פתחו ב-pwndbg. הקצו note, שחררו אותו, וצפו בו עם
view. מה חוזר? הסבירו למה. - הקצו note גדול (גודל 0x500), שחררו, וצפו בו. איזו כתובת דלפה? לאיזה מבנה ב-libc היא מצביעה?
רמז: chunk בגודל 0x500 גדול מדי ל-tcache (המקסימום הוא 0x410 בערך), אז ב-free הוא הולך ל-unsorted bin, ושדות ה-fd/bk שלו מצביעים בחזרה לתוך main_arena שב-libc.
תרגיל 2 - leak libc וleak heap¶
בנו את שלב הleaks של ה-exploit ב-pwntools.
- השיגו את בסיס libc: שחררו chunk גדול, קראו את ה-
fdשלו, וחשבוlibc.addressמ-main_arena. - השיגו את בסיס ה-heap: שחררו chunk קטן (נכנס ל-tcache), קראו את ה-
fdשלו. אם אתם על 2.32+, שימו לב שהערך ממוסך ב-safe-linking - כיצד תשחזרו ממנו את כתובת ה-heap?
רמז: בראש רשימת tcache, ה-fd של ה-chunk הראשון ששוחרר הוא 0 XOR (chunk_addr >> 12), כלומר פשוט chunk_addr >> 12. הכפילו ב-<< 12 וקיבלתם את בסיס העמוד של ה-heap.
רמז 2: כתבו פונקציית עזר mangle(pos, ptr) שמחזירה ptr ^ (pos >> 12) כשה-safe-linking דלוק, ו-ptr כשהוא כבוי. תצטרכו אותה לכל הרעלה.
תרגיל 3 - tcache poisoning לכתיבה שרירותית¶
הפכו את ה-UAF לכתיבה לכתובת שאתם בוחרים.
- הקצו שני chunks באותו גודל (למשל 0x40), שחררו את שניהם - עכשיו הם ב-tcache אחד.
- ערכו את ה-chunk ששוחרר אחרון וכתבו ב-
fdשלו את הכתובת שאתם רוצים (ממוסכת, אם צריך). - הקצו פעמיים באותו גודל. ההקצאה השנייה צריכה לחזור בדיוק לכתובת שבחרתם. אמתו את זה ב-gdb.
רמז: ודאו שהמטרה aligned ל-16 בתים, אחרת glibc 2.32+ יזרוק unaligned tcache chunk detected.
תרגיל 4 - הרצת קוד: בחרו מסלול¶
עכשיו החלק המרכזי. השיגו shell בלי לגעת בהוקים. בחרו אחד משני המסלולים (או, אם יש לכם זמן, ממשו את שניהם):
מסלול א - environ אל המחסנית:
- הרעילו tcache כדי להקצות chunk מעל
libc.sym['environ'], וקראו ממנו כתובת מחסנית. - מצאו ב-gdb את ה-
deltaביןenvironלבין ה-saved RIP של פונקציה שתחזור אחרי שתסיימו לכתוב. - הרעילו שוב כדי להקצות מעל ה-saved RIP, וכתבו לשם chain ret2libc (
pop rdi; ret; "/bin/sh"; system). - גרמו לפונקציה לחזור וקבלו shell.
רמז: כדי למצוא את ה-delta, עצרו ב-gdb בתוך get_int, הריצו p/x (long)environ, ואז stack 60 כדי לאתר את ה-saved RIP של main. ההפרש קבוע בין הרצות.
רמז 2: chain ROP צריכה גאדג'ט ret לalignment לפני system (בעיית movaps).
מסלול ב - FSOP בעת יציאה:
- הרעילו tcache כדי לכתוב על
_IO_2_1_stdout_ולהשיג leak (או השתמשו בleak מתרגיל 2), ולאחר מכן בנוFILEמזויף. - השתמשו ב-
FileStructure()של pwntools כדי לזייףstdoutשמדליף זיכרון, או בנו מבנה house of apple2 מלא ל-RCE. - דרסו את
_IO_list_allשיצביע ל-FILEהמזויף, וצאו מהתוכנית (אפשרות 5) כדי להפעיל את הchain.
רמז: אם אתם על 2.31, ה-__free_hook עדיין קיים - אבל האתגר הוא לא להשתמש בו. התאמנו על המסלול שיעבוד גם על 2.35.
רמז 2: ל-house of apple2 זכרו את שלושת התנאים מההרצאה: _flags בלי _IO_NO_WRITES, _wide_data->_IO_write_base = NULL, ו-_IO_write_ptr > _IO_write_base.
אתגר בונוס¶
הריצו את אותו exploit מול שתי גרסאות libc - 2.31 ו-2.35 - עם דגל אחד ששולט ב-SAFE_LINKING. הסבירו איזה חלק בקוד השתנה בין הגרסאות ולמה מסלול environ עבד בשתיהן כמעט בלי שינוי.
הפתרון המלא, כולל שני המסלולים והפקודות למציאת ה-offsets, נמצא בקובץ הפתרון. נסו לבד לפני שאתם מציצים.