¿Por qué usar MTE?
Los errores de seguridad de la memoria, que son errores en el manejo de la memoria en lenguajes de programación nativos, son problemas de código comunes. Estos generan vulnerabilidades de seguridad y problemas de estabilidad.
Armv9 introdujo la extensión de etiquetado de memoria (MTE) de Arm, una extensión de hardware que te permite detectar errores de uso luego de la liberación y de desbordamiento de búfer en tu código nativo.
Verifica la compatibilidad
A partir de Android 13, ciertos dispositivos son compatibles con MTE. Para verificar si tu dispositivo tiene MTE habilitada, ejecuta el siguiente comando:
adb shell grep mte /proc/cpuinfo
Si el resultado es Features : [...] mte
, tu dispositivo tiene MTE habilitada.
Algunos dispositivos no habilitan MTE de forma predeterminada, pero permiten que los desarrolladores los reinicien con MTE habilitada. Esta es una configuración experimental que no se recomienda para el uso normal, ya que puede disminuir el rendimiento o la estabilidad del dispositivo, pero que puede ser útil para el desarrollo de apps. Para acceder a este modo, ve a Opciones para desarrolladores > Ext. de etiquetado de memoria en la app de Configuración. Si esta opción no está presente, el dispositivo no admite la habilitación de MTE de esta manera.
Modos de operación de MTE
MTE admite dos modos: SYNC y ASYNC. El modo SYNC proporciona mejor información de diagnóstico y, por lo tanto, es más adecuado para fines de desarrollo, mientras que el modo ASYNC tiene un alto rendimiento que permite su habilitación para apps que ya se hayan lanzado.
Modo síncrono (SYNC)
Este modo está optimizado para la depuración por sobre el rendimiento y se puede usar como una herramienta de detección de errores precisa; se debe tener en cuenta que la sobrecarga de rendimiento será mayor. Cuando MTE SYNC está habilitado, también actúa como mitigación de seguridad.
En una discrepancia de etiqueta, el procesador finaliza el proceso en la instrucción de carga o almacenamiento incorrecta con SIGSEGV (con si_code SEGV_MTESERR) y la información completa sobre el acceso a la memoria y la dirección con errores.
Este modo resulta útil durante las pruebas como una alternativa más rápida a HWASan que no requiere volver a compilar el código, o bien durante la producción cuando la app representa una superficie vulnerable a ataques. Además, cuando el modo ASYNC (descrito a continuación) encuentra un error, se puede obtener un informe de errores preciso usando las APIs de entorno de ejecución para cambiar la ejecución al modo SYNC.
Por otra parte, cuando se ejecuta en modo SYNC, el asignador de Android registra el seguimiento de pila de cada asignación y desasignación, y lo usa para proporcionar mejores informes de errores que incluyen una explicación de un error de memoria, como usar después de liberar o el desbordamiento de búfer, y los seguimientos de pila de los eventos de memoria relevantes (consulta Cómo interpretar los informes de MTE para obtener más detalles). Esos informes proporcionan más información contextual y hacen que los errores sean más fáciles de rastrear y corregir que en el modo ASYNC.
Modo asíncrono (ASYNC)
Este modo está optimizado para el rendimiento por sobre la precisión de los informes de errores y se puede usar para detectar, con una baja sobrecarga, errores de seguridad de memoria. En una discrepancia de etiqueta, el procesador continúa la ejecución hasta la entrada de kernel más cercana (como una llamada de sistema o interrupción del temporizador), donde finaliza el proceso con SIGSEGV (código SEGV_MTEAERR) sin registrar la dirección o el acceso a la memoria con errores.
Este modo es útil para mitigar las vulnerabilidades de seguridad de la memoria durante la producción en bases de código probadas donde se sabe que la densidad de errores de seguridad de la memoria es baja, lo que se logra usando el modo SYNC durante las pruebas.
Cómo habilitar MTE
Para un solo dispositivo
Durante la experimentación, los cambios de compatibilidad de la app se pueden usar para establecer el valor predeterminado del atributo memtagMode
para una aplicación que no especifica ningún valor en el manifiesto (o especifica "default"
).
Puedes encontrarlos en Sistema > Avanzado > Opciones para desarrolladores > Cambios en la compatibilidad de la app en el menú de configuración global. La configuración de NATIVE_MEMTAG_ASYNC
o NATIVE_MEMTAG_SYNC
habilita MTE para una aplicación en particular.
Como alternativa, esto se puede configurar usando el comando am
de la siguiente manera:
- Para el modo SYNC:
$ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
- Para el modo ASYNC:
$ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name
En Gradle
Puedes habilitar MTE para todas las compilaciones de depuración de tu proyecto de Gradle si pones
<?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
. Esto anulará el memtagMode
de tu manifiesto con sincronización para las compilaciones de depuración.
Como alternativa, puedes habilitar MTE para todas las compilaciones de un buildType personalizado. Para ello, crea tu propio buildType y coloca el XML en app/src/<name of buildType>/AndroidManifest.xml
.
Para un APK en cualquier dispositivo compatible
MTE está inhabilitada de forma predeterminada. Si quieres que las apps usen MTE, puedes configurar android:memtagMode
en la etiqueta <application>
o <process>
en AndroidManifest.xml
.
android:memtagMode=(off|default|sync|async)
Cuando se establece en la etiqueta <application>
, el atributo afecta a todos los procesos que usa la aplicación y se puede anular en procesos individuales si configuras la etiqueta <process>
.
Cómo compilar con instrumentación
Como se explicó anteriormente, habilitar MTE ayuda a detectar errores de corrupción de memoria en el el montón nativo. Para detectar la corrupción de la memoria en la pila, además de habilitar MTE para la app, el código debe volver a compilarse con instrumentación. El La app resultante solo se ejecutará en dispositivos compatibles con MTE.
Para compilar el código nativo (JNI) de tu app con MTE, haz lo siguiente:
ndk-build
En el archivo Application.mk
, incluye lo siguiente:
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 de tu 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)
Ejecuta tu app
Una vez que habilites MTE, usa y prueba tu app con normalidad. Si se detecta un problema de seguridad de la memoria, tu app falla con una tombstone que se ve de la siguiente manera (ten en cuenta SIGSEGV
con SEGV_MTESERR
para SYNC o 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
Consulta Cómo interpretar los informes de MTE en la documentación de AOSP para obtener más detalles. También puedes depurar tu app con Android Studio, y el depurador se detendrá en la línea que está provocando el acceso no válido a la memoria.
Usuarios avanzados: Cómo usar MTE en tu propio asignador
Si deseas usar MTE para la memoria no asignada a través de los asignadores normales del sistema, debes modificar tu asignador para etiquetar la memoria y los punteros.
Las páginas de tu asignador deben asignarse usando PROT_MTE
en la marca prot
de mmap
(o mprotect
).
Todas las asignaciones etiquetadas deben estar alineadas con 16 bytes, ya que las etiquetas solo se pueden asignar a fragmentos de 16 bytes (también conocidos como gránulos).
Luego, antes de mostrar un puntero, debes usar la instrucción IRG
para generar una etiqueta aleatoria y almacenarla en el puntero.
Usa las siguientes instrucciones para etiquetar la memoria subyacente:
STG
: Etiqueta un solo gránulo de 16 bytes.ST2G
: Etiqueta dos gránulos de 16 bytes.DC GVA
: Etiqueta la línea de caché con la misma etiqueta.
Como alternativa, las siguientes instrucciones también inicializan la memoria en cero:
STZG
: Etiqueta e inicializa en cero un solo gránulo de 16 bytes.STZ2G
: Etiqueta e inicializa en cero dos gránulos de 16 bytes.DC GZVA
: Etiqueta e inicializa en cero la línea de caché con la misma etiqueta.
Ten en cuenta que estas instrucciones no son compatibles con CPUs más antiguas, por lo que debes ejecutarlas de manera condicional cuando MTE esté habilitada. Puedes verificar si MTE está habilitada para tu proceso de la siguiente manera:
#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;
}
La implementación de Scudo puede resultarte útil como referencia.
Más información
Puedes obtener más información en la MTE User Guide for Android OS (Guía del usuario de MTE para SO Android), escrita por Arm.