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