7.4 תקיפת unlink תרגול
תרגול - תקיפת unlink¶
בתרגול הזה נתקוף את האתגר unlink מ-pwnable.kr (קטגוריית Rookiss). זה בינארי של 32 ביט עם unlink כתוב ביד, בלי בדיקות בטיחות, ועם שתי leaks שמוגשות לנו על מגש כסף. המטרה: לזייף מטא-דאטה של chunk, להפעיל את הפרימיטיב, ולקפוץ אל shell() כדי לקרוא את הדגל.
התחברות והכנה - setup¶
מתחברים ב-SSH (הסיסמה מופיעה בעמוד האתגר, בדרך כלל guest):
בתיקיית הבית תמצאו את unlink (הבינארי, setuid), את unlink.c (המקור), ואת flag. עבדו בתיקייה זמנית תחת /tmp ומשכו עותק מקומי לחקירה נוחה:
אם אתם רוצים לתרגל לגמרי מקומית, אפשר להדר את המקור בעצמכם ב-32 ביט (שימו לב שנצטרך את אותן הגנות כמו באתגר):
תרגיל 1 - קריאת המקור וזיהוי הפרימיטיב¶
לפני שנוגעים במקלדת, קוראים את unlink.c עד הסוף.
המטרה: לענות בכתב על השאלות הבאות, כי הן כל האסטרטגיה.
- מהם ההיסטים של
fdו-bkבתוךOBJ, ומה גודל ה-struct? - כתבו את שתי הכתיבות ש-
unlink(B)מבצע, במונחי כתובות והיסטים (רמז:FD = B->fd,BK = B->bk). - אילו שתי leaks התוכנית מדפיסה, ומה כל אחת אומרת (כתובת מחסנית מול כתובת heap)?
- איפה בדיוק מתחילה הoverflow של
gets, ואילו שדות שלBהיא יכולה לדרוס? - מה עושה
shell(), ומה הכתובת שלה (בדקו עםobjdump)?
רמז: gets(A->buf) כותב מהכתובת A->buf והלאה. השדות A->fd ו-A->bk נמצאים לפני buf, אז לא נדרוס אותם - נדרוס את מה שאחרי, כלומר את ה-chunk של B.
תרגיל 2 - מדידת ההגנות והמרחקים¶
עכשיו מודדים, לא מניחים.
תחילה ההגנות:
ודאו שאתם מבינים מה כל שורה אומרת (בפרט: יש canary? יש PIE? מה מצב ה-NX?), ולמה כל אחת מהן מקלה או מקשה עלינו.
עכשיו שתי המדידות הקריטיות:
- המרחק מ-
A->bufועדB->fd. שלחו דפוס De Bruijn ל-gets(cyclic(64)), עצרו ב-gdb ברגע שבוunlinkקורא אתB->fd, וראו איזה חלק מהדפוס נחת בתוךFD. מ-cyclic_findתקבלו את ההיסט. - איזו כתובת
saved ebpעל המחסנית תרצו לדרוס, וכמה היא רחוקה מהleak&A. הריצו תחת gdb, עצרו בכניסה ל-unlink, והסתכלו על הפריימים עםinfo frame/bt. חפשו את ה-saved ebpשל הפריים שממנו נבצע את ה-pivot.
רמז: זכרו את הכלל מההרצאה - אסור שהערך שנכתב יהיה כתובת קוד. לכן המדידה בשלב 2 מכוונת ל-saved ebp (מצביע מחסנית, בר-כתיבה), ולא לכתובת החזרה עצמה.
תרגיל 3 - חישוב ה-payload¶
עכשיו האלגברה. אתם צריכים להרכיב מחרוזת אחת שתישלח ל-gets, ובתוכה:
- מצביע שיהפוך לפריים המזויף ב-heap (מכיל את הכתובת של
shell()במקום שה-retיקרא ממנו). - הערכים המזויפים של
B->fdו-B->bkשיגרמו ל-unlink לכתוב את המצביע הזה אל תוך ה-saved ebpשמדדתם.
המטרה: לכתוב על נייר את הביטוי המדויק ל-B->fd ול-B->bk במונחי stack (הleak &A) ו-heap (הleak A). ודאו ששתי הכתיבות של unlink נוחתות בזיכרון בר-כתיבה, ורק אז המשיכו.
רמז ראשון: אם אתם רוצים *(T) = V, בחרו FD = T - 4 ו-BK = V (כי כתיבה 1 היא *(FD+4) = BK). ה-T שלכם הוא כתובת ה-saved ebp, וה-V שלכם הוא הכתובת של הפריים המזויף ב-heap.
רמז שני: ה-gets כותב מ-A->buf והלאה, אז אתם שולטים בזיכרון החל מ-A+8. שם, במקום ידוע, שתלו את הכתובת של shell() כך שהיא תשמש כ"כתובת החזרה" של הפריים המזויף.
תרגיל 4 - כתיבת ה-exploit והרצה¶
כתבו סקריפט pwntools שקורא את שתי הleaks, בונה את ה-payload, שולח אותו, ופותח shell.
שני דברים מעשיים שחייבים לשים לב אליהם:
- הleaks מגיעות כטקסט הקסדצימלי (
0x...). קראו את השורות ותרגמו עםint(x, 16). - אחרי שה-
shell()תרוץ,system("/bin/sh")פותח shell שקורא מ-stdin. אם שלחתם את ה-payload ואז סגרתם את stdin, ה-shell ייסגר מיד. לכן צריך להשאיר את הקלט פתוח. עם pwntools זהp.interactive(), ובשורת פקודה זה הטריק(python3 exploit.py; cat) | ./unlink.
רמז: שלד להתחלה -
from pwn import *
context.binary = elf = ELF('./unlink')
p = process('./unlink') # locally; against the server: p = remote(...) per the challenge page
p.recvuntil(b'stack address leak: ')
stack = int(p.recvline().strip(), 16) # &A
p.recvuntil(b'heap address leak: ')
heap = int(p.recvline().strip(), 16) # A
shell = elf.symbols['shell']
# ... build the fake frame here and B->fd / B->bk per the calculation from exercise 3 ...
p.sendline(payload)
p.interactive()
הצלחתם כשקיבלתם $ ואתם יכולים להריץ cat flag.
תרגיל 5 (בונוס) - להרגיש את ה-safe unlink¶
האתגר משתמש ב-unlink ידני בלי בדיקות. עכשיו נראה למה ההתקפה הזו לא עוברת מול glibc אמיתי.
כתבו תוכנית קטנה משלכם שמקצה שני chunks, דורסת את המטא-דאטה של השני עם fd/bk מזויפים (כמו בהתקפה הקלאסית), וקוראת ל-free על ה-chunk כדי לגרום ל-unlink האמיתי לרוץ. הדרו והריצו.
המטרה: לראות בעיניים את ההודעה corrupted double-linked list ולהבין איזו מהבדיקות נכשלה. אחר כך, על נייר, תכננו איך הייתם עוקפים אותה אילו בתוכנית היה מצביע גלובלי g אל ה-chunk (רמז: fd = &g - 0x18, bk = &g - 0x10 בבנייה של 64 ביט).
רמז: כדי להישאר ב-unsorted/small bin ולא ב-tcache (שמתנהג אחרת), הקצו chunks גדולים מ-0x408 בתים, או קמפלו מול glibc ישן יותר. את פרטי ה-bins ראינו בפרק 7.1.