לדלג לתוכן

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):

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

הערה על הסביבה: אין צורך לכבות ASLR, כי כל הכתובות שנשתמש בהן (stage, הגאדג'טים, system, /bin/sh) מגיעות מהבינארי עצמו והן קבועות ב-No PIE.

תרגיל 1 - להוכיח שאין מקום

לפני שמפצחים, מבינים את הבעיה.

  1. הריצו checksec --file=./pivot וודאו שאתם רואים No canary, NX enabled, No PIE.
  2. מצאו את ה-offset מתחילת buf עד return address של הקריאה השנייה. רמז: שלחו קלט דמה לקריאה הראשונה, ו-cyclic(80) לשנייה, ובדקו ב-gdb איזה ערך נכנס ל-rsp ב-ret.
  3. חשבו: הקריאה השנייה קוראת בדיוק 80 בתים. כמה בתים נשארים לכם אחרי return address כדי לבנות chain? כתבו את התשובה לעצמכם - זו כל המוטיבציה לשיעור.

רמז: הקריאה קוראת 80 בתים בדיוק. אם ה-offset עד return address הוא 72, אז נשארים לכם 0 בתים לchain. זו בדיוק ההגדרה של "overflow קצרה מדי".

תרגיל 2 - איתור הרכיבים

אתם צריכים חמישה דברים. אתרו כל אחד וכתבו את הכתובת:

  1. גאדג'ט leave ; ret. חפשו: ROPgadget --binary ./pivot | grep ': leave ; ret'.
  2. גאדג'ט pop rdi ; ret.
  3. גאדג'ט ret בודד (לalignment).
  4. כתובת המחרוזת "/bin/sh" בבינארי. רמז: ב-pwntools זה next(elf.search(b'/bin/sh\x00')).
  5. כתובת 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 שמבצע את שני השלבים.

  1. שלב הבמה: הרכיבו ret2libc chain פנימית - pop rdi, כתובת /bin/sh, גאדג'ט ret לalignment, וכתובת system. הקדימו לה 8 בתים של padding (חשבו למה - הרמז למטה).
  2. שלחו את הchain בתגובה ל-stage:.
  3. שלב הפיבוט: בנו את הoverflow הקצרה - padding עד saved rbp, ואז p64(stage) ב-saved rbp, ואז p64(leave_ret) בreturn address.
  4. שלחו בתגובה ל-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 - לאמת את הפיבוט בעיניים

אל תסתפקו ב"זה עבד". תראו את הפיבוט קורה.

  1. חברו gdb עם gdb.attach(p, 'break *%d' % leave_ret) (או שימו breakpoint על כתובת ה-leave ; ret).
  2. עשו single-step דרך ה-leave וה-ret, ובכל צעד הדפיסו p/x $rsp ו-p/x $rbp.
  3. ודאו שאחרי ה-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 בבינארי:

ROPgadget --binary ./pivot | grep 'pop rsp ; ret'

אם הוא קיים, שכתבו את הoverflow כך: return address נדרסת ל-pop rsp ; ret, ומיד אחריה p64(stage). שימו לב להבדל החשוב מ-leave ; ret.

  1. מכיוון שאין כאן pop rbp כפול, האם עדיין צריך את 8 בתי הpadding בתחילת הבמה? תקנו את chain הבמה בהתאם.
  2. הריצו וודאו shell.

רמז: עם pop rsp ; ret, ה-rsp מצביע ישירות ל-stage וה-ret שולף את [stage]. אין בלימת 8 בתים, אז הchain מתחילה כבר ב-stage+0 - הורידו את הpadding המקדים.

רמז שני: אם הגאדג'ט לא קיים בבינארי שלכם (הוא נדיר), אל תתייאשו - הבינו את העיקרון ותשוו אותו ל-leave ; ret שכן עבד. זה מספיק להבנה.

תרגיל 6 (בונוס) - migrate של pwntools ושרת מרוחק

  1. במקום לבנות את הפיבוט ביד, נסו את rop = ROP(elf) עם rop.migrate(stage) וראו איזו chain הוא מייצר. השוו אותה למה שבניתם ידנית.
  2. הפכו את ה-exploit לגמיש עם args.REMOTE, הריצו את הבינארי מאחורי socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./pivot, ותקפו את remote('127.0.0.1', 1337).
  3. חשבו: מה היה משתנה אילו הבינארי היה מקומפל עם PIE? איזו leak הייתם צריכים קודם?

רמז: עם PIE, כתובת stage וכתובות הגאדג'טים כבר לא קבועות. הייתם צריכים קודם לדלוף את בסיס הקוד (למשל דרך format string או stack leak), ואז לחשב את כל הכתובות יחסית אליו. עצם טכניקת הפיבוט לא משתנה - רק מקור הכתובות.