为何选择 MTE?
内存安全 bug 是指以原生编程语言处理内存时遇到的错误,是常见的代码问题。这类问题会导致安全漏洞和稳定性问题。
Armv9 引入了 Arm Memory Tagging Extension (MTE),一种可让您捕获原生代码中的释放后使用和缓冲区溢出 bug 的硬件扩展。
检查支持情况
从 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)
此模式针对可调试性而非性能进行了优化,可以在可接受较高性能开销的情况下用作精确的 bug 检测工具。启用后,MTE SYNC 可以作为有效的安全缓解措施。
如果标记不匹配,处理器会在收到违规的加载或存储指令时终止进程,并返回 SIGSEGV(si_code 为 SEGV_MTESERR)以及有关内存访问和故障地址的完整信息。
此模式适合在测试期间用作 HWASan 的更快替代方案,此时您无需重新编译代码;或当您的应用出现存在漏洞的受攻击面时在生产环境中使用。此外,当 ASYNC 模式(详见下文)发现 bug 时,可以通过使用运行时 API 将执行切换到 SYNC 模式,来获取准确的 bug 报告。
此外,在 SYNC 模式下运行时,Android 分配器会记录每次分配和取消分配的堆栈轨迹,并利用这些信息提供更好的错误报告,其中包含对内存错误的解释(例如释放后使用或缓冲区溢出),以及相关内存事件的堆栈轨迹(如需了解详情,请参阅了解 MTE 报告)。与 ASYNC 模式相比,此类报告可提供更多上下文信息,并简化 bug 跟踪和修复。
异步模式 (ASYNC)
此模式专门针对 bug 报告的性能(而非精确度)进行了优化,可用于对内存安全 bug 进行低开销检测。如果标记不匹配,处理器会继续执行,直到达到最近的内核条目(例如,系统调用或计时器中断)。这时,处理器会通过 SIGSEGV(代码为 SEGV_MTEAERR)终止进程,而不会记录错误地址或内存访问。
此模式适合在生产环境中对经过严格测试的代码库(已知其内存安全 bug 的密度较低)降低内存安全漏洞,这将通过在测试期间使用 SYNC 模式来实现。
启用 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 项目的所有调试 build 启用 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
替换为调试 build 的同步功能。
或者,您也可为自定义 buildType 的所有 build 启用 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 后,请照常使用和测试应用。如果检测到内存安全问题,应用将会崩溃,并提供类似于以下内容的 Tombstone(请注意,SYNC 模式下,返回的 SIGSEGV
代码为 SEGV_MTESERR
;ASYNC 模式下,代码为 SEGV_MTEAERR
):
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
如需了解详情,请参阅 AOSP 文档中的了解 MTE 报告。您还可以使用 Android Studio 调试应用,调试程序会在导致内存访问无效的行停止。
高级用户:在您自己的分配器中使用 MTE
如要对并非通过常规系统分配器分配的内存使用 MTE,您需要修改分配器以标记内存和指针。
您需要通过 mmap
(或 mprotect
)的 prot
标志中的 PROT_MTE
来分配分配器的页面。
所有标记的分配都需要 16 字节对齐,因为标记只能分配给 16 字节的区块(也称为 granule)。
然后,在返回指针之前,您需要使用 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 OS MTE 用户指南。