ARM内存标记扩展 (MTE)

为什么选择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_ASYNCNATIVE_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 指令生成随机标签并将其存储在指针中。

使用以下指令来标记底层内存:

  • STG:标记单个 16 字节粒度
  • ST2G:标记两个 16 字节粒度
  • DC GVA:使用相同的标签标记缓存行

或者,以下指令也会将内存清零:

  • STZG:标记并清零单个 16 字节粒度
  • STZ2G:标记并清零两个 16 字节粒度
  • DC GZVA:使用相同的标签标记并清零缓存行

请注意,这些指令在较旧的 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 用户指南 中了解更多信息。