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

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

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

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

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

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

adb shell grep mte /proc/cpuinfo

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

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

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

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

  • ‫Pixel 8 (שיבה)
  • 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)
  • ‫Pixel 10 ‏ (Frankel)
  • ‫Pixel 10 Pro (Blazer)
  • ‫Pixel 10 Pro XL (Mustang)
  • Pixel 10 Pro Fold (Rango)
  • ‫Pixel 10a (Stallion)

מצבי הפעולה של 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, ודוחות הקריסות של אירועי הזיכרון הרלוונטיים (במאמר הזה יש פרטים נוספים). דוחות כאלה מספקים מידע הקשרי נוסף, ומאפשרים לאתר ולתקן באגים בקלות רבה יותר מאשר במצב ASYNC.

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

המצב הזה מותאם לביצועים ולא לדיוק של דוחות הבאגים, ואפשר להשתמש בו כדי לזהות באגים של בטיחות זיכרון עם תקורה נמוכה. במקרה של אי התאמה בתג, המעבד ממשיך את ההרצה עד לכניסה הקרובה ביותר לליבת המערכת (כמו קריאת מערכת או הפרעה של טיימר), שם הוא מסיים את התהליך עם 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 במניפסט ותפעיל סנכרון בגרסאות ניפוי באגים.

לחלופין, אפשר להפעיל את MTE לכל הגרסאות של 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 כמו שמוסבר קודם עוזרת לזהות באגים של פגיעה בזיכרון ב-native heap. כדי לזהות השחתת זיכרון במחסנית, בנוסף להפעלת 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: תיוג ואתחול לאפס של שורת מטמון עם אותו תג

חשוב לדעת שההוראות האלה לא נתמכות במעבדים ישנים יותר, ולכן צריך להריץ אותן באופן מותנה כש-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.