לדלג לתוכן

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:

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

הערה חשובה: כדי שהתרגיל יעבוד, הבינארי חייב להכיל את __libc_csu_init. אם קמפלתם על מערכת חדשה (glibc 2.34 ומעלה) והפונקציה נעלמה, ראו את תרגיל 5 - שם נתמודד עם זה. אפשר גם לקמפל בסביבה ישנה יותר או להשתמש בבינארי דוגמה קלאסי.

תרגיל 1 - סיור, הגנות, ואישור שאין pop rdx

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

  1. הריצו checksec --file=./demo וּודאו שאתם רואים NX enabled, No canary, No PIE.
  2. הריצו ROPgadget --binary ./demo | grep 'pop rdx ; ret' וּודאו בעצמכם שאין גאדג'ט כזה. זו כל הסיבה שאנחנו זקוקים ל-ret2csu.
  3. אשרו שיש __libc_csu_init בבינארי: objdump -d ./demo | grep '<__libc_csu_init>:'.

רמז: אם pop rdx דווקא כן קיים אצלכם (בגלל גרסת מהדר או ספרייה), התרגיל עדיין תקף כתרגיל לימוד - פשוט נתעלם ממנו ונשתמש ב-csu בכוונה.

תרגיל 2 - מציאת שני הגאדג'טים

מצאו את שתי הכתובות שאתם צריכים בתוך __libc_csu_init.

  1. הריצו objdump -d ./demo | grep -A24 '<__libc_csu_init>:'.
  2. אתרו את הכתובת של גאדג'ט ה-mov/call: השורה mov rdx, r15 (אחריה mov rsi, r14, mov edi, r13d, call QWORD PTR [r12+rbx*8]).
  3. אתרו את הכתובת של גאדג'ט ה-pop: השורה הראשונה pop rbx (אחריה pop rbp, pop r12, pop r13, pop r14, pop r15, ret).
  4. רשמו את שתי הכתובות בצד. אלה csu_call ו-csu_pop.

רמז: גאדג'ט ה-mov/call לא מסתיים ב-ret אלא ב-call, ולכן ROPgadget בברירת מחדל לא יראה לכם אותו. הדרך הבטוחה למצוא אותו היא לפרק את __libc_csu_init עם objdump ולקרוא בעיניים.

תרגיל 3 - מציאת התא של מצביע הפונקציה

גאדג'ט ה-call מבצע call [r12+rbx*8], ולכן r12 צריך להצביע על תא זיכרון שמכיל את הכתובת של print_flag.

  1. מצאו את הכתובת של g_target עם objdump -t ./demo | grep g_target (או nm ./demo | grep g_target, או ב-pwntools elf.symbols['g_target']).
  2. ודאו לעצמכם שהתא הזה באמת מכיל את הכתובת של 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).

  1. מצאו את ה-offset עד return address עם cyclic (הbuffer הוא 64 בתים, saved rbp 8 בתים - אבל תמדדו, אל תניחו).
  2. בנו את הchain: קודם csu_pop, ואז שבעת הערכים (rbx=0, rbp=1, r12=&g_target, r13=arg1, r14=arg2, r15=arg3), ואז csu_call.
  3. אחרי csu_call הוסיפו שבעה סלוטי padding (סלוט ל-add rsp,8 ושישה ל-pop), ואז כתובת יעד סופית נקייה כמו elf.symbols['exit'].
  4. הריצו וּודאו שאתם רואים את שורת הדגל.

רמז: אם אתם מקבלים nope במקום הדגל, כנראה אחד הארגומנטים לא הגיע נכון - עצרו ב-gdb על print_flag והדפיסו p/x $rdi, p/x $rsi, p/x $rdx. זכרו ש-edi הוא רק 32 ביט, אז arg1 חייב להיכנס ב-0xdeadbeef בדיוק.

רמז שני: אם התוכנית קורסת מיד אחרי שהדגל מודפס, בדקו את ספירת סלוטי הpadding אחרי csu_call - כמעט תמיד זה שם.

תרגיל 5 - מה עושים כשאין __libc_csu_init

עכשיו נדמה בינארי מודרני.

  1. קמפלו מחדש בסביבה חדשה, או הריצו objdump -d ./demo | grep '__libc_csu_init' על בינארי שקומפל ב-glibc 2.34 ומעלה, וּראו שהפונקציה נעלמה.
  2. חשבו וכתבו לעצמכם: אילו חלופות יש? רמזו לעצמכם - האם יש גאדג'ט mov rdx אחר בבינארי? מה אם יש לכם leak של libc? באיזה שיעור בפרק נלמד טכניקה שקובעת את כל האוגרים בבת אחת?
  3. הריצו ROPgadget --binary ./demo | grep 'mov rdx' וראו אם נשאר משהו שקול.

רמז: אין פתרון "אחד". המטרה כאן היא שתזהו את המגבלה של ret2csu ותכירו את מפת החלופות: גאדג'טים מ-libc, one_gadget, SROP (6.3), ו-ret2dlresolve (6.4).

תרגיל 6 (בונוס) - קריאה לפונקציית libc דרך ה-GOT

הכלילו את הטכניקה כדי לקרוא לפונקציית libc במקום לפונקציה מקומית.

  1. הוסיפו ל-demo.c קריאה כלשהי ל-write (או השתמשו בכך ש-write מקושרת ממילא), כדי שתהיה לה כניסת GOT.
  2. כוונו את r12 על elf.got['write'] וקראו ל-write(1, addr, len) שמדפיס בתים מכתובת שאתם בוחרים.
  3. שימו לב ש-fd=1 הוא ערך קטן שנכנס יפה ב-edi. הדבר הזה הוא בדיוק הבסיס לleak של כתובת libc לפני מעבר ל-ret2libc.

רמז: call [write@got] יקרא ל-write כי הכניסה ב-GOT מכילה את הכתובת המפוענחת של write. אחרי הקריאה, במקום exit שימו את elf.symbols['main'] כדי לחזור ולגלוש שוב אם תרצו chain ארוכה יותר.