6.2 ret2csu תרגול
תרגול - ret2csu¶
בתרגול הזה תבנו בעצמכם בינארי שאין בו גאדג'ט pop rdx, ותשתמשו בגאדג'טים של __libc_csu_init כדי לקרוא לפונקציה עם שלושה ארגומנטים. המטרה היא שתרגישו על העור את שני הגאדג'טים, את הערכים הקבועים (rbx=0, rbp=1), ואת הpadding אחרי גאדג'ט ה-call. עבדו לפי הסדר - כל תרגיל בונה על הקודם. ההנחות: NX פעיל, בלי canary, בלי PIE.
הכנה - הבינארי¶
צרו את הקובץ demo.c. שימו לב לפונקציה print_flag הדורשת שלושה ערכי קסם, ולמצביע הגלובלי g_target שמצביע עליה (הוא ייתן לנו תא זיכרון בכתובת ידועה שמכיל את כתובת הפונקציה):
// demo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void print_flag(unsigned int a, unsigned long b, unsigned long c) {
if (a == 0xdeadbeef && b == 0xcafebabecafebabe && c == 0x1337133713371337) {
puts("FLAG{ret2csu_three_args_pwned}");
} else {
puts("nope");
}
}
// global pointer -> a memory cell holding the address of print_flag
void (*g_target)(unsigned int, unsigned long, unsigned long) = print_flag;
void vuln(void) {
char buf[64];
puts("send bytes:");
read(0, buf, 400);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
vuln();
return 0;
}
קמפלו בלי canary ובלי PIE:
הערה חשובה: כדי שהתרגיל יעבוד, הבינארי חייב להכיל את __libc_csu_init. אם קמפלתם על מערכת חדשה (glibc 2.34 ומעלה) והפונקציה נעלמה, ראו את תרגיל 5 - שם נתמודד עם זה. אפשר גם לקמפל בסביבה ישנה יותר או להשתמש בבינארי דוגמה קלאסי.
תרגיל 1 - סיור, הגנות, ואישור שאין pop rdx¶
לפני שתוקפים, מיפוי.
- הריצו
checksec --file=./demoוּודאו שאתם רואיםNX enabled,No canary,No PIE. - הריצו
ROPgadget --binary ./demo | grep 'pop rdx ; ret'וּודאו בעצמכם שאין גאדג'ט כזה. זו כל הסיבה שאנחנו זקוקים ל-ret2csu. - אשרו שיש
__libc_csu_initבבינארי:objdump -d ./demo | grep '<__libc_csu_init>:'.
רמז: אם pop rdx דווקא כן קיים אצלכם (בגלל גרסת מהדר או ספרייה), התרגיל עדיין תקף כתרגיל לימוד - פשוט נתעלם ממנו ונשתמש ב-csu בכוונה.
תרגיל 2 - מציאת שני הגאדג'טים¶
מצאו את שתי הכתובות שאתם צריכים בתוך __libc_csu_init.
- הריצו
objdump -d ./demo | grep -A24 '<__libc_csu_init>:'. - אתרו את הכתובת של גאדג'ט ה-mov/call: השורה
mov rdx, r15(אחריהmov rsi, r14,mov edi, r13d,call QWORD PTR [r12+rbx*8]). - אתרו את הכתובת של גאדג'ט ה-pop: השורה הראשונה
pop rbx(אחריהpop rbp,pop r12,pop r13,pop r14,pop r15,ret). - רשמו את שתי הכתובות בצד. אלה
csu_callו-csu_pop.
רמז: גאדג'ט ה-mov/call לא מסתיים ב-ret אלא ב-call, ולכן ROPgadget בברירת מחדל לא יראה לכם אותו. הדרך הבטוחה למצוא אותו היא לפרק את __libc_csu_init עם objdump ולקרוא בעיניים.
תרגיל 3 - מציאת התא של מצביע הפונקציה¶
גאדג'ט ה-call מבצע call [r12+rbx*8], ולכן r12 צריך להצביע על תא זיכרון שמכיל את הכתובת של print_flag.
- מצאו את הכתובת של
g_targetעםobjdump -t ./demo | grep g_target(אוnm ./demo | grep g_target, או ב-pwntoolself.symbols['g_target']). - ודאו לעצמכם שהתא הזה באמת מכיל את הכתובת של
print_flag: פתחו ב-gdb, הדפיסוx/gx &g_targetוהשוו ל-p print_flag.
רמז: אל תבלבלו בין הכתובת של print_flag (הפונקציה) לבין הכתובת של g_target (התא שמכיל מצביע לפונקציה). r12 צריך להיות הכתובת של g_target.
תרגיל 4 - הchain המלאה¶
עכשיו הרכיבו את ה-exploit ב-pwntools וקראו ל-print_flag(0xdeadbeef, 0xcafebabecafebabe, 0x1337133713371337).
- מצאו את ה-offset עד return address עם
cyclic(הbuffer הוא 64 בתים, saved rbp 8 בתים - אבל תמדדו, אל תניחו). - בנו את הchain: קודם
csu_pop, ואז שבעת הערכים (rbx=0,rbp=1,r12=&g_target,r13=arg1,r14=arg2,r15=arg3), ואזcsu_call. - אחרי
csu_callהוסיפו שבעה סלוטי padding (סלוט ל-add rsp,8ושישה ל-pop), ואז כתובת יעד סופית נקייה כמוelf.symbols['exit']. - הריצו וּודאו שאתם רואים את שורת הדגל.
רמז: אם אתם מקבלים nope במקום הדגל, כנראה אחד הארגומנטים לא הגיע נכון - עצרו ב-gdb על print_flag והדפיסו p/x $rdi, p/x $rsi, p/x $rdx. זכרו ש-edi הוא רק 32 ביט, אז arg1 חייב להיכנס ב-0xdeadbeef בדיוק.
רמז שני: אם התוכנית קורסת מיד אחרי שהדגל מודפס, בדקו את ספירת סלוטי הpadding אחרי csu_call - כמעט תמיד זה שם.
תרגיל 5 - מה עושים כשאין __libc_csu_init¶
עכשיו נדמה בינארי מודרני.
- קמפלו מחדש בסביבה חדשה, או הריצו
objdump -d ./demo | grep '__libc_csu_init'על בינארי שקומפל ב-glibc 2.34 ומעלה, וּראו שהפונקציה נעלמה. - חשבו וכתבו לעצמכם: אילו חלופות יש? רמזו לעצמכם - האם יש גאדג'ט
mov rdxאחר בבינארי? מה אם יש לכם leak של libc? באיזה שיעור בפרק נלמד טכניקה שקובעת את כל האוגרים בבת אחת? - הריצו
ROPgadget --binary ./demo | grep 'mov rdx'וראו אם נשאר משהו שקול.
רמז: אין פתרון "אחד". המטרה כאן היא שתזהו את המגבלה של ret2csu ותכירו את מפת החלופות: גאדג'טים מ-libc, one_gadget, SROP (6.3), ו-ret2dlresolve (6.4).
תרגיל 6 (בונוס) - קריאה לפונקציית libc דרך ה-GOT¶
הכלילו את הטכניקה כדי לקרוא לפונקציית libc במקום לפונקציה מקומית.
- הוסיפו ל-
demo.cקריאה כלשהי ל-write(או השתמשו בכך ש-writeמקושרת ממילא), כדי שתהיה לה כניסת GOT. - כוונו את
r12עלelf.got['write']וקראו ל-write(1, addr, len)שמדפיס בתים מכתובת שאתם בוחרים. - שימו לב ש-
fd=1הוא ערך קטן שנכנס יפה ב-edi. הדבר הזה הוא בדיוק הבסיס לleak של כתובת libc לפני מעבר ל-ret2libc.
רמז: call [write@got] יקרא ל-write כי הכניסה ב-GOT מכילה את הכתובת המפוענחת של write. אחרי הקריאה, במקום exit שימו את elf.symbols['main'] כדי לחזור ולגלוש שוב אם תרצו chain ארוכה יותר.