UndefinedBehaviorSanitizer

UndefinedBehaviorSanitizer‏ (UBSan) מבצע כלי למדידת ביצועים בזמן הידור כדי לבדוק סוגים שונים של התנהגות לא מוגדרת. מערכת UBSan מסוגלת לזהות הרבה באגים של התנהגות לא מוגדרת, אבל Android תומכת ב:

  • יישור
  • בוליאני
  • גבולות
  • טיפוסים בני מנייה (enum)
  • float-cast-overflow
  • float-divide-by-zero
  • integer-divide-by-zero
  • nonnull-attribute
  • null
  • חזרה לרצף השגרה
  • returns-nonnull-attribute
  • shift-base
  • shift-exponent
  • signed-integer-overflow
  • לא נגיש
  • unsigned-integer-overflow
  • vla-bound

הבעיה של overflow של מספר שלם ללא סימן (unsigned-integer-overflow) היא לא התנהגות לא מוגדרת מבחינה טכנית, אבל היא כלולה בסנן (sanitizer) ומשמשת במודולים רבים של Android, כולל הרכיבים של mediaserver, כדי למנוע נקודות חולשה סמויות של overflow של מספר שלם.

הטמעה

במערכת ה-build של Android, אפשר להפעיל את UBSan באופן גלובלי או מקומי. כדי להפעיל את UBSan ברמת ה-project, מגדירים את SANITIZE_TARGET בקובץ Android.mk. כדי להפעיל את UBSan ברמת המודול, מגדירים את LOCAL_SANITIZE ומציינים את ההתנהגויות הלא מוגדרות שרוצים לחפש ב-Android.mk. לדוגמה:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_CFLAGS := -std=c11 -Wall -Werror -O0

LOCAL_SRC_FILES:= sanitizer-status.c

LOCAL_MODULE:= sanitizer-status

LOCAL_SANITIZE := alignment bounds null unreachable integer
LOCAL_SANITIZE_DIAG := alignment bounds null unreachable integer

include $(BUILD_EXECUTABLE)

והתצורה המקבילה בתוכנית (Android.bp):

cc_binary {

    cflags: [
        "-std=c11",
        "-Wall",
        "-Werror",
        "-O0",
    ],

    srcs: ["sanitizer-status.c"],

    name: "sanitizer-status",

    sanitize: {
        misc_undefined: [
            "alignment",
            "bounds",
            "null",
            "unreachable",
            "integer",
        ],
        diag: {
            misc_undefined: [
                "alignment",
                "bounds",
                "null",
                "unreachable",
                "integer",
            ],
        },
    },

}

קיצורי דרך של UBSan

ל-Android יש גם שני מקשי קיצור, integer ו-default-ub, שמפעילים בו-זמנית קבוצה של מנקי קוד. המשתנה integer מפעיל את integer-divide-by-zero,‏ signed-integer-overflow ו-unsigned-integer-overflow. default-ub מפעיל את הבדיקות שיש להן בעיות ביצועים מינימליות במהלך הידור: bool, integer-divide-by-zero, return, returns-nonnull-attribute, shift-exponent, unreachable and vla-bound. אפשר להשתמש בכיתה של סניטרציית המספרים השלמים עם SANITIZE_TARGET ו-LOCAL_SANITIZE, אבל אפשר להשתמש ב-default-ub רק עם SANITIZE_TARGET.

דיווח משופר על שגיאות

הטמעת ברירת המחדל של UBSan ב-Android מפעילה פונקציה מסוימת כשמתרחשת התנהגות לא מוגדרת. כברירת מחדל, הפונקציה הזו היא abort. עם זאת, החל מאוקטובר 2016, ל-UBSan ב-Android יש ספריית זמן ריצה אופציונלית שמספקת דיווח מפורט יותר על שגיאות, כולל סוג ההתנהגות שלא מוגדרת שנתקלו בה, פרטי קובץ וקוד מקור. כדי להפעיל את הדיווח על השגיאות האלה באמצעות בדיקות של מספרים שלמים, מוסיפים את הטקסט הבא לקובץ Android.mk:

LOCAL_SANITIZE:=integer
LOCAL_SANITIZE_DIAG:=integer

הערך LOCAL_SANITIZE מפעיל את מנקה הקוד במהלך ה-build. LOCAL_SANITIZE_DIAG מפעיל את מצב האבחון של מנקה הנתונים שצוין. אפשר להגדיר ל-LOCAL_SANITIZE ול-LOCAL_SANITIZE_DIAG ערכים שונים, אבל רק הבדיקות ב-LOCAL_SANITIZE מופעלות. אם בדיקה לא צוינה ב-LOCAL_SANITIZE, אבל צוינה ב-LOCAL_SANITIZE_DIAG, הבדיקה לא מופעלת ולא מוצגות הודעות אבחון.

דוגמה למידע שסופק על ידי ספריית זמן הריצה של UBSan:

pixel-xl:/ # sanitizer-status ubsan
sanitizer-status/sanitizer-status.c:53:6: runtime error: unsigned integer overflow: 18446744073709551615 + 1 cannot be represented in type 'size_t' (aka 'unsigned long')

ניטרול של גלישה במספר שלם

זליגות לא מכוונות של מספרים שלמים עלולות לגרום לפגיעות בזיכרון או לחשיפת מידע במשתנים שמשויכים לגישה לזיכרון או להקצאות זיכרון. כדי להתמודד עם הבעיה הזו, הוספנו ל-Clang את UndefinedBehaviorSanitizer (UBSan) – מנגנון לסינון Overflow של מספרים שלמים חתימתים ולא חתימתים – כדי לחזק את מסגרת המדיה ב-Android 7.0. ב-Android 9, הרחבנו את UBSan כך שיכלול יותר רכיבים ושפרנו את התמיכה בו במערכת ה-build.

המטרה של הבדיקה הזו היא להוסיף בדיקות לפעולות אריתמטיות / הוראות – שעלולות לגרום לחריגה ממלאי הזיכרון – כדי לבטל תהליך בבטחה אם חריגה כזו מתרחשת. הכלי הזה יכול לצמצם סוג שלם של נקודות חולשה של פגיעה בזיכרון וחשיפת מידע, שבהן שורש הבעיה הוא זליגת מידע של מספר שלם, כמו נקודת החולשה המקורית של Stagefright.

דוגמאות ומקור

Integer Overflow Sanitization‏ (IntSan) מסופק על ידי המהדר ומוסיף מכשור לקובץ הבינארי במהלך זמן הידור כדי לזהות זליגות Arithmetic. הוא מופעל כברירת מחדל ברכיבים שונים בפלטפורמה, למשל /platform/external/libnl/Android.bp.

הטמעה

ב-IntSan נעשה שימוש ב-UBSan's signed and unsigned integer overflow sanitizers. אפשר להפעיל את הפעולה הזו ברמת המודול. הוא עוזר לשמור על האבטחה של רכיבים קריטיים של Android, ואין להשבית אותו.

מומלץ מאוד להפעיל את Integer Overflow Sanitization ברכיבים נוספים. המועמדים האידיאליים הם קוד מקורי בעל הרשאות או קוד מקורי שמנתח קלט לא מהימן של משתמשים. יש עלות ריבית קטנה על ביצועים שמשויכת למטהאן, והיא תלויה בשימוש בקוד ובשכיחות של פעולות אריתמטיות. צפוי אחוז קטן של תקורה, ותוכלו לבדוק אם הביצועים יהיו בעייתיים.

תמיכה ב-IntSan בקובצי make

כדי להפעיל את IntSan בקובץ makefile, מוסיפים את השורה הבאה:

LOCAL_SANITIZE := integer_overflow
    # Optional features
    LOCAL_SANITIZE_DIAG := integer_overflow
    LOCAL_SANITIZE_BLOCKLIST := modulename_BLOCKLIST.txt
  • הפונקציה LOCAL_SANITIZE מקבלת רשימה של מנקי נתונים (sanitizers) מופרדים בפסיקים, כאשר integer_overflow היא קבוצה של אפשרויות ארוזות מראש למנקי נתונים נפרדים של מספרים שלמים בעלי סימן ומספרים שלמים ללא סימן עם רשימת ברירת מחדל לחסימה.
  • LOCAL_SANITIZE_DIAG מפעיל את מצב האבחון של חומרי החיטוי. צריך להשתמש במצב אבחון רק במהלך בדיקה, כי הוא לא יבטל במקרה של זליגה, וכך יבטל לחלוטין את יתרון האבטחה של הפחתת הסיכון. פרטים נוספים זמינים במאמר פתרון בעיות.
  • LOCAL_SANITIZE_BLOCKLIST מאפשר לציין קובץ של רשימת חסימה כדי למנוע טיהור של פונקציות וקבצי מקור. פרטים נוספים זמינים במאמר פתרון בעיות.

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

LOCAL_SANITIZE := signed-integer-overflow, unsigned-integer-overflow
    LOCAL_SANITIZE_DIAG := signed-integer-overflow, unsigned-integer-overflow

תמיכה ב-IntSan בקובצי שרטוטים

כדי להפעיל ניטרול של זליגת נתונים של מספר שלם בקובץ תוכנית, כמו /platform/external/libnl/Android.bp, מוסיפים את הקוד הבא:

   sanitize: {
          integer_overflow: true,
          diag: {
              integer_overflow: true,
          },
          BLOCKLIST: "modulename_BLOCKLIST.txt",
       },

בדומה לקובצי make, המאפיין integer_overflow הוא קבוצה של אפשרויות ארוזות מראש לסנני Overflow של מספרים שלמים חתימתים ולא חתימתים, עם רשימת ברירת מחדל של חסומים.

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

המאפיין BLOCKLIST מאפשר לציין קובץ של רשימת חסימה שמאפשר למפתחים למנוע טיהור של פונקציות וקבצי מקור. פרטים נוספים זמינים במאמר פתרון בעיות.

כדי להפעיל את חומרי החיטוי בנפרד, משתמשים באפשרויות הבאות:

   sanitize: {
          misc_undefined: ["signed-integer-overflow", "unsigned-integer-overflow"],
          diag: {
              misc_undefined: ["signed-integer-overflow",
                               "unsigned-integer-overflow",],
          },
          BLOCKLIST: "modulename_BLOCKLIST.txt",
       },

פתרון בעיות

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

כדי למצוא ביטולים שנגרמו על ידי ניטרול ב-builds של משתמשים, מחפשים קריסות של SIGABRT עם הודעות Abort שמציינות זליגה שנלכדה על ידי UBSan, כמו:

pid: ###, tid: ###, name: Binder:###  >>> /system/bin/surfaceflinger <<<
    signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
    Abort message: 'ubsan: sub-overflow'

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

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

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp:2188:32: runtime error: unsigned integer overflow: 0 - 1 cannot be represented in type 'size_t' (aka 'unsigned long')

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

  • שינוי הקוד כדי למנוע את החריגה ממלאי הזיכרון (דוגמה)
  • Overflow מפורש באמצעות הפונקציות __builtin_*_overflow של Clang (דוגמה)
  • השבתת ניטרול הנתונים בפונקציה על ידי ציון המאפיין no_sanitize (דוגמה)
  • השבתת טיהור של פונקציה או קובץ מקור באמצעות קובץ BLOCKLIST (דוגמה)

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

דפוסים נפוצים שעשויים לגרום לחריגות קטנות בנתונים כוללים:

  • המרות סמויות שבהן מתרחש זליגת נתונים (overflow) ללא סימן לפני ההמרה לסוג עם סימן (דוגמה)
  • מחיקה של רשימות מקושרות שמפחיתה את אינדקס הלולאה במהלך המחיקה (דוגמה)
  • הקצאת סוג ללא סימן ל--1 במקום לציין את הערך המקסימלי בפועל (דוגמה)
  • לולאות שמפחיתות מספר שלם ללא סימן בתנאים (דוגמה, דוגמה)

לפני השבתת הניקוי, מומלץ שהמפתחים יבטיחו שבמקרים שבהם הכלי לאיתור שגיאות מזהה זליגה, היא אכן לא מזיקה ואין לה השפעות לוואי לא רצויות או השלכות על האבטחה.

השבתת IntSan

אפשר להשבית את IntSan באמצעות רשימות חסימה או מאפייני פונקציה. מומלץ להשבית רק במקרים נדירים, ורק אם אי אפשר לשפר את הקוד בדרך אחרת או אם יש עלות ריבית מופרזת על הביצועים.

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

אימות

בשלב זה, אין בדיקת CTS ספציפית לטיהור של Overflow של מספר שלם. במקום זאת, מוודאים שבדיקות CTS עוברות עם הפעלת IntSan או בלי הפעלת IntSan, כדי לוודא שהיא לא משפיעה על המכשיר.

חיטוי של גבולות

BoundsSanitizer‏ (BoundSan) מוסיף מכשירי מדידה לקובצי הבינארי כדי להוסיף בדיקות של גבולות סביב הגישה למערך. הבדיקות האלה מתווספות אם המהדר לא יכול להוכיח בזמן הידור שהגישה תהיה בטוחה, ואם גודל המערך יהיה ידוע בזמן הריצה, כדי שניתן יהיה לבדוק אותו. ב-Android 10, BoundSan מופעל ב-Bluetooth ובקודקים. BoundSan מסופק על ידי המהדר ומופעל כברירת מחדל ברכיבים שונים בפלטפורמה.

הטמעה

BoundSan משתמש במסנן גבולות של UBSan. אפשר להפעיל את הפעולה הזו ברמת המודול. הוא עוזר לשמור על אבטחת הרכיבים הקריטיים של Android, ואין להשבית אותו.

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

הפעלת BoundSan בקובצי תוכנית

כדי להפעיל את BoundSan בקובצי תוכנית, מוסיפים את הערך "bounds" למאפיין misc_undefined של ניקוי קוד עבור מודולים בינאריים וספריות:

    sanitize: {
       misc_undefined: ["bounds"],
       diag: {
          misc_undefined: ["bounds"],
       },
       BLOCKLIST: "modulename_BLOCKLIST.txt",
diag

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

רשימת חסימה

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

הפעלת BoundSan בקובצי make

כדי להפעיל את BoundSan בקובצי make, מוסיפים את הערך "bounds" למשתנה LOCAL_SANITIZE של המודולים הבינאריים והספריות:

    LOCAL_SANITIZE := bounds
    # Optional features
    LOCAL_SANITIZE_DIAG := bounds
    LOCAL_SANITIZE_BLOCKLIST := modulename_BLOCKLIST.txt

LOCAL_SANITIZE מקבל רשימה של מנקי נתונים שמופרדים בפסיקים.

LOCAL_SANITIZE_DIAG מפעיל את מצב האבחון. משתמשים במצב אבחון רק במהלך בדיקה. במצב אבחון לא מתבצעת ביטול משימות במקרה של זליגה, מה שמבטל את יתרון האבטחה של הפחתת הסיכון ומגדיל את זמן האחזור של הביצועים. לכן, לא מומלץ להשתמש במצב הזה לגרסאות build בסביבת הייצור.

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

השבתת BoundSan

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

מידע נוסף על השבתת BoundSan באמצעות מאפייני פונקציה ופורמט קובץ של רשימת החסימות זמין במסמכי התיעוד של Clang LLVM. כדי למנוע השפעה על סניטרים אחרים, כדאי להשתמש בשמות קטעים שמציינים את הסניטר הספציפי שרוצים להוסיף ל-BLOCKLIST.

אימות

אין בדיקת CTS ספציפית ל-BoundSan. במקום זאת, צריך לוודא שבדיקות CTS עוברות עם או בלי הפעלת BoundSan, כדי לוודא שהיא לא משפיעה על המכשיר.

פתרון בעיות

אחרי שמפעילים את BoundSan, צריך לבדוק היטב את הרכיבים כדי לוודא שכל הגישה מחוץ למגבלות שלא זוהו בעבר מטופלת.

קל לזהות שגיאות BoundSan כי הן כוללות את הודעת הביטול הבאה של tombstone:

    pid: ###, tid: ###, name: Binder:###  >>> /system/bin/foobar <<<
    signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
    Abort message: 'ubsan: out-of-bounds'

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

    external/foo/bar.c:293:13: runtime error: index -1 out of bounds for type 'int [24]'