חולשות גלישת מספרים שלמים מוסברות - Integer Overflow¶
לפני שמדברים על ניצול חולשות מתוחכם, שווה לעצור רגע על אחת הבעיות הכי בסיסיות ובכל זאת הכי נפוצות בעולם התכנות המערכתי - מה קורה כשמספר "נגמר לו המקום". זו לא בעיה תיאורטית. חלק לא קטן מהחולשות החמורות ביותר שהתגלו במערכות הפעלה, דפדפנים ותוכנות רשת, מקורן בדיוק בבעיה הפשוטה הזו, שנקראת גלישת מספרים שלמים, או Integer Overflow.
מה זה בעצם גלישת מספר שלם¶
כל משתנה מספרי בתוכנה תפוס במקום קבוע בזיכרון, וכתוצאה מכך יש לו טווח ערכים מוגבל. משתנה מסוג unsigned char, לדוגמה, יכול להכיל ערכים בין 0 ל-255 בלבד, כי יש לו בדיוק שמונה ביטים לעבוד איתם. השאלה המעניינת היא מה קורה כשמנסים לחרוג מהטווח הזה - למשל לקחת משתנה כזה שערכו כרגע 255, ולהוסיף לו 1.
באופן אינטואיטיבי היינו מצפים לשגיאה, אבל ברוב שפות התכנות ברמה נמוכה קורה משהו אחר לגמרי: הערך פשוט "מתגלגל" בחזרה להתחלה, וחוזר להיות 0. זו בדיוק המשמעות של גלישה - החישוב לא נכשל, הוא פשוט מחזיר תוצאה שגויה לחלוטין מבחינה מתמטית, בלי שום אזהרה או שגיאה שמעידה שמשהו השתבש.
ההבדל בין גלישה עם סימן לגלישה בלי סימן¶
משתנים בתכנות יכולים להיות עם סימן (signed), כלומר יכולים לייצג גם מספרים שליליים, או בלי סימן (unsigned), כלומר מייצגים רק ערכים חיוביים כולל אפס. ברמה מושגית, ההתנהגות בגלישה שונה בין השניים. במשתנה בלי סימן, כפי שראינו, גלישה למעלה פשוט "מתגלגלת" בחזרה לאפס. במשתנה עם סימן, כשהחישוב חורג מהערך המקסימלי החיובי, התוצאה יכולה "לקפוץ" באופן לא צפוי לצד השלילי של הטווח - כלומר חישוב שאמור להיות תוצאה גדולה וחיובית, הופך פתאום למספר שלילי.
ההבדל הזה חשוב במיוחד כי תוכנה רבה מניחה, בטעות, שתוצאה של חיבור או כפל של שני מספרים חיוביים תמיד תהיה חיובית וגדולה יותר. כשההנחה הזו נשברת בגלל גלישה, כל הלוגיקה שנבנתה עליה מתמוטטת.
למה זה שונה מגלישת חוצץ קלאסית¶
חשוב להבחין בין הבעיה הזו לבין גלישת חוצץ (Buffer Overflow) הקלאסית, שהיא משפחה נפרדת לגמרי. גלישת חוצץ קורית כשכותבים יותר נתונים ממה שיש מקום בזיכרון, ופוגעים בזיכרון שמעבר לגבול המוקצה. גלישת מספרים שלמים, לעומת זאת, היא בעיה חשבונית טהורה - החישוב עצמו נותן תוצאה שגויה. הקשר בין השתיים מגיע רק בהמשך: לעיתים קרובות גלישת מספרים היא בדיוק מה שגורם לחישוב גודל שגוי, שבתורו מוביל לגלישת חוצץ בפועל.
למה זה מסוכן בהקשר אבטחה¶
הבעיה האמיתית מתחילה כשגלישת מספרים משפיעה על החלטה קריטית בתוכנה, ובפרט על חישוב גודל של מקום בזיכרון. תארו לעצמכם תוכנה שמקבלת מהמשתמש כמות פריטים, ומחשבת כמה זיכרון להקצות על ידי הכפלת הכמות בגודל של פריט בודד. אם המשתמש (או תוקף) יצליח לגרום לחישוב הזה לגלוש, התוצאה של החישוב תהיה קטנה בהרבה ממה שהיא אמורה להיות - למשל, במקום להקצות מיליוני בייטים, התוכנה תקצה בטעות רק כמה עשרות.
עכשיו, אם התוכנה ממשיכה ומעתיקה לתוך אותו מקום קטן את כל הנתונים לפי הכמות המקורית שהמשתמש ביקש, בלי לשים לב שההקצאה עצמה כבר הייתה שגויה, נוצר מצב שבו כותבים הרבה יותר מידע ממה שבאמת יש מקום בשבילו. זו בדיוק נקודת המפגש בין גלישת מספרים שלמים לבין חולשות זיכרון אחרות - הגלישה במספר היא לא הפגיעה עצמה, אלא הטריגר שגורם לבעיה חמורה בהרבה בהמשך.
דוגמה מושגית קלאסית¶
חשוב להדגיש - זו דוגמה להמחשת העיקרון בלבד, לא מתכון לניצול. נניח פונקציה שמקבלת מספר פריטים כערך unsigned בגודל קטן, ומכפילה אותו בגודל קבוע כדי לדעת כמה זיכרון להקצות. אם התוקף מצליח להעביר מספר פריטים גדול מספיק, כך שהתוצאה של הכפל חורגת מהטווח של המשתנה, ההקצאה בפועל תהיה קטנה בהרבה מהצפוי. כל שאר הקוד ימשיך לפעול לפי ההנחה ש"הכל תקין", ופה בדיוק נפתח הפתח לבעיה חמורה - הבדל בין הגודל שהוקצה בפועל לבין הגודל שהתוכנה חושבת שהוקצה.
איך מתגוננים מפני גלישת מספרים שלמים¶
ההגנה מפני הבעיה הזו מתחילה בזהירות פשוטה בקוד. מפתחים צריכים לבדוק גבולות באופן מפורש לפני ביצוע חישובים קריטיים, ולוודא שתוצאה של חיבור או כפל לא חורגת מהטווח האפשרי לפני שמשתמשים בה להחלטות כמו הקצאת זיכרון. שפות תכנות וספריות מודרניות מספקות היום פונקציות ייעודיות שמזהות גלישה בזמן החישוב עצמו ומחזירות שגיאה במקום תוצאה שגויה שקטה, ושימוש בהן במקומות רגישים הוא הרגל טוב וחשוב.
מעבר לזה, כלים אוטומטיים לבדיקת קוד וסביבות בדיקה שמריצות תוכנה עם ערכי קלט קיצוניים בכוונה, יכולים לאתר הרבה מהמקרים האלה עוד לפני שהתוכנה מגיעה לייצור. זו אחת הסיבות שבדיקות גבול הן חלק קבוע מכל תהליך פיתוח רציני.
המשך הלימוד בתחום¶
גלישת מספרים שלמים היא רק אחת מהמשפחות הבסיסיות שכל חוקר חולשות צריך להכיר, והיא לרוב שלב ראשון בשרשרת שמובילה לחולשות זיכרון חמורות בהרבה. אם התחום הזה מדבר אליכם, בקורס מחקר חולשות אנחנו בונים את ההבנה הזו שלב אחר שלב, מהיסודות ועד לחולשות מורכבות יותר.
לסיכום¶
גלישת מספרים שלמים קורית כשתוצאה חשבונית חורגת מהטווח שמשתנה יכול להכיל, וגורמת לה "להתגלגל" לערך שגוי לגמרי, בלי אזהרה ובלי שגיאה. כשהתוצאה השגויה הזו משפיעה על החלטות כמו גודל הקצאת זיכרון, נפתח פתח לחולשות הרבה יותר חמורות. ההגנה פשוטה יחסית - בדיקות גבולות ושימוש בכלים שמזהים גלישה - אבל היא דורשת מודעות מתמדת מכל מי שכותב קוד ברמה נמוכה.
בואו לדבר על זה בקהילה - יש שם חוקרים שישמחו לדון בדוגמאות נוספות ובשאלות שעולות לכם.