לדלג לתוכן

2.3 ניתוב הרצה וret2win תרגול

תרגול - ניתוב הרצה וret2win

בתרגול הזה תבנו בעצמכם בינארים vulnerable, תנתבו את ההרצה שלהם לפונקציה מנצחת, ותלמדו להעביר ארגומנטים גם ב-64 ביט וגם ב-32 ביט. זה בדיוק הבסיס לאתגר bof מ-pwnable.kr (שנפתור בהמשך הפרק) ולכל אתגרי ה-ret2win הקלאסיים. עבדו לפי הסדר - כל תרגיל בונה על הקודם.

הכנה - הבינארים

צרו שני קבצי מקור. הראשון, warmup.c, עם פונקציה מנצחת בלי ארגומנטים:

// warmup.c
#include <stdio.h>
#include <unistd.h>

void win() {
    puts("You win! Here is your flag: FLAG{ret2win_warmup}");
}

void vuln() {
    char buf[64];
    puts("warmup> send bytes:");
    read(0, buf, 200);
}

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

השני, keys.c, עם פונקציה מנצחת שדורשת שני מפתחות נכונים ופותחת shell:

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

void win(long key1, long key2) {
    if (key1 == 0xc0ffee && key2 == 0x1337) {
        puts("keys accepted. shell:");
        system("/bin/sh");
    } else {
        puts("nope.");
    }
}

void vuln() {
    char buf[80];
    puts("keys> send bytes:");
    read(0, buf, 300);
}

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

קמפלו את שניהם ל-64 וגם ל-32 ביט (אם חסרות ספריות 32 ביט: sudo apt install gcc-multilib):

gcc -fno-stack-protector -no-pie -o warmup64 warmup.c
gcc -m32 -fno-stack-protector -no-pie -o warmup32 warmup.c
gcc -fno-stack-protector -no-pie -o keys64 keys.c
gcc -m32 -fno-stack-protector -no-pie -o keys32 keys.c

הערה על הסביבה: השאירו את ההגנות כמו שהן - NX פעיל, בלי canary, בלי PIE. לא צריך לכבות ASLR כי אנחנו לא משתמשים בכתובות libc, רק בכתובות קבועות מתוך הבינארי.

תרגיל 1 - סיור והכנה

לפני שתוקפים, מיפוי.

  1. הריצו checksec --file=./keys64 וּודאו שאתם באמת רואים NX enabled, No canary, No PIE.
  2. מצאו את הכתובת של win בכל אחד מארבעת הבינארים, פעם עם objdump -d <bin> | grep '<win>:' ופעם עם nm <bin> | grep ' win'. השוו.
  3. מצאו את ה-offset עד הreturn address של vuln בכל בינארי, בשיטת ה-cyclic מ-2.2. רמז: cyclic 200 לקלט, ואז cyclic_find(...) על הערך שדרס את מצביע ההוראות.

רמז: ה-offset של warmup ו-keys שונה כי הbuffers בגודל שונה (64 מול 80). אל תניחו, תמדדו.

תרגיל 2 - ret2win בלי ארגומנטים (64 ביט)

כתבו exploit ב-pwntools ל-warmup64 שקופץ ל-win ומדפיס את הדגל.

  1. השתמשו ב-context.binary = ELF('./warmup64') וב-elf.symbols['win'].
  2. הרכיבו מטען: הpadding עד ה-offset, ואז כתובת win.
  3. הריצו וּודאו שאתם רואים את שורת הדגל.

רמז: כאן win לא קוראת לשום פונקציית libc כבדה, אז לרוב לא תיתקלו בבעיית alignment. אם בכל זאת יש קריסה, הציצו כבר עכשיו בסעיף הalignment בהרצאה.

תרגיל 3 - ארגומנט אחד ב-64 ביט

עכשיו keys64, אבל בשלב ביניים: דמיינו שרק המפתח הראשון נבדק. המטרה שלכם היא לוודא ש-rdi מכיל את הערך 0xc0ffee ברגע הקפיצה ל-win.

  1. מצאו את הגאדג'ט pop rdi ; ret עם ROPgadget --binary keys64 | grep 'pop rdi ; ret'.
  2. בנו ידנית chain: [pop rdi] [0xc0ffee] [win].
  3. הדפיסו ב-gdb את rdi ברגע הכניסה ל-win וּודאו שהערך נכון (gdb.attach או breakpoint על win).

רמז: הזרימה היא ret של vuln -> הגאדג'ט שולף ל-rdi -> ret של הגאדג'ט קופץ ל-win.

תרגיל 4 - שני ארגומנטים ו-shell (64 ביט)

השלימו את keys64: win(0xc0ffee, 0x1337) שפותח shell.

  1. הוסיפו גאדג'ט לטעינת rsi. חפשו pop rsi ; pop r15 ; ret (או pop rsi ; ret).
  2. שרשרו: קודם rdi, אחר כך rsi, ואז win.
  3. אם ה-shell לא נפתח למרות ששני האוגרים נכונים - זו כמעט בוודאות בעיית הalignment מה-movaps. הוסיפו גאדג'ט ret בודד לalignment לפני הקריאה ונסו שוב.
  4. כשמשיג shell, הריצו cat על משהו כדי לוודא שהוא אינטראקטיבי.

רמז: אחרי שתעבדו ידנית, נסו לשכתב את כל הchain בעזרת אובייקט ROP של pwntools עם rop.call('win', [0xc0ffee, 0x1337]), ואל תשכחו את rop.find_gadget(['ret']) לalignment.

תרגיל 5 - אותו דבר, 32 ביט (cdecl)

כתבו exploit ל-keys32 שפותח shell.

  1. אין כאן גאדג'טים ואין אוגרים - הארגומנטים על המחסנית. סדרו: [padding] [win] [ret מזויף] [key1] [key2].
  2. שימו לב שערכי המפתחות הם כעת 4 בתים כל אחד. תנו ל-flat לארוז אותם.
  3. מה קורה אם תשכחו את סלוט החזרה המזויף? נסו בכוונה, תראו את הקריסה, והבינו למה.

רמז: long ב-32 ביט הוא 4 בתים. סלוט החזרה המזויף יכול להיות ערך דמה כי לא נחזור מ-win אחרי ה-shell, אבל הוא חייב להיות שם כדי לשמור על הalignment של הארגומנטים.

תרגיל 6 (בונוס) - הכללה לשרת מרוחק ול-bof

  1. הפכו את ה-exploit של תרגיל 4 לגמיש עם args.REMOTE, כך שאותו סקריפט ירוץ מקומית ומול remote(host, port). הריצו את הבינארי דרך socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./keys64 וּתקפו את עצמכם.
  2. קראו את תיאור האתגר bof מ-pwnable.kr. הוא דומה ברוח אבל שונה במנגנון: שם דורסים ערך של משתנה מקומי כדי לעבור בדיקה, לא return address. חשבו: איזה חלק מהידע מהתרגול הזה חוזר שם, ומה שונה?

רמז: ב-bof אין פונקציית win שקופצים אליה - הoverflow משנה ארגומנט שנבדק ב-if. אבל שיטת מציאת ה-offset וסידור המטען זהים לחלוטין למה שעשיתם כאן.