Dlaczego MTE?
Typowe problemy z kodem to błędy związane z bezpieczeństwem pamięci, czyli błędy w obsłudze pamięci w natywnych językach programowania. Mogą one powodować luki w zabezpieczeniach i problemy ze stabilnością.
Armv9 wprowadził rozszerzenie MTE (Arm Memory Grouping Extension), rozszerzenie sprzętowe, które pozwala wykrywać w kodzie natywnym błędy użycia po braku pamięci i przepełnienia bufora.
Sprawdź pomoc
Od Androida 13 wybrane urządzenia obsługują MTE. Aby sprawdzić, czy Twoje urządzenie działa z włączonym MTE, uruchom to polecenie:
adb shell grep mte /proc/cpuinfo
Jeśli wynik to Features : [...] mte
, urządzenie działa z włączoną obsługą MTE.
Niektóre urządzenia domyślnie nie włączają MTE, ale umożliwiają deweloperom ponowne uruchamianie z włączonym MTE. Jest to konfiguracja eksperymentalna, która nie jest zalecana do normalnego użytkowania, ponieważ może zmniejszyć wydajność lub stabilność urządzenia, ale może być przydatna podczas tworzenia aplikacji. Aby uzyskać dostęp do tego trybu, otwórz Opcje programisty > Rozszerzenie Memory Tagging Extension w aplikacji Ustawienia. Jeśli ta opcja nie jest dostępna, Twoje urządzenie nie obsługuje tego sposobu włączania MTE.
Tryby działania MTE
MTE obsługuje 2 tryby: SYNC i ASYNC. Tryb SYNCHRONIZACJA zapewnia dokładniejsze informacje diagnostyczne, dzięki czemu lepiej sprawdza się w programowaniu. Natomiast tryb ASYNC jest wydajny i można go włączyć w opublikowanych aplikacjach.
Tryb synchroniczny (SYNC)
Ten tryb jest zoptymalizowany pod kątem debugowania, a nie wydajności, i może być używany jako precyzyjne narzędzie do wykrywania błędów, gdy akceptowalne są większe wymagania dotyczące wydajności. Gdy ta opcja jest włączona, MTE SYNC działa również jako środek łagodzący bezpieczeństwo.
W przypadku niezgodności tagów procesor kończy proces z błędnym wczytywaniem lub przechowywaniem instrukcji za pomocą SIGSEGV (z si_code SEGV_MTESERR) z pełnymi informacjami o dostępie do pamięci i adresem błędów.
Ten tryb jest przydatny podczas testowania jako szybsza alternatywa dla HWASan, która nie wymaga ponownej skompilowania kodu, a także w środowisku produkcyjnym, gdy aplikacja stanowi lukę w zabezpieczeniach. Dodatkowo, gdy tryb ASYNC (opis poniżej) wykryje błąd, można wygenerować dokładny raport o błędzie, przełączając uruchomienie w tryb SYNC za pomocą interfejsów API środowiska wykonawczego.
Dodatkowo w trybie SYNCHRONIZator zapisuje zrzut stosu każdej alokacji i atrybucji oraz używa ich do generowania lepszych raportów o błędach zawierających wyjaśnienie błędów pamięci, np. użycie powolnym lub przepełnienie bufora, oraz zrzuty stosu odpowiednich zdarzeń pamięci (więcej informacji znajdziesz w artykule Omówienie raportów MTE). Takie raporty zawierają więcej informacji kontekstowych oraz ułatwiają namierzanie i naprawianie błędów niż w trybie ASYNC.
Tryb asynchroniczny (ASYNC)
Ten tryb jest zoptymalizowany pod kątem wydajności względem dokładności raportów o błędach i może być używany do szybkiego wykrywania błędów dotyczących bezpieczeństwa pamięci. W przypadku niezgodności tagów procesor kontynuuje wykonywanie kodu do momentu najbliższego wpisu jądra systemu (np. wywołania syscall lub przerwania licznika czasu), co kończy proces przy użyciu SIGSEGV (kod SEGV_MTEAERR) bez rejestrowania adresu błędu lub dostępu do pamięci.
Ten tryb jest przydatny w eliminowaniu luk w zabezpieczeniach pamięci produkcyjnych w dobrze przetestowanych bazach kodu, w których gęstość błędów związanych z bezpieczeństwem pamięci jest niska. Jest to możliwe dzięki użyciu trybu SYNC podczas testowania.
Włącz MTE
Jedno urządzenie
Na potrzeby eksperymentów można użyć zmian dotyczących zgodności aplikacji, aby ustawić domyślną wartość atrybutu memtagMode
dla aplikacji, która nie określa żadnej wartości w pliku manifestu (lub określa "default"
).
Opcje te można znaleźć, wybierając System > Zaawansowane > Opcje programisty > Zmiany zgodności aplikacji
w menu ustawień globalnych. Ustawienie NATIVE_MEMTAG_ASYNC
lub NATIVE_MEMTAG_SYNC
włącza MTE w konkretnej aplikacji.
Można też ustawić tę opcję za pomocą polecenia am
w następujący sposób:
- W trybie SYNCHRONIZACJA:
$ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
- W przypadku trybu ASYNC:
$ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name
W Gradle
Możesz włączyć MTE dla wszystkich kompilacji debugowania w projekcie Gradle, umieszczając
<?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>
na app/src/debug/AndroidManifest.xml
. Spowoduje to zastąpienie polecenia memtagMode
w pliku manifestu przez synchronizację kompilacji do debugowania.
Możesz też włączyć MTE dla wszystkich kompilacji o niestandardowym typie kompilacji. Aby to zrobić, utwórz własny obiekt buildType i umieść kod XML w elemencie app/src/<name of buildType>/AndroidManifest.xml
.
Na potrzeby plików APK na wszystkich obsługujących tę funkcję urządzeniach
MTE jest domyślnie wyłączone. Aplikacje, które chcą używać MTE, mogą to zrobić, ustawiając android:memtagMode
w tagu <application>
lub <process>
w AndroidManifest.xml
.
android:memtagMode=(off|default|sync|async)
Gdy atrybut jest ustawiony w tagu <application>
, wpływa na wszystkie procesy używane przez aplikację i może zostać zastąpiony w przypadku poszczególnych procesów, ustawiając tag <process>
.
Tworzenie z użyciem instrumentacji
Włączenie MTE, jak wyjaśniliśmy wcześniej, pomaga wykrywać błędy uszkodzonej pamięci na natywnej stercie. Aby wykryć uszkodzenie pamięci w stosie, nie tylko włączyć MTE dla aplikacji, trzeba przebudować kod przy użyciu narzędzi. Aplikacja uzyskana w ten sposób będzie działać tylko na urządzeniach obsługujących MTE.
Aby utworzyć natywny kod aplikacji (JNI) za pomocą MTE, wykonaj te czynności:
ndk-build
W pliku 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
W przypadku każdego elementu docelowego w pliku 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)
Uruchamianie aplikacji
Po włączeniu MTE możesz używać aplikacji i przetestować ją jak zwykle. W przypadku wykrycia problemu z bezpieczeństwem pamięci aplikacja ulega awarii i wyświetla się podobny do tego element składowy (zwróć uwagę na SIGSEGV
z SEGV_MTESERR
w przypadku SYNC lub SEGV_MTEAERR
w przypadku 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
Więcej informacji znajdziesz w sekcji Informacje o raportach MTE w dokumentacji AOSP. Możesz też debugować aplikację za pomocą Android Studio. Debuger zatrzymuje się na wierszu powodującym nieprawidłowy dostęp do pamięci.
Zaawansowani użytkownicy: używanie MTE we własnym przydzielaniu
Aby używać MTE w przypadku pamięci, która nie została przydzielona przez zwykłe przydziały systemu, musisz zmodyfikować swój przydział, tak aby oznaczać tagami pamięć i wskaźniki.
Strony przypisujące muszą zostać przydzielone za pomocą parametru PROT_MTE
we fladze prot
z mmap
(lub mprotect
).
Wszystkie otagowane przydziały muszą być wyrównane pod kątem długości 16 bajtów, ponieważ tagi można przypisywać tylko do fragmentów 16-bajtowych (nazywanych też granulami).
Następnie, zanim zwrócisz wskaźnik, musisz użyć instrukcji IRG
, aby wygenerować losowy tag i zapisać go we wskaźniku.
Aby otagować bazową pamięć, wykonaj te czynności:
STG
: dodaj tagi do pojedynczej 16-bajtowej granusyST2G
: dodaj tagi do 2 16-bajtowych granulekDC GVA
: wiersz z pamięci podręcznej tagu z tym samym tagiem
Możesz też wykonać te instrukcje, aby nie inicjować pamięci do zera:
STZG
: dodawanie tagów i inicjowanie zerowe dla pojedynczej 16-bajtowej granusySTZ2G
: dodawanie tagów i inicjowanie bez żadnych 2 16-bajtowych granulekDC GZVA
: tag i brak inicjowania pamięci podręcznej tym samym tagiem
Te instrukcje nie są obsługiwane w przypadku starszych procesorów, musisz je więc uruchamiać warunkowo po włączeniu MTE. Możesz sprawdzić, czy MTE jest włączone w Twoim procesie:
#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;
}
Jako materiał referencyjny możesz użyć implementacji scudo.
Więcej informacji
Więcej informacji znajdziesz w Przewodniku MTE dla systemu operacyjnego Android przygotowanym przez Arm.