Integralność sterowania przepływem

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/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 funkcjibłę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:

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.