5.4 דריסת GOT וחטיפת זרימה תרגול
תרגול - דריסת GOT וחטיפת זרימה¶
בתרגול הזה תשיגו חטיפת זרימת בקרה דרך דריסת GOT בשני אתגרים מ-pwnable.kr, כל אחד עם באג שונה: passcode (הבאג של scanf בלי &) ו-fsb (חולשת format string). בשניהם המטרה זהה: לכתוב כתובת אחת מעל תא ב-GOT, ולגרום לתוכנית לקפוץ לקוד שנותן לכם את הדגל או shell. אל תציצו בפתרון לפני שהרצתם בעצמכם - חצי מהלמידה כאן היא בלמצוא את הכתובות הנכונות ולראות את הקפיצה קורית ב-pwndbg.
לפני שמתחילים, תרגלו מקומית על בינארי הדמה מההרצאה (fmt_demo.c). ודאו שאתם מצליחים לדרוס printf@got ולקבל shell מקומי - זה בדיוק אותו פרימיטיב, בלי לחץ של שרת.
תרגיל 1 - passcode¶
התחברו לאתגר:
הסתכלו ב-passcode.c. שתי הפונקציות welcome ו-login נקראות ברצף מ-main. ב-login יש scanf("%d", passcode1) בלי &, ו-passcode1 לא אותחל. מיד אחרי אותו scanf יש קריאה ל-fflush.
המטרה: לקרוא את הדגל, מבלי לדעת את הסיסמאות.
- רמז 1: הבאג
scanf("%d", passcode1)כותב את המספר שתזינו אל הכתובת ששמורה ב-passcode1. אתם צריכים לשלוט בערך שלpasscode1כדי שהכתובת הזו תהיה תא GOT שתבחרו. - רמז 2: איך שולטים ב-
passcode1שלא אותחל?welcomeו-loginחולקות את אותו שטח מחסנית. הbuffernameשלwelcomeחופף ל-passcode1שלlogin. פרקו את שתי הפונקציות ב-gdb (disassemble welcome,disassemble login) ומצאו את ה-offset שלnameושלpasscode1יחסית ל-ebp. ההפרש הוא כמות בתי המילוי שתצטרכו. - רמז 3: איזה תא GOT כדאי לדרוס? חפשו פונקציה שנקראת מיד אחרי ה-
scanfהראשון. רמז חזק:fflush. השיגו את כתובת התא עםobjdump -R ./passcode | grep fflushאו עםelf.got['fflush']ב-pwntools. - רמז 4: מה הכתובת שתכתבו לתוך התא? פרקו את
loginומצאו את השורה שמריצהsystem("/bin/cat flag")(מחפשיםcall systemואת ה-movשמעליו שטוען את המחרוזת). זו כתובת ה"ניצחון".scanfמקבל%d, אז תזינו אותה כמספר עשרוני. - רמז 5: זהירות מבתים לבנים.
scanf("%100s", name)נעצר ברווח, טאב, שורה חדשה או form feed (0x0c). ודאו שכתובת ה-GOT שבחרתם לא מכילה בית כזה.
בדקו לעצמכם ב-pwndbg: שימו breakpoint על הקריאה ל-fflush, בדקו שהתא ב-GOT אכן מכיל עכשיו את כתובת ה"ניצחון", ותראו את הקפיצה. למה כלל לא נדרשתם ל-passcode2?
תרגיל 2 - fsb¶
התחברו:
הסתכלו בקוד המקור. הבינארי קורא את הקלט שלכם ומעביר אותו ישירות לפונקציית printf-משפחה כמחרוזת format (הבאג printf(buf) הקלאסי), בתוך לולאה. בבינארי קיים גם נתיב שמריץ shell (execve("/bin/sh", ...)), מוגן בתנאי.
המטרה: לקבל shell (ומשם לקרוא את הדגל).
- רמז 1: ראשית הריצו
checksec. ודאו שאין PIE (כתובות קבועות) ושה-RELRO חלקי (GOT כתיב). אם משהו מהם שונה - האסטרטגיה משתנה. - רמז 2: מצאו את ה-offset של מחרוזת הformat. שלחו
AAAAואחריו סדרת%pוראו באיזה מיקום חוזר0x41414141. זה ה-offset שתעבירו ל-pwntools. - רמז 3: בחרו יעד. אפשרות א - תא GOT של פונקציה שנקראת בסבב הבא של הלולאה, שתדרסו בכתובת של קטע ה-
execve("/bin/sh", ...). אפשרות ב - המשתנה הגלובלי שהתנאי בודק (key), שתדרסו בערך שמספק את התנאי. - רמז 4: אל תבנו את מחרוזת ה-
%nביד. השתמשו ב-fmtstr_payload(offset, {target: value})של pwntools - הוא מפצל לכתיבות בית ומחשב רוחב אוטומטית. אתtargetו-valueתשיגו עםelf.got[...],elf.symbols[...]ומהדיסאסמבלי. - רמז 5: אם בחרתם לדרוס GOT - שימו לב איזו פונקציה נקראת אחרי ה-
printfהvulnerable, כי הקפיצה תתרחש רק בקריאה הבאה דרך אותו תא. תכננו את הטיימינג בהתאם למבנה הלולאה.
בדקו לעצמכם: אם דרסתם את ה-key, איך התוכנית עצמה מגיעה ל-shell? אם דרסתם GOT, באיזו קריאה בדיוק התבצעה הקפיצה? הריצו תחת pwndbg עם breakpoint על התא שדרסתם.
שאלות להעמקה¶
- ב-passcode, מה היה משתנה אילו הקומפיילר היה מאתחל את
passcode1לאפס? למה הבאג תלוי בכך שהמשתנה לא אותחל? - למה דריסת GOT עוקפת לחלוטין את ה-canary, בעוד שדריסת return address על המחסנית חייבת לעבור דרכו?
- אם passcode היה מהודר עם RELRO מלא, האם הבאג של
scanfבלי&היה עדיין שמיש? לאיזה יעד הייתם מכוונים אז את הכתיבה? - ב-fsb, למה נוח לדרוס דווקא תא של פונקציה שנקראת שוב בלולאה, ולא פונקציה שכבר סיימה את תפקידה?