Dlaczego MTE?
Błędy związane z bezpieczeństwem pamięci, czyli błędy w obsłudze pamięci w natywnych językach programowania, są częstym problemem w kodzie. Prowadzą one do luk w zabezpieczeniach i problemów ze stabilnością.
Architektura Armv9 wprowadziła rozszerzenie Arm Memory Tagging Extension (MTE), czyli rozszerzenie sprzętowe, które umożliwia wykrywanie w kodzie natywnym błędów odwołania do pamięci po jej zwolnieniu (use-after-free) i przepełnienia bufora (buffer overflow).
Sprawdzanie pomocy
Od Androida 13 wybrane urządzenia obsługują MTE. Aby sprawdzić, czy na urządzeniu jest włączona funkcja MTE, uruchom to polecenie:
adb shell grep mte /proc/cpuinfo
Jeśli wynikiem jest Features : [...] mte, oznacza to, że na urządzeniu jest włączona funkcja MTE.
Niektóre urządzenia nie włączają MTE domyślnie, ale umożliwiają deweloperom ponowne uruchomienie z włączoną funkcją MTE. Jest to konfiguracja eksperymentalna, która nie jest zalecana do normalnego użytku, ponieważ może obniżyć wydajność lub stabilność urządzenia, ale może być przydatna podczas tworzenia aplikacji. Aby uzyskać dostęp do tego trybu, otwórz Opcje programisty > Memory Tagging Extension w aplikacji Ustawienia. Jeśli ta opcja nie jest dostępna, urządzenie nie obsługuje włączania MTE w ten sposób.
Urządzenia obsługujące MTE
Te urządzenia obsługują 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)
Tryby działania MTE
MTE obsługuje 2 tryby: SYNC i ASYNC. Tryb SYNC zapewnia lepsze informacje diagnostyczne, dlatego jest bardziej odpowiedni do celów programistycznych, natomiast tryb ASYNC ma wysoką wydajność, która umożliwia włączenie go w przypadku opublikowanych aplikacji.
Tryb synchroniczny (SYNC)
Ten tryb jest zoptymalizowany pod kątem możliwości debugowania, a nie wydajności. Można go używać jako precyzyjnego narzędzia do wykrywania błędów, gdy dopuszczalne są większe wymagania dotyczące wydajności. Gdy jest włączona, funkcja MTE SYNC działa też jako zabezpieczenie.
W przypadku niezgodności tagu procesor przerywa proces w przypadku instrukcji ładowania lub przechowywania, która spowodowała problem, z sygnałem SIGSEGV (z si_code SEGV_MTESERR) i pełnymi informacjami o dostępie do pamięci oraz adresie, który spowodował błąd.
Ten tryb jest przydatny podczas testowania jako szybsza alternatywa dla HWASan, która nie wymaga ponownej kompilacji kodu, lub w środowisku produkcyjnym, gdy aplikacja stanowi podatną na ataki powierzchnię. Dodatkowo, gdy w trybie ASYNC (opisanym poniżej) zostanie wykryty błąd, dokładny raport o błędzie można uzyskać, używając interfejsów API środowiska wykonawczego do przełączenia wykonywania w tryb SYNC.
Dodatkowo w trybie SYNC alokator Androida rejestruje zrzut stosu każdej alokacji i dealokacji oraz wykorzystuje go do generowania lepszych raportów o błędach, które zawierają wyjaśnienie błędu pamięci, np. odwołania do pamięci po jej zwolnieniu (use-after-free) lub przepełnienia bufora (buffer-overflow), a także zrzuty stosu odpowiednich zdarzeń związanych z pamięcią (więcej informacji znajdziesz w artykule Understanding MTE reports). Takie raporty zawierają więcej informacji kontekstowych i ułatwiają śledzenie i naprawianie błędów niż w trybie ASYNC.
Tryb asynchroniczny (ASYNC)
Ten tryb jest zoptymalizowany pod kątem wydajności, a nie dokładności raportów o błędach. Można go używać do wykrywania błędów związanych z bezpieczeństwem pamięci przy niskim obciążeniu. W przypadku niezgodności tagu procesor kontynuuje wykonywanie aż do najbliższego wejścia do jądra (np. wywołania systemowego lub przerwania zegara), gdzie kończy proces za pomocą sygnału SIGSEGV (kod SEGV_MTEAERR) bez rejestrowania adresu powodującego błąd ani dostępu do pamięci.
Ten tryb jest przydatny do ograniczania podatności na błędy związane z bezpieczeństwem pamięci w środowisku produkcyjnym w przypadku dobrze przetestowanych baz kodu, w których gęstość błędów związanych z bezpieczeństwem pamięci jest niska. Osiąga się to przez używanie trybu SYNC podczas testowania.
Włączanie MTE
Na pojedynczym urządzeniu
W przypadku eksperymentów zmiany kompatybilności aplikacji można wykorzystać do ustawienia domyślnej wartości atrybutu memtagMode w aplikacji, która nie określa żadnej wartości w pliku manifestu (lub określa "default").
Znajdziesz je w menu ustawień globalnych w sekcji System > Zaawansowane > Opcje programisty > Zmiany w zakresie zgodności aplikacji. Ustawienie NATIVE_MEMTAG_ASYNC lub NATIVE_MEMTAG_SYNC włącza MTE w przypadku konkretnej aplikacji.
Możesz też ustawić tę wartość za pomocą polecenia am w ten sposób:
- W przypadku trybu SYNC:
$ 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 projektu 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>
w app/src/debug/AndroidManifest.xml. Spowoduje to zastąpienie ustawienia memtagMode w pliku manifestu synchronizacją w przypadku wersji debugowania.
Możesz też włączyć MTE dla wszystkich wersji niestandardowego typu kompilacji. Aby to zrobić, utwórz własny buildType i umieść kod XML w app/src/<name of buildType>/AndroidManifest.xml.
W przypadku pliku APK na dowolnym obsługiwanym urządzeniu
MTE jest domyślnie wyłączone. Aplikacje, które chcą korzystać z MTE, mogą to zrobić, ustawiając android:memtagMode w tagu <application> lub <process> w pliku AndroidManifest.xml.
android:memtagMode=(off|default|sync|async)
Jeśli atrybut jest ustawiony w tagu <application>, wpływa na wszystkie procesy używane przez aplikację. Można go zastąpić w przypadku poszczególnych procesów, ustawiając tag <process>.
Tworzenie z użyciem instrumentacji
Włączenie MTE w sposób opisany wcześniej pomaga wykrywać błędy uszkodzenia pamięci w stercie natywnej. Aby wykryć uszkodzenie pamięci na stosie, oprócz włączenia MTE w aplikacji kod musi zostać ponownie skompilowany z instrumentacją. Wynikowa aplikacja będzie działać tylko na urządzeniach obsługujących MTE.
Aby skompilować kod natywny (JNI) aplikacji za pomocą MTE:
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 celu 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)
Uruchom aplikację
Po włączeniu MTE korzystaj z aplikacji i testuj ją w normalny sposób. Jeśli zostanie wykryty problem z bezpieczeństwem pamięci, aplikacja ulegnie awarii i wyświetli się zrzut podobny do tego (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 artykule Understanding MTE reports (Wyjaśnienie raportów MTE) w dokumentacji AOSP. Możesz też debugować aplikację w Android Studio. Debugger zatrzyma się w wierszu powodującym nieprawidłowy dostęp do pamięci.
Zaawansowani użytkownicy: używanie MTE we własnym narzędziu do przydzielania
Aby używać MTE w przypadku pamięci nieprzydzielonej za pomocą zwykłych alokatorów systemowych, musisz zmodyfikować alokator, aby tagować pamięć i wskaźniki.
Strony dla alokatora muszą być przydzielane za pomocą PROT_MTE we fladze prot atrybutu mmap (lub mprotect).
Wszystkie przydziały z tagami muszą być wyrównane do 16 bajtów, ponieważ tagi można przypisywać tylko do 16-bajtowych bloków (zwanych też ziarnami).
Następnie przed zwróceniem wskaźnika musisz użyć instrukcji IRG, aby wygenerować losowy tag i zapisać go we wskaźniku.
Aby otagować pamięć bazową, wykonaj te instrukcje:
STG: otaguj pojedynczy 16-bajtowy granulat.ST2G: tag two 16-byte granulesDC GVA: oznaczanie wiersza pamięci podręcznej tym samym tagiem;
Możesz też wyzerować pamięć, wykonując te instrukcje:
STZG: otaguj i wyzeruj pojedynczy 16-bajtowy granulat.STZ2G: otaguj i wyzeruj 2 granule o rozmiarze 16 bajtów.DC GZVA: tagowanie i zerowanie wiersza pamięci podręcznej z tym samym tagiem.
Pamiętaj, że te instrukcje nie są obsługiwane na starszych procesorach, więc musisz je warunkowo uruchamiać, gdy jest włączona funkcja 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;
}
Pomocne może być wdrożenie scudo.
Więcej informacji
Więcej informacji znajdziesz w przewodniku użytkownika MTE dla systemu operacyjnego Android opracowanym przez firmę Arm.