Arm Memory Tagging Extension (MTE)

為什麼要使用 MTE?

記憶體安全錯誤是在原生程式設計語言中處理記憶體時發生的錯誤,也是常見的程式碼問題,會導致安全漏洞並影響穩定性。

Armv9 推出的 Memory Tagging Extension (MTE) 是一項硬體延伸技術,可讓您在原生程式碼中找出 UAF 問題 (使用已釋放記憶體) 及堆積緩衝區溢位的錯誤。

檢查支援情形

從 Android 13 開始,部分特定裝置將可支援 MTE。如要檢查裝置執行時是否啟用 MTE,請執行下列指令:

adb shell grep mte /proc/cpuinfo

如果結果是 Features : [...] mte,表示裝置執行時便已啟用 MTE。

部分裝置預設不會啟用 MTE,但開發人員可重新啟動這類裝置並啟用 MTE。這項設定屬於實驗功能,可能造成裝置效能或穩定性降低,因此不建議用於一般用途,但在開發應用程式時仍十分實用。如要使用這個模式,請在「設定」應用程式中依序點選「開發人員選項」>「Memory Tagging Extension」。如果找不到這個選項,表示您的裝置不支援以此方式啟用 MTE。

MTE 運作模式

MTE 支援 SYNC 和 ASYNC 兩種模式。SYNC 模式可提供更完善的診斷資訊,因此較適合用於開發作業,而 ASYNC 模式可提高效能,則適於在已發布的應用程式上啟用。

同步模式 (SYNC)

相較於效能,這個模式改善的是偵錯能力。如果您認為效能負載高並不構成問題,可以利用此工具準確偵測錯誤。此外,您也可將啟用 MTE SYNC 做為因應安全性問題的有效措施。

如果標記不相符,處理程式會透過 SIGSEGV (含 si_code SEGV_MTESERR) 以及記憶體存取和錯誤位址的完整資訊,終止違規載入或儲存指示的處理程序。

此模式十分適合用於測試,可做為比 HWASan 快速的替代方案,無須重新編譯程式碼;或是在實際工作環境中,用於應用程式出現安全漏洞攻擊途徑的場合。此外,當 ASYNC 模式 (如下所述) 發現錯誤時,您只要使用執行階段 API 將執行作業切換為 SYNC 模式,即可取得準確的錯誤報告。

另外,在 SYNC 模式下執行時,Android 配置器會記錄每項配置和取消配置的堆疊追蹤情形,並用於提供更完善的錯誤報告,方便您查看 UAF (使用已釋放記憶體) 或緩衝區溢位等記憶體錯誤,以及相關記憶體事件的堆疊追蹤 (詳情請參閱「解讀 MTE 報告」)。相較於 ASYNC 模式,這類報告可提供更多背景資訊,讓您更輕鬆地追蹤及修正錯誤。

非同步模式 (ASYNC)

相較於錯誤報告的準確度,此模式較重視效能,因此即使負載低也能偵測記憶體安全錯誤。當標記不相符時,處理程式仍會繼續執行,直到最接近的核心項目 (例如發生系統呼叫或計時器中斷的情形) 為止,此時,該程序會透過 SIGSEGV (code SEGV_MTEAERR ) 終止程序,而不會記錄錯誤位址或記憶體存取。

如果您在測試期間使用 SYNC 模式,程式碼集上的記憶體安全錯誤密度就會降低,此時就很適合針對這些經過完善測試的程式碼集,透過 ASYNC 模式減少實際工作環境中的記憶體安全漏洞問題。

啟用 MTE

單一裝置

進行實驗時,如果應用程式尚未在資訊清單中指定任何值 (或指定 "default"),您可以透過應用程式相容性變更,為上述應用程式設定 memtagMode 屬性的預設值。

如要查看這些設定,請在全域設定選單中依序點選「系統」>「進階」>「開發人員選項」>「應用程式相容性變更」。只要設定 NATIVE_MEMTAG_ASYNCNATIVE_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 中

您可以為 Gradle 專案的所有偵錯版本啟用 MTE,方法是將

<?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

您也可以為自訂 buildType 的所有版本啟用 MTE。如要這麼做,請建立自己的 buildType,並將 XML 放入 app/src/<name of buildType>/AndroidManifest.xml

適用於任何支援裝置的 APK

MTE 預設為停用。想要使用 MTE 的應用程式可在 AndroidManifest.xml<application><process> 標記底下設定 android:memtagMode

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

<application> 標記上設定屬性時,屬性會影響應用程式使用的所有程序;如要針對個別程序覆寫這項屬性,請設定 <process> 標記。

透過檢測設備建構

如前所述,啟用 MTE 有助於偵測 原生堆積除了啟用 應用程式的 MTE,必須使用檢測設備重新建構程式碼。 生成的應用程式只能在支援 MTE 的裝置上執行

如要使用 MTE 建構應用程式的原生 (JNI) 程式碼,請按照下列步驟操作:

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 後,請照常使用及測試您的應用程式。如果偵測到記憶體安全問題,應用程式就會停止運作,您也會發現類似下方的空值標記 (請注意,含 SEGV_MTESERRSIGSEGV 適用於 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

詳情請參閱 Android 開放原始碼計畫說明文件中的「解讀 MTE 報告」一文。您也可以使用 Android Studio 對應用程式進行偵錯,偵錯工具會停在造成記憶體存取無效的那一行。

進階使用者:在自己的配置器中使用 MTE

如要針對未透過一般系統配置器分配的記憶體使用 MTE,您需要修改配置器來標記記憶體和指標。

只要是自訂配置器的頁面,就需要使用 mmap (或 mprotect) 的 prot 標記中的 PROT_MTE 進行分配。

所有標記的配置都需為 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 實作

瞭解詳情

如需瞭解詳情,請參閱 Arm 編寫的 Android 作業系統適用的 MTE 使用手冊