Integralność sterowania przepływem

Od 2016 r. około 86% wszystkich luk w Androidzie jest związanych z bezpieczeństwem pamięci. Większość luk jest wykorzystywana przez osoby przeprowadzające atak, które zmieniają normalny przepływ sterowania w aplikacji, aby wykonywać dowolne złośliwe działania z wszystkimi uprawnieniami wykorzystywanej aplikacji. Integralność przepływu sterowania (CFI) to mechanizm zabezpieczeń, który uniemożliwia wprowadzanie zmian w oryginalnym grafie przepływu sterowania skompilowanego pliku binarnego, co znacznie utrudnia przeprowadzanie takich ataków.

W Androidzie 8.1 włączyliśmy implementację CFI w LLVM w stosie multimediów. W Androidzie 9 włączyliśmy CFI w większej liczbie komponentów, a także w jądrze. CFI systemu jest domyślnie włączone, ale musisz włączyć CFI jądra.

CFI w LLVM wymaga kompilacji z optymalizacją czasu łączenia (LTO). LTO zachowuje reprezentację kodu bitowego LLVM plików obiektowych do czasu łączenia, co pozwala kompilatorowi lepiej określić, jakie optymalizacje można zastosować. Włączenie LTO zmniejsza rozmiar końcowego pliku binarnego i zwiększa wydajność, ale wydłuża czas kompilacji. Podczas testowania w Androidzie połączenie LTO i CFI powoduje pomijalne obciążenie rozmiaru kodu i wydajności. W kilku przypadkach oba te parametry uległy poprawie.

Więcej informacji technicznych o CFI i sposobie obsługi innych kontroli przepływu sterowania znajdziesz w dokumentacji projektu LLVM.

Przykłady i źródło

CFI jest udostępniane 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łączone na urządzeniach z architekturą Arm64 w przypadku zestawu komponentów w /platform/build/target/product/cfi-common.mk. Jest też bezpośrednio włączone w przypadku zestawu plików makefile/blueprint komponentów multimedialnych, takich jak /platform/frameworks/av/media/libmedia/Android.bp i /platform/frameworks/av/cmds/stagefright/Android.mk.

Implementowanie CFI systemu

CFI jest domyślnie włączone, jeśli używasz Clang i systemu kompilacji Androida. CFI pomaga chronić użytkowników Androida, dlatego 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 niezaufane dane wejściowe użytkownika. Jeśli używasz Clang i systemu kompilacji Androida, możesz włączyć CFI w nowych komponentach, dodając kilka wierszy do plików makefile lub blueprint.

Obsługa CFI w plikach makefile

Aby włączyć CFI w pliku makefile, 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. W trybie diagnostycznym podczas awarii w logcat są wyświetlane dodatkowe informacje debugowania, które są 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. Czarną listę możesz użyć jako ostatnią deskę ratunku, aby rozwiązać problemy, które mogą występować u użytkowników. Więcej informacji znajdziesz w sekcji Wyłączanie CFI.

Obsługa CFI w plikach blueprint

Aby włączyć CFI w pliku blueprint, 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ć kilka problemów związanych 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 do skoków tylko do funkcji, które mają ten sam typ dynamiczny co typ statyczny używany w wywołaniu. CFI ogranicza wywołania funkcji wirtualnych i niewirtualnych do skoków tylko do obiektów, które są klasą pochodną typu statycznego obiektu używanego do wywołania. Oznacza to, że jeśli masz kod, który narusza którekolwiek z tych założeń, instrumentacja dodana przez CFI zostanie przerwana. Na przykład ślad stosu pokazuje SIGABRT, a logcat zawiera wiersz informujący o tym, że CFI wykryło niezgodność.

Aby to naprawić, upewnij się, że wywoływana funkcja ma ten sam typ, który został zadeklarowany statycznie. Oto 2 przykłady CL:

Innym możliwym problemem jest próba włączenia CFI w kodzie, który zawiera wywołania pośrednie do asemblera. Ponieważ kod asemblera nie jest typowany, powoduje to niezgodność typów.

Aby to naprawić, utwórz otoczki kodu natywnego dla każdego wywołania asemblera i nadaj im tę samą sygnaturę 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 kontrole CFI w tych funkcjach, co zwiększa powierzchnię ataku.

Wyłączanie CFI

Nie zaobserwowaliśmy żadnego obciążenia wydajności, więc nie musisz wyłączać CFI. Jeśli jednak występuje problem, który ma wpływ na użytkownika, możesz selektywnie wyłączyć CFI w przypadku poszczególnych funkcji lub plików źródłowych, podając plik czarnej listy narzędzia do czyszczenia podczas kompilacji. Czarna lista instruuje kompilator, aby wyłączył instrumentację CFI w określonych lokalizacjach.

System kompilacji Androida obsługuje czarne listy poszczególnych komponentów (umożliwiające wybieranie plików źródłowych lub poszczególnych funkcji, które nie będą instrumentowane przez CFI) zarówno w przypadku Make, jak i Soong. Więcej informacji o formacie pliku czarnej listy znajdziesz w dokumentacji Clang.

Weryfikacja

Obecnie nie ma testów CTS przeznaczonych 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.