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 יהיו קבועות):
תרגיל 1 - למפות את ה-heap¶
לפני שדורסים, מבינים איפה הכל יושב.
- הריצו
checksec --file=./hofואמתוNo PIE,NX enabled,No canary. - פתחו את הבינארי ב-gdb, שימו breakpoint אחרי ה-
malloc(0x18)הראשון, והריצוheapב-pwndbg. זהו: את chunk ה-tcache_perthread (בגודל0x290), את chunk A, ואת ה-top chunk. - השוו את הכתובת של chunk A למה שהתוכנית הדליפה ב-
[leak]. אמתו לעצמכם ש-כתובת ה-top chunk שווה ל-leak + 0x10. הסבירו למה (רמז: מה הגודל האמיתי של chunk A?).
רמז: malloc(0x18) מקבל chunk בגודל 0x20 (0x18 נתונים + כותרת, מעוגל ל-16). לכן ה-top יושב 0x20 בתים אחרי כותרת A, שהם 0x10 בתים אחרי ה-mem שleaked.
תרגיל 2 - לדרוס את גודל ה-top¶
עכשיו הoverflow.
- חשבו: כמה בתים צריך לכתוב מ-
a(ה-mem של A) כדי להגיע בדיוק לשדה ה-sizeשל ה-top? רמז:prev_sizeשל ה-top יושב ב-a + 0x10, וה-sizeב-a + 0x18. - שלחו בשלב
[1]מטען שממלא את A ואת ה-prev_sizeשל ה-top, ואז דורס את שדה ה-sizeב-0xffffffffffffffff. - שימו breakpoint אחרי ה-
readהזה, והריצו שובheap(אוtop). ודאו שגודל ה-top עכשיו0xffffffffffffffff.
רמז: המטען הוא b'A' * 0x18 + p64(0xffffffffffffffff) - בדיוק 0x20 בתים.
תרגיל 3 - להזיז את ה-top ליעד¶
זה הלב של הטכניקה.
- קבעו את היעד:
target = elf.symbols['hook'](השתמשו ב-ELF('./hof')). ודאו שהכתובת aligned ל-16. - חשבו את
evil_size = (target - 0x10 - top) & 0xffffffffffffffff. שימו לב שהיעד נמוך מה-heap, אז הערך יוצא ענק - זה תקין. - שלחו את
evil_sizeבשלב[2](בעשרוני, כי הקוד קוראscanf("%lu")). - שימו breakpoint אחרי ה-
malloc(n), ובדקו את מצביע ה-top ב-pwndbg (top). ודאו שהוא נחת בדיוק עלtarget - 0x10.
רמז: אם ה-top נחת ליד היעד אבל לא בדיוק עליו (סטייה של 0x8 או 0x10), זה עיגול של request2size. כווננו את evil_size בהפרש שראיתם וחזרו על הבדיקה.
תרגיל 4 - הפיצוח המלא¶
חברו הכל.
- בשלב
[3], כתבו לכתובת שחזרה (שהיא&hook) אתp64(elf.symbols['win']). - הריצו את הסקריפט המלא ב-pwntools וקבלו shell (
p.interactive()). - הריצו
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;
}
קמפלו:
תרגיל 5 - House of Spirit דרך tcache¶
- בשלב
[1], ציירו chunk מזויף: שימוsize = 0x41ב-fake[1](זה שדה ה-sizeשל chunk שה-mem שלו הוא&fake[2]). את השאר אפשר להשאיר אפסים. - בשלב
[2], שחררו את המצביע&fake[2]. חשבו את הכתובת מתוך ה-leakשלfake:fake_mem = leak + 0x10. שלחו אותו בעשרוני. - בשלב
[3], ה-malloc(0x30)אמור להחזיר את&fake[2]. אמתו ב-gdb שהמצביע שחזר נמצא על המחסנית, בדיוק ב-leak + 0x10. - עכשיו נצלו את זה: מצאו עם
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 (חשיבה) - גבולות וגרסאות¶
- הריצו את
hofמול glibc 2.31 (Ubuntu 20.04). מה השגיאה שאתם מקבלים בשלב ה-malloc(n)? הסבירו איזו בדיקה תפסה אתכם. - בגרסה ישנה מאוד בלי tcache (glibc 2.23), האם תרגיל 5 היה עובד כמו שהוא? מה עוד הייתם צריכים לצייר ב-
fake? רמז: חשבו על ה-chunk הפיזי הבא. - אם ה-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]).