7.2 שימוש לאחר שחרור תרגול
תרגול - שימוש לאחר שחרור¶
בתרגול הזה נפתור את האתגר uaf מ-pwnable.kr - תוכנית C++ שבה שחרור אובייקט משאיר מצביע תלוי, ומיחזור הבלוק המשוחרר מאפשר לנו לדרוס את ה-vptr ולחטוף קריאה וירטואלית אל give_shell. זה בדיוק המנגנון שראינו בהרצאה, עכשיו על בינארי אמיתי. לפני שתגעו באתגר עצמו, כדאי לחמם על דמו מקומי כדי שהמנגנון יהיה ברור לכם ביד.
האתגר נמצא בקטגוריית Rookiss, ופותרים אותו על השרת עצמו דרך SSH. הסיסמה היא 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 והבינו את התוכנית לפני שתתקפו. ענו לעצמכם בכתב על השאלות הבאות - בלי לנחש, מתוך המקור:
- מה עושה כל אחת משלוש האופציות בתפריט (use / after / free)?
- אופציה 3 משחררת כמה אובייקטים ובאיזה סדר?
- אופציה 2 ("after") - מאיפה מגיע הגודל של ההקצאה, ומאיפה מגיע התוכן שנכתב אליה?
- מה גודל האובייקט
Man/Womanבבתים? מאילו שדות הוא מורכב? -
איזו מתודה וירטואלית מוכרזת ראשונה במחלקה
Human, ואיזו שנייה? באיזה slot כל אחת יושבת? -
רמז 1: את גודל האובייקט אפשר לחשב מהשדות (vptr + int + string) ולאמת ב-gdb מתוך הארגומנט ל-
operator new(הסתכלו עלrdiלפני הקריאה ל-_Znwm). - רמז 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 והסקריפט הסופי, נמצא בקובץ הפתרון.