7.6 מבוא לניצול חולשות פתרון
פתרון תרגיל 1 - ניתוח תוכנית פגיעה¶
-
החולשה: שימוש ב-
gets(name)לקריאת קלט לתוך חוצץname[32]. הפונקציה gets לא מגבילה את כמות הבתים שהיא קוראת, ולכן אם המשתמש מקליד יותר מ-32 תווים, הבתים הנוספים גולשים מעבר ל-buffer ודורסים ערכים על המחסנית - כולל את כתובת החזרה. -
מבנה המחסנית של greet (מערכת 64 ביט):
-
כמות הבתים עד כתובת החזרה:
- 32 בתים (name) + 8 בתים (saved rbp) = 40 בתים
-
כלומר, מהבית ה-41 ואילך אנחנו דורסים את כתובת החזרה
-
תיאורטית, התוקף יקליד:
- 40 בתים כלשהם (כדי למלא name ולדרוס saved rbp)
- ואז את הכתובת
0x401156(כתובת secret) בסדר little-endian - כלומר: 40 תווים 'A' ואז
\x56\x11\x40\x00\x00\x00\x00\x00 - כשgreet מבצעת
ret, היא תקפוץ לsecret במקום לחזור ל-main
פתרון תרגיל 2 - חישוב offset לכתובת חזרה¶
-
מבנה המחסנית (לפי ההנחות שבתרגיל):
כתובות גבוהות +-------------------------------+ | כתובת חזרה (8 בתים) | +-------------------------------+ | saved rbp (8 בתים) | +-------------------------------+ | padding (8 בתים) ליישור | +-------------------------------+ | x = 42 (4 בתים) | +-------------------------------+ | y = 17 (4 בתים) | +-------------------------------+ | buffer[0..127] (128 בתים) | +-------------------------------+ כתובות נמוכות (rsp) -
ה-offset מתחילת buffer עד כתובת החזרה:
128 (buffer) + 4 (y) + 4 (x) + 8 (padding) + 8 (saved rbp) = 152 בתים -
אם נקליד 130 תווים 'A':
- 128 הבתים הראשונים ממלאים את buffer
- 2 הבתים הנוספים דורסים את תחילת y
-
הערך של y יהיה פגום (2 בתים תחתונים ידרסו על ידי 'A' = 0x41)
-
אם נקליד 140 תווים 'A':
- 128 בתים ממלאים buffer
- 4 בתים דורסים y (y = 0x41414141 = 1094795585)
- 4 בתים דורסים x (x = 0x41414141 = 1094795585)
- 4 בתים דורסים את תחילת ה-padding
ערכי x ו-y שניהם יהיו 0x41414141 (1094795585 בעשרוני) כי 'A' = 0x41.
פתרון תרגיל 3 - שימוש ב-checksec¶
- הבדלים בפלט checksec:
גרסה 1 (בלי הגנות):
גרסה 2 (רק canary):
גרסה 3 (canary + NX):
גרסה 4 (כל ההגנות):
- גרסה 1 הכי פגיעה כי:
- אין canary - גלישת חוצץ יכולה לדרוס את כתובת החזרה בלי שנזהה
- NX כבוי - אפשר להריץ קוד שנכתב על המחסנית
-
אין PIE - כתובות הקוד קבועות ונשארות אותו דבר בכל הרצה
-
גרסה 4 הכי מוגנת כי:
- יש canary - גלישת חוצץ תתפס
- NX פעיל - לא ניתן להריץ קוד מהמחסנית
- PIE פעיל - כתובות הקוד אקראיות
-
Full RELRO - הGOT מוגן מכתיבה
-
כן! RELRO הוא מנגנון שגם בלי דגל מפורש, הלינקר מפעיל אותו ברמה מסוימת (Partial RELRO). בגרסה 4 עם PIE, gcc גם מפעיל Full RELRO אוטומטית. בנוסף, FORTIFY_SOURCE עשוי להיות פעיל בהתאם להגדרות ברירת המחדל של ההפצה.
פתרון תרגיל 4 - מדוע gets() מסוכנת¶
- דיאגרמה של המחסנית עם gets וקלט ארוך:
לפני gets: אחרי gets עם קלט ארוך:
+-------------------+ +-------------------+
| return address | | AAAA AAAA | <-- נדרס!
+-------------------+ +-------------------+
| saved rbp | | AAAA AAAA | <-- נדרס!
+-------------------+ +-------------------+
| buffer[0..N] | | AAAA AAAA AAAA... | <-- מלא
+-------------------+ +-------------------+
הפונקציה gets קוראת תווים עד שהיא מגיעה ל-newline או EOF. אין לה שום מגבלה על כמות הבתים. היא לא יודעת מה גודל ה-buffer ופשוט כותבת כל מה שנכנס.
- fgets כחלופה בטוחה:
הפונקציה fgets מקבלת את הגודל המקסימלי כפרמטר שני ולא תקרא יותר בתים מזה. היא גם מוסיפה null terminator בסוף.
- פונקציות מסוכנות וחלופות:
| פונקציה מסוכנת | חלופה בטוחה | הסבר |
|---|---|---|
gets(buf) |
fgets(buf, size, stdin) |
מגבילה את כמות הקלט |
strcpy(dst, src) |
strncpy(dst, src, n) |
מגבילה את כמות ההעתקה |
sprintf(buf, fmt, ...) |
snprintf(buf, size, fmt, ...) |
מגבילה את גודל הפלט |
scanf("%s", buf) |
scanf("%63s", buf) |
מוסיפים רוחב מקסימלי |
- ההבדל בין strcpy ל-strncpy:
שימו לב ש-strncpy לא מבטיחה null terminator אם src ארוך מ-n. לכן מומלץ תמיד להוסיף:
פתרון תרגיל 5 - קנרי על המחסנית¶
-
קמפלנו עם stack protector.
-
עם קלט קצר ("hello") - התוכנית עובדת כרגיל:
-
עם קלט ארוך (50 תווים 'A') - התוכנית קורסת עם הודעה:
-
ההודעה
stack smashing detectedאומרת שהקנרי נדרס. מה קרה: - הקלט (50 תווים) גלש מעבר ל-buffer (16 בתים)
- הגלישה דרסה את הקנרי שנמצא בין buffer לבין saved rbp
- לפני שהפונקציה מבצעת ret, היא בודקת את ערך הקנרי
-
הקנרי השתנה (נדרס על ידי 'A') -> התוכנית מזהה את ההתקפה ומבצעת abort
-
מבנה המחסנית עם canary:
כתובות גבוהות +---------------------------+ | כתובת חזרה (8 בתים) | +---------------------------+ | saved rbp (8 בתים) | +---------------------------+ | CANARY (8 בתים) | <-- ערך אקראי שנבדק לפני ret +---------------------------+ | buffer[0..15] (16 בתים) | <-- gets כותב לכאן +---------------------------+ כתובות נמוכות (rsp) -
למה הקנרי מתחיל בבית null (
\x00): - רוב פונקציות המחרוזות (כמו strcpy, gets, printf עם %s) עוצרות כשמגיעות לבית null
- אם תוקף מנסה לקרוא את הקנרי (למשל על ידי הדפסה עם printf), הוא ייתקע בבית ה-null וימנע ממנו לקרוא את ערך הקנרי
- בלי null byte, תוקף יכול לגרום לתוכנה להדפיס את הקנרי (information leak), ואז לכתוב אותו בחזרה כשהוא דורס את המחסנית
- עם null byte בתחילת הקנרי, התוקף לא יכול לגלות את ערכו באמצעות פונקציות מחרוזת