לדלג לתוכן

7.2 שימוש לאחר שחרור תרגול

תרגול - שימוש לאחר שחרור

בתרגול הזה נפתור את האתגר uaf מ-pwnable.kr - תוכנית C++ שבה שחרור אובייקט משאיר מצביע תלוי, ומיחזור הבלוק המשוחרר מאפשר לנו לדרוס את ה-vptr ולחטוף קריאה וירטואלית אל give_shell. זה בדיוק המנגנון שראינו בהרצאה, עכשיו על בינארי אמיתי. לפני שתגעו באתגר עצמו, כדאי לחמם על דמו מקומי כדי שהמנגנון יהיה ברור לכם ביד.

האתגר נמצא בקטגוריית Rookiss, ופותרים אותו על השרת עצמו דרך SSH. הסיסמה היא guest.

ssh uaf@pwnable.kr -p2222        # password: guest

בתוך החשבון תמצאו את הבינארי uaf, את קוד המקור uaf.cpp, ואת קובץ הדגל flag שאין לכם הרשאה לקרוא ישירות. המטרה: להשיג shell בהרשאות הבינארי ולקרוא את הדגל.


תרגיל 1 - חימום מקומי על vtable

לפני pwnable.kr, בנו והריצו את הדמו מההרצאה (vuln.cpp) אצלכם. הדרו עם -no-pie, הריצו checksec, ותקפו אותו עם השלד מההרצאה.

המטרה: להבין בעיניים איך free -> alloc -> use מחליף את ה-vptr ומקפיץ ל-give_shell.

  • רמז 1: פתחו ב-gdb, עצרו על main, והריצו x/gx m לפני ואחרי כל צעד. עקבו אחרי מה שיושב בבית הראשון של האובייקט (ה-vptr).
  • רמז 2: אחרי delete m, הסתכלו שוב על x/gx m. שימו לב שה-vptr כבר לא מצביע ל-vtable - הוא מכיל מצביע fd של ה-bin. זו הסיבה שאסור לקרוא ל-use מיד אחרי free בלי למחזר.
  • רמז 3: מצאו את כתובת ה-vtable עם nm vuln | grep _ZTV. הבינו למה ה-vptr המזויף שאתם כותבים הוא vtable + 8 (כלומר slot0 פחות 8).

תרגיל 2 - קריאת המקור והבנת המבנה

התחברו לאתגר, קראו את uaf.cpp והבינו את התוכנית לפני שתתקפו. ענו לעצמכם בכתב על השאלות הבאות - בלי לנחש, מתוך המקור:

  1. מה עושה כל אחת משלוש האופציות בתפריט (use / after / free)?
  2. אופציה 3 משחררת כמה אובייקטים ובאיזה סדר?
  3. אופציה 2 ("after") - מאיפה מגיע הגודל של ההקצאה, ומאיפה מגיע התוכן שנכתב אליה?
  4. מה גודל האובייקט Man/Woman בבתים? מאילו שדות הוא מורכב?
  5. איזו מתודה וירטואלית מוכרזת ראשונה במחלקה Human, ואיזו שנייה? באיזה slot כל אחת יושבת?

  6. רמז 1: את גודל האובייקט אפשר לחשב מהשדות (vptr + int + string) ולאמת ב-gdb מתוך הארגומנט ל-operator new (הסתכלו על rdi לפני הקריאה ל-_Znwm).

  7. רמז 2: זכרו שה-string כאן הוא מהסוג הישן, מצביע יחיד של 8 בתים. זה מה שמביא את האובייקט ל-24 בתים בדיוק.

תרגיל 3 - מציאת הכתובת לכתיבה

זהו הלב הטכני. אתם צריכים למצוא את הערך המדויק שתכתבו לתוך ה-vptr כדי ש-introduce יקפוץ ל-give_shell.

המטרה: למצוא את כתובת slot 0 (איפה יושב give_shell בטבלה), ולהבין למה כותבים אותה פחות 8.

  • רמז 1: nm uaf | grep -i give_shell ייתן לכם את כתובת הפונקציה עצמה. אבל אתם צריכים את כתובת ה-slot בטבלה, לא את הפונקציה. הן שונות.
  • רמז 2: פתחו ב-gdb, עצרו אחרי שהאובייקטים נוצרים, והריצו x/gx m כדי לראות את ה-vptr האמיתי. זו כתובת slot 0. עכשיו x/2gx <vptr> ותראו את give_shell ב-slot 0 ואת introduce ב-slot 1.
  • רמז 3: הקריאה של introduce מבצעת call [vptr+8]. אתם רוצים ש-[vptr+8] יהיה give_shell. אם slot0 מכיל את give_shell, איזה ערך צריך vptr המזויף להיות כדי ש-vptr+8 יצביע ל-slot0? (רמז בתוך רמז: הזיזו את הטבלה 8 בתים אחורה.)

תרגיל 4 - בניית הקובץ וההרצה

עכשיו מרכיבים הכל. אתם צריכים: קובץ שמכיל את ה-vptr המזויף, ורצף לחיצות תפריט בסדר הנכון.

המטרה: להריץ ./uaf 24 <קובץ>, למחזר את הבלוקים המשוחררים, ולהשיג shell.

  • רמז 1: צרו את הקובץ עם 8 הבתים של ה-vptr המזויף ב-little-endian. אפשר עם echo -ne '\x..\x..\...' או עם python. הבתים הראשונים הם מה שחשוב - אפשר לרפד לגודל 24 אם בא לכם.
  • רמז 2: הריצו את הבינארי עם הגודל 24 בתור argv[1] והקובץ בתור argv[2]: ./uaf 24 /tmp/pwn.
  • רמז 3: באיזה סדר ללחוץ? זכרו: צריך קודם לשחרר, ואז למחזר את שני הבלוקים לפני שקוראים ל-use. חשבו כמה פעמים ללחוץ 2. (רמז: יותר מפעם אחת - למה?)
  • רמז 4: אחרי שה-shell נפתח, ה-stdin שלכם צריך להישאר פתוח כדי להקליד cat flag. אם אתם מזינים את הפקודות דרך pipe, סיימו ב-cat כדי לחבר את ה-stdin שלכם ל-shell.
  • רמז 5 (למה זה לפעמים לא עובד בניסיון ראשון): אם קיבלתם קריסה במקום shell, בדקו האם מחזרתם את שני האובייקטים. הקצאה אחת בלבד משאירה את m עם vptr זבל, ו-m->introduce() קורס לפני שהגיע ל-give_shell.

תרגיל 5 - אוטומציה עם pwntools (בונוס)

אחרי שהצלחתם ידנית, כתבו סקריפט pwntools שעושה את הכל: מכין את הקובץ, מריץ את הבינארי עם הארגומנטים הנכונים, שולח את רצף התפריט, ופותח shell אינטראקטיבי. שאפו לחלץ את כתובת ה-slot מהבינארי במקום להקשיח אותה - כך הסקריפט ישרוד גם אם תהדרו מחדש.

  • רמז 1: process(['./uaf', '24', '/tmp/pwn']) מריץ עם ארגומנטים. כתבו את הקובץ עם open('/tmp/pwn','wb').write(p64(fake_vptr)) לפני ההרצה.
  • רמז 2: את ה-vptr המזויף אפשר לחשב מהסמל של ה-vtable: elf.symbols['_ZTV3Man'] + 8. אם הסמל לא קיים בבינארי, חזרו לערך הקשיח מ-gdb.
  • רמז 3: השתמשו ב-sendlineafter כדי לסנכרן מול הדפסות התפריט, ואל תשכחו את ההקצאה הכפולה.

בנו את הכל צעד-צעד. קודם ודאו שאתם רואים את התפריט, אחר כך שהשחרור וההקצאות עוברות, ורק בסוף הוסיפו את קריאת ה-use. הפתרון המלא, כולל הפלט של gdb והסקריפט הסופי, נמצא בקובץ הפתרון.