Warum MTE?
Speichersicherheitsfehler, also Fehler bei der Speicherverwaltung in nativen Programmiersprachen, sind häufige Codefehler. Sie führen zu Sicherheitslücken und Stabilitätsproblemen.
Mit Armv9 wurde die Arm Memory Tagging Extension (MTE) eingeführt, eine Hardwareerweiterung, mit der Sie Use-After-Free- und Pufferüberlauffehler in Ihrem nativen Code erkennen können.
Unterstützung prüfen
Ab Android 13 wird MTE auf ausgewählten Geräten unterstützt. Führen Sie den folgenden Befehl aus, um zu prüfen, ob MTE auf Ihrem Gerät aktiviert ist:
adb shell grep mte /proc/cpuinfo
Wenn das Ergebnis Features : [...] mte ist, ist MTE
auf Ihrem Gerät aktiviert.
Auf einigen Geräten ist MTE nicht standardmäßig aktiviert, aber Entwickler können einen Neustart mit aktiviertem MTE durchführen. Dies ist eine experimentelle Konfiguration, die für den normalen Gebrauch nicht empfohlen wird, da sie die Leistung oder Stabilität des Geräts beeinträchtigen kann. Sie kann jedoch für die App-Entwicklung nützlich sein. Um auf diesen Modus zuzugreifen, rufen Sie in der App „Einstellungen“ die Entwickleroptionen > Memory Tagging Extension auf. Wenn diese Option nicht vorhanden ist, unterstützt Ihr Gerät die Aktivierung von MTE auf diese Weise nicht.
Geräte mit MTE-Unterstützung
Die folgenden Geräte unterstützen MTE:
- Pixel 8 (Shiba)
- Pixel 8 Pro (Husky)
- Pixel 8a (Akita)
- Pixel 9 (Tokay)
- Pixel 9 Pro (Caiman)
- Pixel 9 Pro XL (Komodo)
- Pixel 9 Pro Fold (Comet)
- Pixel 9a (Tegu)
MTE-Betriebsmodi
MTE unterstützt zwei Modi: SYNC und ASYNC. Der SYNC-Modus bietet bessere Diagnoseinformationen und eignet sich daher besser für Entwicklungszwecke. Der ASYNC-Modus bietet eine hohe Leistung, sodass er für veröffentlichte Apps aktiviert werden kann.
Synchroner Modus (SYNC)
Dieser Modus ist für die Debugging-Fähigkeit und nicht für die Leistung optimiert. Er kann als präzises Tool zur Fehlererkennung verwendet werden, wenn ein höherer Leistungsaufwand akzeptabel ist. Wenn MTE SYNC aktiviert ist, dient es auch als Sicherheitsmaßnahme.
Bei einer Tag-Abweichung beendet der Prozessor den Prozess bei der betreffenden Lade- oder Speicheranweisung mit SIGSEGV (mit si_code SEGV_MTESERR) und vollständigen Informationen zum Speicherzugriff und zur fehlerhaften Adresse.
Dieser Modus ist beim Testen als schnellere Alternative zu HWASan nützlich, da Sie Ihren Code nicht neu kompilieren müssen. Er kann auch in der Produktion verwendet werden, wenn Ihre App eine anfällige Angriffsfläche darstellt. Wenn im ASYNC-Modus (siehe unten) ein Fehler gefunden wurde, kann außerdem ein genauer Fehlerbericht erstellt werden, indem die Laufzeit-APIs verwendet werden, um die Ausführung in den SYNC-Modus zu wechseln.
Wenn Sie im SYNC-Modus ausgeführt werden, zeichnet der Android-Allocator außerdem den Stacktrace jeder Zuordnung und Freigabe auf und verwendet ihn, um bessere Fehlerberichte zu erstellen. Diese enthalten eine Erklärung eines Speicherfehlers, z. B. Use-After-Free oder Pufferüberlauf, und die Stacktraces der relevanten Speicher ereignisse (weitere Informationen finden Sie unter MTE-Berichte verstehen). Solche Berichte enthalten mehr Kontextinformationen und machen es einfacher, Fehler zu verfolgen und zu beheben als im ASYNC-Modus.
Asynchroner Modus (ASYNC)
Dieser Modus ist für die Leistung und nicht für die Genauigkeit von Fehlerberichten optimiert. Er kann für die Erkennung von Speichersicherheitsfehlern mit geringem Aufwand verwendet werden. Bei einer Tag-Abweichung wird die Ausführung fortgesetzt, bis der nächste Kerneleintrag (z. B. ein Systemaufruf oder eine Timerunterbrechung) erreicht ist. Dort wird der Prozess mit SIGSEGV (Code SEGV_MTEAERR) beendet, ohne die fehlerhafte Adresse oder den Speicherzugriff aufzuzeichnen.
Dieser Modus ist nützlich, um Speichersicherheitslücken in der Produktion in gut getesteten Codebasen zu beheben, bei denen die Dichte von Speichersicherheitsfehlern bekanntermaßen gering ist. Dies wird durch die Verwendung des SYNC-Modus während des Tests erreicht.
MTE aktivieren
Für ein einzelnes Gerät
Für Tests können Änderungen der App-Kompatibilität verwendet werden, um den Standard
wert des memtagMode Attributs für eine Anwendung festzulegen, die keinen
Wert im Manifest angibt (oder "default" angibt).
Diese finden Sie im Menü der globalen Einstellungen unter „System“ > „Erweitert“ > „Entwickleroptionen“ > „Änderungen der App-Kompatibilität“. Wenn Sie NATIVE_MEMTAG_ASYNC oder NATIVE_MEMTAG_SYNC festlegen, wird MTE für eine bestimmte Anwendung aktiviert.
Alternativ kann dies mit dem Befehl am so festgelegt werden:
- Für den SYNC-Modus:
$ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name - Für den ASYNC-Modus:
$ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name
In Gradle
Sie können MTE für alle Debug-Builds Ihres Gradle-Projekts aktivieren, indem Sie
<?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>
in app/src/debug/AndroidManifest.xml einfügen. Dadurch wird memtagMode im Manifest für Debug-Builds mit „sync“ überschrieben.
Alternativ können Sie MTE für alle Builds eines benutzerdefinierten buildType aktivieren. Erstellen Sie dazu
einen eigenen buildType und fügen Sie das
XML in app/src/<name of buildType>/AndroidManifest.xml ein.
Für eine APK auf einem beliebigen geeigneten Gerät
MTE ist standardmäßig deaktiviert. Apps, die MTE verwenden möchten, können
dies tun, indem sie android:memtagMode unter dem <application> oder <process>
Tag in der AndroidManifest.xml festlegen.
android:memtagMode=(off|default|sync|async)
Wenn das Attribut für das Tag <application> festgelegt ist, wirkt es sich auf alle von der Anwendung verwendeten
Prozesse aus. Es kann für einzelne Prozesse überschrieben werden, indem
das Tag <process> festgelegt wird.
Mit Instrumentierung erstellen
Wenn Sie MTE wie oben beschrieben aktivieren, können Sie Speicherfehler im nativen Heap erkennen. Um Speicherfehler im Stack zu erkennen, muss der Code zusätzlich zur Aktivierung von MTE für die App mit Instrumentierung neu erstellt werden. Die resultierende App wird nur auf MTE-fähigen Geräten ausgeführt.
So erstellen Sie den nativen (JNI-)Code Ihrer App mit MTE:
ndk-build
In der Datei 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
Für jedes Ziel in der Datei 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)
App ausführen
Nachdem Sie MTE aktiviert haben, verwenden und testen Sie Ihre App wie gewohnt. Wenn ein Problem mit der Speichersicherheit erkannt wird, stürzt Ihre App mit einem Tombstone ab, der so aussieht (beachten Sie SIGSEGV mit SEGV_MTESERR für SYNC oder SEGV_MTEAERR für 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
Weitere Informationen finden Sie in der AOSP-Dokumentation unter MTE-Berichte verstehen. Sie können Ihre App auch mit Android Studio debuggen. Der Debugger hält dann an der Zeile an, die den ungültigen Speicherzugriff verursacht.
Fortgeschrittene Nutzer: MTE im eigenen Allocator verwenden
Wenn Sie MTE für Speicher verwenden möchten, der nicht über die normalen System-Allocatoren zugewiesen wurde, müssen Sie Ihren Allocator so ändern, dass Speicher und Pointer getaggt werden.
Die Seiten für Ihren Allocator müssen mit PROT_MTE im
prot Flag von mmap (oder mprotect) zugewiesen werden.
Alle getaggten Zuweisungen müssen 16 Byte ausgerichtet sein, da Tags nur für 16-Byte-Blöcke (auch als Granulate bezeichnet) zugewiesen werden können.
Bevor Sie einen Pointer zurückgeben, müssen Sie mit der IRG Anweisung
ein zufälliges Tag generieren und im Pointer speichern.
Folgen Sie der Anleitung unten, um den zugrunde liegenden Speicher zu taggen:
STG: Ein einzelnes 16-Byte-Granulat taggenST2G: Zwei 16-Byte-Granulate taggenDC GVA: Cacheline mit demselben Tag taggen
Alternativ initialisieren die folgenden Anweisungen auch den Speicher mit Nullen:
STZG: Ein einzelnes 16-Byte-Granulat taggen und mit Nullen initialisierenSTZ2G: Zwei 16-Byte-Granulate taggen und mit Nullen initialisierenDC GZVA: Cacheline mit demselben Tag taggen und mit Nullen initialisieren
Beachten Sie, dass diese Anweisungen auf älteren CPUs nicht unterstützt werden. Sie müssen sie also bedingt ausführen, wenn MTE aktiviert ist. So prüfen Sie, ob MTE für Ihren Prozess aktiviert ist:
#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;
}
Die Scudo-Implementierung kann als Referenz hilfreich sein.
Weitere Informationen
Weitere Informationen finden Sie im MTE-Nutzerhandbuch für Android-Betriebssystem von Arm.