Od 2016 roku około 86% wszystkich luk w zabezpieczeniach Androida jest związanych z bezpieczeństwem pamięci. Większość luk w zabezpieczeniach jest wykorzystywana przez osoby przeprowadzające atak, które zmieniają normalny przepływ sterowania aplikacji, aby wykonywać dowolne szkodliwe działania z wszystkimi uprawnieniami wykorzystywanej aplikacji. Integralność przepływu sterowania (CFI) to mechanizm zabezpieczeń, który uniemożliwia zmiany w oryginalnym grafie przepływu sterowania skompilowanego pliku binarnego, co znacznie utrudnia przeprowadzanie takich ataków.
W Androidzie 8.1 włączyliśmy w stosie multimediów implementację CFI w LLVM. W Androidzie 9 włączyliśmy CFI w większej liczbie komponentów, a także w jądrze. System CFI jest domyślnie włączony, ale musisz włączyć CFI jądra.
CFI w LLVM wymaga kompilacji z użyciem optymalizacji w czasie łączenia (LTO). LTO zachowuje reprezentację kodu bitowego LLVM plików obiektowych do czasu łączenia, co pozwala kompilatorowi lepiej określać, jakie optymalizacje można przeprowadzić. Włączenie LTO zmniejsza rozmiar końcowego pliku binarnego i zwiększa wydajność, ale wydłuża czas kompilacji. Podczas testów na Androidzie połączenie LTO i CFI powoduje znikome obciążenie rozmiaru i wydajności kodu. W kilku przypadkach oba te parametry uległy poprawie.
Więcej informacji technicznych o CFI i innych kontrolach przekazywania sterowania znajdziesz w dokumentacji projektu LLVM.
Przykłady i źródło
CFI jest dostarczany przez kompilator i dodaje instrumentację do pliku binarnego podczas kompilacji. Obsługujemy CFI w łańcuchu narzędzi Clang i systemie kompilacji Androida w AOSP.
CFI jest domyślnie włączona na urządzeniach z architekturą Arm64 w przypadku komponentów w /platform/build/target/product/cfi-common.mk
.
Jest też bezpośrednio włączona w plikach makefile/blueprint zestawu komponentów multimedialnych, takich jak /platform/frameworks/av/media/libmedia/Android.bp
i /platform/frameworks/av/cmds/stagefright/Android.mk
.
Implementowanie systemowego CFI
CFI jest domyślnie włączone, jeśli używasz Clang i systemu kompilacji Androida. CFI pomaga chronić użytkowników Androida, więc nie należy go wyłączać.
Zdecydowanie zalecamy włączenie CFI w przypadku dodatkowych komponentów. Idealnymi kandydatami są uprzywilejowany kod natywny lub kod natywny, który przetwarza dane wejściowe użytkownika, do których nie masz zaufania. Jeśli używasz kompilatora clang i systemu kompilacji Androida, możesz włączyć CFI w nowych komponentach, dodając kilka wierszy do plików makefile lub plików blueprint.
Obsługa CFI w plikach makefile
Aby włączyć CFI w pliku make, np. /platform/frameworks/av/cmds/stagefright/Android.mk
, dodaj:
LOCAL_SANITIZE := cfi # Optional features LOCAL_SANITIZE_DIAG := cfi LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
LOCAL_SANITIZE
określa CFI jako narzędzie do czyszczenia podczas kompilacji.LOCAL_SANITIZE_DIAG
włącza tryb diagnostyczny dla CFI. Tryb diagnostyczny wyświetla dodatkowe informacje debugowania w logcat podczas awarii, co jest przydatne podczas tworzenia i testowania kompilacji. Pamiętaj jednak, aby usunąć tryb diagnostyczny w kompilacjach produkcyjnych.LOCAL_SANITIZE_BLACKLIST
umożliwia komponentom selektywne wyłączanie instrumentacji CFI w przypadku poszczególnych funkcji lub plików źródłowych. Listę zablokowanych możesz wykorzystać w ostateczności, aby rozwiązać problemy użytkowników, które w inny sposób nie mogą zostać wyeliminowane. Więcej informacji znajdziesz w sekcji Wyłączanie CFI.
Obsługa CFI w plikach planu
Aby włączyć CFI w pliku planu, np. /platform/frameworks/av/media/libmedia/Android.bp
, dodaj:
sanitize: { cfi: true, diag: { cfi: true, }, blacklist: "cfi_blacklist.txt", },
Rozwiązywanie problemów
Jeśli włączasz CFI w nowych komponentach, możesz napotkać problemy z błędami niezgodności typu funkcji i błędami niezgodności typu kodu asemblera.
Błędy niezgodności typu funkcji występują, ponieważ CFI ogranicza wywołania pośrednie tylko do funkcji, które mają ten sam typ dynamiczny co typ statyczny użyty w wywołaniu. CFI ogranicza wywołania funkcji wirtualnych i niewirtualnych do obiektów, które są klasą pochodną typu statycznego obiektu użytego do wywołania. Oznacza to, że jeśli masz kod, który narusza któreś z tych założeń, instrumentacja dodawana przez CFI zostanie przerwana. Na przykład ślad stosu pokazuje SIGABRT, a logcat zawiera wiersz o tym, że funkcja sprawdzania integralności przepływu sterowania wykryła niezgodność.
Aby to naprawić, upewnij się, że wywoływana funkcja ma ten sam typ, który został zadeklarowany statycznie. Oto 2 przykładowe listy zmian:
- Bluetooth: /c/platform/system/bt/+/532377
- NFC: /c/platform/system/nfc/+/527858
Innym możliwym problemem jest próba włączenia CFI w kodzie, który zawiera pośrednie wywołania asemblera. Ponieważ kod asemblera nie jest wpisywany, powoduje to niezgodność typów.
Aby rozwiązać ten problem, utwórz otoczki kodu natywnego dla każdego wywołania asemblera i nadaj im ten sam podpis funkcji co wskaźnik wywołujący. Otoczka może wtedy bezpośrednio wywoływać kod asemblera. Ponieważ gałęzie bezpośrednie nie są instrumentowane przez CFI (nie można ich przekierować w czasie działania, więc nie stanowią zagrożenia dla bezpieczeństwa), rozwiąże to problem.
Jeśli jest zbyt wiele funkcji asemblera i nie można ich wszystkich naprawić, możesz też dodać do czarnej listy wszystkie funkcje, które zawierają wywołania pośrednie do asemblera. Nie jest to zalecane, ponieważ wyłącza sprawdzanie CFI w tych funkcjach, co zwiększa podatność na ataki.
Wyłączanie CFI
Nie zaobserwowaliśmy żadnych dodatkowych obciążeń, więc nie musisz wyłączać CFI. Jeśli jednak ma to wpływ na użytkownika, możesz selektywnie wyłączyć CFI w przypadku poszczególnych funkcji lub plików źródłowych, podając w czasie kompilacji plik z czarną listą narzędzia do oczyszczania. Czarna lista nakazuje kompilatorowi wyłączenie instrumentacji CFI w określonych lokalizacjach.
System kompilacji Androida obsługuje czarne listy poszczególnych komponentów (umożliwiające wybór plików źródłowych lub poszczególnych funkcji, które nie będą podlegać instrumentacji CFI) zarówno w przypadku Make, jak i Soong. Więcej informacji o formacie pliku z czarną listą znajdziesz w dokumentacji Clang.
Weryfikacja
Obecnie nie ma testu CTS przeznaczonego specjalnie dla CFI. Zamiast tego upewnij się, że testy CTS przechodzą z włączonym lub wyłączonym CFI, aby sprawdzić, czy CFI nie ma wpływu na urządzenie.