לדלג לתוכן

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

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

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

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

    High addresses
    +---------------------------+
    | Return address (8 bytes)  |  <-- return address
    +---------------------------+
    | saved rbp (8 bytes)       |
    +---------------------------+
    | name[0..31] (32 bytes)    |  <-- gets writes here
    +---------------------------+
    Low addresses (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. מבנה המחסנית (לפי ההנחות שבתרגיל):

    High addresses
    +-------------------------------+
    | Return address (8 bytes)      |
    +-------------------------------+
    | saved rbp (8 bytes)           |
    +-------------------------------+
    | padding (8 bytes) for alignment |
    +-------------------------------+
    | x = 42 (4 bytes)              |
    +-------------------------------+
    | y = 17 (4 bytes)              |
    +-------------------------------+
    | buffer[0..127] (128 bytes)    |
    +-------------------------------+
    Low addresses (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 - גלישת buffer יכולה לדרוס את כתובת החזרה בלי שנזהה
  3. NX כבוי - אפשר להריץ קוד שנכתב על המחסנית
  4. אין PIE - כתובות הקוד קבועות ונשארות אותו דבר בכל הרצה

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

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

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


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

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

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

  1. fgets כחלופה בטוחה:
    // gets doesn't know the size of the buffer
    gets(buffer);
    
    // fgets receives the buffer size as a parameter and won't write beyond that size
    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: copies all of src to dst, with no limit.
    // if src is longer than dst - overflow!
    strcpy(dst, src);
    
    // strncpy: copies at most n bytes from src to dst.
    // won't overflow beyond n bytes.
    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:

    High addresses
    +---------------------------+
    | Return address (8 bytes)  |
    +---------------------------+
    | saved rbp (8 bytes)       |
    +---------------------------+
    | CANARY (8 bytes)          |  <-- random value checked before ret
    +---------------------------+
    | buffer[0..15] (16 bytes)  |  <-- gets writes here
    +---------------------------+
    Low addresses (rsp)
    

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

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