מה זה GOT ו-PLT, ולמה הן יעד תקיפה קלאסי¶
כל תוכנית שכותבים ב-C משתמשת בפונקציות מספריות חיצוניות - printf, malloc, strcpy. השאלה המעניינת היא, איך התוכנית בכלל יודעת איפה בזיכרון נמצאת הפונקציה הזאת, אם היא לא חלק מהקוד שלה? התשובה נמצאת בשני מבנים שכל חוקר חולשות חייב להכיר - GOT ו-PLT.
הבעיה שהם פותרים¶
כשמקמפלים תוכנית שמשתמשת בספריה משותפת (shared library) כמו glibc, הקוד המקומפל לא יודע מראש באיזו כתובת זיכרון הפונקציות של הספריה יישבו. הכתובת הזאת נקבעת רק כשהתוכנית נטענת לזיכרון בזמן ריצה, ובגלל ASLR היא אפילו משתנה בכל הרצה מחדש. אז איך הקוד קופץ לכתובת שהוא לא יכול לדעת מראש בזמן הקומפילציה?
איך PLT ו-GOT עובדים ביחד¶
PLT - Procedure Linkage Table. זהו קטע קוד קטן שקיים בתוך הבינארי עצמו, בשביל כל פונקציה חיצונית שהתוכנית קוראת לה. כשהתוכנית "קוראת" ל-printf, בפועל היא לא קופצת ישירות לספריה, אלא לקטע ה-PLT המתאים.
GOT - Global Offset Table. זוהי טבלה של כתובות בזיכרון, שאמורה להכיל את הכתובת האמיתית של כל פונקציה חיצונית. קטע ה-PLT שהוזכר קופץ בפועל לכתובת שכתובה בתוך ה-GOT.
השילוב בין השניים מאפשר מנגנון חכם שנקרא Lazy Binding - בפעם הראשונה שקוראים לפונקציה מסוימת, ה-GOT עדיין לא מכיל את הכתובת האמיתית שלה, אז ה-PLT קופץ בחזרה למנגנון הקישור הדינמי (linker), שמאתר את הכתובת האמיתית של הפונקציה, כותב אותה לתוך ה-GOT, וקופץ אליה. בפעם השנייה שקוראים לאותה פונקציה, ה-GOT כבר מכיל את הכתובת הנכונה, וה-PLT קופץ ישירות אליה בלי לעבור שוב דרך הלינקר. ככה נחסך זמן טעינה מיותר לפונקציות שאף פעם לא נקראות.
למה זה מעניין תוקפים¶
טבלת ה-GOT היא בעצם רשימה של כתובות שהתוכנית סומכת עליה בעיוורון, ומקפיצה אליהן בכל פעם שקוראים לפונקציה מתאימה. אם תוקף מוצא דרך לכתוב לזיכרון (למשל דרך חולשת מחרוזת פורמט או גלישת חוצץ מדויקת), אחת המטרות האטרקטיביות ביותר היא לדרוס ערך בתוך ה-GOT. אם אתם דורסים את הערך של printf בטבלה ומחליפים אותו בכתובת של פונקציה אחרת, כל קריאה עתידית ל-printf תקפוץ במקום זאת לפונקציה שבחרתם - כולל, במקרים מסוימים, קפיצה לקוד שאתם שולטים בו.
זו טכניקה שנקראת GOT overwrite, והיא הייתה כלי סטנדרטי בארגז הכלים של חוקרי חולשות במשך שנים רבות.
למה זה קשה יותר היום¶
בגלל שהמנגנון הזה כל כך רגיש, פותחה הגנה בשם RELRO (Relocation Read-Only). ב-Full RELRO, כל טבלת ה-GOT מסומנת כקריאה בלבד מיד אחרי שהתוכנית נטענת, כך שאין בכלל Lazy Binding, וכל הכתובות מיושבות מראש. זה הופך GOT overwrite לבלתי אפשרי, כי אין דרך לכתוב לטבלה בכלל. ב-Partial RELRO, שנפוץ יותר בגלל שיקולי ביצועים, רק חלק מהמבנים מוגנים, ולפעמים עדיין אפשר לתקוף.
זו הסיבה שאחד הצעדים הראשונים בניתוח בינארי הוא בדיקת ה-RELRO שלו עם כלי כמו checksec, בדיוק כמו שבודקים NX ו-stack canary.
איך זה מתחבר לשאר עולם הניצול¶
הבנת GOT ו-PLT היא לא רק ידע תיאורטי, היא הבסיס להבין למה חוקרי חולשות כל כך מתעניינים בדליפת כתובות מזיכרון. ברגע שמבינים שה-GOT הוא בעצם מפת ניווט של התוכנית, קל הרבה יותר להבין למה דליפת כתובת אחת בזיכרון יכולה לשבור לגמרי הגנה כמו ASLR.
בקורס מחקר חולשות אנחנו מסבירים את מנגנון הקישור הדינמי לעומק, כי בלי להבין אותו קשה מאוד להבין מה בדיוק קורה כשמנצלים חולשות כתיבה שרירותית בזיכרון.
יש לכם שאלה על checksec או על ניתוח מנגנון RELRO בבינארי ספציפי? קפצו לדיסקורד שלנו ותשאלו.
לסיכום¶
GOT ו-PLT הם המנגנון ששולח כל קריאה לפונקציה חיצונית אל היעד הנכון בזמן ריצה, ובדיוק בגלל זה הם הופכים ליעד כל כך אטרקטיבי לתקיפה. הבנה טובה שלהם היא מפתח להבנת חולשות כתיבה שרירותית, ולמה RELRO היא הגנה כל כך חשובה בעולם מחקר החולשות המודרני.