לדלג לתוכן

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 כדי לפשט את הצעדים הראשונים:

gcc -o notes notes.c -no-pie -fno-stack-protector
checksec --file=./notes

הנחות ההגנה שתראו ב-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, הבינו מה יש לכם ביד.

  1. הריצו checksec וכתבו במפורש אילו הגנות פעילות.
  2. פתחו ב-pwndbg. הקצו note, שחררו אותו, וצפו בו עם view. מה חוזר? הסבירו למה.
  3. הקצו note גדול (גודל 0x500), שחררו, וצפו בו. איזו כתובת דלפה? לאיזה מבנה ב-libc היא מצביעה?

רמז: chunk בגודל 0x500 גדול מדי ל-tcache (המקסימום הוא 0x410 בערך), אז ב-free הוא הולך ל-unsorted bin, ושדות ה-fd/bk שלו מצביעים בחזרה לתוך main_arena שב-libc.

תרגיל 2 - leak libc וleak heap

בנו את שלב הleaks של ה-exploit ב-pwntools.

  1. השיגו את בסיס libc: שחררו chunk גדול, קראו את ה-fd שלו, וחשבו libc.address מ-main_arena.
  2. השיגו את בסיס ה-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 לכתיבה לכתובת שאתם בוחרים.

  1. הקצו שני chunks באותו גודל (למשל 0x40), שחררו את שניהם - עכשיו הם ב-tcache אחד.
  2. ערכו את ה-chunk ששוחרר אחרון וכתבו ב-fd שלו את הכתובת שאתם רוצים (ממוסכת, אם צריך).
  3. הקצו פעמיים באותו גודל. ההקצאה השנייה צריכה לחזור בדיוק לכתובת שבחרתם. אמתו את זה ב-gdb.

רמז: ודאו שהמטרה aligned ל-16 בתים, אחרת glibc 2.32+ יזרוק unaligned tcache chunk detected.

תרגיל 4 - הרצת קוד: בחרו מסלול

עכשיו החלק המרכזי. השיגו shell בלי לגעת בהוקים. בחרו אחד משני המסלולים (או, אם יש לכם זמן, ממשו את שניהם):

מסלול א - environ אל המחסנית:

  1. הרעילו tcache כדי להקצות chunk מעל libc.sym['environ'], וקראו ממנו כתובת מחסנית.
  2. מצאו ב-gdb את ה-delta בין environ לבין ה-saved RIP של פונקציה שתחזור אחרי שתסיימו לכתוב.
  3. הרעילו שוב כדי להקצות מעל ה-saved RIP, וכתבו לשם chain ret2libc (pop rdi; ret; "/bin/sh"; system).
  4. גרמו לפונקציה לחזור וקבלו shell.

רמז: כדי למצוא את ה-delta, עצרו ב-gdb בתוך get_int, הריצו p/x (long)environ, ואז stack 60 כדי לאתר את ה-saved RIP של main. ההפרש קבוע בין הרצות.

רמז 2: chain ROP צריכה גאדג'ט ret לalignment לפני system (בעיית movaps).

מסלול ב - FSOP בעת יציאה:

  1. הרעילו tcache כדי לכתוב על _IO_2_1_stdout_ ולהשיג leak (או השתמשו בleak מתרגיל 2), ולאחר מכן בנו FILE מזויף.
  2. השתמשו ב-FileStructure() של pwntools כדי לזייף stdout שמדליף זיכרון, או בנו מבנה house of apple2 מלא ל-RCE.
  3. דרסו את _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, נמצא בקובץ הפתרון. נסו לבד לפני שאתם מציצים.