לדלג לתוכן

2.1 אנטומיה של גלישה ושליטה בRIP תרגול

תרגול - אנטומיה של overflow ושליטה ב-RIP

בתרגול הזה נעבור מהתיאוריה לידיים. נתחיל בהבנת המבנה של המסגרת דרך GDB, נפתור את האתגר bof מ-pwnable.kr על ידי דריסת משתנה מקומי, ונסגור בשליטה מלאה ב-RIP על דמו מקומי. אל תדלגו על תרגיל 1, הוא הבסיס לשני האחרים.

רקע - האתגר bof מ-pwnable.kr

האתגר bof הוא אתגר בן 5 נקודות בקטגוריה Toddler's Bottle. בעמוד האתגר תמצאו שני קבצים להורדה: את הבינארי bof ואת קוד המקור bof.c. השירות הvulnerable מאזין ברשת:

nc pwnable.kr 9000

כלומר, בשונה מאתגרים שדרשו התחברות ב-ssh, כאן פשוט מתחברים לפורט 9000 ושולחים את ה-payload. הבינארי הוא 32 ביט, אז השתמשו ב-p32 ולא ב-p64.

קוד המקור, בקצרה: main קורא ל-func(0xdeadbeef), הפונקציה func קוראת קלט עם gets לתוך buffer מקומי, ואז בודקת אם הארגומנט key שווה ל-0xcafebabe. אם כן, היא מריצה system("/bin/sh").


תרגיל 1 - לקרוא את המסגרת ב-GDB

לפני שתוקפים משהו, צריך לראות אותו. הידרו את הדמו מהשיעור והציצו לתוך המסגרת.

צרו קובץ demo.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void win() {
    puts("[+] you took control!");
    system("/bin/sh");
}

void vuln() {
    char buf[64];
    printf("send: ");
    read(0, buf, 256);
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    vuln();
    puts("bye");
    return 0;
}

הידרו אותו בלי הגנות:

gcc -fno-stack-protector -no-pie -g -o demo demo.c

המשימות שלכם:

  1. הריצו checksec --file=./demo ורשמו את מצב ה-RELRO, Canary, NX ו-PIE. האם התוצאה תואמת למה שציפיתם מדגלי ההידור?
  2. מצאו את כתובת הפונקציה win בשתי דרכים שונות: פעם עם objdump -d demo | grep win, ופעם מתוך pwntools עם ELF('./demo').symbols['win']. ודאו ששתי הדרכים נותנות אותה כתובת.
  3. פתחו את הבינארי ב-GDB (עדיף עם pwndbg), שימו breakpoint על הפונקציה vuln, הריצו, ופרקו אותה עם disassemble vuln. באיזו כתובת יחסית ל-rbp יושב הbuffer buf?
  4. חשבו: כמה בתים צריך לכתוב מתחילת buf כדי להגיע בדיוק לreturn address? רשמו את המספר, נשתמש בו בתרגיל 3.

רמז: בדיסאסמבלי חפשו את ההוראה lea rax, [rbp-0x??] שממש לפני הקריאה ל-read. המרחק לreturn address הוא הערך הזה ועוד 8 בתים (בשביל ה-rbp השמור).

רמז: אם אתם רוצים לאמת את החישוב במקום לנחש, שוברים אחרי ה-read, מדפיסים את כתובת הbuffer ואת $rbp+8, ומחסרים. את השיטה האוטומטית עם cyclic נראה בשיעור הבא.


תרגיל 2 - לפתור את bof

זה התרגיל המרכזי. המטרה: לפתוח shell על השרת של pwnable.kr ולקרוא את קובץ ה-flag.

המשימות:

  1. הורידו את bof ואת bof.c מעמוד האתגר. קראו את המקור והבינו מדוע key ניתן לדריסה למרות שהוא הוזרק כקבוע בקוד.
  2. פרקו את func עם objdump -d bof (או ב-GDB). מצאו שני מספרים: באיזו כתובת יחסית ל-ebp יושב הbuffer, ובאיזו כתובת יושב הארגומנט key.
  3. חשבו את ה-offset מהbuffer ועד key. זה מספר הבתים לpadding לפני שכותבים את הערך הקסום.
  4. כתבו סקריפט pwntools שמתחבר ל-pwnable.kr פורט 9000, שולח את ה-payload, ופותח מעטפת אינטראקטיבית. בתוך המעטפת הריצו cat flag.

רמז: זו מערכת 32 ביט עם cdecl. הארגומנט key עבר על המחסנית, מעל הreturn address. הbuffer נמצא מתחת. כלומר גם כאן הbuffer שאתם שולטים בו נמצא מתחת ליעד.

רמז: בדיסאסמבלי חפשו lea eax, [ebp-0x??] (הbuffer) ו-cmp dword [ebp+0x?], 0xcafebabe (הבדיקה על key). ה-offset הוא ההפרש בין שתי הכתובות האלה.

רמז: לחיבור השתמשו ב-remote('pwnable.kr', 9000), לערך הקסום ב-p32(0xcafebabe), ולסיום ב-p.interactive(). שימו לב: כאן אתם דורסים את key בלבד, לא את הreturn address.


תרגיל 3 - שליטה מלאה ב-RIP

עכשיו נשתלט על מצביע ההוראות. נחזור לדמו מתרגיל 1, ונגרום ל-vuln לחזור אל win במקום אל main.

המשימות:

  1. השתמשו ב-offset שחישבתם בתרגיל 1 (המרחק מהbuffer לreturn address).
  2. בנו payload: הpadding באורך ה-offset, ואחריו הכתובת של win כ-p64.
  3. הריצו את הסקריפט מול process('./demo'), ובדקו אם קיבלתם shell.
  4. אם ה-shell נפתח ומיד קורס עם SIGSEGV, אל תיבהלו. חשבו על alignment המחסנית ותקנו.

רמז: השתמשו ב-ELF('./demo').symbols['win'] כדי לא לרשום כתובת קשיחה בקוד.

רמז: מלכודת ה-movaps. אם ה-shell מת בתוך system, הכניסו כתובת של גאדג'ט ret בודד לפני הכתובת של win. ב-pwntools אפשר להשיג אותו עם ROP(elf).find_gadget(['ret'])[0]. הוא מזיז את המחסנית ב-8 בתים ומיישר אותה ל-16.

רמז: אם אתם רוצים לוודא שאתם באמת דורסים את הreturn address בכתובת הנכונה, הריצו את הסקריפט עם gdb.attach(p) או פשוט הריצו dmesg אחרי קריסה כדי לראות לאיזו כתובת ניסיתם לקפוץ.


בונוס - כשה-offset לא עגול

הידרו את הדמו שוב, הפעם עם buffer בגודל אחר, למשל char buf[72], או בלי -g. חשבו מחדש את ה-offset. האם הוא בדיוק 72 + 8? הקומפיילר לפעמים מוסיף alignment padding שמשנה את התמונה. נסו למצוא את ה-offset האמיתי ידנית ב-GDB. אם זה מתחיל להיות מעצבן, זה בדיוק הרגע להעריך את cyclic, שנפגוש בשיעור 2.2.