8.3 בינארי זעיר תרגול
תרגול - בינארי זעיר (tiny_easy)¶
בתרגול הזה תפצחו את tiny_easy מ-pwnable.kr - ELF זערורי ומופשט מסמלים שכל מה שהוא עושה זה לקרוס. הדרך לפצח אותו עוברת דרך הבנה מדויקת של מצב התהליך ברגע הכניסה: מה יש על המחסנית, מה ב-eax, ולאן בדיוק הקוד קופץ. כדי לתרגל בנוחות נבנה קודם בינארי מקומי שמחקה את tiny_easy בול, נפצח אותו, ורק אז נכוון לשרת האמיתי. עבדו לפי הסדר - כל תרגיל בונה על הקודם.
האתגר נמצא בקטגוריה Toddler's Bottle. מתחברים אליו כך:
בתיקיית הבית תמצאו בינארי בשם tiny_easy (עם ביט setuid) וקובץ flag שרק המשתמש המיוחס יכול לקרוא. שימו לב: אין כאן קובץ מקור .c. הבינארי כל כך קטן שאתם אמורים לפרק אותו ולהבין אותו בעצמכם. עבדו בתיקייה זמנית תחת /tmp כמו שלמדנו ב-0.5.
הכנה - בינארי מקומי שמחקה את tiny_easy¶
לפני שנוגעים בשרת, נבנה עותק מקומי להתאמן עליו. צרו קובץ אסמבלי tiny.asm שמחקה את נקודת הכניסה של tiny_easy:
; tiny.asm - mimics the entry point of tiny_easy
BITS 32
global _start
_start:
pop eax ; eax = argc
pop eax ; eax = argv[0] (pointer to the program name string)
mov eax, [eax] ; eax = *(argv[0]) = the first four bytes of the program name
jmp eax ; jump to the address held in those four bytes
קמפלו וקשרו אותו כבינארי לא-PIE עם מחסנית ניתנת להרצה (אם חסרות ספריות 32 ביט: sudo apt install gcc-multilib nasm):
כבו ASLR לצורך פיתוח ראשוני:
תרגיל 1 - סקירה של הבינארי המינימלי¶
לפני שתוקפים, מבינים מול מה עומדים.
- הריצו
file tiny,wc -c tiny, ו-checksec --file=tiny. שימו לב לגודל הזערורי, ל-NX disabled, ול-No PIE. - הריצו
readelf -h tiny | grep Entryורשמו את נקודת הכניסה. הריצוreadelf -l tinyוראו כמה מקטעיLOADיש ובאילו הרשאות. - חשבו: מדוע ה-NX מכובה למרות שלא ביקשתם זאת במפורש? מה בקובץ (או מה חסר בו) גורם לכך?
רמז: חפשו אם קיים מקטע GNU_STACK בפלט של readelf -l. אם הוא חסר, הkernel מפעיל READ_IMPLIES_EXEC וכל דף קריא הופך גם לניתן להרצה.
רמז: נקודת הכניסה נמצאת בדיוק אחרי כותרת ה-ELF (52 בתים) וכותרת התוכנית (32 בתים). נסו לחשב את הכתובת ולראות שהיא מתאימה לפלט של readelf.
תרגיל 2 - פירוק הקוד ומעקב אחרי eax¶
עכשיו נבין מה הבינארי עושה בפועל.
- הריצו
objdump -d tinyוכתבו לעצמכם את ההוראות בנקודת הכניסה. - עקבו אחרי
eaxהוראה-הוראה, כשמפת המחסנית בכניסה (argc,argv[],envp[]) לפניכם. מהeaxמחזיק אחרי כל אחד משני ה-pop? מה אחרי ה-mov? - ענו במדויק: לאיזו כתובת קופצת ההוראה האחרונה, ומאיפה מגיע הערך של הכתובת הזו?
- הריצו
./tinyרגיל וראו את הקריסה. השתמשו ב-dmesg | tailאו ב-gdb כדי לראות באיזו כתובת ה-eipקרס. האם הכתובת מתאימה לבתים הראשונים של המחרוזת./tiny?
רמז: אחרי שני ה-pop, eax מחזיק את המצביע argv[0]. ההוראה mov eax,[eax] קוראת את ארבעת הבתים הראשונים של המחרוזת ש-argv[0] מצביע אליה - כלומר של שם התוכנית.
רמז: ./ti בקידוד little-endian הוא 0x69742e2e. אם הקריסה ב-eip היא בכתובת הזו, הבנתם נכון: יעד הקפיצה נשלט לגמרי מארבעת הבתים הראשונים של שם התוכנית.
תרגיל 3 - מצב התהליך בכניסה ב-gdb¶
נאשר את מפת המחסנית מההרצאה בעיניים שלנו.
- פתחו את הבינארי ב-gdb ועצרו בכניסה עם
starti(עוצר על ההוראה הראשונה של_start). - הסתכלו על ראש המחסנית:
x/8x $esp. הערך הראשון הואargc. אחריו מצביעיargv, אחריוNULL, אחריו מצביעיenvp. - עקבו אחרי מצביע
argv[0](x/sעל הערך שב-$esp+4) וראו שהוא מצביע על מחרוזת שם התוכנית. - הכניסו משתנה סביבה מזהה (למשל
AAAA=BBBBCCCC), הריצו שוב, וחפשו אותו על המחסנית:search -s "BBBBCCCC". שימו לב שהוא יושב גבוה, בראש המחסנית.
רמז: הפקודה starti ב-pwndbg עוצרת עוד לפני הקפיצה, כשהמחסנית עדיין במצב הכניסה שהkernel בנה. זה הרגע המושלם לראות את argc/argv/envp.
תרגיל 4 - השתלת shellcode בסביבה וכיוון הקפיצה (מקומי)¶
עכשיו נפרוץ את הבינארי המקומי.
- בנו shellcode של 32 ביט שמריץ
execve("/bin/sh", 0, 0)(היד-כתוב מ-3.1, אוasm(shellcraft.i386.linux.sh())). - הכניסו למשתנה סביבה מזחלת ענקית של
\x90(למשל0x10000בתים) ואחריה ה-shellcode. - מצאו ב-gdb כתובת עמוק בתוך המזחלת (
search -s "\x90\x90\x90\x90\x90\x90\x90\x90"על ה-[stack]). - הפעילו את הבינארי דרך
execveעםargv[0] = p32(addr), כשה-addrהוא כתובת בתוך המזחלת. ודאו שנפתח shell. נסוid.
רמז: השליטה שלכם היא ב-argv[0]. כדי לשים שם בתים גולמיים חייבים execve - ב-pwntools זה process(['./tiny'], executable='./tiny', argv=[p32(addr)], env={...}).
רמז: מכיוון שאתם קוראים רק את ארבעת הבתים הראשונים של argv[0], מספיק ש-p32(addr) יהיה בלי בית אפס. כתובת מחסנית טיפוסית כמו 0xffffd800 נקייה.
רמז: אם זה מפספס, ודאו שלא שיניתם את גודל הסביבה בין החיפוש ב-gdb לבין ההרצה - כל שינוי כזה מזיז את כל המחסנית.
תרגיל 5 - התמודדות עם ASLR ולולאת brute force¶
עד עכשיו עבדנו עם ASLR כבוי. על השרת האמיתי הוא כנראה פעיל.
- הדליקו ASLR מחדש:
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space, והריצו את ה-exploit מתרגיל 4. הוא כנראה יפספס, כי כתובת המחסנית זזה בכל הרצה. - עטפו את ההפעלה בלולאה: קבעו כתובת ניחוש אחת בתוך תחום המזחלת, והריצו שוב ושוב עד שנפתח shell. אחרי כל כישלון סגרו את התהליך ונסו שוב.
- מדדו כמה ניסיונות לקח. נסו להגדיל את המזחלת ולראות שמספר הניסיונות יורד.
רמז: כל הרצה מגרילה מחסנית חדשה. בסופו של דבר המזחלת הגדולה שלכם "נוחתת" מעל הכתובת שניחשתם, וה-shellcode רץ. ב-32 ביט האנטרופיה נמוכה, אז כמה מאות עד אלפי ניסיונות מספיקים.
רמז: ככל שהמזחלת גדולה יותר, כך היא מכסה יותר כתובות אפשריות, ופחות ניסיונות צריך. מזחלת של 0x10000 בתים ומעלה היא נקודת פתיחה טובה.
תרגיל 6 - פיצוח tiny_easy האמיתי על השרת¶
עכשיו למטרה האמיתית.
- התחברו ל-
ssh tiny_easy@pwnable.kr -p2222, העתיקו את הבינארי לתיקייה זמנית ב-/tmp, ופרקו אותו עםobjdump -d. ודאו שהמנגנון זהה למה שתרגלתם (אמתו את הבתים - הם עשויים להיות מעט שונים). - התאימו את ה-exploit: הריצו את הבינארי האמיתי דרך
execveעםargv[0]נשלט ו-shellcode בסביבה, בלולאת brute force בגלל ה-ASLR. - כשנפתח shell, הריצו
cat flag. זכרו שה-shell רץ בהרשאות המשתמש המיוחס בזכות ביט ה-setuid.
רמז: אם אין pwntools על השרת, כתבו תוכנית עטיפה קצרה בפייתון שקוראת ל-os.execve('./tiny_easy', [struct.pack('<I', addr)], env), והריצו אותה בלולאת shell: while true; do python3 wrap.py 2>/dev/null; done.
רמז: אם קשה לזהות מתי נפתח shell בתוך הלולאה, שלחו פקודה כמו echo HIT; cat flag וחפשו את המחרוזת HIT בפלט.
תרגיל 7 (מחשבה) - להכליל את מה שלמדנו¶
בלי לכתוב קוד, ענו:
- מדוע דווקא ELF מינימלי (בלי
GNU_STACK) נותן לנו מחסנית ניתנת להרצה בחינם? מה היה משתנה אם הבינארי היה כוללGNU_STACKעם דגלRWבלבד? - הexploit נשען על כך שהקוד קופץ אל ערך שמגיע מ-
argv[0]. אילו עוד מקורות קלט "מוקדמים" (לפניmain) עשויים להיות בשליטת התוקף בבינארי כזה? - מדוע השתמשנו במשתנה סביבה כדי לאחסן את ה-shellcode ולא, נניח, בתוך
argv[0]עצמו? מה המגבלה עלargv[0]שמונעת לשים בו את כל ה-shellcode?
רמז: חשבו על הגודל ועל קטיעה. argv[0] צריך שארבעת הבתים הראשונים שלו יהיו כתובת קפיצה - אבל אפשר לשים אחריהם עוד בתים. משתנה סביבה נותן מרחב גדול ונוח למזחלת ול-shellcode בלי לפגוע בכתובת הקפיצה.