Rozszerzenie Arm Memory Tagging Extension (MTE)

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 granusy
  • ST2G: dodaj tagi do 2 16-bajtowych granulek
  • DC 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 granusy
  • STZ2G: dodawanie tagów i inicjowanie bez żadnych 2 16-bajtowych granulek
  • DC 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.