為什麼要使用 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_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 中
您可以為 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_MTESERR
的 SIGSEGV
適用於 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
指令產生隨機標記並將其儲存在指標中。
請使用下列指令標記基礎記憶體:
如要同時零初始化記憶體,請使用以下指令:
請注意,上述指令「不適用於」舊版 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 使用手冊。