לדלג לתוכן

6.3 SROP תרגול

תרגול - SROP

בתרגול הזה תבנו exploit של SROP שמריץ execve("/bin/sh", NULL, NULL) על בינארי שכמעט אין בו גאדג'טים. המטרה: להטמיע בידיים את הרעיון שמסגרת sigcontext מזויפת + rax=15 + הוראת syscall שקולים לגאדג'ט שטוען את כל האוגרים.

הבינארי

צרו וקמפלו את הבינארי הבא (אותו אחד מההרצאה). הוא סטטי, בלי PIE ובלי canary, כדי שכל הגאדג'טים והמחרוזות יהיו בכתובות קבועות:

// vuln.c
#include <unistd.h>

void vuln(void) {
    char buf[32];
    read(0, buf, 1024);   // overflow - 1024 bytes into a 32-byte buffer
}

int main(void) {
    vuln();
    return 0;
}
gcc -static -no-pie -fno-stack-protector -o vuln vuln.c
checksec --file=./vuln

ודאו בפלט של checksec שאתם רואים NX enabled, No canary, No PIE. עכשיו גשו לעבודה.

תרגיל 1 - shell עם מסגרת אחת

המטרה: shell יחיד באמצעות SROP frame בודד, כשהמחרוזת "/bin/sh" כבר קיימת בתוך הבינארי הסטטי.

שלבים שתצטרכו לעבור:

  1. מצאו את ה-offset עד return address. אל תנחשו - מדדו.
  2. מצאו הוראת syscall (רצוי syscall ; ret) וגאדג'ט pop rax ; ret.
  3. מצאו את כתובת המחרוזת "/bin/sh" בבינארי.
  4. בנו מסגרת sigcontext מזויפת שמסדרת execve("/bin/sh", NULL, NULL), והרכיבו את הchain שקוראת ל-sigreturn.

רמז 1: את ה-offset מוצאים כרגיל - cyclic(200) כקלט, מסתכלים על הכתובת שגרמה ל-SIGSEGV ב-gdb, ו-cyclic_find(...).

רמז 2: את הגאדג'טים אפשר למצוא עם ROPgadget --binary vuln | grep -E ': syscall|pop rax', או לתת ל-pwntools למצוא לבד עם ROP(elf).find_gadget([...]).

רמז 3: את המחרוזת מוצאים עם next(elf.search(b'/bin/sh\x00')). שימו לב לבית ה-null בסוף.

רמז 4: מספרי הקריאות ב-x86-64: execve הוא 59, ו-rt_sigreturn הוא 15. הארגומנטים עוברים ב-rdi, rsi, rdx.

רמז 5: הסדר בchain הוא: הpadding, ואז pop rax, ואז הערך 15, ואז כתובת syscall, ואז bytes(frame). המסגרת חייבת לשבת בדיוק במקום שאליו rsp יצביע כשה-syscall ירוץ.

רמז 6: אל תשכחו context.binary = ELF('./vuln') לפני SigreturnFrame(), אחרת המסגרת תיבנה לארכיטקטורה הלא נכונה.

שאלה למחשבה: האם ה-exploit הזה ידרוש leak של כתובת כלשהי? מה קורה אם תדליקו ASLR מלא (echo 2 > /proc/sys/kernel/randomize_va_space) ותריצו שוב? הסבירו לעצמכם למה.

תרגיל 2 - בלי /bin/sh, שרשור שתי מסגרות

עכשיו התרחיש המעניין: מה אם המחרוזת "/bin/sh" בכלל לא נמצאת בבינארי? המטרה: לכתוב את המחרוזת בעצמכם ל-.bss, ואז להריץ עליה execve.

הרעיון: שתי מסגרות SROP משורשרות. המסגרת הראשונה קוראת ל-read שכותב "/bin/sh\0" לכתובת קבועה ב-.bss. המסגרת השנייה מריצה execve על אותה כתובת.

רמז 1: אזור ה-.bss כתיב וכתובתו קבועה (אין PIE). ב-pwntools: bss = elf.bss(0x300) נותן כתובת כתיבה בתוך ה-.bss.

רמז 2: הפעם frame.rip של המסגרת הראשונה חייב להיות syscall ; ret דווקא, לא syscall חשוף. למה? כי אחרי ש-read יחזור, אתם רוצים ש-ret יעביר את השליטה לchain שלב שני. את היעד קובעים ב-frame.rsp.

רמז 3: לאן להצביע את frame.rsp של המסגרת הראשונה? למקום שבו תשב chain שלב שני. טריק נקי: כתבו גם את המחרוזת וגם את chain שלב שני לתוך אותו בלוק שנקרא ל-.bss, ותכוונו את frame.rsp להיסט של chain שלב שני בתוך אותו בלוק. כך אתם לא תלויים בשום כתובת מחסנית, ו-ASLR לא מפריע.

רמז 4: הchain שלב שני היא בדיוק אותו דפוס: p64(pop_rax) + p64(15) + p64(syscall_ret) + bytes(frame2), כאשר frame2 מסדרת execve(bss, 0, 0).

רמז 5: מספר הקריאה של read הוא 0. הארגומנטים: rdi=0 (stdin), rsi=bss, rdx = כמה בתים לקרוא.

שאלה למחשבה: כמה פעמים אתם שולחים קלט לתהליך בתרגיל הזה, ולמה? (שימו לב שיש שתי קריאות read: זו של הoverflow המקורית, וזו שהמסגרת הראשונה מפעילה.)

אתגר בונוס - בלי pop rax בכלל

נניח שהורדתם את גאדג'ט pop rax ; ret מהמשוואה - אין כזה בבינארי. איך עדיין תגיעו ל-rax = 15?

רמז: read מחזירה ב-rax את מספר הבתים שנקראו. אם תגרמו ל-read לקרוא בדיוק 15 בתים, ומיד אחרי כן הזרימה תגיע להוראת syscall - קיבלתם sigreturn בלי אף pop rax. תכננו chain שמנצלת את זה.

בהצלחה. אם נתקעתם, הפתרון המלא עם כל הchains מחכה בקובץ הפתרון.