לדלג לתוכן

3.2 הזרקה והרצה ללא NX תרגול

תרגול - הזרקה והרצה ללא NX

בתרגול הזה תבנו בעצמכם בינארי vulnerable עם מחסנית ניתנת להרצה, ותפרצו אותו בשתי דרכים שונות: פעם בשיטת הכתובת הישירה (עם מזחלת NOP), ופעם בשיטת הטרמפולינה jmp esp. לבסוף, כבונוס, תשתמשו במשתנה סביבה. המטרה היא שתרגישו על הידיים את ההבדל בין שיטה ששבירה מול ASLR לבין שיטה שעמידה בפניו. עבדו לפי הסדר - כל תרגיל בונה על הקודם.

הכנה - הבינארי והסביבה

צרו קובץ מקור vuln.c:

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

void vuln() {
    char buf[64];
    puts("send your payload:");
    read(0, buf, 400);   // overflow: 400 bytes into a 64-byte buffer
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    vuln();
    return 0;
}

קמפלו אותו ל-32 וגם ל-64 ביט, עם מחסנית ניתנת להרצה, בלי canary ובלי PIE (אם חסרות ספריות 32 ביט: sudo apt install gcc-multilib):

gcc -m32 -z execstack -fno-stack-protector -no-pie -o vuln32 vuln.c
gcc      -z execstack -fno-stack-protector -no-pie -o vuln64 vuln.c

כבו ASLR (לרוב התרגילים אנחנו מסתמכים על כתובת יציבה):

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

תרגיל 1 - אימות התנאים

לפני שתוקפים, ודאו שהתקיפה בכלל אפשרית.

  1. הריצו checksec --file=./vuln32 וּ-checksec --file=./vuln64. ודאו שאתם רואים NX disabled ו-Has RWX segments, וגם No canary ו-No PIE.
  2. בדקו את דגלי מקטע המחסנית: readelf -l vuln64 | grep GNU_STACK. ודאו שאתם רואים RWE ולא RW.
  3. ודאו ש-ASLR כבוי: cat /proc/sys/kernel/randomize_va_space צריך להחזיר 0.

רמז: אם checksec מראה NX enabled, שכחתם את -z execstack. קמפלו מחדש. בלי התנאי הזה כל שאר התרגול לא יעבוד.

תרגיל 2 - מציאת offset וכתובת ישירה (64 ביט)

עכשיו נפרוץ את vuln64 בשיטת הכתובת הישירה.

  1. מצאו את ה-offset עד הreturn address עם cyclic (בשיטה מ-2.2): שלחו cyclic 200, ראו איזה ערך דרס את rip, והריצו cyclic_find(...).
  2. ב-gdb, שימו breakpoint אחרי ה-read, שלחו קלט מזהה (למשל הרבה A), וחפשו איפה הוא נחת: search -s "AAAAAAAA". רשמו את כתובת תחילת הbuffer.
  3. כתבו exploit ב-pwntools: padding עד ה-offset, ואז return address שמצביעה לתחילת הbuffer, ואז ה-shellcode. השתמשו ב-shellcode של 64 ביט (היד-כתוב מ-3.1, או asm(shellcraft.amd64.linux.sh())).
  4. הריצו וּודאו שנפתח shell. נסו id.

רמז: כדי שהקפיצה תנחת על ה-shellcode, שימו את ה-shellcode בתחילת הbuffer, וכוונו את הreturn address בדיוק לשם. אבל כתובת הbuffer שראיתם ב-gdb כנראה שונה מההרצה האמיתית - זה בדיוק מה שתרגיל 3 פותר.

תרגיל 3 - מזחלת NOP והתמודדות עם אי-דיוק

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

  1. שנו את המבנה: שימו את ה-shellcode אחרי הreturn address, עם מזחלת גדולה של \x90 לפניו (למשל 512 בתים).
  2. כוונו את הreturn address אל אמצע המזחלת, לא אל ההתחלה המדויקת.
  3. חברו gdb.attach(p) לתהליך, חפשו את המזחלת (search -s "\x90\x90\x90\x90"), ובחרו כתובת עמוק בתוכה.
  4. הריצו את ה-exploit מחוץ ל-gdb (סתם python3 exploit.py) וּודאו שהוא עדיין עובד. אם לא, הגדילו את המזחלת.

רמז: המזחלת סופגת את ההפרש בין הסביבה של gdb לזו של הרצה רגילה. אם המזחלת גדולה מספיק, אותה כתובת תעבוד בשני המקרים. זו הסיבה שהמזחלת הומצאה.

רמז: ככל שהbuffer קטן, עדיף לשים את המזחלת וה-shellcode אחרי הreturn address - שם יש לכם את כל מרחב ה-read (400 בתים) ולא רק 64.

תרגיל 4 - טרמפולינה jmp esp (32 ביט)

עכשיו הטכניקה החזקה: פריצה בלי לדעת בכלל את כתובת המחסנית, על vuln32.

  1. מצאו גאדג'ט jmp esp בבינארי: ROPgadget --binary vuln32 | grep 'jmp esp', או חפשו את הבתים ff e4 עם objdump -d vuln32 | grep 'ff e4'.
  2. בנו payload: padding עד ה-offset, ואז כתובת הגאדג'ט jmp esp בהreturn address, ואז ה-shellcode של 32 ביט מיד אחריה.
  3. הריצו וּודאו שנפתח shell.
  4. עכשיו הדבר המעניין: הדליקו ASLR מחדש (echo 2 | sudo tee /proc/sys/kernel/randomize_va_space) והריצו שוב. ההתקפה עדיין עובדת! הסבירו לעצמכם למה.

רמז: ברגע ה-ret, esp מצביע בדיוק על הסלוט שאחרי הreturn address - שם שמתם את ה-shellcode. הגאדג'ט jmp esp קופץ לשם. לא נגעתם בכתובת מחסנית בכלל.

רמז: הסיבה שזה עובד גם עם ASLR: הבינארי לא PIE, אז כתובת הגאדג'ט jmp esp קבועה. ה-ASLR מערבב את המחסנית, אבל לא אכפת לנו כי esp ממילא מצביע לאן שצריך.

תרגיל 5 (בונוס) - shellcode במשתנה סביבה

מה עושים כשהbuffer קטן מכדי להכיל shellcode + מזחלת? מכניסים אותם למשתנה סביבה.

  1. הכניסו למשתנה סביבה SC מזחלת גדולה של \x90 ואחריה ה-shellcode.
  2. חשבו את כתובת המשתנה על המחסנית. כתבו את תוכנית העזר getenvaddr (מההרצאה), או אמתו את הכתובת ישירות ב-gdb עם search -s SC.
  3. כתבו exploit שדורס את הreturn address בכתובת של SC (מכוונת לתוך מזחלת ה-NOP שבתוכו). הריצו וּודאו shell.

רמז: אם משתמשים ב-pwntools, אפשר להעביר את הסביבה ישירות: process('./vuln32', env={'SC': b'\x90'*400 + sc}), ואז לחפש את הכתובת של SC ב-gdb.attach.

רמז: כתובת משתנה הסביבה תלויה באורך שם התוכנית (argv[0]) ובגודל שאר הסביבה. אם הכתובת קופצת, מזחלת גדולה יותר בתוך SC תבלע את הסטייה.

תרגיל 6 (מחשבה) - מתי כל שיטה נכשלת

בלי לכתוב קוד, ענו:

  1. אילו משלוש השיטות שורדות הדלקת ASLR, ואילו מתות? למה?
  2. מה קורה לכל השיטות אם המתכנת יקמפל מחדש בלי -z execstack (כלומר עם NX פעיל)?
  3. התוכנית קוראת עם scanf("%s", buf) במקום read. איזו בעיה חדשה נוצרת ל-shellcode ולכתובות, ומדוע read היה נוח יותר?

רמז: חשבו מה כל שיטה מניחה - כתובת מחסנית יציבה? כתובת קוד יציבה? מחסנית ניתנת להרצה? קלט שמעביר בתים אפסיים?