בלבול טיפוסים מוסבר - Type Confusion¶
תוכנה, בבסיסה, היא הרבה מאוד בייטים בזיכרון, וההבדל בין "זה מספר" לבין "זה אובייקט מורכב עם מבנה פנימי" הוא בעצם רק פרשנות - איך שכבת התוכנה שמעל מחליטה לקרוא את הבייטים האלה. ברוב הזמן הפרשנות הזו נכונה ועקבית, אבל כשמצליחים לגרום לתוכנה לפרש בלוק זיכרון בצורה שגויה, נפתחת אחת המשפחות המעניינות והמסוכנות ביותר של חולשות - בלבול טיפוסים, או Type Confusion.
מה זה בעצם בלבול טיפוסים¶
בלבול טיפוסים קורה כשתוכנה מתייחסת לבלוק זיכרון מסוים כאילו הוא שייך לטיפוס אחד, בעוד שבפועל, ברגע הריצה, הוא בכלל מטיפוס אחר לגמרי. כל טיפוס נתונים במערכת - מספר, מחרוזת, אובייקט מורכב - מגיע עם ציפייה למבנה זיכרון מסוים: כמה בייטים הוא תופס, אילו שדות נמצאים באילו היסטים, ואיך לפרש כל חלק. כשהתוכנה מניחה שמדובר בטיפוס אחד, אבל בפועל הזיכרון מכיל טיפוס אחר עם מבנה שונה, כל קריאה או כתיבה שהיא מבצעת לפי ההנחה השגויה הזו יוצאת מהתחום שבאמת שייך לאובייקט.
זה שונה מבלבול פשוט של ערכים - הבעיה כאן היא לא "מספר שגוי", אלא פרשנות שגויה לגמרי של איך לקרוא נתח שלם של זיכרון, מה שיכול לחשוף שדות פנימיים, מצביעים, או אפילו כתובות בזיכרון שאסור למשתמש רגיל לראות בכלל.
למה זה נפוץ בפרט במנועי שפות דינמיות¶
שפות דינמיות כמו JavaScript מתהדרות בגמישות - משתנה יכול להתחיל כמספר ולהפוך למחרוזת, אובייקט יכול לקבל שדות חדשים תוך כדי ריצה, ומערך יכול לשנות את סוג האיברים שהוא מכיל. הגמישות הזו נהדרת למפתחים, אבל היא מטילה עומס עצום על מנוע השפה - Chrome עם V8, פיירפוקס עם SpiderMonkey, ספארי עם JavaScriptCore - שצריך לעקוב בזמן אמת אחרי הטיפוס האמיתי של כל אובייקט, ולפעמים אף לשנות את הייצוג הפנימי שלו כדי לשמור על ביצועים טובים.
בדיוק המורכבות הזו, של מעקב אחרי טיפוסים משתנים בזמן ריצה, יוצרת פתח לבאגים שבהם מנוע השפה "שוכח" לעדכן את המידע על הטיפוס האמיתי של אובייקט, או מבצע אופטימיזציה שמניחה הנחה שגויה על הטיפוס שלו. ברגע כזה, קוד שמצפה לאובייקט מסוג אחד עלול לקבל בפועל גישה לאובייקט ממש אחר, עם מבנה זיכרון שונה לגמרי.
הבעיה הדומה בקוד עם ריבוי-פנים¶
תופעה קרובה קיימת גם בשפות סטטיות יותר כמו C++, במיוחד בקוד שמשתמש בפולימורפיזם - היכולת של אובייקט ממחלקה אחת להתייחס אליו כאילו הוא ממחלקת אב שונה. כשקוד ממיר מצביע מטיפוס אחד לטיפוס אחר בלי לוודא שההמרה באמת חוקית ובטוחה, ואז מפרש את הזיכרון לפי הטיפוס החדש, נוצרת בדיוק אותה בעיית יסוד - קריאה של זיכרון לפי מבנה שלא תואם את מה שבאמת שם.
איך הפער בין הפרשנות למציאות נפתח לניצול¶
ברגע שתוקף מצליח לגרום לתוכנה להאמין שבלוק זיכרון מסוים הוא מטיפוס X, בעוד שבפועל הוא כתב לשם נתונים בפורמט של טיפוס Y, הוא בעצם משיג שליטה חלקית על איך התוכנה תפרש את הזיכרון הזה. אם אחד השדות שהתוכנה "בטוחה" לגביהם הוא בפועל מצביע שהתוקף שולט בערכו, פתאום אפשר לגרום לתוכנה לקרוא או לכתוב לכתובות זיכרון שלא היו אמורות להיות נגישות בכלל. זו הדרך שבה בלבול טיפוסים הופך מבאג תיאורטי לפרימיטיב ניצול ממשי - יכולת לקרוא ולכתוב זיכרון מעבר לגבולות שהתוכנית התכוונה לאפשר.
הקשר לחולשות JIT ודפדפנים¶
בלבול טיפוסים הוא אחד הכלים המרכזיים בארגז הכלים של מי שחוקר חולשות בדפדפנים, ובמיוחד באזורים שבהם קוד מתקמפל ומבצע אופטימיזציות תוך כדי ריצה. כשמנוע השפה מבצע הנחות אגרסיביות על טיפוסי משתנים כדי להריץ קוד מהר יותר, כל טעות קטנה בהנחות האלה יכולה להפוך לבלבול טיפוסים שמאפשר גישה בלתי מורשית לזיכרון. זו הסיבה שחברות הדפדפנים משקיעות משאבים כל כך גדולים בבדיקת ואימות הטיפוסים במנועי ה-JavaScript שלהן, ולמה חולשות מהסוג הזה נחשבות למבוקשות ומתוגמלות במיוחד בתוכניות גילוי חולשות.
איפה לומדים לחקור חולשות מהסוג הזה¶
בלבול טיפוסים הוא נושא שדורש הבנה טובה גם של איך שפות תכנות מייצגות נתונים בזיכרון, וגם של איך מנועים מודרניים עובדים מבפנים. בקורס מחקר חולשות אנחנו בונים את הבסיס הזה שלב אחר שלב, כדי שתוכלו להבין לא רק מה זו חולשה כזו, אלא איך בכלל מגיעים למצב שמזהים אותה.
לסיכום¶
בלבול טיפוסים קורה כשתוכנה מפרשת בלוק זיכרון לפי טיפוס שגוי, כזה שלא תואם את מה שבאמת נמצא שם. הבעיה הזו נפוצה בעיקר במנועי שפות דינמיות, שצריכים לעקוב אחרי טיפוסים משתנים בזמן ריצה, וגם בקוד עם פולימורפיזם שממיר מצביעים בין טיפוסים בלי בדיקה מספקת. הפער הזה בין מה שהתוכנה "חושבת" לגבי הזיכרון לבין מה שבאמת שם הוא בדיוק מה שהופך את הבאג הזה לכלי ניצול רב עוצמה.
יש לכם דוגמאות או שאלות בנושא? בואו לדבר על זה בקהילה.