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

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

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

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

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

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

ה-CFI מסופק על ידי הקומפיילר ומוסיף מכשור לבינארי במהלך זמן הקומפילציה. אנחנו תומכים ב-CFI בשרשרת הכלים של Clang ובמערכת build של 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 build. התכונה CFI עוזרת לשמור על הבטיחות של משתמשי Android, ולכן לא מומלץ להשבית אותה.

למעשה, מומלץ מאוד להפעיל CFI לרכיבים נוספים. המועמדים האידיאליים הם קוד Native עם הרשאות או קוד Native שמבצע עיבוד של קלט של משתמשים לא מהימן. אם אתם משתמשים ב-clang ובמערכת ה-build של Android, אתם יכולים להפעיל CFI ברכיבים חדשים על ידי הוספת כמה שורות לקובצי ה-make או לקובצי ה-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 במהלך קריסות, וזה שימושי במהלך פיתוח ובדיקה של הגרסאות שלכם. עם זאת, חשוב להקפיד להסיר את מצב האבחון מגרסאות ה-build של סביבת הייצור.
  • 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 ברכיבים חדשים, יכול להיות שיתקבלו כמה בעיות שקשורות לאי התאמה בין סוגי פונקציות ולאי התאמה בין סוגי קודים של Assembly.

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

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

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

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

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

השבתת CFI

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

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

אימות

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