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 - סיור והכנה¶
לפני שתוקפים, מיפוי.
- הריצו
checksec --file=./keys64וּודאו שאתם באמת רואיםNX enabled,No canary,No PIE. - מצאו את הכתובת של
winבכל אחד מארבעת הבינארים, פעם עםobjdump -d <bin> | grep '<win>:'ופעם עםnm <bin> | grep ' win'. השוו. - מצאו את ה-offset עד הreturn address של
vulnבכל בינארי, בשיטת ה-cyclicמ-2.2. רמז:cyclic 200לקלט, ואזcyclic_find(...)על הערך שדרס את מצביע ההוראות.
רמז: ה-offset של warmup ו-keys שונה כי הbuffers בגודל שונה (64 מול 80). אל תניחו, תמדדו.
תרגיל 2 - ret2win בלי ארגומנטים (64 ביט)¶
כתבו exploit ב-pwntools ל-warmup64 שקופץ ל-win ומדפיס את הדגל.
- השתמשו ב-
context.binary = ELF('./warmup64')וב-elf.symbols['win']. - הרכיבו מטען: הpadding עד ה-offset, ואז כתובת
win. - הריצו וּודאו שאתם רואים את שורת הדגל.
רמז: כאן win לא קוראת לשום פונקציית libc כבדה, אז לרוב לא תיתקלו בבעיית alignment. אם בכל זאת יש קריסה, הציצו כבר עכשיו בסעיף הalignment בהרצאה.
תרגיל 3 - ארגומנט אחד ב-64 ביט¶
עכשיו keys64, אבל בשלב ביניים: דמיינו שרק המפתח הראשון נבדק. המטרה שלכם היא לוודא ש-rdi מכיל את הערך 0xc0ffee ברגע הקפיצה ל-win.
- מצאו את הגאדג'ט
pop rdi ; retעםROPgadget --binary keys64 | grep 'pop rdi ; ret'. - בנו ידנית chain:
[pop rdi] [0xc0ffee] [win]. - הדפיסו ב-gdb את
rdiברגע הכניסה ל-winוּודאו שהערך נכון (gdb.attachאו breakpoint עלwin).
רמז: הזרימה היא ret של vuln -> הגאדג'ט שולף ל-rdi -> ret של הגאדג'ט קופץ ל-win.
תרגיל 4 - שני ארגומנטים ו-shell (64 ביט)¶
השלימו את keys64: win(0xc0ffee, 0x1337) שפותח shell.
- הוסיפו גאדג'ט לטעינת
rsi. חפשוpop rsi ; pop r15 ; ret(אוpop rsi ; ret). - שרשרו: קודם
rdi, אחר כךrsi, ואזwin. - אם ה-shell לא נפתח למרות ששני האוגרים נכונים - זו כמעט בוודאות בעיית הalignment מה-
movaps. הוסיפו גאדג'טretבודד לalignment לפני הקריאה ונסו שוב. - כשמשיג shell, הריצו
catעל משהו כדי לוודא שהוא אינטראקטיבי.
רמז: אחרי שתעבדו ידנית, נסו לשכתב את כל הchain בעזרת אובייקט ROP של pwntools עם rop.call('win', [0xc0ffee, 0x1337]), ואל תשכחו את rop.find_gadget(['ret']) לalignment.
תרגיל 5 - אותו דבר, 32 ביט (cdecl)¶
כתבו exploit ל-keys32 שפותח shell.
- אין כאן גאדג'טים ואין אוגרים - הארגומנטים על המחסנית. סדרו:
[padding] [win] [ret מזויף] [key1] [key2]. - שימו לב שערכי המפתחות הם כעת 4 בתים כל אחד. תנו ל-
flatלארוז אותם. - מה קורה אם תשכחו את סלוט החזרה המזויף? נסו בכוונה, תראו את הקריסה, והבינו למה.
רמז: long ב-32 ביט הוא 4 בתים. סלוט החזרה המזויף יכול להיות ערך דמה כי לא נחזור מ-win אחרי ה-shell, אבל הוא חייב להיות שם כדי לשמור על הalignment של הארגומנטים.
תרגיל 6 (בונוס) - הכללה לשרת מרוחק ול-bof¶
- הפכו את ה-exploit של תרגיל 4 לגמיש עם
args.REMOTE, כך שאותו סקריפט ירוץ מקומית ומולremote(host, port). הריצו את הבינארי דרךsocat TCP-LISTEN:1337,reuseaddr,fork EXEC:./keys64וּתקפו את עצמכם. - קראו את תיאור האתגר
bofמ-pwnable.kr. הוא דומה ברוח אבל שונה במנגנון: שם דורסים ערך של משתנה מקומי כדי לעבור בדיקה, לא return address. חשבו: איזה חלק מהידע מהתרגול הזה חוזר שם, ומה שונה?
רמז: ב-bof אין פונקציית win שקופצים אליה - הoverflow משנה ארגומנט שנבדק ב-if. אבל שיטת מציאת ה-offset וסידור המטען זהים לחלוטין למה שעשיתם כאן.