למה כדאי להשתמש ב-MTE?
באגים שקשורים לבטיחות הזיכרון הם שגיאות בטיפול בזיכרון בשפות תכנות Native, והם בעיות נפוצות בקוד. הם מובילים לנקודות חולשה באבטחה וגם לבעיות יציבות.
ב-Armv9 הוצג Arm Memory Tagging Extension (MTE), תוסף חומרה שמאפשר לכם לזהות באגים מסוג 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)
מצבי הפעולה של MTE
MTE תומך בשני מצבים: SYNC ו-ASYNC. מצב SYNC מספק מידע אבחוני טוב יותר ולכן מתאים יותר למטרות פיתוח, בעוד שמצב ASYNC מאפשר ביצועים גבוהים ולכן אפשר להפעיל אותו באפליקציות שפורסמו.
מצב סינכרוני (SYNC)
המצב הזה מותאם ליכולת איתור באגים ולא לביצועים, ואפשר להשתמש בו ככלי מדויק לזיהוי באגים, אם תקורת ביצועים גבוהה יותר מקובלת. כשמפעילים את MTE SYNC, הוא משמש גם כאמצעי להפחתת סיכוני אבטחה.
במקרה של אי התאמה בתג, המעבד מסיים את התהליך בהוראת הטעינה או האחסון הבעייתית עם SIGSEGV (עם si_code SEGV_MTESERR) ומידע מלא על הגישה לזיכרון ועל כתובת השגיאה.
המצב הזה שימושי במהלך בדיקות כחלופה מהירה יותר ל-HWASan שלא מחייבת הידור מחדש של הקוד, או בסביבת ייצור, כשהאפליקציה מייצגת משטח תקיפה פגיע. בנוסף, אם נמצא באג במצב ASYNC (שמתואר בהמשך), אפשר להשתמש בממשקי ה-API של זמן הריצה כדי להעביר את ההפעלה למצב SYNC ולקבל דוח באגים מדויק.
בנוסף, כשמפעילים את המקצה ב-Android במצב SYNC, הוא מתעד את דוח הקריסות של כל הקצאה וביטול הקצאה, ומשתמש בהם כדי לספק דוחות שגיאות טובים יותר שכוללים הסבר על שגיאת זיכרון, כמו use-after-free או buffer-overflow, ודוחות קריסות של אירועי הזיכרון הרלוונטיים (פרטים נוספים זמינים במאמר הסבר על דוחות MTE). דוחות כאלה מספקים מידע הקשרי נוסף, ומקלים על איתור באגים ותיקונם בהשוואה למצב 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 במניפסט ותסנכרן את גרסאות ה-debug.
לחלופין, אפשר להפעיל 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, משתמשים באפליקציה ובודקים אותה כרגיל. אם מזוהה בעיה שקשורה לבטיחות הזיכרון, האפליקציה קורסת עם tombstone שנראה בערך כך (שימו לב ל-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.