Koncepcje stojące za ARMv8-M TrustZone

Korzystanie z GCC CMSE (Cortex-M Security Extensions) z rdzeniami ARM Cortex-M33 i M23

Wprowadzenie i motywacja

Ten artykuł jest wynikiem moich pierwszych kroków z nowym nRF9160 System-in-Package Nordic Semiconductor. Ten SiP może być używany do tworzenia węzłów czujników IoT z komórkowym połączeniem z Internetem. Integruje modem dla nowych "zoptymalizowanych pod kątem IoT" wariantów standardu LTE o niskim poborze mocy, a mianowicie NB.IoT i LTE-M, które mają zastąpić tradycyjną łączność M2M za pomocą sieci 2G.

Koncepcyjnie SiP jest bardzo interesujący dla nas w Lobaro, ponieważ zrywa z powszechną koncepcją posiadania jednego mikroprocesora aplikacyjnego komunikującego się przez linię szeregową za pomocą poleceń AT z oddzielnym modemem komórkowym. Poza tym wykorzystuje najnowszy rdzeń procesora ARM Cortex-M33 z nowymi funkcjami bezpieczeństwa ukierunkowanymi na szczególnie bezpieczne aplikacje IoT. Modem LTE jest bezpośrednio zintegrowany z SiP i komunikuje się z procesorem aplikacji za pomocą komunikacji międzyprocesorowej opartej na pamięci współdzielonej. To ścisłe połączenie pozwala na lepszą optymalizację niskiego poboru mocy i mniejsze wymiary PCB, a także łatwiejsze zabezpieczenie węzła IoT.

Procesor Cortex-M33 wewnątrz nRF9160 wykorzystuje nową architekturę ARMv8-M, która oferuje nową funkcję o nazwie "ARM TrustZone". Poniższy artykuł odzwierciedla moją interpretację podstawowych pojęć i ich praktycznego zastosowania przy użyciu kompilatora GNU ARM GCC i jego funkcji CMSE (=Cortex M Security Extensions). Ponieważ obecnie (maj 2019 r.) nie ma zbyt wielu informacji do znalezienia w Internecie, mam nadzieję, że ten artykuł może pomóc w szybszym rozpoczęciu tematu niż ja.

Chociaż używam SiP nRF9160 Cortex-M33, większość koncepcji przedstawionych poniżej jest również ważna dla innych urządzeń Cortex-M33 i Cortex-M23 z obsługą ARMv8-M różnych dostawców krzemu. To samo dotyczy korzystania z kompilatorów ARM innych niż GCC.

Rdzeń Cortex-M33, (c) ARM®

Bezpieczne i niezabezpieczone stany pracy CPU

Oprócz klasycznych stanów operacyjnych CPU Thread (normalny kod) i Handler (IRQ/kod wyjątku), architektura ARMv8-M wprowadza bezpieczne i niezabezpieczone atrybuty. Po zresetowaniu procesor zawsze uruchamia się w trybie bezpiecznym i może uzyskać dostęp do wszystkich regionów pamięci i korzystać z dowolnego urządzenia peryferyjnego bez ograniczeń. Przypomina to normalny tryb pracy starszych architektur, takich jak Cortex-M4 lub Cortex-M3. Procesor oparty na ARMv8-M może pozostać w tym trybie bez korzystania z funkcji TrustZone, ale wtedy tworzenie bezpiecznych aplikacji staje się trudniejsze. Musisz dwukrotnie sprawdzić każdy fragment swojego kodu, ponieważ jeśli istnieje błąd, (zdalny) atakujący może uzyskać dostęp do całego systemu i / lub przechowywanych danych uwierzytelniających.

Ideą TrustZone jest ukrycie istotnego dla bezpieczeństwa kodu, danych i urządzeń peryferyjnych w niewielkim obszarze urządzenia. Region ten będzie nazywany "bezpiecznym", podczas gdy inne części urządzenia będą traktowane jako (potencjalnie) "niezabezpieczone". Podział regionów pamięci, przerwań i urządzeń peryferyjnych na bezpieczne i niezabezpieczone zależy od użytkownika. Zwykle pierwsze linie kodu po włączeniu zasilania będą podejmować te decyzje w oparciu o potrzeby aplikacji. W "Secure Partition Manager" przykład kodu dla nRF9160 Nordic zdecydował się użyć partycji pamięci flash 256kB bezpiecznej i 768kB niezabezpieczonej.

Rdzeń CPU utrzymuje dwie wersje banków (bezpieczne / niezabezpieczone) rejestrów istotnych dla wykonania, np. wskaźniki stosu i blok sterowania systemem (SCB). Bieżący tryb bezpieczeństwa CPU definiuje, który wariant rejestru zostanie użyty. Mechanizm bankowy jest również obecny w przypadku niektórych urządzeń peryferyjnych (np. SysTick), które są zaimplementowane dwukrotnie w sprzęcie. Bezpieczny program zawsze może uzyskać dostęp do obu wersji. Po skonfigurowaniu niezabezpieczonego wskaźnika stosu i lokalizacji niezabezpieczonego adresu wektorowego bezpieczny kod rozruchowy używa wskaźnika funkcji do tabeli wektorów niezabezpieczonego oprogramowania układowego. Od tego momentu CPU uruchamia niezabezpieczone oprogramowanie układowe.

Niezabezpieczone oprogramowanie układowe nie może być świadome istnienia bezpiecznego oprogramowania układowego. Może być ono "normalnie" rozwijane i wdrażane, tak jak na starszych rdzeniach, z jego skryptem łączącym wskazującym tylko na zdefiniowane niezabezpieczone regiony pamięci. Po przeskoczeniu do niezabezpieczonego oprogramowania układowego procesor działa w trybie niezabezpieczonym i nie może już bezpośrednio uzyskać dostępu do żadnego bezpiecznego kodu, danych lub urządzeń peryferyjnych bez wyzwalania wyjątku bezpieczeństwa. Jedynym prawidłowym sposobem uzyskania dostępu do bezpiecznych funkcji z niezabezpieczonego kodu jest wywołanie specjalnych funkcji bramy / wejścia / okleiny, które może zapewnić bezpieczna strona. Pozostała część tego artykułu pokazuje, jak to działa w szczegółach przy użyciu ARM GCC i jego implementacji CMSE (Cortex M Security-Extensions).

Niezabezpieczone regiony pamięci do wywołania (NSC) i instrukcja ASM bezpiecznej bramy (SG)

Aby umożliwić wywoływanie funkcji niezabezpieczonych -> zabezpieczonych, pewna (niewielka) część zabezpieczonego regionu pamięci flash musi zostać skonfigurowana jako "Non-secure callable" (NSC) w bezpiecznym kodzie oprogramowania układowego. W nRF9160 odbywa się to za pomocą urządzenia peryferyjnego SPU.

Jak opisano wcześniej, niezabezpieczony kod nie może wywoływać funkcji w domenie bezpiecznej pamięci bez zgłaszania wyjątku w rdzeniu ARMv8-M. Oznaczając niewielką część bezpiecznej pamięci jako NSC, ograniczenie to zostało zmniejszone w taki sposób, że wywołania / rozgałęzienia do lokalizacji zawierających instrukcję asemblera "SG" (Secure Gateway) jako pierwszy kod operacyjny są teraz dozwolone. Od punktu po instrukcji SG program wykona kolejne rozgałęzienie do implementacji bezpiecznych funkcji znajdujących się w bardziej restrykcyjnej bezpiecznej pamięci, np. nieoznaczonej jako NSC. Wywołanie bezpiecznej funkcji z niezabezpieczonego kodu w ten sposób jest dwuetapowym procesem z łańcuchowymi skokami najpierw do pamięci oznaczonej jako NSC (z instrukcją SG), a następnie przejściem do rzeczywistego ciała funkcji w bezpiecznej pamięci. Dzięki temu procesowi wejścia do bezpiecznej pamięci są oddzielone od reszty bezpiecznej pamięci. Powód nieużywania instrukcji SG bezpośrednio na początku funkcji w bezpiecznej pamięci i unikania dwuetapowego procesu został wyjaśniony przez pomoc techniczną ARM tutaj: Powody wprowadzenia regionów NSC.

Definiowanie niezabezpieczonych funkcji wywoływalnych w bezpiecznym oprogramowaniu sprzętowym

Na szczęście kompilatory ARMv8-M ukrywają opisany wcześniej dwuetapowy proces przed programistą podczas wywoływania bezpiecznej funkcji z niezabezpieczonego oprogramowania układowego. Bezpieczna funkcja zaprojektowana jako punkt wejścia dla niezabezpieczonego oprogramowania układowego musi zostać oznaczona atrybutem wejścia niezabezpieczonego po jej zadeklarowaniu w bezpiecznym oprogramowaniu układowym:

// jakiś plik c bezpiecznego projektu firmware definiujący funkcje bramki veneer
// musi być skompilowany z flagą -mcmse gcc (!)
#include "arm_cmse.h"
__attribute__((cmse_nonsecure_entry)) void ControlCriticalIO(){
// zrób kilka krytycznych rzeczy
}

Nakazuje to kompilatorowi automatyczne wygenerowanie dwóch części kodu dla opisanego powyżej procesu, jeśli kompilacja odbywa się z użyciem "-mcmse" flaga ARM gcc.

// Flagi kompilatora GCC używane podczas tworzenia bezpiecznej strony
arm-none-eabi-gcc -o secureFirmware.elf -mcmse [...]

Rzeczywiste ciało funkcji zostanie umieszczone przez linker w bezpiecznej sekcji .text jak zwykle, ale część z instrukcjami SG i rozgałęzieniami zostanie umieszczona w specjalnej sekcji o nazwie ".gnu.sgstubs" lub podobne dla kompilatorów innych niż GNU C.

Małe funkcje przekierowania w regionie NSC są również nazywane funkcjami "okleiny" lub "SG stups", ponieważ są tak małe, wykonując tylko jedną instrukcję SG i rozgałęzienie. Plik łącznika bezpiecznego oprogramowania układowego musi umieścić dowolny kod forniru w bezpiecznym regionie pamięci, który bezpieczne oprogramowanie układowe zadeklaruje jako NSC jako część bezpiecznego procesu rozruchowego przed wskoczeniem do niezabezpieczonego oprogramowania układowego.

// Sekcja Linkerscript dla oklein TrustZone Secure Gateway
.gnu.sgstubs : ALIGN (32)
{
    . = ALIGN(32);
    _start_sg = ..;
    *(.gnu.sgstubs*)
    . = ALIGN(32);
    _end_sg = .;
} > FLASH-REGION-WITH-NSC-ENABLED

Wywoływanie zabezpieczonych funkcji z niezabezpieczonego oprogramowania układowego (NS->S)

Zwykle bezpieczne oprogramowanie układowe jest tworzone i uruchamiane niezależnie od obrazu niezabezpieczonego oprogramowania układowego. Aby użyć jakiejkolwiek funkcjonalności bezpiecznego oprogramowania układowego, plik nagłówkowy (.h) opisujący wywoływalne bezpieczne funkcje musi być dołączony do niezabezpieczonych źródeł. Oprócz tego niezabezpieczone oprogramowanie sprzętowe musi wiedzieć, gdzie w części NSC bezpiecznej pamięci zostały wcześniej umieszczone punkty wejścia okleiny. Odbywa się to poprzez połączenie specjalnego pliku obiektowego (np. CMSE_importLib.o) z niezabezpieczonym oprogramowaniem układowym zawierającym potrzebne informacje adresowe. Ten plik obiektowy musi zostać wygenerowany podczas procesu łączenia bezpiecznego oprogramowania układowego przy użyciu następujących opcji linkera cmse:

// Flagi GCC LINKER używane podczas linkowania po stronie SECURE
arm-none-eabi-gcc -Xlinker --cmse-implib -Xlinker --out-implib=CMSE_importLib.o -Xlinker --sort-section=alignment [...]

Opcjonalny "-in-implib=CMSE_importLib.o" może być użyty do zachowania wpisów już zdefiniowanych w starszych wersjach pliku obiektowego w dokładnie tej samej lokalizacji pamięci również w nowej wersji. Umożliwia to dodawanie do bezpiecznego obrazu bez konieczności aktualizowania również niezabezpieczonego obrazu (który mógł być wcześniej połączony ze starszą wersją biblioteki importu).

Korzystając z pliku .h i łącząc się z obiektem CMSE_importLib.o, niezabezpieczone oprogramowanie układowe może teraz wywołać funkcję ControlCriticalIO() z powyższego przykładu.

// Flagi GCC LINKER używane podczas linkowania po stronie niezabezpieczonej
arm-none-eabi-gcc -o non-secureFirmware.elf CMSE_importLib.o [...]

Wywoływanie niezabezpieczonych funkcji (wywołania zwrotnego) z bezpiecznego oprogramowania układowego (S->NS)

W tym artykule pokazano, w jaki sposób można wykorzystać ARM TrustZone do stworzenia dwóch odizolowanych części oprogramowania układowego połączonych dobrze zdefiniowanymi niewielkimi funkcjami bramy / okleiny. Umożliwiają one wywoływanie funkcji bezpiecznego oprogramowania układowego z niezabezpieczonego oprogramowania układowego.

Drugi kierunek, czyli bezpieczne oprogramowanie układowe wywołujące niezabezpieczoną funkcję, działa od razu, jeśli adres niezabezpieczonej funkcji zostanie w jakiś sposób przekazany bezpiecznemu kodowi. Zwykle odbywa się to poprzez zdefiniowanie bezpiecznych funkcji bramy z niezabezpieczonym wskaźnikiem wywołania zwrotnego jako parametrem:

// jakiś plik c bezpiecznego projektu firmware definiujący funkcje bramki veneer
// musi być skompilowany z flagą -mcmse gcc (!)
#include "arm_cmse.h"
typedef void (*funcptr_ns) (void) __attribute__((cmse_nonsecure_call));
void ControlCriticalIO(funcptr_ns callback_fn) __attribute__((cmse_nonsecure_entry)){
 funcptr_ns cb = callback_fn; // zachowaj lotny wskaźnik przed niezabezpieczonym kodem
 
 // sprawdzenie, czy dany wskaźnik do niezabezpieczonej pamięci jest rzeczywiście niezabezpieczony zgodnie z oczekiwaniami
 cb = cmse_check_address_range(cb, sizeof(cb), CMSE_NONSECURE);
  
 if (cb != 0) {
    /* zrób kilka krytycznych rzeczy, np. użyj innych bezpiecznych funkcji */
    cb(); // wywołanie niezabezpieczonej funkcji zwrotnej
 } else {
   // nie rób nic, jeśli wskaźnik jest nieprawidłowy
 }
}

Atrybut "nonsecure_call" jest potrzebny do poinstruowania bezpiecznego kompilatora, aby wyczyścił niezbanowane rejestry ogólnego przeznaczenia przed powrotem do niezabezpieczonego kodu, ponieważ nie zrobienie tego byłoby potencjalnym zagrożeniem bezpieczeństwa. Poza tym instruuje kompilator, aby wyczyścił LSB adresu wskaźnika funkcji w podstawowej instrukcji asemblera rozgałęzienia. W ten sposób procesor przejdzie ze stanu bezpiecznego do niezabezpieczonego w rozgałęzieniu.

Ponieważ podane niezabezpieczone wywołanie zwrotne musi być traktowane jak zmienna lotna, której zawartość może zostać zmieniona przez niezabezpieczone przerwania, potrzebna jest kopia wskaźnika funkcji "cb". Przed wywołaniem wywołania zwrotnego jego wskaźnik musi zostać sprawdzony pod kątem wskazywania tylko niezabezpieczonej pamięci. Jeśli wskazywałby na bezpieczną pamięć, stanowiłoby to również zagrożenie dla bezpieczeństwa.

Rejestr rdzenia Cortex-M33, (c) ARM®

Wskazówki i dalsza lektura

Należy pamiętać, że skrypt łączący dla bezpiecznej strony musi uwzględniać sekcję dla funkcji okleiny, a skrypt łączący dla niezabezpieczonej strony musi odzwierciedlać partycje pamięci utworzone w bezpiecznym oprogramowaniu układowym. W przypadku włączenia niezabezpieczonych funkcji wywoływalnych do pliku archiwum (*.a), musi on zostać połączony z opcją linkera "whole-archive" do bezpiecznego oprogramowania układowego. Tylko w ten sposób biblioteka importu CMSE będzie zawierać potrzebne funkcje okleiny.

Kilka zawiłości pisania bezpiecznego kodu ARMv8-M - ARM Limited
SAM-L11-Security-ReferenceGuide-AN-DS70005365A.pdf - Microchip Technology
Trustzone z ARMv8-m i NXP lpc55s69-evk - Erich Styger
Przykład CM33 freeRTOS

napisany przez Theodora Rohde (CEO Lobaro GmbH) w dniu 4 maja 2019 r.

4 komentarzy:
  1. Daniel Oliveira mówi:

    Cześć Theodor,

    Dziękuję za ten post i szczegółowe wyjaśnienie architektury Armv8-M z rozszerzeniami bezpieczeństwa. Btw, czy masz jakieś projekty oparte na gcc na githubie związane z tym tematem?

    Pozdrowienia,
    Daniel

  2. Tobias mówi:

    Witaj Daniel,
    Planujemy wydać minimalny przykład dla nRF9160 przy użyciu rozszerzeń GCC CMSE i FreeRTOS. Ma to być alternatywne środowisko dla SiP. Myślę, że zajmie to około 1 miesiąca.

    Pozdrowienia
    Theo

  3. Neculai Agavriloaie mówi:

    Cześć Theodor,

    Po około 20 latach pracy z MSP430, chciałem coś zmienić.
    W moim nowym projekcie chcę wykorzystać technologię Trustzone i naprawdę musiałem zrozumieć, jak ona działa.

    Ta strona jest jedynym miejscem, w którym tryb działania jest bardzo prosty i jasno opisany.

    Pozdrowienia,
    Neculai

Komentarze wyłączone