Wdróż DM-Vity

Android w wersji 4.4 i nowszej obsługuje weryfikację podczas uruchamiania za pomocą opcjonalnej funkcji jądra device-mapper-verity (dm-verity), która zapewnia przejrzyste sprawdzanie integralności urządzeń blokowych. Funkcja dm-verity pomaga zapobiegać trwałym rootkitom, które mogą przechowywać uprawnienia roota i stanowić zagrożenie dla urządzeń. Ta funkcja pomaga użytkownikom Androida mieć pewność, że po uruchomieniu urządzenie jest w tym samym stanie, w jakim było używane ostatnio.

Potencjalnie szkodliwe aplikacje z przywilejami roota mogą ukrywać się przed programami wykrywającymi i w inny sposób maskować swoją obecność. Oprogramowanie do rootowania może to zrobić, ponieważ często ma więcej uprawnień niż detektory, co pozwala mu „kłamać” programom wykrywającym.

Funkcja dm-verity umożliwia sprawdzenie urządzenia blokowego, czyli podstawowej warstwy pamięci masowej systemu plików, i określenie, czy odpowiada ono oczekiwanej konfiguracji. Wykorzystuje do tego drzewo skrótów kryptograficznych. Dla każdego bloku (zwykle 4 kB) jest hash SHA-256.

Wartości hasz są przechowywane w drzewie stron, dlatego do weryfikacji reszty drzewa należy zaufać tylko haszowi „root” najwyższego poziomu. Możliwość modyfikacji dowolnego bloku byłaby równoznaczna z przełamaniem hasza kryptograficznego. Schemat tej struktury znajdziesz na poniższym diagramie.

dm-verity-hash-table

Rysunek 1. Tabela haszy dm-verity

Na partycji rozruchowej znajduje się klucz publiczny, który musi zostać zweryfikowany zewnętrznie przez producenta urządzenia. Klucz ten służy do weryfikacji podpisu dla tego wartości skrótu kontrolnego i potwierdzenia, że partycja systemowa urządzenia jest chroniona i niezmieniona.

Operacja

Ochrona dm-verity działa w rdzeniu. Jeśli więc oprogramowanie do rootowania skompromituje system przed uruchomieniem jądra, zachowa dostęp. Aby ograniczyć to ryzyko, większość producentów weryfikuje jądro za pomocą klucza zapisanego na urządzeniu. Klucza nie można zmienić po opuszczeniu przez urządzenie fabryki.

Producenci używają tego klucza do weryfikacji podpisu w pierwszym bootloaderze, który z kolei weryfikuje podpis na kolejnych poziomach, w bootloaderze aplikacji i ostatecznie w rdzeniu. Każdy producent, który chce korzystać z weryfikowanego uruchamiania, powinien mieć metodę weryfikacji integralności jądra. Zakładając, że jądro zostało zweryfikowane, może ono sprawdzić urządzenie blokowe i sprawdzić, czy jest zamontowane.

Jednym ze sposobów weryfikacji urządzenia blokowego jest bezpośrednie wygenerowanie hasha jego zawartości i porównanie go ze zmagazynowaną wartością. Próba zweryfikowania całego urządzenia blokowego może jednak zająć dużo czasu i wymagać znacznego zużycia energii. Urządzenia potrzebowałyby dużo czasu na uruchomienie, a potem byłyby znacznie rozładowane przed użyciem.

Zamiast tego dm-verity weryfikuje bloki pojedynczo i tylko wtedy, gdy każdy z nich jest używany. Podczas odczytu do pamięci blok jest szyfrowany równolegle. Następnie skrót jest weryfikowany w drzewie. Ponieważ odczyt bloku jest tak kosztowną operacją, opóźnienie spowodowane weryfikacją na poziomie bloku jest stosunkowo niewielkie.

Jeśli weryfikacja się nie powiedzie, urządzenie wygeneruje błąd wejścia/wyjścia, wskazujący, że blok nie może być odczytany. Wygląda na to, że system plików jest uszkodzony, co jest zgodne z oczekiwaniami.

Aplikacje mogą działać bez danych wynikowych, np. gdy te wyniki nie są wymagane do realizacji głównej funkcji aplikacji. Jeśli jednak aplikacja nie może działać bez tych danych, nie uda się jej uruchomić.

Korekta błędów w transmisji

Android 7.0 i nowsze wersje zwiększają odporność dm-verity dzięki korekcie błędów w przód (FEC). Implementacja AOSP zaczyna się od wspólnego kodu korygującego błędy Reed-Solomon i zawiera technikę zwaną przeplataniem, która pozwala zmniejszyć ilość zajmowanego miejsca i zwiększyć liczbę uszkodzonych bloków, które można odzyskać. Więcej informacji o FEC znajdziesz w artykule Szczegółowe informacje o weryfikacji podczas uruchamiania z funkcją FEC.

Implementacja

Podsumowanie

  1. wygenerować obraz systemu w formacie ext4.
  2. Wygeneruj drzewo haszowania dla tego obrazu.
  3. Utwórz tabelę dm-verity dla tego drzewa haszowania.
  4. Podpisz tę tabelę dm-verity, aby wygenerować podpis tabeli.
  5. Zawiąż podpis tabeli i tabelę dm-verity w metadanych verity.
  6. Połącz obraz systemu, metadane weryfikacji i drzewo haszowania.

Szczegółowe informacje o drzewie haszy i tabeli dm-verity znajdziesz w artykule The Chromium Projects – Verified Boot.

Generowanie drzewa haszy

Jak wspomniano we wstępie, drzewo haszowania jest nieodłącznym elementem dm-verity. Narzędzie cryptsetup wygeneruje dla Ciebie drzewo haszowania. Możesz też zdefiniować zgodny format:

<your block device name> <your block device name> <block size> <block size> <image size in blocks> <image size in blocks + 8> <root hash> <salt>

Aby utworzyć skrót, obraz systemu jest dzielony na poziomie 0 na bloki 4 KB, z których każdy ma przypisany skrót SHA-256. Warstwa 1 jest tworzona przez złączenie tylko tych skrótów SHA-256 w bloki 4 KB, co powoduje powstanie znacznie mniejszego obrazu. Warstwa 2 jest tworzona w identyczny sposób za pomocą haszy SHA-256 warstwy 1.

Proces ten jest powtarzany, dopóki identyfikatory SHA256 poprzedniej warstwy nie zmieszczą się w pojedynczym bloku. Gdy otrzymasz identyfikator SHA256 tego bloku, otrzymasz też hasz korzenia drzewa.

Rozmiar drzewa haszy (i odpowiednio zajmowane miejsce na dysku) zmienia się wraz z rozmiarem weryfikowanej partycji. W praktyce rozmiar drzew hashowania jest zazwyczaj niewielki, często poniżej 30 MB.

Jeśli masz blok w warstwie, który nie jest całkowicie wypełniony przez hasze poprzedniej warstwy, musisz go uzupełnić zerami, aby uzyskać oczekiwane 4 K. Dzięki temu wiesz, że drzewo szyfrowania nie zostało usunięte, a zamiast tego wypełniono je pustymi danymi.

Aby wygenerować drzewo haszowania, złącz zaszyfrowane ciągi znaków warstwy 2 z zaszyfrowanymi ciągami znaków warstwy 1, a zaszyfrowane ciągi znaków warstwy 3 z zaszyfrowanymi ciągami znaków warstwy 2 itd. Zapisz wszystkie te informacje na dysku. Pamiętaj, że nie odnosi się to do warstwy 0 w łańcuchu haszowania.

Podsumujmy, jak wygląda ogólny algorytm tworzenia drzewa haszy:

  1. Wybierz losową sól (kodowanie szesnastkowe).
  2. Rozpakuj obraz systemu na bloki 4 K.
  3. W przypadku każdego bloku uzyskaj jego (zasolony) identyfikator SHA256.
  4. Połącz te hasze, aby utworzyć poziom
  5. Uzupełnij poziom zerami 0, aby uzyskać granicę bloku 4K.
  6. Połącz poziom z drzewem haszowania.
  7. Powtarzaj kroki 2–6, używając poprzedniego poziomu jako źródła dla następnego, aż będziesz mieć tylko jeden ciąg znaków.

Wynikiem jest pojedynczy ciąg znaków, czyli tzw. root hash. Ten klucz i sól są używane podczas tworzenia tabeli mapowania dm-verity.

Tworzenie tabeli mapowania dm-verity

Utwórz tabelę mapowania dm-verity, która identyfikuje urządzenie blokowe (lub docelowe) dla jądra i lokalizacji drzewa haszowania (które jest tą samą wartością). To mapowanie jest używane do generowania i uruchamiania fstab. Tabela zawiera też informacje o rozmiarze bloków i wartości hash_start, czyli początkowej lokalizacji drzewa haszowania (a w szczególności numer bloku od początku obrazu).

Szczegółowy opis pól tabeli mapowania docelowych pól weryfikacji znajdziesz w cryptsetup.

Podpisz tabelę dm-verity

Podpisz tabelę dm-verity, aby wygenerować podpis tabeli. Podczas weryfikacji partycji najpierw sprawdzany jest podpis tabeli. Jest to wykonywane za pomocą klucza w obrazie rozruchowym w stałym miejscu. Klucze są zwykle uwzględniane w systemach kompilacji producentów w celu automatycznego uwzględniania ich na urządzeniach w stałym miejscu.

Aby zweryfikować partycję za pomocą tej kombinacji podpisu i klucza:

  1. Dodaj klucz RSA-2048 w formacie zgodnym z libmincrypt do partycji /boot w /verity_key. Określ lokalizację klucza używanego do weryfikacji drzewa haszowania.
  2. W pliku fstab odpowiedniego wpisu dodaj do flagi fs_mgr flagę verify.

Umieszczanie sygnatury tabeli w metadanych

Połącz podpis tabeli i tabelę dm-verity z metadanymi verity. Cały blok metadanych jest wersjonowany, więc można go rozszerzyć, na przykład o drugi rodzaj podpisu lub o zmianę kolejności.

W ramach kontroli poprawności z każdym zestawem metadanych tabeli powiązany jest magiczny numer, który pomaga zidentyfikować tabelę. Długość jest zawarta w nagłówku obrazu systemu ext4, co umożliwia wyszukiwanie metadanych bez znajomości zawartości samych danych.

Dzięki temu nie zweryfikujesz niezatwierdzonej partycji. Jeśli tak, brak tego magicznego numeru spowoduje przerwanie procesu weryfikacji. Ta liczba jest podobna do 0xb001b001.

Wartości bajtów w szeszxie:

  • pierwszy bajt = b0
  • drugi bajt = 01
  • trzeci bajt = b0
  • Czwarty bajt = 01

Ten diagram przedstawia podział metadanych weryfikacji:

<magic number>|<version>|<signature>|<table length>|<table>|<padding>
\-------------------------------------------------------------------/
\----------------------------------------------------------/   |
                            |                                  |
                            |                                 32K
                       block content

Tabela poniżej opisuje te pola metadanych.

Tabela 1. Weryfikowanie pól metadanych

Pole Cel Rozmiar Wartość
magiczna liczba używany przez fs_mgr jako kontrola poprawności 4 bajty 0xb001b001
Wersja służy do wersji bloku metadanych; 4 bajty obecnie 0
podpis podpis tabeli w postaci zaokrąglonej wartości PKCS1.5 256 bajtów
długość tabeli długość tabeli dm-verity w bajtach. 4 bajty
stół tabela dm-verity opisana wcześniej; bajty długości tabeli
padding ta struktura jest wypełniona zerami o wartości 0 do długości 32 tys. 0

Optymalizacja dm-verity

Aby uzyskać najlepszą wydajność dm-verity, wykonaj te czynności:

  • W rdzeniu włącz NEON SHA-2 w przypadku ARMv7 i rozszerzenia SHA-2 w przypadku ARMv8.
  • Eksperymentuj z różnymi ustawieniami funkcji odczytu w przód i prefetch_cluster, aby znaleźć najlepszą konfigurację dla swojego urządzenia.