Extension MTE (Memory Tagging Extension) Arm

Pourquoi utiliser MTE ?

Les bugs de sécurité au niveau de la mémoire, qui sont des erreurs de gestion de la mémoire dans les langages de programmation natifs, sont des problèmes de code courants. Ils entraînent des failles de sécurité ainsi que des problèmes de stabilité.

Armv9 a lancé l'extension matérielle MTE (Memory Tagging Extension) Arm, qui vous permet de détecter les bugs d'utilisation après libération (use-after-free) et de dépassement de mémoire tampon (buffer-overflow) dans votre code natif.

Vérifier si votre appareil est compatible

À partir d'Android 13, certains appareils sont compatibles avec MTE. Pour vérifier si votre appareil fonctionne avec MTE, exécutez la commande suivante :

adb shell grep mte /proc/cpuinfo

Si le résultat est Features : [...] mte, votre appareil fonctionne avec MTE.

Certains appareils n'activent pas MTE par défaut, mais permettent aux développeurs de redémarrer avec MTE activé. Il s'agit d'une configuration expérimentale qui n'est pas recommandée pour une utilisation normale, car cela peut réduire les performances ou la stabilité de l'appareil, mais elle peut être utile pour le développement d'applications. Pour accéder à ce mode, sélectionnez Options pour les développeurs > Memory Tagging Extension dans l'application Paramètres. Si cette option n'est pas disponible, cela signifie que votre appareil ne prend pas en charge l'activation de MTE de cette façon.

Modes de fonctionnement avec MTE

MTE est compatible avec deux modes : SYNC et ASYNC. Le mode SYNC fournit de meilleures informations de diagnostic et est donc plus adapté à des fins de développement, tandis que le mode ASYNC offre de hautes performances, ce qui permet de l'activer pour les applications publiées.

Mode synchrone (SYNC)

Ce mode est optimisé pour le débogage plutôt que pour les performances. Il peut être utilisé comme un outil précis de détection de bugs quand un impact plus important sur les performances est acceptable. Lorsque ce mode est activé avec MTE, il agit également comme une mesure de sécurité.

En cas de non-concordance des tags, le processeur met fin au processus sur l'instruction de chargement ou de stockage incriminée avec SIGSEGV (avec si_code SEGV_MTESERR) et des informations complètes sur l'accès à la mémoire et l'adresse défaillante.

Ce mode sert lors des tests comme alternative plus rapide à HWASan (il ne vous oblige pas à recompiler votre code), ou bien en production quand votre application représente une surface d'attaque vulnérable. En outre, lorsque le mode ASYNC (décrit ci-après) a détecté un bug, vous pouvez obtenir un rapport de bug précis à l'aide des API d'exécution pour passer l'exécution en mode SYNC.

De plus, lors de l'exécution en mode SYNC, l'outil d'allocation d'Android enregistre la trace de la pile de chaque allocation et désallocation, et les utilise pour fournir des rapports d'erreur plus clairs comportant une explication de l'erreur de mémoire (use-after-free ou buffer-overflow, par exemple), ainsi que les traces de pile des événements de mémoire pertinents (pour en savoir plus, consultez la section Comprendre les rapports MTE). Ces rapports fournissent des informations avec davantage de contexte, et permettent de suivre et de corriger les bugs plus facilement qu'en mode ASYNC.

Mode asynchrone (ASYNC)

Ce mode est optimisé pour les performances plutôt que pour la précision des rapports de bugs. Il peut également être utilisé pour détecter les bugs de sécurité de la mémoire à faible coût. En cas de non-concordance des tags, le processeur poursuit l'exécution jusqu'à l'entrée de noyau la plus proche (telle qu'un appel système ou une interruption de minuteur), où il met fin au processus avec SIGSEGV (code SEGV_MTEAERR) sans enregistrer l'adresse défaillante ni l'accès à la mémoire.

Ce mode est utile pour limiter les risques de failles de sécurité au niveau de la mémoire en production sur des codebases bien testés, où la densité de bugs de sécurité liés à la mémoire est connue pour être faible (ce qui est obtenu en utilisant le mode SYNC pendant les tests).

Activer MTE

Pour un seul appareil

Pour effectuer des tests, vous pouvez changer la compatibilité des applications afin de définir la valeur par défaut de l'attribut memtagMode pour une application où aucune valeur n'est spécifiée dans le fichier manifeste (ou dont la valeur est "default").

Pour cela, accédez à Système > Paramètres avancés > Options pour les développeurs > Changement de compatibilité des applications (dans le menu des paramètres généraux). Si vous définissez NATIVE_MEMTAG_ASYNC ou NATIVE_MEMTAG_SYNC, MTE est activé pour une application particulière.

Vous pouvez également définir cela à l'aide de la commande am comme suit :

  • Pour le mode SYNC : $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • Pour le mode ASYNC : $ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

Dans Gradle

Vous pouvez activer MTE pour toutes les versions de débogage de votre projet Gradle en inscrivant

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

dans app/src/debug/AndroidManifest.xml. Cela remplacera memtagMode par la synchronisation des versions de débogage dans votre fichier manifeste.

Vous pouvez également activer MTE pour tous les builds d'un buildType personnalisé. Pour ce faire, créez votre propre buildType et placez le fichier XML dans app/src/<name of buildType>/AndroidManifest.xml.

Pour un APK sur un appareil compatible

MTE est désactivé par défaut. Les applications qui souhaitent utiliser MTE peuvent le faire en définissant android:memtagMode sous la balise <application> ou <process> dans le fichier AndroidManifest.xml.

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

Lorsqu'il est défini sur la balise <application>, l'attribut affecte tous les processus utilisés par l'application et peut être ignoré pour chaque processus en spécifiant la balise <process>.

Compiler avec l'instrumentation

L'activation de MTE, comme expliqué précédemment, permet de détecter les bugs de corruption de mémoire sur le le tas de mémoire natif. Pour détecter la corruption de mémoire sur la pile, en plus d'activer MTE pour l'application, le code doit être recréé avec l'instrumentation. La l'application obtenue ne s'exécutera que sur les appareils compatibles avec MTE.

Pour compiler le code natif (JNI) de votre application avec MTE, procédez comme suit:

ndk-build

Dans votre fichier 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

Pour chaque cible de votre fichier 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)

Exécuter votre application

Après avoir activé MTE, utilisez et testez votre application normalement. Si un problème de sécurité est détecté au niveau de la mémoire, votre application plante avec un Tombstone semblable à ceci (notez le SIGSEGV avec SEGV_MTESERR pour SYNC ou SEGV_MTEAERR pour 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

Pour en savoir plus, consultez Comprendre les rapports MTE dans la documentation AOSP. Vous pouvez également déboguer votre application avec Android Studio. Le débogueur s'arrêtera à la ligne qui entraîne l'accès non valide à la mémoire.

Utiliser MTE dans votre propre outil d'allocation (réservé aux utilisateurs expérimentés)

Pour utiliser MTE pour la mémoire non allouée via les outils d'allocation de système habituels, vous devez modifier votre outil d'allocation afin d'ajouter des balises à la mémoire et aux pointeurs.

Les pages de votre outil d'allocation doivent être allouées à l'aide de PROT_MTE dans l'indicateur prot de mmap (ou mprotect).

Toutes les allocations avec balise doivent être alignées sur 16 octets, car les balises ne peuvent être attribuées que pour des fragments de 16 octets.

Ensuite, avant de renvoyer un pointeur, vous devez utiliser l'instruction IRG pour générer une balise aléatoire et la stocker dans ce pointeur.

Utilisez les instructions suivantes pour ajouter des balises à la mémoire sous-jacente :

  • STG : ajouter une balise à un seul fragment de 16 octets
  • ST2G : ajouter une balise à deux fragments de 16 octets
  • DC GVA : ajouter une balise identique à la ligne de cache

Vous pouvez également initialiser la mémoire à zéro en suivant les instructions ci-dessous :

  • STZG : ajouter une balise à un seul fragment de 16 octets et l'initialiser à zéro
  • STZ2G : ajouter une balise à deux fragments de 16 octets et les initialiser à zéro
  • DC GZVA : ajouter une balise à la ligne de cache et l'initialiser à zéro

Notez que ces instructions ne fonctionnent pas sur les anciens processeurs. Vous devez donc les exécuter de manière conditionnelle lorsque MTE est activé. Vous pouvez vérifier si MTE est activé pour votre processus :

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

Vous trouverez peut-être l'implémentation scudo utile.

En savoir plus

Pour en savoir plus, consultez le guide de l'utilisateur MTE pour l'OS Android, écrit par Arm.