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 (ברירת המחדל בהפצות מודרניות) ואחת בלי, כדי שנראה את ההבדל בכתובות:
התקינו אם צריך: gdb (רצוי עם pwndbg), binutils (בשביל readelf, objdump, nm) ו-checksec.
תרגיל 1 - מפת הזיכרון של תהליך חי¶
הריצו את basics_pie, ובזמן שהוא תקוע על getchar, הסתכלו על מפת הזיכרון שלו.
המשימות:
- הריצו את הבינארי, קחו את ה-pid שהוא מדפיס, והדפיסו את
/proc/<pid>/maps. - זהו במפה את חמשת האזורים האלה וכתבו לעצמכם את טווח הכתובות וההרשאות של כל אחד: הקוד (
.text), אזור ה-rwשל הבינארי, ה-[heap], ה-[stack], וה-libc. - איזה אזור הוא
r-x? איזהrw-? האם יש אזור שהוא גם וגם? מה זה אומר לגבי הזרקת shellcode למחסנית? - הריצו את הבינארי פעמיים (שתי הרצות נפרדות) והשוו את כתובת הבסיס של הקוד. האם היא זהה? ולמה?
רמז: את ה-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, והשוו.
המשימות:
- הריצו
checksec --file=./basics_pieו-checksec --file=./basics_nopie. מה ההבדל בעמודת ה-PIE? - הריצו
readelf -hעל שני הקבצים. מה ה-Typeשל כל אחד (DYNאוEXEC)? - הסתכלו במפת הזיכרון של
basics_nopie. באיזו כתובת בסיס יושב הקוד? האם היא משתנה בין הרצות? - הסבירו במילים שלכם: למה כדי לתקוף בינארי PIE אנחנו חייבים קודם לדלוף כתובת, ובבינארי לא-PIE לא?
רמז: בינארי לא-PIE ב-64 ביט נטען כמעט תמיד לבסיס 0x400000. שימו לב לכתובות שמתחילות ב-0x40....
תרגיל 3 - איתור GOT ו-PLT¶
נשארים על הבינארי (אפשר על שתי הגרסאות, מעניין להשוות). בלי להריץ אותו, נחקור את הטבלאות בעזרת הכלים הסטטיים.
המשימות:
- הדפיסו את ה-relocations עם
readelf -r ./basics_nopie. מצאו את הכניסות מסוגJUMP_SLOT. איזו כניסת GOT שייכת ל-puts? ואיזו ל-strcpy? רשמו את ה-Offset(הכתובת) של כל אחת. - עשו דיסאסמבלי ל-PLT:
objdump -d -j .plt ./basics_nopie. מצאו את הקטע שלputs@plt. לאן ה-jmpהראשון שלו קופץ? - ודאו שהכתובת שאליה ה-PLT קופץ היא בדיוק ה-
Offsetשלputsמטבלת ה-relocations. הן צריכות להתאים. הסבירו למה. - עשו דיסאסמבלי ל-
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 בזמן אמת.
המשימות:
- פתחו את
basics_nopieב-gdb, שימו עצירה עלgreet(break greet), והריצו עם ארגומנט:run AAAA. - ברגע שאתם בתוך
greet, הסתכלו על ראש המחסנית (x/gx $rsp). הערך שם אמור להיות הreturn address - הכתובת בתוךmainשאליה נחזור. הצליבו אותה עם הדיסאסמבלי שלmain: מה ההוראה שנמצאת מיד אחרי ה-call greet? - רשמו את הערכים הנוכחיים של
$rspו-$rip. - שימו עצירה על הוראת ה-
retשלgreet. הדרך הכי פשוטה:disassemble greet, מצאו את כתובת ה-ret, ו-break *<כתובת>. המשיכו (continue) עד שתגיעו לשם. - עכשיו הרגע הקריטי. לפני ה-
ret: מה הערך ב-$rsp? מה הערך שנמצא בכתובת שאליה$rspמצביע (x/gx $rsp)? צעדו הוראה אחת (stepi/si). עכשיו: איך השתנה$rsp? מה הערך החדש של$rip? האם הוא שווה בדיוק לערך ששלפנו מראש המחסנית? - נסחו במילים שלכם, בשורה אחת, מה בדיוק
retעשה לשני האוגרים האלה.
רמז: x/gx $rsp מדפיס 8 בתים (giant) בהקסה מהכתובת שב-rsp. זה בדיוק מה שה-ret עומד לשלוף.
רמז: אם אתם על pwndbg, כל stepi מציג לכם אוטומטית את מצב האוגרים והמחסנית. עקבו אחרי השורות RSP ו-RIP בפלט.
רמז מסכם: אחרי ה-ret, אתם אמורים לראות ש-$rip קיבל את הערך שהיה בראש המחסנית, ו-$rsp גדל ב-8. אם זה מה שראיתם - הבנתם את הבסיס של כל ROP.
תרגיל 5 (אתגר) - מה קורה כשדורסים את הreturn address¶
זה הצצה קדימה, לטעימה. אנחנו לא בונים exploit מלא, רק צופים בשליטה ב-rip.
- הריצו את
basics_nopieעם ארגומנט ארוך במיוחד:run $(python3 -c "print('A'*200)")בתוך gdb. - התוכנית תקרוס. הסתכלו על ערך ה-
$rip(או ה-$pc) בזמן הקריסה. מה אתם רואים? מאיפה הגיע הערך הזה? - בדקו: האם
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 נלמד לעקוף בפרק הרלוונטי.