为何使用 MTE?
内存安全 bug(即原生编程语言中处理内存时发生的错误)是常见的代码问题。它们会导致安全漏洞和稳定性问题。
Armv9 引入了 Arm Memory Tagging Extension (MTE),这是一种硬件扩展,可用于捕获原生代码中的 use-after-free 和 buffer-overflow bug。
检查支持情况
从 Android 13 开始,部分设备支持 MTE。要检查您的设备是否已启用 MTE,请运行以下命令
adb shell grep mte /proc/cpuinfo
如果结果是 Features : [...] mte
,则表示您的设备已启用 MTE。
有些设备默认未启用 MTE,但允许开发者重启设备并启用 MTE。这是一种实验性配置,不建议用于正常用途,因为它可能会降低设备性能或稳定性,但对应用开发可能有用。要访问此模式,请依次转到“设置”应用中的开发者选项 > Memory Tagging Extension。如果此选项不存在,则表示您的设备不支持以这种方式启用 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)
此模式针对可调试性而非性能进行了优化,在可以接受较高性能开销的情况下,可以用作精确的 bug 检测工具。启用后,MTE SYNC 也可用作一种安全缓解措施。
在标签不匹配时,处理器会终止存在问题的加载或存储指令所在的进程,并发送 SIGSEGV(si_code 为 SEGV_MTESERR),以及有关内存访问和故障地址的完整信息。
此模式在测试期间很有用,它可以作为 HWASan 的快速替代方案,且无需重新编译代码;在生产环境中,当您的应用代表易受攻击的攻击面时也很有用。此外,当 ASYNC 模式(如下所述)发现 bug 时,可以通过使用运行时 API 将执行切换到 SYNC 模式来获取准确的 bug 报告。
此外,在 SYNC 模式下运行时,Android 分配器会记录每次分配和释放的堆栈轨迹,并使用它们提供更完善的错误报告,其中包括内存错误的解释(例如 use-after-free 或 buffer-overflow)以及相关内存事件的堆栈轨迹(请参阅了解 MTE 报告以了解详情)。与 ASYNC 模式相比,此类报告可提供更多上下文信息,使 bug 更易于跟踪和修复。
异步模式 (ASYNC)
此模式针对性能而非 bug 报告准确性进行了优化,可用于以较低开销检测内存安全 bug。在标签不匹配时,处理器会继续执行,直到最近的内核入口点(例如 syscall 或计时器中断),然后在那里终止进程,并发送 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 中
您可以通过将以下内容放入 app/src/debug/AndroidManifest.xml
来为 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
覆盖为 sync。
或者,您可以为自定义 buildType 的所有版本启用 MTE。为此,请创建您自己的 buildType 并将 XML 放入 app/src/<name of buildType>/AndroidManifest.xml
中。
对于任意支持 MTE 的设备上的 APK
MTE 默认处于停用状态。想要使用 MTE 的应用可以在 AndroidManifest.xml
中的 <application>
或 <process>
标签下设置 android:memtagMode
。
android:memtagMode=(off|default|sync|async)
在 <application>
标签上设置此属性会影响应用使用的所有进程,并且可以通过设置 <process>
标签来针对各个进程进行覆盖。
使用插桩构建
如前所述,启用 MTE 有助于检测原生堆上的内存损坏 bug。为了检测堆栈上的内存损坏,除了为应用启用 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
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
,si_code 为 SEGV_MTESERR
;ASYNC 模式下的 SIGSEGV
,si_code 为 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 字节块(也称为 granules)。
然后,在返回指针之前,您需要使用 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 用户指南中了解更多信息。