Warum MTE?
Fehler bei der Speichersicherheit, also Fehler bei der Verarbeitung des Arbeitsspeichers in nativen Programmiersprachen, sind häufige Codeprobleme. 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 Fehler in Ihrem nativen Code erkennen können, die nach der kostenlosen Nutzung verwendet werden oder Pufferüberlaufprobleme verursachen.
Support prüfen
Ab Android 13 unterstützen ausgewählte Geräte MTE. Führen Sie den folgenden Befehl aus, um zu prüfen, ob Ihr Gerät mit aktivierter MTE ausgeführt wird:
adb shell grep mte /proc/cpuinfo
Wenn das Ergebnis Features : [...] mte
lautet, wird auf Ihrem Gerät MTE aktiviert.
Auf einigen Geräten ist MTE nicht standardmäßig aktiviert, Entwickler können jedoch mit aktiviertem MTE einen Neustart durchführen. Dies ist eine experimentelle Konfiguration, die für den normalen Gebrauch nicht empfohlen wird, da sie die Geräteleistung oder -stabilität beeinträchtigen kann, aber für die App-Entwicklung nützlich sein kann. Wenn Sie auf diesen Modus zugreifen möchten, rufen Sie in der App „Einstellungen“ Entwickleroptionen > Erweiterung „Memory Tagging Extension“ auf. Wenn diese Option nicht verfügbar ist, wird die Aktivierung von MTE auf Ihrem Gerät nicht unterstützt.
MTE-Betriebsmodi
MTE unterstützt zwei Modi: SYNC und ASYNC. Der SYNC-Modus bietet bessere Diagnoseinformationen und ist daher besser für Entwicklungszwecke geeignet, während der ASYNC-Modus eine hohe Leistung bietet, sodass er für freigegebene Anwendungen aktiviert werden kann.
Synchroner Modus (SYNC)
Dieser Modus ist für die Fehlerbehebung optimiert und kann als präzises Tool zur Fehlererkennung verwendet werden, wenn ein höherer Leistungsaufwand akzeptabel ist. Wenn MTE SYNC aktiviert ist, dient auch die Sicherheitsminderung.
Bei einem nicht übereinstimmenden Tag beendet der Prozessor den Prozess für den problematischen Ladevorgang oder die Speicheranweisung mit SIGSEGV (mit si_code SEGV_MTESERR) und vollständigen Informationen über den Arbeitsspeicherzugriff und die fehlerhafte Adresse.
Dieser Modus ist während des Tests als schnellere Alternative zu HWASan nützlich, bei der Sie Ihren Code nicht neu kompilieren müssen, oder in der Produktion, wenn Ihre Anwendung eine anfällige Angriffsfläche darstellt. Wenn der ASYNC-Modus (siehe unten) einen Fehler gefunden hat, kann ein genauer Fehlerbericht abgerufen werden. Dazu verwenden Sie die Laufzeit-APIs, um die Ausführung in den SYNC-Modus zu wechseln.
Darüber hinaus zeichnet der Android-Allocator im SYNC-Modus den Stacktrace jeder Zuweisung und Freigabe auf und verwendet sie, um bessere Fehlerberichte bereitzustellen, die eine Erklärung eines Speicherfehlers (z. B. Use-After-Free oder Pufferüberlauf) und die Stacktraces der relevanten Speicherereignisse enthalten (weitere Informationen finden Sie unter MTE-Berichte). Solche Berichte bieten mehr Kontextinformationen und erleichtern das Nachverfolgen und Beheben von Fehlern als im ASYNC-Modus.
Asynchroner Modus (ASYNC)
Dieser Modus ist im Hinblick auf Leistung statt Genauigkeit von Fehlerberichten optimiert und kann für die Erkennung von Arbeitsspeicher-Sicherheitsfehlern mit geringem Mehraufwand verwendet werden. Bei einem nicht übereinstimmenden Tag setzt der Prozessor die Ausführung bis zum nächsten Kernel-Eintrag (z. B. Systemaufruf oder Timer-Unterbrechung) fort, wo er den Prozess mit SIGSEGV (Code SEGV_MTEAERR) beendet, ohne die fehlerhafte Adresse oder den fehlerhaften Speicherzugriff aufzuzeichnen.
Dieser Modus ist nützlich, um Sicherheitslücken in der Produktionsumgebung auf gut getesteten Codebasen zu minimieren, bei denen die Dichte der sicherheitsrelevanten Programmfehler im Arbeitsspeicher gering ist. Dies wird durch die Verwendung des SYNC-Modus während des Tests erreicht.
MTE aktivieren
Für ein einzelnes Gerät
Zum Testen können Änderungen an der App-Kompatibilität verwendet werden, um den Standardwert des Attributs memtagMode
für eine Anwendung festzulegen, die keinen Wert im Manifest oder "default"
angibt.
Sie finden sie im globalen Einstellungsmenü unter „System“ > „Erweitert“ > „Entwickleroptionen“ > „Änderungen an 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
. Dadurch wird memtagMode
des Manifests mit der Synchronisierung für Debug-Builds überschrieben.
Alternativ können Sie MTE für alle Builds eines benutzerdefinierten buildType aktivieren. Erstellen Sie dazu Ihren eigenen buildType und fügen Sie den XML-Code in app/src/<name of buildType>/AndroidManifest.xml
ein.
Für ein APK auf einem beliebigen kompatiblen Gerät
MTE ist standardmäßig deaktiviert. Anwendungen, die MTE verwenden möchten, können dies tun, indem Sie android:memtagMode
unter dem <application>
- oder <process>
-Tag im 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 zuvor erläutert aktivieren, können Fehler durch Speicherbeschädigungen auf dem nativen Heap erkannt werden. Zum Erkennen von Speicherbeschädigungen im Stack muss nicht nur MTE für die Anwendung aktiviert werden, sondern der Code mit Instrumentierung neu erstellt werden. Die resultierende App kann nur auf MTE-fähigen Geräten ausgeführt werden.
So erstellen Sie den nativen Code Ihrer Anwendung (JNI) mit MTE:
NK-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ühren Sie für jedes Ziel in der Datei CMakeLists.txt folgende Schritte aus:
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
Verwenden und testen Sie Ihre Anwendung nach der Aktivierung von MTE wie gewohnt. Wenn ein Problem mit der Speichersicherheit erkannt wird, stürzt Ihre App mit einem Tombstone ab, der in etwa 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 Informationen zu MTE-Berichten. Sie können auch Fehler in Ihrer App mit Android Studio beheben. Der Debugger stoppt dann an der Zeile, die den ungültigen Speicherzugriff verursacht.
Fortgeschrittene Nutzer: MTE in Ihrem eigenen Allocator verwenden
Wenn Sie MTE für Arbeitsspeicher verwenden möchten, der nicht über die normalen Systemzuordnungen zugewiesen wurde, müssen Sie Ihren Zuweisungsspeicher so ändern, dass Arbeitsspeicher und Zeiger getaggt werden.
Die Seiten für Ihren Allocator müssen mithilfe von PROT_MTE
im Flag prot
von mmap
(oder mprotect
) zugewiesen werden.
Alle getaggten Zuweisungen müssen auf 16 Byte ausgerichtet sein, da Tags nur für 16-Byte-Blöcke (auch als Granulen bezeichnet) zugewiesen werden können.
Bevor Sie einen Zeiger zurückgeben, müssen Sie mit der Anweisung IRG
ein zufälliges Tag generieren und im Zeiger speichern.
So taggen Sie den zugrunde liegenden Arbeitsspeicher:
STG
: eine einzelne 16-Byte-Granule taggenST2G
: Zwei 16-Byte-Granulen taggenDC GVA
: Tag-Cache-Zeile mit demselben Tag
Alternativ wird mit den folgenden Anweisungen auch der Speicher mit Nullen initialisiert:
STZG
: mit Tags versehen und mit Null eine einzelne 16-Byte-Granule initialisierenSTZ2G
: Zwei 16-Byte-Granulen taggen und mit Null initialisierenDC GZVA
: Tag und Cache-Zeile mit Nullinitialisierung mit demselben Tag
Beachten Sie, dass diese Anleitung auf älteren CPUs nicht unterstützt wird. Sie müssen sie also bedingt ausführen, wenn MTE aktiviert ist. Sie können prüfen, 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 das Android-Betriebssystem von Arm.