לדלג לתוכן

0.3 חזרה ממוקדת על הבסיס תרגול

תרגול - חזרה ממוקדת על הבסיס

בתרגול הזה לא נתקוף כלום עדיין. המטרה היא להעביר את כל מה שדיברנו עליו בהרצאה מהראש לאצבעות: לראות במו עינינו את מפת הזיכרון של תהליך חי, לאתר את ה-GOT וה-PLT בבינארי אמיתי, ולצעוד ב-gdb על call ו-ret בזמן שאנחנו צופים באוגרים זזים. עשו את הכל בעצמכם, אל תסתפקו בקריאה.

כל התרגילים רצים על בינארי אחד קטן שנקמפל בעצמנו. שמרו את הקוד הבא בשם basics.c:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

char global_msg[] = "hello from data";     // goes to .data
char global_buf[64];                        // goes to .bss

void greet(const char *name) {
    char local[32];
    strcpy(local, name);
    printf("Hi, %s\n", local);
}

int main(int argc, char **argv) {
    printf("pid is %d, press ENTER\n", getpid());
    getchar();                              // stops here so we have time to look at the map
    greet(argc > 1 ? argv[1] : "world");
    puts("done");
    return 0;
}

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

gcc -g -o basics_pie   basics.c
gcc -g -no-pie -fno-pie -o basics_nopie basics.c

התקינו אם צריך: gdb (רצוי עם pwndbg), binutils (בשביל readelf, objdump, nm) ו-checksec.


תרגיל 1 - מפת הזיכרון של תהליך חי

הריצו את basics_pie, ובזמן שהוא תקוע על getchar, הסתכלו על מפת הזיכרון שלו.

המשימות:

  1. הריצו את הבינארי, קחו את ה-pid שהוא מדפיס, והדפיסו את /proc/<pid>/maps.
  2. זהו במפה את חמשת האזורים האלה וכתבו לעצמכם את טווח הכתובות וההרשאות של כל אחד: הקוד (.text), אזור ה-rw של הבינארי, ה-[heap], ה-[stack], וה-libc.
  3. איזה אזור הוא r-x? איזה rw-? האם יש אזור שהוא גם וגם? מה זה אומר לגבי הזרקת shellcode למחסנית?
  4. הריצו את הבינארי פעמיים (שתי הרצות נפרדות) והשוו את כתובת הבסיס של הקוד. האם היא זהה? ולמה?

רמז: את ה-pid הבינארי מדפיס בעצמו. חלופה נוחה: cat /proc/$(pgrep -n basics_pie)/maps.

רמז: כדי לראות רק את השורות של הבינארי שלנו, סננו: grep basics_pie /proc/<pid>/maps.

רמז לשאלה 4: בדקו את מצב ה-ASLR עם cat /proc/sys/kernel/randomize_va_space. ערך 2 פירושו אקראיות מלאה.


תרגיל 2 - PIE לעומת לא-PIE

עכשיו חזרו על תרגיל 1 עם basics_nopie, והשוו.

המשימות:

  1. הריצו checksec --file=./basics_pie ו-checksec --file=./basics_nopie. מה ההבדל בעמודת ה-PIE?
  2. הריצו readelf -h על שני הקבצים. מה ה-Type של כל אחד (DYN או EXEC)?
  3. הסתכלו במפת הזיכרון של basics_nopie. באיזו כתובת בסיס יושב הקוד? האם היא משתנה בין הרצות?
  4. הסבירו במילים שלכם: למה כדי לתקוף בינארי PIE אנחנו חייבים קודם לדלוף כתובת, ובבינארי לא-PIE לא?

רמז: בינארי לא-PIE ב-64 ביט נטען כמעט תמיד לבסיס 0x400000. שימו לב לכתובות שמתחילות ב-0x40....


תרגיל 3 - איתור GOT ו-PLT

נשארים על הבינארי (אפשר על שתי הגרסאות, מעניין להשוות). בלי להריץ אותו, נחקור את הטבלאות בעזרת הכלים הסטטיים.

המשימות:

  1. הדפיסו את ה-relocations עם readelf -r ./basics_nopie. מצאו את הכניסות מסוג JUMP_SLOT. איזו כניסת GOT שייכת ל-puts? ואיזו ל-strcpy? רשמו את ה-Offset (הכתובת) של כל אחת.
  2. עשו דיסאסמבלי ל-PLT: objdump -d -j .plt ./basics_nopie. מצאו את הקטע של puts@plt. לאן ה-jmp הראשון שלו קופץ?
  3. ודאו שהכתובת שאליה ה-PLT קופץ היא בדיוק ה-Offset של puts מטבלת ה-relocations. הן צריכות להתאים. הסבירו למה.
  4. עשו דיסאסמבלי ל-main (objdump -d ./basics_nopie) ומצאו את הקריאה ל-puts. שימו לב שהיא קוראת ל-puts@plt, לא ל-puts עצמו. למה?

רמז: הכתובת שמופיעה בפעולת ה-jmp בתוך ה-PLT היא הכתובת שבתוך ה-GOT. השוו אותה ל-Offset מהתרגיל הקודם.

רמז: אפשר להצליב שמות עם objdump -R ./basics_nopie (מדפיס relocations דינמיים לצד שמות).


תרגיל 4 - לצעוד על call ו-ret ב-gdb

זה התרגיל החשוב ביותר. נצעד על call ו-ret הוראה-אחרי-הוראה, ונצפה במחסנית וב-rip בזמן אמת.

המשימות:

  1. פתחו את basics_nopie ב-gdb, שימו עצירה על greet (break greet), והריצו עם ארגומנט: run AAAA.
  2. ברגע שאתם בתוך greet, הסתכלו על ראש המחסנית (x/gx $rsp). הערך שם אמור להיות הreturn address - הכתובת בתוך main שאליה נחזור. הצליבו אותה עם הדיסאסמבלי של main: מה ההוראה שנמצאת מיד אחרי ה-call greet?
  3. רשמו את הערכים הנוכחיים של $rsp ו-$rip.
  4. שימו עצירה על הוראת ה-ret של greet. הדרך הכי פשוטה: disassemble greet, מצאו את כתובת ה-ret, ו-break *<כתובת>. המשיכו (continue) עד שתגיעו לשם.
  5. עכשיו הרגע הקריטי. לפני ה-ret: מה הערך ב-$rsp? מה הערך שנמצא בכתובת שאליה $rsp מצביע (x/gx $rsp)? צעדו הוראה אחת (stepi / si). עכשיו: איך השתנה $rsp? מה הערך החדש של $rip? האם הוא שווה בדיוק לערך ששלפנו מראש המחסנית?
  6. נסחו במילים שלכם, בשורה אחת, מה בדיוק ret עשה לשני האוגרים האלה.

רמז: x/gx $rsp מדפיס 8 בתים (giant) בהקסה מהכתובת שב-rsp. זה בדיוק מה שה-ret עומד לשלוף.

רמז: אם אתם על pwndbg, כל stepi מציג לכם אוטומטית את מצב האוגרים והמחסנית. עקבו אחרי השורות RSP ו-RIP בפלט.

רמז מסכם: אחרי ה-ret, אתם אמורים לראות ש-$rip קיבל את הערך שהיה בראש המחסנית, ו-$rsp גדל ב-8. אם זה מה שראיתם - הבנתם את הבסיס של כל ROP.


תרגיל 5 (אתגר) - מה קורה כשדורסים את הreturn address

זה הצצה קדימה, לטעימה. אנחנו לא בונים exploit מלא, רק צופים בשליטה ב-rip.

  1. הריצו את basics_nopie עם ארגומנט ארוך במיוחד: run $(python3 -c "print('A'*200)") בתוך gdb.
  2. התוכנית תקרוס. הסתכלו על ערך ה-$rip (או ה-$pc) בזמן הקריסה. מה אתם רואים? מאיפה הגיע הערך הזה?
  3. בדקו: האם strcpy בתוך greet היא זו שגרמה לoverflow? כמה בתים בערך היה צריך כדי להגיע להreturn address, בהינתן ש-local הוא בגודל 32?

רמז: הערך 0x4141... הוא הקוד ההקסה של האות A. אם אתם רואים אותו ב-$rip, זה אומר שדרסתם את הreturn address עם ה-A-ים - כלומר, יש לכם שליטה על היעד של ה-ret. בפרק 2 נהפוך את השליטה הזו ל-exploit אמיתי.

הערה חשובה: אם קימפלתם עם canary (ברירת מחדל ב-gcc מודרני), התוכנית עלולה לעצור עם stack smashing detected לפני שתגיעו לדריסת הreturn address. לצורך הטעימה הזו אפשר לקמפל בלי canary: gcc -g -no-pie -fno-pie -fno-stack-protector -o basics_nopie basics.c. את הcanary נלמד לעקוף בפרק הרלוונטי.