לדלג לתוכן

7.4 תקיפת unlink תרגול

בתרגול הזה נתקוף את האתגר unlink מ-pwnable.kr (קטגוריית Rookiss). זה בינארי של 32 ביט עם unlink כתוב ביד, בלי בדיקות בטיחות, ועם שתי leaks שמוגשות לנו על מגש כסף. המטרה: לזייף מטא-דאטה של chunk, להפעיל את הפרימיטיב, ולקפוץ אל shell() כדי לקרוא את הדגל.

התחברות והכנה - setup

מתחברים ב-SSH (הסיסמה מופיעה בעמוד האתגר, בדרך כלל guest):

ssh unlink@pwnable.kr -p2222

בתיקיית הבית תמצאו את unlink (הבינארי, setuid), את unlink.c (המקור), ואת flag. עבדו בתיקייה זמנית תחת /tmp ומשכו עותק מקומי לחקירה נוחה:

scp -P2222 unlink@pwnable.kr:~/unlink ./unlink
scp -P2222 unlink@pwnable.kr:~/unlink.c ./unlink.c

אם אתם רוצים לתרגל לגמרי מקומית, אפשר להדר את המקור בעצמכם ב-32 ביט (שימו לב שנצטרך את אותן הגנות כמו באתגר):

gcc -m32 -no-pie -fno-stack-protector -z execstack -o unlink unlink.c

תרגיל 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 - מדידת ההגנות והמרחקים

עכשיו מודדים, לא מניחים.

תחילה ההגנות:

checksec ./unlink

ודאו שאתם מבינים מה כל שורה אומרת (בפרט: יש canary? יש PIE? מה מצב ה-NX?), ולמה כל אחת מהן מקלה או מקשה עלינו.

עכשיו שתי המדידות הקריטיות:

  1. המרחק מ-A->buf ועד B->fd. שלחו דפוס De Bruijn ל-gets (cyclic(64)), עצרו ב-gdb ברגע שבו unlink קורא את B->fd, וראו איזה חלק מהדפוס נחת בתוך FD. מ-cyclic_find תקבלו את ההיסט.
  2. איזו כתובת 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.

האתגר משתמש ב-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.