Embedded

O bezpieczeństwie urządzeń wbudowanych – część druga. Nowa nadzieja.

Październik 18, 2019 0
Podziel się:

Każde przemyślane i zaprojektowane rozwiązanie
da się zastąpić skończoną liczbą prowizorek.
Pan Weasley

Słowem wstępu

Gdy sięgnę pamięcią do początków swojej przygody z systemami wbudowanymi, to mogę śmiało stwierdzić, że kwestie bezpieczeństwa traktowane były po macoszemu. W większości przypadków projektanci/architekci nie analizowali problemu kompleksowo, żyjąc w przeświadczeniu, że wystarczy zastosowanie elementarnej nawet kryptografii i problem będzie rozwiązany.

Zwykle skutkowało to implementowaniem różnego rodzaju mniej lub bardziej udanych rozwiązań, na przysłowiową ostatnią chwilę, przed wdrożeniem urządzenia do produkcji. Często były to wątpliwej jakości prowizorki urągające (nawet wedle dawnych standardów) podstawowym zasadom implementacji zabezpieczeń.

W ramach poniższego artykułu postaram się zabrać Was, drodzy czytelnicy, na wycieczkę po jasnej stronie księżyca i pokazać pobieżnie (moim zdaniem) poprawne i kompleksowe podejście do problemu zapewnienia bezpieczeństwa w szeroko pojętym świecie embedded.

Metody jakimi posługują się osoby łamiące zabezpieczenia urządzeń wbudowanych celem wyciągnięcia przechowywanych w nich danych opisałem w pierwszej części artykułu.

Jasna strona mocy

Chcąc dostatecznie zabezpieczyć projektowane urządzenie należy, przed wybraniem platformy sprzętowej i napisaniem choćby jednej linii kodu, zadać sobie szereg bardzo ważnych pytań:

  • Jakiego typu poufne dane będą przechowywane w naszym urządzeniu?

W tym właśnie momencie specyfikujemy i dokładnie opisujemy wszystkie “skarby” jakie nasze urządzenie będzie przechowywać, a mogą to być m.in:

      • loginy oraz hasła
      • klucze szyfrujące
      • dane biometryczne użytkowników
      • dane konfiguracyjne urządzenia (jeżeli za jej pomocą możemy odblokować określone funkcjonalności)

Powstałą w ten sposób listę cennych zasobów należy następnie utrzymywać i aktualizować na wzór projektowego rejestru ryzyk.

  • W jaki sposób potencjalny atakujący będzie chciał wspomniane poufne dane z urządzenia wykraść?

W tym momencie przyda nam się szeroka wiedza na temat metod (ataków) stosowanych w celu łamania zabezpieczeń w tej konkretnej grupie urządzeń – pisałem o nich w ramach pierwszej części artykułu. Należy przy tym śledzić najnowsze trendy, narzędzia oraz wydarzenia/problemy/odkryte luki w zabezpieczeniach na portalach typu niebezpiecznik.pl

Dodatkowo warto zastanowić się nad wpływem wycieku/utraty wspomnianych danych na bezpieczeństwo konkretnego egzemplarza urządzenia oraz całej rodziny urządzeń.

  • Jak zamierzamy zminimalizować ryzyko ataku na chronione zasoby?

Ostatnim elementem triady bezpieczeństwa jest opracowanie mechanizmów ochrony naszych skarbów. W ogólności staramy się chronić dane, które zidentyfikowaliśmy w punkcie pierwszym przed zagrożeniami zidentyfikowanymi w punkcie drugim.  Warto też rozważyć czy rzeczywiście potrzebujemy przechowywać wspomniane dane (skarby) w naszym urządzeniu.

I czy na pewno zidentyfikowaliśmy wszelkie możliwe zagrożenia?  

Zacznijmy od wyboru platformy

Mając już zidentyfikowane wielkie trio (assets/threats/countermeasures) możemy przystąpić do projektowania konkretów rozwiązania.

Na pierwszy ogień musimy dokonać wyboru platformy (mikrokontrolera/SoCa). Oprócz standardowych rozważań na temat:

  • ilości potrzebnej pamięci Flash/RAM/EEPROM
  • prędkości zegara
  • potrzebnych peryferii
  • ilości wyprowadzeń i obudowy
  • zakresu temperatur roboczych
  • dostępności oraz ceny

należy zastanowić się jak platforma może nas wesprzeć od strony zabezpieczeń poprzez:

  • sprzętowe bloki realizujące funkcje kryptograficzne (AES, SHA, RSA, ECC)
  • obecność prawdziwego (nie pseudolosowego) generatora liczb losowych
  • zabezpieczoną przed atakiem/niepowołanym odczytem pamięć nieulotną

W idealnym świecie urządzenie powinno mieć wbudowany moduł HSM (Hardware Security Module) implementujący wszelkie prymitywy kryptograficzne oraz funkcjonalność do bezpiecznej generacji kluczy szyfrujących i ich przechowywania.

Dodatkowo warto rozważyć platformę, która posiada możliwie szerokie spektrum wbudowanych sensorów dla potrzeb:

  • monitorowania napięcia zasilania
  • badania jakości sygnału zegarowego, taktującego CPU
  • wykrywania ataków fizycznych za pomocą np. wbudowanej funkcji active shield
  • szeregu wbudowanych w strukturę krzemową sensorów pozwalających na wykrycie promieniowania świetlnego czy laserowego

Większość czołowych producentów mikrokontrolerów ma w swoim portfolio rodzinę układów, dedykowanych do zastosowań bezpiecznych w branży samochodowej, IoT czy elektronice użytkowej. W tym miejscu wymienię choćby ST33 (https://www.st.com/en/secure-mcus/st33-arm-sc300.html) czy RH850/P1X (https://www.renesas.com/eu/en/products/microcontrollers-microprocessors/rh850.html)

Biblioteka kryptograficzna – przydatny skrzydłowy

Po wybudowaniu solidnego fundamentu naszego rozwiązania musimy zastanowić się czy wybrana przez nas platforma zapewnia wszelkie potrzebne wsparcie kryptograficzne. Jeżeli nie, to rozwiązaniem jest (poza dodaniem zewnętrznego koprocesora kryptograficznego) dobranie dedykowanej, software’owej biblioteki kryptograficznej.

W zależności od możliwości wybranej przez nas platformy możemy posiłkować się szeregiem różnych produktów, m.in:

  • WolfSSL (https://www.wolfssl.com/products/wolfssl/)
  • WolfCrypt (https://www.wolfssl.com/products/wolfcrypt-2/)
  • mbed TLS (https://tls.mbed.org/)
  • OpenSSL (https://www.openssl.org/)
  • I wieloma, wieloma innymi

Dobrym źródłem informacji do wstępnego wyboru biblioteki/bibliotek jest wikipedia, gdzie znajdziemy bardzo pobieżne porównanie wielu dostępnych bibliotek (https://en.wikipedia.org/wiki/Comparison_of_TLS_implementations)

Wśród wymienionych powyżej produktów znajdziemy zarówno te komercyjne jak i wolne (zwykle na licencji GPL). Dodatkowo większość z nich może być użyta w pełnej jak i okrojonej formie, dopasowanej do dostępnych zasobów platformy.

Dobrym przykładem jest tutaj biblioteka WolfSSL i jej odchudzona, prostsza i lżejsza forma – WolfCrypt.

Zarządzanie kluczami szyfrującymi

Przechowywanie

Większość algorytmów kryptograficznych nie może być zastosowana bez podania klucza szyfrującego. Klucz szyfrujący, zwłaszcza w kryptografii symetrycznej powinien być szczególnie chroniony. Jego utrata oznacza bowiem kompromitację systemu.

Pytanie więc gdzie i jak należy przechować (zapisać) klucz szyfrujący w naszym urządzeniu, aby zwykły zrzut pamięci (memory dump) nie pozwolił atakującemu na jego odczytanie? Niestety nie ma uniwersalnego remedium na ten problem, możemy próbować:

  • przechowywać klucz w bezpiecznym module HSM (jest to najlepsze rozwiązanie)
  • opracować algorytm generowania klucza w locie z długiego ciągu danych, przechowywanych w pamięci urządzenia (w przypadku braku w systemie HSMa)
  • zapisać klucz w pamięci w formie “zaciemnionej” (obfuscate) – np. przy  użyciu XOR z jakąś unikalną wartością, przechowywaną w systemie (jest to najsłabsze rozwiązanie, ale ciągle lepsze od przechowywania klucza jawnie w pamięci)
  • zapisywać klucz szyfrujący partiami pod kilkoma adresami w pamięci (dalej w zaciemnionej postaci)

Wszystkie w/w metody (poza użyciem HSM) są kompromisem między osiągniętymi wynikami, a możliwościami, jakie zapewnia nam konkretna platforma. Należy bezwzględnie wystrzegać się przechowywania klucza szyfrującego w formie jawnej, zarówno w nieulotnej jak i ulotnej (RAM) pamięci urządzenia.

Nawet zastosowanie prostego maskowania (XOR) klucza załadowanego do RAM znacząco utrudnia atakującemu uzyskanie klucza w jawnej postaci, choćby za pomocą metod opisanych w poprzedniej części artykułu.

Dywersyfikacja

Kolejną zaniedbywaną kwestią jest dywersyfikacja kluczy szyfrujących. Nie raz spotykałem wersje deweloperskie urządzeń wyposażone od razu w docelowy zestaw kluczy szyfrujących, często z możliwością ich wyświetlenia poprzez interfejs debugowy.

Takie rozwiązanie jest wyraźnym proszeniem się o kłopoty. Utrata nawet pojedynczego egzemplarza urządzenia na skutek jego zgubienia  bądź kradzieży (np. w trakcie przesyłania między oddziałami firmy) kompromituje od razu wszystkie pozostałe.

Rozwiązaniem powyższego problemu jest przygotowanie co najmniej dwóch zestawów kluczy szyfrujących:

  • jednego, używanego na etapie dewelopmentu i testów,
  • drugiego, trafiającego do egzemplarzy produkcyjnych

W idealnym świecie unikalne zestawy kluczy produkcyjnych powinny zostać wygenerowane w sposób w pełni automatyczny i bez udziału człowieka. Następnie unikalne paczki z kluczami trafiają do urządzeń.

Klucze - O bezpieczeństwie urządzeń wbudowanych – część druga. Nowa nadzieja.

Dywersyfikacja kluczy szyfrujących na etapie produkcji urządzenia

Podejście takie, mimo pewnych trudności (konieczność posiadania bazy kluczy produkcyjnych zamiast pojedynczego klucza produkcyjnego) zapewnia nam dodatkowo, że utrata kluczy z pojedynczego urządzenia nie pozwoli na kompromitację pozostałych.

Programowanie defensywne

Przechodząc do implementacji oprogramowanie należy mieć świadomość, że poszczególne rozwiązania programistyczne mocno wpływają na ilość informacji emitowanej w ramach “kanałów bocznych” (side channel analysis).

Dodatkowo należy zabezpieczyć się przed:

  • manipulacją rejestrami procesora
  • zmianą zawartości komórek w pamięci RAM
  • próbą wpływania na przebieg wykonywanego programu (flow control)
  • zmianą kolejności wywołań funkcji (call control)

Aby lepiej uzmysłowić sobie konieczność stosowania programowania defensywnego, spójrzmy na poniższy przykład i zadajmy sobie pytanie czy i jak często widzimy w naszym kodzie podobne deklaracje?

ProblemyPoziomo - O bezpieczeństwie urządzeń wbudowanych – część druga. Nowa nadzieja.

Wartości zwracane z funkcji (kody) szczególnie podane na atak/analizę

Jeżeli nasza odpowiedź jest twierdząca to znak, że znajomość technik zwanych z języka Shakespere’a “secure coding style” powinna znaleźć się na naszej liście. W ramach wspomnianej techniki implementujemy m.in:

  • użycie złożonych wartośći, różniących się na więcej niż jednym bicie (z odrzuceniem wartości skrajnych typu zero/0xFFFFFFFF)
  • kontrolę przepływu kodu za pomocą dedykowanych liczników (DFA counters)
  • podwójne sprawdzanie kluczowych zmiennych (z przeładowaniem rejestru) przed włączeniem krytycznych funkcjonalności
  • mechanizm okna, celem kontroli poprawności wskaźników przy transmisji danych
  • specyficznego użycia konstrukcji if-else, ze sprawdzeniem wszystkich negatywnych wariantów i dopiero na końcu przejściem do pozytywnego

Wszystkie w/w techniki prowadzą do powstania bardziej bezpiecznego kodu źródłowego, który jednak dla osoby nie zaznajomionej z problematyką bezpieczeństwa będzie wydawał się dziwny, nieoptymalny i nielogiczny.

Poniżej widzimy przykład podwójnego sprawdzenia poprawności weryfikacji hasła. Tak napisany kod chroni przed manipulacją wartością zmiennej passwordVerification za pomocą np. promienia laserowego skierowanego na rejestr, do którego zmienna została załadowana. Po pierwszym sprawdzeniu następuje kolejne, przy czym wynik sprawdzenia inny od poprzedniego traktowany jest jako wykryty atak. W efekcie wywołania makra ATTACK następuje wywołanie dedykowanego attack handler’a.

Należy przy tym mieć na uwadze, że kompilator może usunąć drugie sprawdzenie bowiem w jego mniemaniu jest ono bezsensowne. Stąd konieczność regularnego przeglądu kodu źródłowego, również po stronie asemblera.

doubleCheck - O bezpieczeństwie urządzeń wbudowanych – część druga. Nowa nadzieja.

Przykład podwójnego sprawdzenia kluczowego warunku. Przy czym drugie sprawdzenie musi dać identyczny wynik jak pierwsze.

Bezpieczeństwo idealne

Na zakończenie moich (znowu) przydługich rozważań pragnę zaznaczyć, że nikomu jeszcze nie udało się zabezpieczyć urządzenia w sposób idealny, nie dający się złamać bez względu na poniesione nakłady. Odrzucę jednak stwierdzenie, że wszystko co napisałem wyżej pójdzie na marne.

Projektowaniu i implementacji zabezpieczeń nie powinna przyświecać chęć uczynienia urządzenia perfekcyjnie bezpiecznym, “niełamalnym”. Należy skupić się na takim dobraniu rozwiązań, aby ich złamanie było dla atakującego nieopłacalne. Przykładowo – jeżeli złamanie naszych zabezpieczeń przysporzy atakującemu 10 $ zysku to należy dobrać takie zabezpieczenia, aby na ich złamanie poświęcił 1000$.

Podsumowanie

Bezpieczeństwo urządzenia wbudowanego jest złożeniem wielu czynników na które składają się:

  • projekt oprogramowania, uwzględniający kwestie bezpieczeństwa
    • identyfikację chronionych danych
    • świadomość zagrożeń i metod używanych do łamania zabezpieczeń
    • pomysł na przeciwdziałanie zagrożeniom
  • wsparcie sprzętowe (kryptografia, wbudowane sensory)
  • dobór biblioteki kryptograficznej
  • dywersyfikacja i bezpieczne przechowywanie kluczy szyfrujących
  • implementacja kodu źródłowego zgodnie z filozofią programowania defensywnego
  • regularny przegląd kodu źródłowego pod kątem bezpieczeństwa

Powyższe zalecenia należy stosować w całości i bez wyjątków jeżeli chcemy otrzymać naprawdę bezpieczne rozwiązanie. Wszelkie chodzenie na skróty i stosowanie półśrodków kończy się zwykle osiąganiem ćwierćrezultatów. W efekcie powstały system, mimo zastosowania szerokiej gamy zabezpieczeń, może zostać złamany poprzez wykorzystanie pojedynczej (a czasami jedynej) jego słabości.

.223rem

 

Oceń ten post
Kategorie: Embedded
Mateusz Januszkiewicz
Autor: Mateusz Januszkiewicz
Ze światem urządzeń wbudowanych związany jestem od 10 lat. Na co dzień pracuję na stanowisku Architekta Rozwiązań w Centrum Kompetencji Embedded gdzie mam do czynienia z różnymi ARMowymi i nie-ARMowymi platformami oraz dotykam tematów związanych z niskopoziomowym bezpieczeństwem. W wolnym czasie zajmuję się strzelectwem i majsterkowaniem przez duże O.

Imię i nazwisko (wymagane)

Adres email (wymagane)

Temat

Treść wiadomości

Zostaw komentarz