תוסף לתיוג זיכרון זרוע (MTE)

למה כדאי להשתמש ב-MTE?

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

ב-Armv9 הושק תוסף תיוג הזיכרון (MTE) של Arm, תוסף חומרה שמאפשר לזהות באגים מסוג use-after-free ו-buffer-overflow בקוד ה-native.

בדיקה אם יש תמיכה

החל מ-Android 13, במכשירים נבחרים יש תמיכה ב-MTE. כדי לבדוק אם המכשיר פועל עם MTE מופעל, מריצים את הפקודה הבאה:

adb shell grep mte /proc/cpuinfo

אם התוצאה היא Features : [...] mte, המשמעות היא שהמכשיר פועל עם MTE מופעל.

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

מכשירים עם תמיכה ב-MTE

המכשירים הבאים תומכים ב-MTE:

  • Pixel 8‏ (Shiba)
  • Pixel 8 Pro‏ (Husky)
  • Pixel 8a‏ (Akita)
  • Pixel 9‏ (Tokay)
  • Pixel 9 Pro‏ (Caiman)
  • Pixel 9 Pro XL‏ (Komodo)
  • Pixel 9 Pro Fold‏ (Comet)
  • Pixel 9a‏ (Tegu)

מצבי הפעולה של MTE

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

מצב סינכרוני (SYNC)

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

במקרה של אי-התאמה בתג, המעבד מסיים את התהליך בהוראת הטעינה או האחסון הבעייתית באמצעות SIGSEGV (עם si_code SEGV_MTESERR) ומידע מלא על הגישה לזיכרון ועל הכתובת שבה הייתה השגיאה.

המצב הזה שימושי במהלך בדיקות, כחלופה מהירה יותר ל-HWASan שלא דורשת להדר את הקוד מחדש, או בסביבת הייצור, כשהאפליקציה מייצגת שטח התקפה פגיע. בנוסף, כשמתגלה באג במצב ASYNC (כפי שמתואר בהמשך), אפשר לקבל דוח באג מדויק באמצעות ממשקי ה-API בסביבת זמן הריצה כדי להעביר את הביצוע למצב SYNC.

בנוסף, כשהמכשיר פועל במצב SYNC, מנהל הקצאת הזיכרון של Android מתעד את מעקב הסטאק של כל הקצאה וביטול הקצאה, ומשתמש בהם כדי לספק דוחות שגיאה טובים יותר שכוללים הסבר על שגיאת זיכרון, כמו use-after-free או buffer-overflow, ומעקב הסטאק של אירועי הזיכרון הרלוונטיים (פרטים נוספים זמינים במאמר הסבר על דוחות MTE). דוחות כאלה מספקים מידע נוסף לפי הקשר, ומאפשרים לאתר ולתקן באגים בקלות רבה יותר מאשר במצב ASYNC.

מצב אסינכרוני (ASYNC)

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

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

הפעלת MTE

למכשיר אחד

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

אפשר למצוא אותם בתפריט ההגדרות הגלובלי, בקטע 'מערכת' > 'מתקדם' > 'אפשרויות למפתחים' > 'שינויים בתאימות לאפליקציות'. ההגדרה NATIVE_MEMTAG_ASYNC או NATIVE_MEMTAG_SYNC מפעילה את MTE לאפליקציה מסוימת.

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

  • במצב SYNC: $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • במצב ASYNC: $ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

ב-Gradle

כדי להפעיל את MTE לכל גרסאות ה-debug של פרויקט Gradle, מוסיפים את הקוד הבא:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application android:memtagMode="sync" tools:replace="android:memtagMode"/>
</manifest>

לתוך app/src/debug/AndroidManifest.xml. הפעולה הזו תגביל את הערך של memtagMode במניפסט לסנכרון של גרסאות build לניפוי באגים.

לחלופין, אפשר להפעיל את MTE לכל גרסאות ה-build של buildType בהתאמה אישית. כדי לעשות זאת, יוצרים buildType משלכם ומכניסים את קובץ ה-XML לקובץ app/src/<name of buildType>/AndroidManifest.xml.

חבילה של קובץ APK בכל מכשיר מתאים

התכונה MTE מושבתת כברירת מחדל. כדי להשתמש ב-MTE באפליקציות, צריך להגדיר את הערך android:memtagMode בתג <application> או <process> בקטע AndroidManifest.xml.

android:memtagMode=(off|default|sync|async)

כשהמאפיין מוגדר בתג <application>, הוא משפיע על כל התהליכים שבהם האפליקציה משתמשת. אפשר לשנות את ההגדרה שלו בתהליכים מסוימים על ידי הגדרת התג <process>.

פיתוח עם מכשירי מדידה

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

כדי ליצור את הקוד המקורי (JNI) של האפליקציה באמצעות MTE:

ndk-build

בקובץ Application.mk:

APP_CFLAGS := -fsanitize=memtag -fno-omit-frame-pointer -march=armv8-a+memtag
APP_LDFLAGS := -fsanitize=memtag -fsanitize-memtag-mode=sync -march=armv8-a+memtag

CMake

לכל יעד ב-CMakeLists.txt:

target_compile_options(${TARGET} PUBLIC -fsanitize=memtag -fno-omit-frame-pointer -march=armv8-a+memtag)
target_link_options(${TARGET} PUBLIC -fsanitize=memtag -fsanitize-memtag-mode=sync -march=armv8-a+memtag)

הפעלת האפליקציה

אחרי הפעלת MTE, משתמשים באפליקציה ובודקים אותה כרגיל. אם מזוהה בעיה של בטיחות בזיכרון, האפליקציה קורסת עם הודעה על זיכרון שגוי שנראה כך (שימו לב להחלפת SIGSEGV ב-SEGV_MTESERR עבור SYNC או ב-SEGV_MTEAERR עבור ASYNC):

pid: 13935, tid: 13935, name: sanitizer-statu  >>> sanitizer-status <<<
uid: 0
tagged_addr_ctrl: 000000000007fff3
signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x800007ae92853a0
Cause: [MTE]: Use After Free, 0 bytes into a 32-byte allocation at 0x7ae92853a0
x0  0000007cd94227cc  x1  0000007cd94227cc  x2  ffffffffffffffd0  x3  0000007fe81919c0
x4  0000007fe8191a10  x5  0000000000000004  x6  0000005400000051  x7  0000008700000021
x8  0800007ae92853a0  x9  0000000000000000  x10 0000007ae9285000  x11 0000000000000030
x12 000000000000000d  x13 0000007cd941c858  x14 0000000000000054  x15 0000000000000000
x16 0000007cd940c0c8  x17 0000007cd93a1030  x18 0000007cdcac6000  x19 0000007fe8191c78
x20 0000005800eee5c4  x21 0000007fe8191c90  x22 0000000000000002  x23 0000000000000000
x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
x28 0000000000000000  x29 0000007fe8191b70
lr  0000005800eee0bc  sp  0000007fe8191b60  pc  0000005800eee0c0  pst 0000000060001000

backtrace:
      #00 pc 00000000000010c0  /system/bin/sanitizer-status (test_crash_malloc_uaf()+40) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #01 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #02 pc 00000000000019cc  /system/bin/sanitizer-status (main+1032) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000487d8  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+96) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)

deallocated by thread 13935:
      #00 pc 000000000004643c  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::quarantineOrDeallocateChunk(scudo::Options, void*, scudo::Chunk::UnpackedHeader*, unsigned long)+688) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 00000000000421e4  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::deallocate(void*, scudo::Chunk::Origin, unsigned long, unsigned long)+212) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 00000000000010b8  /system/bin/sanitizer-status (test_crash_malloc_uaf()+32) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)

allocated by thread 13935:
      #00 pc 0000000000042020  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::allocate(unsigned long, scudo::Chunk::Origin, unsigned long, bool)+1300) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 0000000000042394  /apex/com.android.runtime/lib64/bionic/libc.so (scudo_malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 000000000003cc9c  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #03 pc 00000000000010ac  /system/bin/sanitizer-status (test_crash_malloc_uaf()+20) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #04 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
Learn more about MTE reports: https://source.android.com/docs/security/test/memory-safety/mte-report

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

משתמשים מתקדמים: שימוש ב-MTE במקצה משאבים משלכם

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

צריך להקצות את הדפים למרכז הקצאת המשאבים באמצעות PROT_MTE בדגל prot של mmap (או mprotect).

כל ההקצאות המתויגות צריכות להיות מותאמות ל-16 בייטים, כי אפשר להקצות תגים רק למקטעים של 16 בייטים (שנקראים גם גרגרים).

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

כדי לתייג את הזיכרון הבסיסי:

  • STG: תיוג של גרגר יחיד באורך 16 בייטים
  • ST2G: תגים לשני גרגירים של 16 בייטים
  • DC GVA: תג שורה במטמון עם אותו תג

לחלופין, אפשר להשתמש בהוראות הבאות כדי לאפס את הזיכרון:

  • STZG: תיוג של גרגר יחיד באורך 16 בייטים והגדרת אפס לערכים שלו
  • STZ2G: תיוג של שני גרגרים של 16 בייטים והגדרתם לאפס
  • DC GZVA: תיוג של שורת מטמון עם אותו תג והפעלת אפסה (zero-initialization)

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

#include <sys/prctl.h>

bool runningWithMte() {
      int mode = prctl(PR_GET_TAGGED_ADDR_CTRL, 0, 0, 0, 0);
      return mode != -1 && mode & PR_MTE_TCF_MASK;
}

כדאי לעיין במאמר הטמעת scudo.

מידע נוסף

מידע נוסף זמין במדריך למשתמש של MTE ל-Android OS שנכתב על ידי Arm.