לדלג לתוכן

7.6 מבוא לניצול חולשות פתרון

פתרון תרגיל 1 - ניתוח תוכנית פגיעה

  1. החולשה: שימוש ב-gets(name) לקריאת קלט לתוך חוצץ name[32]. הפונקציה gets לא מגבילה את כמות הבתים שהיא קוראת, ולכן אם המשתמש מקליד יותר מ-32 תווים, הבתים הנוספים גולשים מעבר ל-buffer ודורסים ערכים על המחסנית - כולל את כתובת החזרה.

  2. מבנה המחסנית של greet (מערכת 64 ביט):

    כתובות גבוהות
    +---------------------------+
    | כתובת חזרה (8 בתים)      |  <-- return address
    +---------------------------+
    | saved rbp (8 בתים)        |
    +---------------------------+
    | name[0..31] (32 בתים)     |  <-- gets כותב לכאן
    +---------------------------+
    כתובות נמוכות (rsp)
    

  3. כמות הבתים עד כתובת החזרה:

  4. 32 בתים (name) + 8 בתים (saved rbp) = 40 בתים
  5. כלומר, מהבית ה-41 ואילך אנחנו דורסים את כתובת החזרה

  6. תיאורטית, התוקף יקליד:

  7. 40 בתים כלשהם (כדי למלא name ולדרוס saved rbp)
  8. ואז את הכתובת 0x401156 (כתובת secret) בסדר little-endian
  9. כלומר: 40 תווים 'A' ואז \x56\x11\x40\x00\x00\x00\x00\x00
  10. כשgreet מבצעת ret, היא תקפוץ לsecret במקום לחזור ל-main

פתרון תרגיל 2 - חישוב offset לכתובת חזרה

  1. מבנה המחסנית (לפי ההנחות שבתרגיל):

    כתובות גבוהות
    +-------------------------------+
    | כתובת חזרה (8 בתים)          |
    +-------------------------------+
    | saved rbp (8 בתים)            |
    +-------------------------------+
    | padding (8 בתים) ליישור       |
    +-------------------------------+
    | x = 42 (4 בתים)              |
    +-------------------------------+
    | y = 17 (4 בתים)              |
    +-------------------------------+
    | buffer[0..127] (128 בתים)    |
    +-------------------------------+
    כתובות נמוכות (rsp)
    

  2. ה-offset מתחילת buffer עד כתובת החזרה:
    128 (buffer) + 4 (y) + 4 (x) + 8 (padding) + 8 (saved rbp) = 152 בתים

  3. אם נקליד 130 תווים 'A':

  4. 128 הבתים הראשונים ממלאים את buffer
  5. 2 הבתים הנוספים דורסים את תחילת y
  6. הערך של y יהיה פגום (2 בתים תחתונים ידרסו על ידי 'A' = 0x41)

  7. אם נקליד 140 תווים 'A':

  8. 128 בתים ממלאים buffer
  9. 4 בתים דורסים y (y = 0x41414141 = 1094795585)
  10. 4 בתים דורסים x (x = 0x41414141 = 1094795585)
  11. 4 בתים דורסים את תחילת ה-padding

ערכי x ו-y שניהם יהיו 0x41414141 (1094795585 בעשרוני) כי 'A' = 0x41.


פתרון תרגיל 3 - שימוש ב-checksec

  1. הבדלים בפלט checksec:

גרסה 1 (בלי הגנות):

RELRO: Partial RELRO    STACK CANARY: No canary found    NX: NX disabled    PIE: No PIE

גרסה 2 (רק canary):

RELRO: Partial RELRO    STACK CANARY: Canary found       NX: NX disabled    PIE: No PIE

גרסה 3 (canary + NX):

RELRO: Partial RELRO    STACK CANARY: Canary found       NX: NX enabled     PIE: No PIE

גרסה 4 (כל ההגנות):

RELRO: Full RELRO       STACK CANARY: Canary found       NX: NX enabled     PIE: PIE enabled

  1. גרסה 1 הכי פגיעה כי:
  2. אין canary - גלישת חוצץ יכולה לדרוס את כתובת החזרה בלי שנזהה
  3. NX כבוי - אפשר להריץ קוד שנכתב על המחסנית
  4. אין PIE - כתובות הקוד קבועות ונשארות אותו דבר בכל הרצה

  5. גרסה 4 הכי מוגנת כי:

  6. יש canary - גלישת חוצץ תתפס
  7. NX פעיל - לא ניתן להריץ קוד מהמחסנית
  8. PIE פעיל - כתובות הקוד אקראיות
  9. Full RELRO - הGOT מוגן מכתיבה

  10. כן! RELRO הוא מנגנון שגם בלי דגל מפורש, הלינקר מפעיל אותו ברמה מסוימת (Partial RELRO). בגרסה 4 עם PIE, gcc גם מפעיל Full RELRO אוטומטית. בנוסף, FORTIFY_SOURCE עשוי להיות פעיל בהתאם להגדרות ברירת המחדל של ההפצה.


פתרון תרגיל 4 - מדוע gets() מסוכנת

  1. דיאגרמה של המחסנית עם gets וקלט ארוך:
לפני gets:                    אחרי gets עם קלט ארוך:
+-------------------+         +-------------------+
| return address    |         | AAAA AAAA         |  <-- נדרס!
+-------------------+         +-------------------+
| saved rbp         |         | AAAA AAAA         |  <-- נדרס!
+-------------------+         +-------------------+
| buffer[0..N]      |         | AAAA AAAA AAAA... |  <-- מלא
+-------------------+         +-------------------+

הפונקציה gets קוראת תווים עד שהיא מגיעה ל-newline או EOF. אין לה שום מגבלה על כמות הבתים. היא לא יודעת מה גודל ה-buffer ופשוט כותבת כל מה שנכנס.

  1. fgets כחלופה בטוחה:
    // gets לא יודעת את גודל ה-buffer
    gets(buffer);
    
    // fgets מקבלת את גודל ה-buffer כפרמטר ולא תכתוב מעבר לגודל הזה
    fgets(buffer, sizeof(buffer), stdin);
    

הפונקציה fgets מקבלת את הגודל המקסימלי כפרמטר שני ולא תקרא יותר בתים מזה. היא גם מוסיפה null terminator בסוף.

  1. פונקציות מסוכנות וחלופות:
פונקציה מסוכנת חלופה בטוחה הסבר
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) מוסיפים רוחב מקסימלי
  1. ההבדל בין strcpy ל-strncpy:
    // strcpy: מעתיקה את כל src ל-dst, ללא הגבלה.
    // אם src ארוך מ-dst - גלישה!
    strcpy(dst, src);
    
    // strncpy: מעתיקה לכל היותר n בתים מ-src ל-dst.
    // לא תגלוש מעבר ל-n בתים.
    strncpy(dst, src, n);
    

שימו לב ש-strncpy לא מבטיחה null terminator אם src ארוך מ-n. לכן מומלץ תמיד להוסיף:

strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';


פתרון תרגיל 5 - קנרי על המחסנית

  1. קמפלנו עם stack protector.

  2. עם קלט קצר ("hello") - התוכנית עובדת כרגיל:

    Enter text: hello
    You entered: hello
    

  3. עם קלט ארוך (50 תווים 'A') - התוכנית קורסת עם הודעה:

    Enter text: You entered: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    *** stack smashing detected ***: terminated
    Aborted
    

  4. ההודעה stack smashing detected אומרת שהקנרי נדרס. מה קרה:

  5. הקלט (50 תווים) גלש מעבר ל-buffer (16 בתים)
  6. הגלישה דרסה את הקנרי שנמצא בין buffer לבין saved rbp
  7. לפני שהפונקציה מבצעת ret, היא בודקת את ערך הקנרי
  8. הקנרי השתנה (נדרס על ידי 'A') -> התוכנית מזהה את ההתקפה ומבצעת abort

  9. מבנה המחסנית עם canary:

    כתובות גבוהות
    +---------------------------+
    | כתובת חזרה (8 בתים)      |
    +---------------------------+
    | saved rbp (8 בתים)        |
    +---------------------------+
    | CANARY (8 בתים)           |  <-- ערך אקראי שנבדק לפני ret
    +---------------------------+
    | buffer[0..15] (16 בתים)   |  <-- gets כותב לכאן
    +---------------------------+
    כתובות נמוכות (rsp)
    

  10. למה הקנרי מתחיל בבית null (\x00):

  11. רוב פונקציות המחרוזות (כמו strcpy, gets, printf עם %s) עוצרות כשמגיעות לבית null
  12. אם תוקף מנסה לקרוא את הקנרי (למשל על ידי הדפסה עם printf), הוא ייתקע בבית ה-null וימנע ממנו לקרוא את ערך הקנרי
  13. בלי null byte, תוקף יכול לגרום לתוכנה להדפיס את הקנרי (information leak), ואז לכתוב אותו בחזרה כשהוא דורס את המחסנית
  14. עם null byte בתחילת הקנרי, התוקף לא יכול לגלות את ערכו באמצעות פונקציות מחרוזת