4.5 one gadget וret2system תרגול
תרגול - one_gadget וret2system¶
בתרגול הזה תפתחו shell בשתי דרכים על אותו בינארי: פעם עם one_gadget (הקיצור) ופעם עם ret2system מלא (הדרך האמינה). המטרה היא שתרגישו על העור מתי כל שיטה עובדת, ובעיקר שתחוו את הרגע שבו one_gadget נכשל בגלל אילוץ שלא מתקיים, ותלמדו לפתור את זה - גם על ידי סיפוק האילוץ, וגם על ידי נפילה חזרה לchain המלאה. אל תעתיקו מהפתרון; תריצו, תיפלו, תתקנו.
סביבה והנחות¶
כל התרגילים רצים מקומית על לינוקס. ההנחות שלנו: ASLR פעיל (הדרך הרגילה במערכת), אבל הבינארי מדליף לכם כתובת libc בהתחלה, אז אתם יכולים לחשב את בסיס הספרייה - בדיוק כמו שעשיתם ב-4.4. שאר ההגנות: NX פעיל, בלי canary, הבינארי no-pie.
הכלים שתצטרכו: python3 עם pwntools, one_gadget (מתקינים עם gem install one_gadget), ROPgadget, ו-gdb עם pwndbg. חשוב: תעבדו מול עותק מקומי של ה-libc.so.6 שהבינארי טוען. תמצאו אותו עם ldd ./og_demo.
הבינארי לדוגמה¶
שמרו בשם og_demo.c וקמפלו לפי ההוראות. הבינארי מדליף את הכתובת של puts בזמן ריצה (זה מדמה את הדלף שהשגתם ב-4.4), ואז קורא קלט לתוך buffer בoverflow:
// og_demo.c
#include <stdio.h>
#include <unistd.h>
int main() {
char buf[64];
setvbuf(stdout, NULL, _IONBF, 0);
printf("puts @ %p\n", &puts); // leak a libc address
puts("send payload:");
read(0, buf, 256); // overflow: 256 bytes into a 64-byte buffer
return 0;
}
קמפול (מכבים canary ו-PIE כדי לבודד את הטכניקה; NX נשאר פעיל כברירת מחדל):
בדקו את ההגנות לפני שמתחילים:
ודאו: NX enabled, No canary, No PIE.
תרגיל 1 - מיפוי: מוצאים את ה-one_gadget-ים ואת ה-offset¶
לפני שתוקפים, ממפים.
המטרה:
- מצאו את ה-
libc.so.6שהבינארי טוען (ldd ./og_demo), והריצו עליוone_gadget. רשמו לעצמכם את כל המועמדים ואת האילוץ של כל אחד. - זהו איזה אילוץ הוא על אוגר ואיזה הוא על המחסנית (
[rsp+X] == NULL). איזה מהם נראה לכם קל יותר לספק, ולמה? - מצאו את ה-offset מהתחלת הbuffer עד הreturn address בעזרת
cyclic. (רמז: buffer 64 + saved rbp 8).
רמז: כדי לדעת איזו גרסת libc יש לכם, strings ./libc.so.6 | grep "GNU C Library". חשוב, כי הכתובות והאילוצים משתנים בין גרסאות.
רמז: את הדלף אתם קוראים מהשורה הראשונה של הפלט. שימו לב שהוא בהקסדצימלי, אז המרה עם int(x, 16).
תרגיל 2 - חישוב בסיס libc מהדלף¶
עכשיו תתרגמו את הדלף לכתובת בסיס.
המטרה:
- כתבו את תחילת ה-exploit: פותחים תהליך, קוראים את הדלף של
puts, ומחשביםlibc.address = leak - libc.symbols['puts']. - הדפיסו את
libc.address. הריצו כמה פעמים - הבסיס משתנה בכל הרצה (ASLR), אבל שלושת ה-nibble-ים התחתונים תמיד000(alignment עמוד). זו בדיקת שפיות טובה.
רמז: libc = ELF('./libc.so.6') ואז libc.address = ... מיישר את כל הסמלים אוטומטית. אחרי זה libc.symbols['system'] כבר יחזיר את הכתובת האמיתית בריצה.
תרגיל 3 - פותחים shell עם one_gadget¶
הדרך הקצרה.
המטרה:
- בחרו one_gadget שהאילוץ שלו על המחסנית (
[rsp+X] == NULL), כי אותו קל לספק. - בנו payload:
paddingעד ה-offset, ואז כתובת ה-one_gadget המחושבת (libc.address + off), ואז מספיק בתי אפס כדי לספק את האילוץ. - שלחו, וקבלו shell. הריצו
idלהוכחה.
רמז: אחרי ה-ret שקופץ ל-one_gadget, rsp מצביע על מה שכתבתם מיד אחרי כתובת הגאדג'ט. אם האילוץ הוא [rsp+0x40] == NULL, אתם צריכים שהמילה 8 סלוטים קדימה תהיה אפס. הכי פשוט: מלאו את הכל באפסים. נסו flat({offset: [og, 0, 0, 0, 0, 0, 0, 0, 0, 0]}).
רמז: אם זה קורס, פתחו ב-gdb, שימו breakpoint על כתובת הגאדג'ט (b *הכתובת_המחושבת), והריצו x/gx $rsp+0x40. אם זה לא אפס - האילוץ לא מסופק, תוסיפו/תזיזו אפסים.
תרגיל 4 - הכישלון: one_gadget שהאילוץ שלו לא מתקיים¶
עכשיו בכוונה תיכשלו, כדי להבין את הגבול.
המטרה:
- קחו one_gadget שהאילוץ שלו על אוגר (למשל
rcx == NULLאוr12 == NULL), ותנסו לקפוץ אליו בלי להכין את האוגר. - הריצו תחת gdb. איפה זה קורס? הסתכלו על ערך האוגר מהאילוץ ברגע הקפיצה - האם הוא אפס?
- תעדו: מה בדיוק נכשל? האם
execveבכלל נקרא, או שקרסנו לפני? (רמז: הסתכלו אם הגעתם לכתובת ה-execveבכלל.)
רמז: אילוץ על אוגר קשה לספק כי הערך שלו תלוי במה שקרה בתוכנית עד עכשיו, לא בכם. לפעמים אחרי puts דווקא יש שם אפס במקרה - ואז זה יעבוד "בקסם". אל תסמכו על זה.
רמז: אם יש בבינארי או ב-libc גאדג'ט pop rcx ; ret (או pop r12 ; ret), אתם יכולים לאפס את האוגר לפני הקפיצה: [pop_reg, 0, og]. נסו את זה עם ROPgadget --binary ./libc.so.6 | grep 'pop r12'.
תרגיל 5 - הנפילה חזרה: ret2system מלא¶
כשה-one_gadget לא משתף פעולה, יש דרך שתמיד עובדת.
המטרה:
- בנו ret2system מלא בתוך libc:
pop rdi ; ret-> כתובת"/bin/sh"מתוך libc -> גאדג'טretלalignment ->system. - את המחרוזת
"/bin/sh"מצאו עםnext(libc.search(b'/bin/sh\x00'))- היא קיימת בתוך libc, אתם לא צריכים אותה בבינארי. - אל תשכחו את גאדג'ט ה-
retהבודד לalignment מחסנית ל-16 בתים (אחרתsystemתקרוס ב-movaps). - קבלו shell. השוו: כמה סלוטים תפס ה-one_gadget לעומת ה-ret2system?
רמז: תנו ל-pwntools לעשות את העבודה: rop = ROP(libc), rop.raw(rop.find_gadget(['ret'])[0]), rop.call(system, [binsh]). זה ימצא את pop rdi בתוך libc לבד.
רמז: אם ה-shell לא נפתח אבל אין קריסה ברורה, זו כמעט תמיד בעיית alignment. תוודאו שגאדג'ט ה-ret הבודד נמצא לפני הקריאה ל-system. אם עדיין קורס, נסו להוריד אותו - לפעמים הalignment הפוך.
תרגיל 6 (אתגר) - "תנסה את כולם" ואמינות¶
הפכו את ה-exploit לחסין.
המטרה:
- כתבו לולאה שעוברת על כל ה-one_gadget-ים מהתרגיל הראשון ומנסה כל אחד, עד שאחד פותח shell. אחרי כל ניסיון תבדקו אם נפתח shell (למשל שלחו
echo PWNEDותבדקו אם חזר). - הוסיפו לסוף fallback ל-ret2system המלא - אם אף one_gadget לא עבד, תעברו לchain המלאה. עכשיו יש לכם exploit שמנסה קצר קודם ונופל לבטוח.
- חשבו: על שרת אמיתי אתם לא רוצים לפתוח 4 תהליכים ולהתפלל. איך הייתם מזהים מראש, ב-gdb מקומי, איזה one_gadget יעבוד, כדי לשלוח את הנכון בניסיון הראשון?
רמז: לזיהוי מראש - שימו breakpoint על הreturn address של הפונקציה הvulnerable (הרגע שלפני הקפיצה), ובדקו את כל האוגרים והמחסנית מול האילוצים של כל מועמד. המועמד שהאילוץ שלו כבר מתקיים שם הוא הבחירה הבטוחה.
רמז: מבנה נקי ל-fallback הוא פונקציה exploit(strategy) שמקבלת 'one_gadget' או 'ret2system', ולולאה חיצונית שמנסה אסטרטגיות לפי סדר. ככה הקוד קריא וקל להוסיף שיטות.