הטמעת dm-verity

Android 4.4 ואילך תומך ב'הפעלה מאומתת' באמצעות תכונת ליבה (kernel) של מיפוי מכשירים (dm-verity), שמספקת שקיפות בדיקת תקינות של מכשירים חוסמים. dm-verity עוזר במניעת Rootkits מתמידים שיכול לשמור על הרשאות בסיס ולהגן על מכשירים. הזה תכונה שעוזרת למשתמשי Android לוודא שכאשר מפעילים מכשיר, המכשיר נמצא באותו מיקום כשהשימוש האחרון היה בשימוש.

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

התכונה dm-verity מאפשרת לבדוק מכשיר בלוק, שכבת האחסון הבסיסית של מערכת הקבצים, ולקבוע אם היא תואמת להגדרה הצפויה. לשם כך נעשה שימוש בעץ גיבוב (hash) קריפטוגרפי. לכל בלוק (בדרך כלל 4k), יש גיבוב מסוג SHA256.

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

dm-verity-hash-table

איור 1. טבלת גיבוב של dm-verity

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

פעולה

הגנת ה-dm-verity נמצאת בליבה (kernel). לכן, אם תוכנת ה-rooting תסכן את המערכת לפני שהליבה תופעל, היא תישאר עם הגישה הזו. כדי לצמצם את הסיכון הזה, רוב היצרנים מאמתים את הליבה באמצעות מפתח שנצרב במכשיר. לא ניתן לשנות את המפתח הזה כשהמכשיר יוצא מהמפעל.

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

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

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

אם האימות נכשל, המכשיר יפיק שגיאת קלט/פלט (I/O) שמציינת שהחסימה לא ניתן לקרוא אותו. נראה שמערכת הקבצים פגומה, כצפוי.

אפליקציות יכולות להמשיך לפעול בלי הנתונים שהתקבלו, למשל אם התוצאות האלה לא נדרשות לתפקוד הראשי של האפליקציה. אבל, לפעמים אם האפליקציה לא יכולה להמשיך ללא הנתונים, היא תיכשל.

תיקון שגיאות העברה (FEC)

ב-Android מגרסה 7.0 ואילך, היציבות של dm-verity משופרת באמצעות תיקון שגיאות קדימה (FEC). ההטמעה ב-AOSP מתחילה בקוד הנפוץ לתיקון שגיאות של Reed-Solomon, ומחילה טכניקה שנקראת interleaving כדי לצמצם את שטח האחסון העודף ולהגדיל את מספר הבלוקים הפגומים שאפשר לשחזר. פרטים נוספים על FEC מופיעים במאמר הפעלה מאומתת עם תיקון שגיאות (FEC) שחלה עליה אכיפה קפדנית.

הטמעה

סיכום

  1. יצירת תמונת מערכת ext4.
  2. יוצרים עץ גיבוב לתמונה הזו.
  3. יוצרים טבלת dm-verity לעץ הגיבוב הזה.
  4. חותמים על טבלת dm-verity כדי ליצור חתימה של הטבלה.
  5. קיבוץ של חתימת הטבלה וטבלה של dm-verity למטא-נתונים אמיתיים.
  6. שרשור של קובץ האימג' של המערכת, המטא-נתונים של Verity ועץ הגיבוב.

תיאור מפורט של עץ הגיבוב וטבלת dm-verity זמין במאמר The Chromium Projects – Verified Boot.

יצירת עץ הגיבוב

כפי שמתואר בהקדמה, עץ הגיבוב הוא חלק בלתי נפרד מ-dm-verity. הכלי cryptsetup יוצר בשבילכם עץ גיבוב. לחלופין, אפשרות תואמת מוגדרת כאן:

<your block device name> <your block device name> <block size> <block size> <image size in blocks> <image size in blocks + 8> <root hash> <salt>

כדי ליצור את הגיבוב, תמונת המערכת מחולקת בשכבה 0 לבלוקים של 4k, כל אחד הוקצה גיבוב SHA256. שכבה 1 נוצרת על ידי צירוף של גיבובי SHA256 בלבד לבלוקים של 4K, וכתוצאה מכך נוצרת תמונה קטנה בהרבה. נוצרה שכבה 2 באופן זהה, באמצעות הגיבובים מסוג SHA256 של שכבה 1.

הפעולה הזו מתבצעת עד שהגיבוב של SHA256 של השכבה הקודמת יכול להיכנס לבלוק אחד. כשמקבלים את ה-SHA256 של הבלוק הזה, מקבלים את הגיבוב הבסיסי (root) של העץ.

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

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

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

לסיכום, האלגוריתם הכללי ליצירת עץ הגיבוב הוא:

  1. בוחרים מלח אקראי (קידוד הקסדצימלי).
  2. ביטול ה-sparse של קובץ האימג' של המערכת לבלוקים של 4k.
  3. לכל בלוק, מקבלים את הגיבוב SHA256 (המולא) שלו.
  4. שרשור של הגיבובים האלה כדי ליצור רמה
  5. ממקמים את הרמה עם 0s עד לגבול של 4k.
  6. משרשרים את הרמה לעץ הגיבוב (hash).
  7. חוזרים על שלבים 2 עד 6, תוך שימוש ברמה הקודמת כמקור לרמה הבאה, עד שמתקבל רק גיבוב אחד.

התוצאה היא גיבוב יחיד, שהוא גיבוב השורש שלכם. המחרוזת הזו והמלח משמשים במהלך היצירה של טבלת המיפוי של dm-verity.

יצירת טבלת המיפוי של dm-verity

יוצרים את טבלת המיפוי dm-verity, שמזהה את מכשיר החסימה (או את היעד) לגבי הליבה והמיקום של עץ הגיבוב (שהוא אותו הערך). הזה משמש ליצירה ולהפעלה של fstab. בטבלה מופיע גם הגודל של הבלוק ו-hash_start, מיקום ההתחלה של עץ הגיבוב (במיוחד, מספר הבלוק שלו מתחילת התמונה).

אפשר לקרוא מידע נוסף בקטע cryptsetup (הגדרה של קריפטו) תיאור מפורט של השדות בטבלת המיפוי של יעד האימות.

חתימה על הטבלה dm-verity

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

כדי לאמת את המחיצה באמצעות החתימה ושילוב המפתחות הזה:

  1. מוסיפים מפתח RSA-2048 בפורמט שתואם ל-libmincrypt אל קטגוריה אחת (/boot) בשעה /verity_key. זיהוי המיקום של המפתח שמשמש לאימות בעץ הגיבוב (hash).
  2. בקובץ fstab של הרשומה הרלוונטית, מוסיפים את הערך verify לדגלים fs_mgr.

מקבצים את חתימת הטבלה במטא-נתונים

משלבים את חתימת הטבלה ואת הטבלה dm-verity למטא-נתונים של אימות. כל קטע המטא-נתונים מקבל גרסאות כך שניתן להרחיב אותו, למשל כדי להוסיף שנייה סוג של חתימה או לשנות את הסדר.

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

כך אפשר לוודא שלא בחרתם לאמת מחיצה לא מאומתת. אם כן, היעדר מספר קסם זה יעצור את תהליך האימות. המספר הזה דומה למספר 0xb001b001.

ערכי הבייטים בפורמט הקסדצימלי הם:

  • הבייט הראשון = b0
  • בייט שני = 01
  • הבאיט השלישי = b0
  • הבייט הרביעי = 01

בתרשים הבא מוצג פירוט המטא-נתונים של Verity:

<magic number>|<version>|<signature>|<table length>|<table>|<padding>
\-------------------------------------------------------------------/
\----------------------------------------------------------/   |
                            |                                  |
                            |                                 32K
                       block content

הטבלה הזו מתארת את שדות המטא-נתונים האלה.

טבלה 1. שדות מטא-נתונים של Verity

שדה המטרה גודל ערך
מספר קסם משמש את fs_mgr כבדיקת שפיות 4 בייטים 0xb001b001
גרסה שמשמשת לגרסה של בלוק המטא-נתונים 4 בייטים כרגע 0
חתימה החתימה של הטבלה בפורמט PKCS1.5 עם תוספת נתונים 256 בייטים
אורך הטבלה האורך של טבלת dm-verity בבייטים 4 בייטים
שולחן טבלת dm-verity שתיארנו למעלה אורך הטבלה בבייט
padding המבנה הזה ממולא ב-0 עד 32 אלף אורך 0

אופטימיזציה של dm-verity

כדי להפיק את הביצועים הטובים ביותר מ-dm-verity, כדאי:

  • בליבה (kernel), מפעילים את NEON SHA-2 עבור ARMv7 ואת SHA-2 ל-ARMv8.
  • התנסות עם תכונות שונות של Readahead ו-prefetch_cluster כדי למצוא את התצורה הטובה ביותר עבור המכשיר שלך.