למה כדאי להשתמש ב-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.