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

למה כדאי לצפות ב-MTE?

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

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

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

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

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

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

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

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

בנוסף, כשהוא פועל במצב 'סנכרון', המקצה ל-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 באופן הבא:

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

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