Integralność sterowania przepływem

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

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

CFI LLVM wymaga kompilacji z optymalizacją w czasie łączenia (LTO). LTO zachowuje reprezentację kodu bitowego LLVM plików obiektów do czasu kompilacji, co pozwala kompilatorowi lepiej określić, jakie optymalizacje można wykonać. Włączenie LTO zmniejsza rozmiar końcowego pliku binarnego i poprawia wydajność, ale wydłuża czas kompilacji. Podczas testowania na Androidzie połączenie LTO i CFI miało nieznaczny wpływ na rozmiar kodu i wydajność. W niektórych przypadkach obie te wartości uległy poprawie.

Więcej informacji technicznych na temat CFI oraz sposobu obsługi innych kontroli w przód znajdziesz w dokumentacji 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łączone na urządzeniach Arm64 dla zestawu komponentów w /platform/build/target/product/cfi-common.mk. Jest ona też bezpośrednio włączona w zestawie plików makefile/blueprint komponentów multimedialnych, takich jak /platform/frameworks/av/media/libmedia/Android.bp/platform/frameworks/av/cmds/stagefright/Android.mk.

Wdrażanie systemu CFI

Jeśli używasz Clanga i systemu kompilacji Androida, CFI jest domyślnie włączone. CFI pomaga chronić użytkowników Androida, dlatego nie należy go wyłączać.

Zdecydowanie zalecamy włączenie CFI w przypadku dodatkowych komponentów. Najlepszymi kandydatami są uprzywilejowany kod natywny lub kod natywny, który przetwarza dane wejściowe nieznanego pochodzenia. Jeśli używasz clanga i systemu kompilacji Androida, możesz włączyć CFI w nowych komponentach, dodając kilka wierszy do plików make lub plików blueprint.

Obsługa CFI w makefile

Aby włączyć CFI w pliku make, takim jak /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 oczyszczacz podczas kompilacji.
  • LOCAL_SANITIZE_DIAG włącza tryb diagnostyczny dla CFI. Tryb diagnostyczny umożliwia wyświetlanie dodatkowych informacji debugowania w logcat podczas awarii, co jest przydatne podczas tworzenia i testowania wersji. Pamiętaj jednak, aby usunąć tryb diagnostyczny z kompilacji produkcyjnych.
  • LOCAL_SANITIZE_BLACKLIST pozwala komponentom selektywnie wyłączać pomiary CFI w przypadku poszczególnych funkcji lub plików źródłowych. Możesz użyć listy wykluczeń jako ostatecznego rozwiązania, aby rozwiązać problemy, które mogą dotyczyć użytkowników. Więcej informacji znajdziesz w artykule Wyłączanie funkcji 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łączysz CFI w nowych komponentach, możesz napotkać kilka problemów z błędami niezgodności typu funkcjibłędami niezgodności typu kodu montażowego.

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 wirtualne i niewirtualne wywołania funkcji członka tylko do obiektów, które są pochodną klasą 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ń, pomiar dodany przez CFI zostanie przerwany. Na przykład ślad stosu pokazuje SIGABRT, a logcat zawiera wiersz o niezgodności integralności przepływu sterowania.

Aby to naprawić, sprawdź, czy 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 pośrednie wywołania assembly. Kod w języku asemblera nie jest wpisywany, co powoduje niezgodność typu.

Aby to naprawić, utwórz opakowania kodu natywnego dla każdego wywołania assembly i nadaj im tę samą sygnaturę funkcji co punkt wywołania. Następnie może bezpośrednio wywołać kod montażu. Odgałęzienia bezpośrednie nie są instrumentowane przez CFI (nie można ich przekierować w czasie wykonywania, więc nie stanowią zagrożenia dla bezpieczeństwa), więc to rozwiąże problem.

Jeśli jest zbyt wiele funkcji assembly, których nie można naprawić, możesz też dodać do listy zablokowanych wszystkie funkcje zawierające pośrednie wywołania assembly. Nie jest to zalecane, ponieważ powoduje wyłączenie kontroli CFI w tych funkcjach, przez 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 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 plik z czarną listą sanityzatora w momencie kompilacji. Lista wykluczeń instruuje kompilator, aby wyłączył instrumentację CFI w określonych lokalizacjach.

System kompilacji Androida obsługuje listy wykluczeń dla poszczególnych komponentów (umożliwiając wybór plików źródłowych lub poszczególnych funkcji, które nie będą objęte instrumentacją 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 do sprawdzania zgodności z CFI. Zamiast tego sprawdź, czy testy CTS są przeprowadzane z włączonym lub wyłączonym CFI, aby upewnić się, że CFI nie wpływa na urządzenie.