בקרה על תקינות זרימת הנתונים

החל משנת 2016, כ-86% מכל הפרצות ב-Android קשורות לבטיחות הזיכרון. רוב נקודות החולשה מנוצלות על ידי תוקפים שמשנים את זרימת הבקרה הרגילה של אפליקציה כדי לבצע פעולות זדוניות שרירותיות עם כל ההרשאות של האפליקציה שנפרצה. שלמות זרימת הבקרה (CFI) היא מנגנון אבטחה שמונע שינויים בגרף המקורי של זרימת הבקרה של קובץ בינארי שעבר קומפילציה, וכך מקשה מאוד על ביצוע מתקפות כאלה.

ב-Android 8.1, הפעלנו את ההטמעה של CFI ב-LLVM במערך המדיה. ב-Android 9, הפעלנו CFI בעוד רכיבים וגם בקרנל. התכונה CFI של המערכת מופעלת כברירת מחדל, אבל צריך להפעיל את התכונה CFI של ליבת המערכת.

ה-CFI של LLVM מחייב קומפילציה עם אופטימיזציה בזמן הקישור (LTO). האופטימיזציה בזמן הקישור שומרת על ייצוג הביטקוד של LLVM של קובצי אובייקט עד לזמן הקישור, מה שמאפשר לקומפיילר להבין טוב יותר אילו אופטימיזציות אפשר לבצע. הפעלת LTO מקטינה את הגודל של הקובץ הבינארי הסופי ומשפרת את הביצועים, אבל מגדילה את זמן ההידור. בבדיקות ב-Android, השילוב של LTO ו-CFI מוביל לתקורה זניחה בגודל הקוד ובביצועים. במקרים מסוימים, שניהם השתפרו.

לפרטים טכניים נוספים על CFI ועל אופן הטיפול בבדיקות אחרות של בקרת העברה, אפשר לעיין במסמכי התיעוד של LLVM.

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

ה-CFI מסופק על ידי הקומפיילר ומוסיף מכשור לבינארי במהלך זמן הקומפילציה. אנחנו תומכים ב-CFI בשרשרת הכלים של Clang ובמערכת הבנייה של Android ב-AOSP.

ה-CFI מופעל כברירת מחדל במכשירי Arm64 עבור קבוצת הרכיבים ב-/platform/build/target/product/cfi-common.mk. היא מופעלת גם ישירות בקובצי makefile או בקובצי blueprint של קבוצת רכיבי מדיה, כמו /platform/frameworks/av/media/libmedia/Android.bp ו-/platform/frameworks/av/cmds/stagefright/Android.mk.

הטמעה של CFI במערכת

התכונה CFI מופעלת כברירת מחדל אם משתמשים ב-Clang ובמערכת הבנייה של Android. התכונה CFI עוזרת לשמור על הבטיחות של משתמשי Android, ולכן לא מומלץ להשבית אותה.

למעשה, מומלץ מאוד להפעיל CFI לרכיבים נוספים. המועמדים האידיאליים הם קוד מקורי עם הרשאות או קוד מקורי שמבצע עיבוד של קלט משתמש לא מהימן. אם אתם משתמשים ב-clang ובמערכת הבנייה של Android, אתם יכולים להפעיל CFI ברכיבים חדשים על ידי הוספת כמה שורות לקובצי ה-makefiles או לקובצי ה-blueprint.

תמיכה ב-CFI בקובצי makefile

כדי להפעיל CFI בקובץ make, כמו /platform/frameworks/av/cmds/stagefright/Android.mk, מוסיפים את השורה הבאה:

LOCAL_SANITIZE := cfi
# Optional features
LOCAL_SANITIZE_DIAG := cfi
LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
  • LOCAL_SANITIZE מציין את CFI ככלי לניקוי במהלך ה-build.
  • LOCAL_SANITIZE_DIAG מפעיל את מצב האבחון של CFI. במצב אבחון, מודפס מידע נוסף לניפוי באגים ב-logcat במהלך קריסות, וזה שימושי במהלך פיתוח ובדיקה של הגרסאות שלכם. חשוב לזכור להסיר את מצב האבחון בגרסאות ייצור.
  • LOCAL_SANITIZE_BLACKLIST מאפשר לרכיבים להשבית באופן סלקטיבי את מכשור ה-CFI לפונקציות או לקובצי מקור ספציפיים. אתם יכולים להשתמש ברשימה שחורה כמוצא אחרון כדי לפתור בעיות שמשפיעות על המשתמשים. לפרטים נוספים, אפשר לקרוא את המאמר בנושא השבתת CFI.

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

כדי להפעיל CFI בקובץ תוכנית, כמו /platform/frameworks/av/media/libmedia/Android.bp, מוסיפים:

   sanitize: {
        cfi: true,
        diag: {
            cfi: true,
        },
        blacklist: "cfi_blacklist.txt",
    },

פתרון בעיות

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

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

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

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

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

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

השבתת CFI

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

מערכת ה-Build של Android מספקת תמיכה ברשימות שחורות לכל רכיב (שמאפשרות לכם לבחור קובצי מקור או פונקציות ספציפיות שלא יקבלו מכשור CFI) גם ל-Make וגם ל-Soong. לפרטים נוספים על הפורמט של קובץ רשימה שחורה, אפשר לעיין במסמכי העזרה של Clang upstream.

אימות

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