Extensão ARM Memory Tagging Extension (MTE)

Por que usar a MTE?

Os bugs de segurança de memória, que são erros no processamento de memória em linguagens de programação nativas, são problemas comuns de código. Eles causam vulnerabilidades de segurança, bem como problemas de estabilidade.

A Armv9 introduziu a Arm Memory Tagging Extension (MTE), uma extensão de hardware que permite capturar bugs use-after-free e de estouro de buffer no código nativo.

Procurar suporte

No Android 13 e versões mais recentes, alguns dispositivos têm suporte à MTE. Para conferir se o seu dispositivo está sendo executado com a MTE ativada, use o seguinte comando:

adb shell grep mte /proc/cpuinfo

Se o resultado for Features : [...] mte, isso significa que o dispositivo está sendo executado com a MTE ativada.

Alguns dispositivos não ativam a MTE por padrão, mas permitem que os desenvolvedores reinicializem o dispositivo com a MTE ativada. Essa é uma configuração experimental que não é recomendada para uso normal, porque pode reduzir o desempenho ou a estabilidade do dispositivo, mas pode ser útil para o desenvolvimento de apps. Para acessar esse modo, navegue até Opções do desenvolvedor > Memory Tagging Extension no seu app Configurações. Se essa opção não estiver presente, isso significa que o dispositivo não oferece suporte para ativar a MTE dessa forma.

Modos de operação da MTE

A MTE oferece suporte a dois modos: SYNC e ASYNC. O modo SYNC fornece informações de diagnóstico melhores e, portanto, é mais adequado para fins de desenvolvimento, enquanto o modo ASYNC tem alto desempenho que permite que ele seja ativado para apps lançados.

Modo síncrono (SYNC)

Esse modo é otimizado para melhor capacidade de depuração em vez de desempenho e pode ser usado como uma ferramenta precisa de detecção de bugs, quando uma sobrecarga maior for aceitável. Quando ativada, a MTE SYNC também atua como uma mitigação de segurança.

Em casos de incompatibilidade de tag, o processador encerra o processo na carga incompatível ou na instrução de armazenamento com SIGSEGV (com si_code SEGV_MTESERR) e informações completas sobre o acesso à memória e o endereço com falha.

Esse modo é útil durante os testes como uma alternativa mais rápida ao HWASan que não exige que você recompile o código ou em produção, quando o app representa uma superfície de ataque vulnerável. Além disso, quando o modo ASYNC (descrito abaixo) encontra um bug, é possível gerar um relatório preciso do bug usando as APIs de execução para alternar a execução para o modo SYNC.

Além disso, ao executar no modo SYNC, o alocador do Android registra o stack trace de cada alocação e desalocação e os usa para fornecer relatórios de erros melhores que incluem a explicação de um erro de memória, como use-after-free ou de estouro de buffer e os stack traces dos eventos de memória relevantes. Consulte Noções básicas sobre relatórios de MTE para mais detalhes. Esses relatórios fornecem mais informações contextuais e facilitam o rastreamento e a correção de bugs no modo ASYNC.

Modo assíncrono (ASYNC)

Esse modo é otimizado para desempenho acima da precisão de relatórios de bugs e pode ser usado para detecção de baixa sobrecarga de bugs de segurança de memória. Em casos de incompatibilidade de tag, o processador continua a execução até a entrada do kernel mais próxima (como uma chamada de sistema ou interrupção de timer), em que encerra o processo com SIGSEGV (código SEGV_MTEAERR) sem registrar o endereço ou acesso à memória com falha.

Esse modo é útil para atenuar vulnerabilidades de segurança de memória na produção em bases de código bem testadas em que é sabido que a densidade de bugs de segurança de memória é baixa, o que é possível usando o modo SYNC durante o teste.

Ativar a MTE

Para um único dispositivo

Para experimentação, as mudanças de compatibilidade de app podem ser usadas para definir o valor padrão do atributo memtagMode de um aplicativo que não especifique nenhum valor no manifesto (ou que especifique "default").

Elas podem ser encontradas em "Sistema" > "Avançado" > "Opções do desenvolvedor" > "Mudanças na compatibilidade do app" no menu de configurações globais. Definir NATIVE_MEMTAG_ASYNC ou NATIVE_MEMTAG_SYNC ativa a MTE de um aplicativo específico.

Ou você pode usar o comando am desta forma:

  • Para o modo SYNC: $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • Para o modo ASYNC: $ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

No Gradle

Você pode ativar a MTE para todos os builds de depuração do seu projeto do Gradle colocando

<?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>

em app/src/debug/AndroidManifest.xml. Isso vai substituir o memtagMode do manifesto pela sincronização para builds de depuração.

Como alternativa, você pode ativar a MTE para todos os builds de um buildType personalizado. Para fazer isso, crie seu próprio buildType e coloque o XML em app/src/<name of buildType>/AndroidManifest.xml.

Para um APK em qualquer dispositivo compatível

A MTE fica desativada por padrão. Para usá-la, os apps podem definir android:memtagMode na tag <application> ou <process> no AndroidManifest.xml.

android:memtagMode=(off|default|sync|async)

Quando definido na tag <application>, o atributo afeta todos os processos usados pelo aplicativo e pode ser substituído por processos individuais definindo a tag <process>.

Criar com instrumentação

Ativar a MTE, conforme explicado anteriormente, ajuda a detectar bugs de corrupção de memória no heap nativo. Detectar a corrupção de memória na pilha, além de ativar MTE para o app, o código precisa ser recriado com instrumentação. A o app resultante só será executado em dispositivos compatíveis com MTE.

Para criar o código nativo (JNI) do seu app com a MTE, faça o seguinte:

ndk-build

No seu arquivo 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

Para cada destino no 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)

Executar o app

Depois de ativar a MTE, use e teste o app normalmente. Se um problema de segurança de memória for detectado, o app vai falhar com um Tombstone semelhante a este (observe o SIGSEGV com SEGV_MTESERR para SYNC ou SEGV_MTEAERR para ASYNC):

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

Consulte Noções básicas sobre relatórios da MTE na documentação do AOSP para saber mais. Também é possível depurar o app com o Android Studio. O depurador será interrompido na linha que está causando o acesso inválido à memória.

Usuários avançados: como usar a MTE no seu próprio alocador

Para usar a MTE na memória não alocada com os alocadores de sistema normais, modifique o alocador para adicionar uma tag na memória e nos ponteiros.

As páginas do alocador precisam ser alocadas usando PROT_MTE na flag prot de mmap (ou mprotect).

Todas as alocações com tags precisam estar alinhadas a 16 bytes, já que as tags só podem ser atribuídas a blocos de 16 bytes, também conhecidos como grânulos.

Antes de retornar um ponteiro, use a instrução IRG para gerar e armazenar uma tag aleatória dentro dele.

Use as seguintes instruções para adicionar uma tag na memória:

  • STG: adicionar uma tag em um único grânulo de 16 bytes
  • ST2G: adicionar uma tag em dois grânulos de 16 bytes
  • DC GVA: adicionar a mesma tag à linha de cache

Como alternativa, as seguintes instruções também inicializam a memória do zero:

  • STZG: adicionar uma tag e inicializar um único grânulo de 16 bytes do zero
  • STZ2G: adicionar uma tag e inicializar dois grânulos de 16 bytes do zero
  • DC GZVA: adicionar a mesma tag e inicializar a linha de cache do zero

CPUs mais antigas não têm suporte a essas instruções. Portanto, elas precisam ser executadas condicionalmente quando a MTE está ativada. Confira se a MTE está ativada:

#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;
}

A implementação do scudo pode ser útil como referência.

Saiba mais

Consulte o Guia do usuário da MTE para o SO Android (link em inglês) escrito pela Arm para saber mais.