为什么选择 MTE?
内存安全错误(即在原生编程语言中处理内存时发生的错误)是常见的代码问题。它们会导致安全漏洞以及稳定性问题。
Armv9 引入了 Arm 内存标记扩展 (MTE),这是一种硬件扩展,允许您捕获原生代码中的使用后释放和缓冲区溢出错误。
检查支持情况
从 Android 13 开始,某些选定的设备支持 MTE。要检查您的设备是否启用了 MTE,请运行以下命令:
adb shell grep mte /proc/cpuinfo
如果结果为 Features : [...] mte
,则表示您的设备已启用 MTE。
某些设备默认情况下不会启用 MTE,但允许开发人员重新启动以启用 MTE。这是一种实验性配置,不建议在正常使用时启用,因为它可能会降低设备性能或稳定性,但对应用开发很有用。要访问此模式,请在“设置”应用中导航到 开发者选项 > 内存标记扩展。如果此选项不存在,则表示您的设备不支持以这种方式启用 MTE。
MTE 操作模式
MTE 支持两种模式:SYNC 和 ASYNC。SYNC 模式提供更好的诊断信息,因此更适合于开发目的,而 ASYNC 模式具有高性能,使其能够在发布的应用中启用。
同步模式 (SYNC)
此模式针对可调试性而不是性能进行了优化,可用作精确的错误检测工具,当可以接受较高的性能开销时。启用后,MTE SYNC 也充当安全缓解措施。
在标签不匹配时,处理器会终止导致错误的加载或存储指令上的进程,并发出 SIGSEGV 信号(si_code 为 SEGV_MTESERR),以及有关内存访问和错误地址的完整信息。
此模式在测试期间很有用,因为它是不需要重新编译代码的 HWASan 的更快替代方案,或者在生产环境中,当您的应用表示易受攻击的攻击面时。此外,当 ASYNC 模式(如下所述)发现错误时,可以通过使用运行时 API 将执行切换到 SYNC 模式来获取准确的错误报告。
此外,在 SYNC 模式下运行时,Android 分配器会记录每次分配和释放的堆栈跟踪,并使用它们提供更好的错误报告,其中包括对内存错误(例如使用后释放或缓冲区溢出)的解释以及相关内存事件的堆栈跟踪(请参阅 了解 MTE 报告 以获取更多详细信息)。与 ASYNC 模式相比,此类报告提供了更多上下文信息,并使错误更容易追踪和修复。
异步模式 (ASYNC)
此模式针对性能而不是错误报告的准确性进行了优化,可用于低开销检测内存安全错误。在标签不匹配时,处理器会继续执行,直到最近的内核入口(例如系统调用或定时器中断),然后在不记录错误地址或内存访问的情况下,以 SIGSEGV 信号(代码为 SEGV_MTEAERR)终止进程。
这种模式适用于在经过良好测试的代码库(其中内存安全漏洞密度已知较低)的生产环境中缓解内存安全漏洞,这是通过在测试期间使用 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 中
您可以通过将以下内容
<?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
中,为 Gradle 项目的所有调试版本启用 MTE。这将覆盖您的清单的 memtagMode
,使其在调试版本中同步。
或者,您可以为自定义 buildType 的所有版本启用 MTE。为此,请创建您自己的 buildType,并将 XML 放入 app/src/<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,您需要修改分配器以标记内存和指针。
分配器的页面需要使用 PROT_MTE
在 mmap
(或 mprotect
)的 prot
标志中进行分配。
所有标记的分配都需要 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 用户指南 中了解更多信息。