6.1 stack pivoting תרגול
תרגול - הזזת מחסנית - stack pivoting¶
בתרגול הזה תתמודדו עם המצב הכי מתסכל בoverflows מחסנית: יש לכם שליטה על return address, אבל אין לכם מקום לchain. תלמדו לבנות בעצמכם בינארי כזה, לזהות למה הchain לא נכנסת, לכתוב אותה מראש אל אזור bss, ולבצע פיבוט אליה עם leave ; ret. אחר כך תחזרו על אותו רעיון עם גאדג'טים אחרים. עבדו לפי הסדר - כל תרגיל בונה על הקודם.
הכנה - הבינאר הvulnerable¶
צרו קובץ pivot.c. שימו לב לשתי הקריאות: הראשונה גדולה ואל bss, השנייה קצרה וvulnerable.
// pivot.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char stage[0x400]; // large bss area - we'll prepare the chain here
void unused() {
system("/bin/echo dummy"); // pulls system and the "/bin/sh" string into the binary
}
void vuln() {
char buf[64];
write(1, "stage:\n", 7);
read(0, stage, 0x400); // large read into bss
write(1, "overflow:\n", 10);
read(0, buf, 80); // short overflow - right up to the end of the return address
}
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
vuln();
return 0;
}
קמפלו כך (משאירים NX פעיל, בלי canary, בלי PIE):
הערה על הסביבה: אין צורך לכבות ASLR, כי כל הכתובות שנשתמש בהן (stage, הגאדג'טים, system, /bin/sh) מגיעות מהבינארי עצמו והן קבועות ב-No PIE.
תרגיל 1 - להוכיח שאין מקום¶
לפני שמפצחים, מבינים את הבעיה.
- הריצו
checksec --file=./pivotוודאו שאתם רואיםNo canary,NX enabled,No PIE. - מצאו את ה-offset מתחילת
bufעד return address של הקריאה השנייה. רמז: שלחו קלט דמה לקריאה הראשונה, ו-cyclic(80)לשנייה, ובדקו ב-gdb איזה ערך נכנס ל-rspב-ret. - חשבו: הקריאה השנייה קוראת בדיוק 80 בתים. כמה בתים נשארים לכם אחרי return address כדי לבנות chain? כתבו את התשובה לעצמכם - זו כל המוטיבציה לשיעור.
רמז: הקריאה קוראת 80 בתים בדיוק. אם ה-offset עד return address הוא 72, אז נשארים לכם 0 בתים לchain. זו בדיוק ההגדרה של "overflow קצרה מדי".
תרגיל 2 - איתור הרכיבים¶
אתם צריכים חמישה דברים. אתרו כל אחד וכתבו את הכתובת:
- גאדג'ט
leave ; ret. חפשו:ROPgadget --binary ./pivot | grep ': leave ; ret'. - גאדג'ט
pop rdi ; ret. - גאדג'ט
retבודד (לalignment). - כתובת המחרוזת
"/bin/sh"בבינארי. רמז: ב-pwntools זהnext(elf.search(b'/bin/sh\x00')). - כתובת
systemב-PLT (elf.plt['system']), וכתובת אזור הבמה (elf.symbols['stage']).
רמז: אם grep 'pop rdi ; ret' לא מחזיר כלום, נסו גם pop rdi ; pop rbp ; ret או השתמשו ב-ROP(elf).find_gadget(['pop rdi', 'ret']).
תרגיל 3 - הפיבוט המלא עם leave ; ret¶
זה הליבה. בנו exploit ב-pwntools שמבצע את שני השלבים.
- שלב הבמה: הרכיבו ret2libc chain פנימית -
pop rdi, כתובת/bin/sh, גאדג'טretלalignment, וכתובתsystem. הקדימו לה 8 בתים של padding (חשבו למה - הרמז למטה). - שלחו את הchain בתגובה ל-
stage:. - שלב הפיבוט: בנו את הoverflow הקצרה - padding עד
saved rbp, ואזp64(stage)ב-saved rbp, ואזp64(leave_ret)בreturn address. - שלחו בתגובה ל-
overflow:והריצוp.interactive(). אתם אמורים לקבל shell.
רמז: ה-leave ; ret שקופצים אליו מבצע mov rsp, rbp ; pop rbp ; ret. ה-pop rbp הזה בולע את 8 הבתים הראשונים של הבמה - לכן צריך padding בהתחלה, והchain האמיתית מתחילה ב-stage+8.
רמז שני: אם ה-shell לא נפתח ואתם קורסים על movaps בתוך system, הוסיפו או הסירו את גאדג'ט ה-ret הבודד לפני system כדי לתקן את הalignment.
תרגיל 4 - לאמת את הפיבוט בעיניים¶
אל תסתפקו ב"זה עבד". תראו את הפיבוט קורה.
- חברו gdb עם
gdb.attach(p, 'break *%d' % leave_ret)(או שימו breakpoint על כתובת ה-leave ; ret). - עשו single-step דרך ה-
leaveוה-ret, ובכל צעד הדפיסוp/x $rspו-p/x $rbp. - ודאו שאחרי ה-
leaveהשני,rspשווה בדיוק ל-stage, ואחרי ה-retאתם קופצים אלpop rdiשב-stage+8.
רמז: x/8gx $rsp מיד אחרי הפיבוט אמור להראות לכם את הchain שלכם - pop rdi, כתובת /bin/sh, וכן הלאה. אם אתם רואים זבל, ה-saved rbp שלכם לא הצביע לאן שחשבתם.
תרגיל 5 - אותו פיצוח עם pop rsp ; ret¶
עכשיו נחליף שיטת פיבוט. חפשו אם קיים גאדג'ט pop rsp ; ret בבינארי:
אם הוא קיים, שכתבו את הoverflow כך: return address נדרסת ל-pop rsp ; ret, ומיד אחריה p64(stage). שימו לב להבדל החשוב מ-leave ; ret.
- מכיוון שאין כאן
pop rbpכפול, האם עדיין צריך את 8 בתי הpadding בתחילת הבמה? תקנו את chain הבמה בהתאם. - הריצו וודאו shell.
רמז: עם pop rsp ; ret, ה-rsp מצביע ישירות ל-stage וה-ret שולף את [stage]. אין בלימת 8 בתים, אז הchain מתחילה כבר ב-stage+0 - הורידו את הpadding המקדים.
רמז שני: אם הגאדג'ט לא קיים בבינארי שלכם (הוא נדיר), אל תתייאשו - הבינו את העיקרון ותשוו אותו ל-leave ; ret שכן עבד. זה מספיק להבנה.
תרגיל 6 (בונוס) - migrate של pwntools ושרת מרוחק¶
- במקום לבנות את הפיבוט ביד, נסו את
rop = ROP(elf)עםrop.migrate(stage)וראו איזו chain הוא מייצר. השוו אותה למה שבניתם ידנית. - הפכו את ה-exploit לגמיש עם
args.REMOTE, הריצו את הבינארי מאחוריsocat TCP-LISTEN:1337,reuseaddr,fork EXEC:./pivot, ותקפו אתremote('127.0.0.1', 1337). - חשבו: מה היה משתנה אילו הבינארי היה מקומפל עם PIE? איזו leak הייתם צריכים קודם?
רמז: עם PIE, כתובת stage וכתובות הגאדג'טים כבר לא קבועות. הייתם צריכים קודם לדלוף את בסיס הקוד (למשל דרך format string או stack leak), ואז לחשב את כל הכתובות יחסית אליו. עצם טכניקת הפיבוט לא משתנה - רק מקור הכתובות.