לדלג לתוכן

5.4 דריסת GOT וחטיפת זרימה תרגול

תרגול - דריסת GOT וחטיפת זרימה

בתרגול הזה תשיגו חטיפת זרימת בקרה דרך דריסת GOT בשני אתגרים מ-pwnable.kr, כל אחד עם באג שונה: passcode (הבאג של scanf בלי &) ו-fsb (חולשת format string). בשניהם המטרה זהה: לכתוב כתובת אחת מעל תא ב-GOT, ולגרום לתוכנית לקפוץ לקוד שנותן לכם את הדגל או shell. אל תציצו בפתרון לפני שהרצתם בעצמכם - חצי מהלמידה כאן היא בלמצוא את הכתובות הנכונות ולראות את הקפיצה קורית ב-pwndbg.

לפני שמתחילים, תרגלו מקומית על בינארי הדמה מההרצאה (fmt_demo.c). ודאו שאתם מצליחים לדרוס printf@got ולקבל shell מקומי - זה בדיוק אותו פרימיטיב, בלי לחץ של שרת.


תרגיל 1 - passcode

התחברו לאתגר:

ssh passcode@pwnable.kr -p2222

הסתכלו ב-passcode.c. שתי הפונקציות welcome ו-login נקראות ברצף מ-main. ב-login יש scanf("%d", passcode1) בלי &, ו-passcode1 לא אותחל. מיד אחרי אותו scanf יש קריאה ל-fflush.

המטרה: לקרוא את הדגל, מבלי לדעת את הסיסמאות.

  • רמז 1: הבאג scanf("%d", passcode1) כותב את המספר שתזינו אל הכתובת ששמורה ב-passcode1. אתם צריכים לשלוט בערך של passcode1 כדי שהכתובת הזו תהיה תא GOT שתבחרו.
  • רמז 2: איך שולטים ב-passcode1 שלא אותחל? welcome ו-login חולקות את אותו שטח מחסנית. הbuffer name של 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

התחברו:

ssh fsb@pwnable.kr -p2222

הסתכלו בקוד המקור. הבינארי קורא את הקלט שלכם ומעביר אותו ישירות לפונקציית 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, למה נוח לדרוס דווקא תא של פונקציה שנקראת שוב בלולאה, ולא פונקציה שכבר סיימה את תפקידה?