מנגנון retguard ב - OpenBSD

$ datediff 'july 11 2018'
707 days ago
$  bc -le "707/365" -e quit
1.936...

אני עדיין משתמש ב - OpenBSD כמערכת הראשית שלי ואני מרוצה. למעשה הסיבה העיקרית שאני משתמש בה היא שהיא הרבה יותר פשוטה מ - Linux, ומערכות פשוטות קל יותר להבין. למדתי שכאשר אני מוגבל למערכת פשוטה, הפתרונות שלי לבעיות במערכת הרבה יותר יצירתיות, מפני שהאפשרויות שלי מוגבלות. אני תמיד יכול לכתוב עוד קוד שיפתור לי בעיה מסוימת, אבל לפני שאני עושה את זה אני שואל את עצמי איך אני יכול לפתור את הבעיה בשימוש בכלים קיימים, ולרוב אני מוצא פתרון אלגנטי. בפוסט הנוכחי אני יסביר כיצד מפתחי OpenBSD מצאו פתרון פשוט לבעיה שנראתה מורכבת.

retguard הינו מנגנון להגנה על כתובת החזרה בתכנה, לפני שאני יספר לכם כיצד הוא עובד, אני אתחיל בלהסביר למה צריך להגן על כתובת החזרה בתוכנה.

היסטוריה

בשנות ה - 70 של המאה הקודמת, בתחילת מהפכת המחשבים האישיים האנשים שתיכננו את הארכיטקטורות של המעבדים הפופולרים 8086 ו - 6502 לא חשבו על אבטחה יותר מדי. אחרי הכל המטרה היתה ליצור מעבד יעיל בעלות נמוכה שגם משתמשים פרטים יוכלו לרכוש. רשת האינטרנט לא היתה קיימת כמובן ומחשבים יועדו בעיקר לשימוש בידי אנשים עם רקע טכני. ההנחה היתה שמי שהשתמש במחשב באותה תקופה גם ידע לכתוב קוד. כל מחשב היה בא עם מדריך תכנות והמשתמשים היו כותבים את התוכנות בעצמם בשפות כמו - Assembly או BASIC. למשתמש היתה גישה לזכרון המחשב באופן ישיר, הוא יכל לשנות ערכים בזכרון, לטעון קוד ולהריץ אותו ללא מגבלות קריטיות. עם התפתחות הטכנולוגיה מחשבים הפכו פשוטים יותר לשימוש, מערכות הפעלה עם ממשק גרפי החלו לצוץ וכתוצאה מכך גם אנשים ללא רקע טכני החלו להשתמש במחשבים. לצד מהפכת המחשבים האישיים התפתחה גם תרבות ה - hacking שאנו מכירים כיום. ווירוסים התחילו לצוץ בתחילת שנות ה- 80, בתחילה הם לא גרמו נזק למחשבים ושימשו בעיקר למתיחות, אך הם המחישו את פוטנציאל הנזק שניתן לעשות עם תכנה זדונית. הרבה מאוד חברות השתמשו במחשבים לצרכים שונים, ופריצה לאותם מחשבים יכלה לגרום להפסדים גדולים. דרישה חדשה נוצרה: אבטחה. כיצד מאבטחים מחשב עם ארכיטקטורה שלא תוכננה להיות מאובטחת? התשובה המידית היא בתוכנה, אך ההשלכות של זה כמובן היו בביצועים. אלו היו זמנים טובים למי שרצה לפרוץ למחשבים. הזכרון היה קבוע קוד יכל לרוץ מכל מקום וההגנות היו מועטות מאוד. רק בתחילת שנות ה - 80 מעבדים החלו לממש מנגנוני הגנה בחומרה, 80286 היה המעבד הראשון מבית אינטל שתמך ב - protected mode ו - privilege levels. חולשות רבות באותה תקופה (וגם כיום) ניצלו את העובדה שבארכיטקטורות מסוימות, בעת קריאה לפונקציה כתובת החזרה נשמרת במחסנית.

כתובת החזרה

כאשר פונקציה בתכנה כלשהי נקראת, התכנה שומרת את הכתובת לחזרה במחסנית. כתובת החזרה היא הכתובת בזכרון שמצביעה להוראה הבאה לביצוע מיד לאחר הפונקציה הנקראת. לדוגמה:

    1 _start:
    2   call my_function
    3
    4   pushq   %rax
    5   ; do stuff..
    6
...
 exit:
    xorq    %rdi, %rdi
    ; call sys_exit

my_function:
    movq    $5, %rax
    retq

בקוד הבא כתובת החזרה שתשמר במחסנית תצביע לקוד שנמצא בשורה 4, ההוראה call דוחפת את כתובת החזרה למחסנית וקופצת אל my_function, בסוף ביצוע הפונקציה ההוראה ret שולפת את כתובת החזרה מהמחסנית וקופצת אליה. זהו מנגנון פשוט שמאפשר להריץ את התכנה שלנו בצורה רציפה.

פעילות תקינה של התכנית שלנו תלויה בכך שהערכים שבמחסנית לא ישתנו באופן בלתי צפוי. במחסנית בין היתר נשמרים משתנים מקומיים (אוטומטים), ואנחנו לא רוצים שהם ישתנו באופן לא צפוי במהלך ריצת התכנית שלנו, כמו כן אנחנו רוצים שכתובת החזרה תצביע למיקום הנכון כאשר הפונקציה שלנו סיימה לרוץ. אך מה יקרה אם תוקף ישנה את כתובת החזרה במחסנית לכתובת אחרת לפני שההוראה ret תוציא אותה מהמחסנית? או ישנה את הערך של משתנה כלשהו בפונקציה? ניחשתם נכון, התכנית שלנו תתנהג באופן בלתי צפוי. הסיבה שמחשבים משרתים אותנו נאמנה היא שהם צפויים. אנחנו אומרים להם מה לעשות ועם עושים, אנחנו לא יכולים לכתוב קוד שיטפל בקלט לא ידוע. הקוד שלנו נכתב בהתאם להתנהגות ידועה מראש.

הרבה מאוד חולשות מנצלות את העובדה שניתן "לדרוס" את כתובת החזרה. למרות שבחומרה ותכנה (מערכת הפעלה) מודרנים ישנם מנגנונים שמקשים להריץ קוד זדוני זוהי עדיין נקודת תורפה. בכדי למנוע את זאת, אנחנו צריכים לפתח מנגנון שיוודא שכתובת החזרה לא השתנתה בתוך פונקציה. אבל איך נעשה את זה? אפשרות אחת היא לא להשתמש ב - call ו - ret כאשר אנו קוראים לפונקציה שעלולה להיות מסוכנת, אך פתרון שכזה אינו פרגמטי מפני שהוא שובר את הצורה שבה שהארכיטקטורה עובדת. ישנן דרכים שונות לפתרון הבעיה הזאת, אחת מהם הוא מנגנון פשוט בשם retguard שממומש במערכת ההפעלה OpenBSD כך הוא עובד (הסבר בהמשך):

 1 mov    __retguard_1899(%rip),%r11
 2 xor    (%rsp),%r11
 3 push   %rbp
 4 mov    %rsp,%rbp
 5 push   %r11

; function code here

10 pop    %r11
11 pop    %rbp
12 xor    (%rsp),%r11
13 cmp    __retguard_1899(%rip),%r11
14 je     0x1912 <hello+63>
15 int3
...
17 retq

בארכיטקטורת 8086 בעת כניסה לפונקציה מתבצעים מספר דברים: התכנית מכינה את המחסנית - היא שומרת את ה - stack frame pointer, מקצה מקום למשתנים מקומיים (ב - System V AMD64 ABI זה לא תמיד יהיה המקרה ראו Redzone) ומגדירה stack pointer חדש שיחסי למסגרת המחסנית הנוכחית. כתובת החזרה נמצאת בראש המחסנית.

בשורה 1 אנחנו מעתיקים לתוך הרגיסטר %r11 תוכן רנדומלי שמיוצר על ידי מערכת הפעלה התוכן הרנדומלי ישמש "כמפתח הצפנה" לכתובת החזרה. בשורה 2 אנחנו "מצפינים את כתובת החזרה עם מפתח ההצפנה, באמצעות פעולת XOR פשוטה. בשורות 3-5 אנחנו שומרים את ה - stack frame pointer הקודם במחסנית, מגדירים stack frame pointer חדש ושומרים את כתובת החזרה המוצפנת במחסנית.

לאחר שהפונקציה סיימה את פעולתה, אנחנו בודקים אם כתובת החזרה השתנה. בשורות 10-11 אנחנו מוצאים את כתובת החזרה המוצפנת מהמחסנית ומשחזרים את ה - stack frame pointer הקודם. עכשיו ראש המחסנית מצביע על כתובת החזרה. בשורה 12 אנחנו מבצעים פעולת XOR לכתובת החזרה המוצפנת וכתובת החזרה שנמצאת בראש המחסנית (שאומרה להיות הכתובת המקורית), אם הכל עבר בשלום הרגיסטר %r11 אמור להכיל את התוכן הרנדומלי המקורי שמערכת ההפעלה סיפקה לנו, אחרת זה אומר שכתובת החזרה השתנתה במהלך ריצת הפונקציה והתכנית תסתיים מיד - פשוט.

סדר הפעולת בצורה מופשטת (pseudocode):

in:
    r11 := random_data
    return_addr ^ r11

out:
    return_addr ^ r11
    when r11 is random_data
        return
    else
        abort

כמובן שהפתרון הנ"ל אינו מושלם והוא מוסיף overhead לכל פונקציה, אך הוא עובד. ב - OpenBSD ביצועים הם לא בראש סדר העדיפויות. מה יותר חשוב ביצועים או אבטחה? אנחנו צריכים למצוא איזון בין השנים, יותר מדי אבטחה תגרום לכך שהמערכת שלנו לא תהיה פנקציונלית. בעולם האמיתי אין שחור או לבן. אתם מעדיפים מערכת מאובטחת יותר או מהירה יותר? זה סוביקטיבי. בכל מקרה אני לא אכנס לנושא הזה בפוסט הנוכחי, אבל אני בהחלט אבדוק בעתיד בכמה הביצועים נפגעים עם retguard. מי שמעוניין לקרוא קצת יותר על המנגנון אני ממליץ להתחיל מפה.