Wskazówki dla deweloperów języka C++ dotyczące ataków Speculative Execution Side Channels

Ten artykuł zawiera wskazówki dla deweloperów ułatwiające identyfikowanie i ograniczanie spekulacyjnych luk w zabezpieczeniach sprzętowych kanału po stronie wykonywania w oprogramowaniu C++. Te luki w zabezpieczeniach mogą ujawniać poufne informacje w granicach zaufania i mogą mieć wpływ na oprogramowanie działające na procesorach obsługujących spekulacyjne, poza kolejnością wykonywania instrukcji. Ta klasa luk w zabezpieczeniach została po raz pierwszy opisana w styczniu 2018 r. oraz dodatkowe informacje i wskazówki można znaleźć w biuletynie zabezpieczeń firmy Microsoft.

Wskazówki podane w tym artykule są związane z klasami luk w zabezpieczeniach reprezentowanych przez:

  1. CVE-2017-5753, znany również jako wariant Spectre 1. Ta klasa luk w zabezpieczeniach sprzętu jest powiązana z kanałami bocznymi, które mogą wystąpić z powodu wykonywania spekulatywnego, które występuje w wyniku błędnejpredykcji gałęzi warunkowej. Kompilator Microsoft C++ w programie Visual Studio 2017 (począwszy od wersji 15.5.5) obejmuje obsługę /Qspectre przełącznika, który zapewnia ograniczenie czasu kompilacji dla ograniczonego zestawu potencjalnie narażonych wzorców kodowania związanych z CVE-2017-5753. Przełącznik /Qspectre jest również dostępny w programie Visual Studio 2015 Update 3 do kb 4338871. Dokumentacja flagi /Qspectre zawiera więcej informacji na temat jego wpływu i użycia.

  2. CVE-2018-3639, znany również jako obejście magazynu spekulatywnego (SSB). Ta klasa luk w zabezpieczeniach sprzętu jest powiązana z kanałami bocznymi, które mogą wystąpić z powodu spekulacyjnego wykonywania obciążenia przed magazynem zależnym w wyniku błędnegopredykcji dostępu do pamięci.

Dostępne wprowadzenie do luk w zabezpieczeniach kanału po stronie wykonywania spekulatywnego można znaleźć w prezentacji zatytułowanej Case of Spectre and Meltdown przez jeden z zespołów badawczych, które odkryły te problemy.

Co to są luki w zabezpieczeniach sprzętu kanału po stronie wykonywania spekulatywnego?

Nowoczesne procesory CPU zapewniają wyższy stopień wydajności dzięki wykorzystaniu spekulacyjnych i poza kolejnością wykonywania instrukcji. Na przykład jest to często realizowane przez przewidywanie celu gałęzi (warunkowych i pośrednich), co umożliwia procesorowi CPU rozpoczęcie spekulatywnego wykonywania instrukcji w przewidywanym miejscu docelowym gałęzi, co pozwala uniknąć zatrzymania do momentu rozwiązania rzeczywistego celu gałęzi. W przypadku, gdy procesor CPU później wykryje, że wystąpił błąd, cały stan maszyny, który został obliczony spekulacyjnie, zostanie odrzucony. Gwarantuje to, że nie ma architektonicznie widocznych efektów błędnie przygotowanych spekulacji.

Chociaż wykonywanie spekulatywne nie ma wpływu na stan widoczny architektury, może pozostawić ślady reszt w stanie nie architektury, takie jak różne pamięci podręczne używane przez procesor CPU. Są to te ślady resztkowe wykonywania spekulatywnego, które mogą spowodować powstanie luk w zabezpieczeniach kanału bocznego. Aby lepiej to zrozumieć, rozważ następujący fragment kodu, który zawiera przykład CVE-2017-5753 (Obejście sprawdzania granic):

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

W tym przykładzie ReadByte jest dostarczany bufor, rozmiar buforu i indeks do tego buforu. Parametr indeksu, określony przez untrusted_indexelement , jest dostarczany przez mniej uprzywilejowany kontekst, taki jak proces nieadministracyjnych. Jeśli untrusted_index wartość jest mniejsza niż buffer_size, znak w tym indeksie jest odczytywany i buffer używany do indeksowania w udostępnionym regionie pamięci, do którego odwołuje się shared_bufferelement .

Z perspektywy architektury ta sekwencja kodu jest całkowicie bezpieczna, ponieważ gwarantuje, że untrusted_index zawsze będzie mniejsza niż buffer_size. Jednak w obecności wykonywania spekulatywnego istnieje możliwość, że procesor CPU błędnie określi gałąź warunkową i wykona treść instrukcji if, nawet jeśli untrusted_index jest większa niż lub równa buffer_size. W związku z tym procesor CPU może spekulacyjnie odczytywać bajt spoza granic buffer (co może być wpisem tajnym), a następnie użyć tej wartości bajtu, aby obliczyć adres kolejnego obciążenia za pośrednictwem shared_buffermetody .

Mimo że procesor w końcu wykryje tę błędnąpredykcję, pozostałe skutki uboczne mogą pozostać w pamięci podręcznej procesora CPU, które ujawniają informacje o wartości bajtu odczytanej poza granicami z buffer. Te skutki uboczne można wykryć za pomocą mniej uprzywilejowanego kontekstu działającego w systemie, sondując, jak szybko uzyskuje się dostęp do poszczególnych wierszy shared_buffer pamięci podręcznej. Kroki, które można wykonać w tym celu, to:

  1. Wywołaj ReadByte wiele razy z untrusted_index mniejszą niż buffer_size. Kontekst ataku może spowodować wywołanie ReadByte kontekstu ofiary (np. za pośrednictwem RPC), tak aby predyktor gałęzi był trenowany tak, aby nie był traktowany jako untrusted_index mniejszy niż buffer_size.

  2. Opróżnij wszystkie wiersze pamięci podręcznej w pliku shared_buffer. Kontekst ataku musi opróżnić wszystkie wiersze pamięci podręcznej w udostępnionym regionie pamięci, do których odwołuje się shared_bufferelement . Ponieważ region pamięci jest współużytkowany, jest to proste i można to zrobić przy użyciu funkcji wewnętrznych, takich jak _mm_clflush.

  3. Wywołaj wywołanie ReadByte z untrusted_index większą niż buffer_size. Kontekst ataku powoduje wywołanie ReadByte kontekstu ofiary, tak aby niepoprawnie przewidywał, że gałąź nie zostanie podjęta. Powoduje to, że procesor spekulacyjnie wykonuje treść bloku if z wartością untrusted_index większą niż buffer_size, co prowadzi do odczytu bufferpoza granicą elementu . shared_buffer W związku z tym indeksowany jest przy użyciu potencjalnie tajnej wartości, która została odczytowana poza granicami, co powoduje załadowanie odpowiedniego wiersza pamięci podręcznej przez procesor.

  4. Odczytaj każdy wiersz pamięci podręcznej w pliku , shared_buffer aby zobaczyć, do którego z nich uzyskuje się najszybszy dostęp. Kontekst atakujący może odczytywać poszczególne wiersze pamięci podręcznej i shared_buffer wykrywać wiersz pamięci podręcznej, który ładuje się znacznie szybciej niż inne. Jest to wiersz pamięci podręcznej, który prawdopodobnie został wprowadzony w kroku 3. Ponieważ w tym przykładzie istnieje relacja 1:1 między wartością bajtu a wierszem pamięci podręcznej, dzięki temu osoba atakująca może wywnioskować rzeczywistą wartość bajtu, który został odczytany poza granicami.

Powyższe kroki stanowią przykład użycia techniki znanej jako FLUSH+RELOAD w połączeniu z wykorzystaniem wystąpienia CVE-2017-5753.

Na jakie scenariusze oprogramowania może mieć wpływ?

Tworzenie bezpiecznego oprogramowania przy użyciu procesu takiego jak cykl projektowania zabezpieczeń (SDL) zwykle wymaga od deweloperów zidentyfikowania granic zaufania, które istnieją w aplikacji. Granica zaufania istnieje w miejscach, w których aplikacja może wchodzić w interakcje z danymi dostarczonymi przez mniej zaufany kontekst, taki jak inny proces w systemie lub proces trybu użytkownika nieadministracyjnych w przypadku sterownika urządzenia w trybie jądra. Nowa klasa luk w zabezpieczeniach obejmujących kanały po stronie wykonywania spekulacyjnego ma zastosowanie do wielu granic zaufania w istniejących modelach zabezpieczeń oprogramowania, które izolują kod i dane na urządzeniu.

Poniższa tabela zawiera podsumowanie modeli zabezpieczeń oprogramowania, w których deweloperzy mogą być zainteresowani tymi lukami w zabezpieczeniach:

Granica zaufania opis
Granica maszyny wirtualnej Aplikacje, które izolować obciążenia na oddzielnych maszynach wirtualnych, które odbierają niezaufane dane z innej maszyny wirtualnej, mogą być zagrożone.
Granica jądra Sterownik urządzenia w trybie jądra, który odbiera niezaufane dane z procesu trybu użytkownika nieadministracyjnych, może być zagrożony.
Granica procesu Aplikacja, która odbiera niezaufane dane z innego procesu uruchomionego w systemie lokalnym, takiego jak zdalne wywołanie procedury (RPC), pamięć współdzielona lub inne mechanizmy komunikacji między procesami (IPC) mogą być zagrożone.
Granica enklawy Aplikacja wykonywana w bezpiecznej enklawie (takiej jak Intel SGX), która odbiera niezaufane dane spoza enklawy, może być zagrożona.
Granica języka Aplikacja, która interpretuje lub kompiluje just in time (JIT) i wykonuje niezaufany kod napisany w języku wyższego poziomu, może być zagrożony.

Aplikacje, które mają powierzchnię ataków uwidocznioną dla dowolnego z powyższych granic zaufania, powinny przejrzeć kod na powierzchni ataków, aby zidentyfikować i ograniczyć możliwe wystąpienia luk w zabezpieczeniach kanału po stronie wykonywania spekulacyjnego. Należy zauważyć, że granice zaufania narażone na zdalne powierzchnie ataków, takie jak protokoły sieci zdalnej, nie zostały wykazane, aby były zagrożone lukami w zabezpieczeniach kanału po stronie wykonywania spekulacyjnego.

Potencjalnie podatne na zagrożenia wzorce kodowania

Luki w zabezpieczeniach kanału po stronie wykonywania spekulatywnego mogą powstać w wyniku wielu wzorców kodowania. W tej sekcji opisano potencjalnie podatne na zagrożenia wzorce kodowania i podano przykłady dla każdego z nich, ale należy pamiętać, że mogą istnieć różnice w tych motywach. W związku z tym deweloperzy powinni stosować te wzorce jako przykłady, a nie jako wyczerpującą listę wszystkich potencjalnie narażonych wzorców kodowania. Te same klasy luk w zabezpieczeniach bezpieczeństwa pamięci, które mogą istnieć obecnie w oprogramowaniu, mogą również istnieć wzdłuż spekulatywnych i poza kolejnością ścieżek wykonywania, w tym, ale nie tylko w przypadku przekroczania buforu, dostępu do macierzy poza granicami, niezainicjowanego użycia pamięci, pomyłek typu itd. Te same typy pierwotne, których osoby atakujące mogą używać do wykorzystania luk w zabezpieczeniach pamięci wzdłuż ścieżek architektury, mogą również dotyczyć ścieżek spekulacyjnych.

Ogólnie rzecz biorąc, spekulacyjne kanały po stronie wykonywania związane z błędnąpredykcją gałęzi warunkowej mogą wystąpić, gdy wyrażenie warunkowe działa na danych, które mogą być kontrolowane lub wpływane przez mniej zaufany kontekst. Na przykład może to obejmować wyrażenia warunkowe używane w ifinstrukcjach , for, while, switchlubternary. Dla każdej z tych instrukcji kompilator może wygenerować gałąź warunkową, którą procesor CPU może następnie przewidzieć docelową gałąź dla środowiska uruchomieniowego.

Dla każdego przykładu wstawiono komentarz z frazą "SPEKULACJA BARIERA", w której deweloper może wprowadzić barierę jako środek zaradczy. Omówiono to bardziej szczegółowo w sekcji dotyczącej ograniczania ryzyka.

Spekulacyjne obciążenie poza granicami

Ta kategoria wzorców kodowania obejmuje błędnąpredykcję gałęzi warunkowej, która prowadzi do spekulacyjnego dostępu do pamięci poza granicami.

Ładowanie poza granicami tablicy przekazuje obciążenie

Ten wzorzec kodowania jest pierwotnie opisanym wzorcem kodowania podatnym na zagrożenia dla luki CVE-2017-5753 (obejście sprawdzania granic). W dalszej części tego artykułu szczegółowo opisano ten wzorzec.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        // SPECULATION BARRIER
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

Podobnie, obciążenie tablicy poza granicami może wystąpić w połączeniu z pętlą, która przekracza warunek zakończenia z powodu błędnego dyktowania. W tym przykładzie gałąź warunkowa skojarzona z x < buffer_size wyrażeniem może błędnie dyktować i spekulacyjnie wykonywać treść for pętli, gdy x jest większa lub równa buffer_size, co powoduje spekulacyjne obciążenie poza granicami.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadBytes(unsigned char *buffer, unsigned int buffer_size) {
    for (unsigned int x = 0; x < buffer_size; x++) {
        // SPECULATION BARRIER
        unsigned char value = buffer[x];
        return shared_buffer[value * 4096];
    }
}

Ładowanie poza granicami tablicy przekazujące gałąź pośrednią

Ten wzorzec kodowania obejmuje przypadek, w którym błędnapredykcja gałęzi warunkowej może prowadzić do dostępu poza granicą do tablicy wskaźników funkcji, która następnie prowadzi do gałęzi pośredniej do adresu docelowego, który został odczytany poza granicami. Poniższy fragment kodu zawiera przykład, który to pokazuje.

W tym przykładzie do polecenia DispatchMessage jest udostępniany niezaufany identyfikator komunikatu za pomocą parametru untrusted_message_id . Jeśli untrusted_message_id wartość jest mniejsza niż MAX_MESSAGE_ID, jest używana do indeksowania w tablicy wskaźników funkcji i gałęzi do odpowiadającego obiektu docelowego gałęzi. Ten kod jest bezpieczny pod względem architektury, ale jeśli procesor CPU błędniepredykuje gałąź warunkową, może to spowodować DispatchTable indeksowanie według untrusted_message_id , gdy jego wartość jest większa lub równa MAX_MESSAGE_ID, co prowadzi do braku dostępu. Może to spowodować spekulacyjne wykonanie z adresu docelowego gałęzi, który pochodzi poza granice tablicy, co może prowadzić do ujawnienia informacji w zależności od kodu, który jest wykonywany spekulacyjnie.

#define MAX_MESSAGE_ID 16

typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);

const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];

void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
    if (untrusted_message_id < MAX_MESSAGE_ID) {
        // SPECULATION BARRIER
        DispatchTable[untrusted_message_id](buffer, buffer_size);
    }
}

Podobnie jak w przypadku braku ograniczeń obciążenia tablicy, które karmią inne obciążenie, ten warunek może również wystąpić w połączeniu z pętlą, która przekracza warunek zakończenia z powodu błędnego przysyłania.

Magazyn poza granicami tablicy przekazujący gałąź pośrednią

Podczas gdy w poprzednim przykładzie pokazano, jak spekulacyjne obciążenie poza granicami może mieć wpływ na cel gałęzi pośredniej, możliwe jest również, aby magazyn poza granicami zmodyfikował obiekt docelowy gałęzi pośredniej, taki jak wskaźnik funkcji lub adres zwrotny. Może to potencjalnie prowadzić do spekulacyjnego wykonania z adresu określonego przez osobę atakującą.

W tym przykładzie niezaufany indeks jest przekazywany przez untrusted_index parametr . Jeśli untrusted_index jest mniejsza niż liczba pointers elementów tablicy (256 elementów), podana wartość wskaźnika w ptr pliku jest zapisywana w tablicy pointers . Ten kod jest bezpieczny pod względem architektury, ale jeśli procesor CPU błędniepredykuje gałąź warunkową, może to spowodować ptr spekulacyjne zapisanie poza granice tablicy przydzielonej pointers stosem. Może to prowadzić do spekulatywnego uszkodzenia adresu zwrotnego dla elementu WriteSlot. Jeśli osoba atakująca może kontrolować wartość ptr, może być w stanie spowodować spekulacyjne wykonanie z dowolnego adresu, gdy WriteSlot zwraca się wzdłuż ścieżki spekulatywnej.

unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
    void *pointers[256];
    if (untrusted_index < 256) {
        // SPECULATION BARRIER
        pointers[untrusted_index] = ptr;
    }
}

Podobnie, jeśli zmienna lokalna wskaźnika funkcji o nazwie func została przydzielona na stosie, może być możliwe spekulacyjne zmodyfikowanie adresu, który func odnosi się do wystąpienia błędu gałęzi warunkowej. Może to spowodować spekulacyjne wykonanie z dowolnego adresu, gdy wskaźnik funkcji jest wywoływany.

unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
    void *pointers[256];
    void (*func)() = &callback;
    if (untrusted_index < 256) {
        // SPECULATION BARRIER
        pointers[untrusted_index] = ptr;
    }
    func();
}

Należy zauważyć, że oba te przykłady obejmują spekulacyjne modyfikacje stosu przydzielonych wskaźników gałęzi pośredniej. Istnieje możliwość, że modyfikacja spekulacyjna może również wystąpić w przypadku zmiennych globalnych, pamięci przydzielonej stertą, a nawet pamięci tylko do odczytu w niektórych procesorach CPU. W przypadku pamięci przydzielonej do stosu kompilator języka Microsoft C++ już podejmuje kroki, aby utrudnić spekulacyjne modyfikowanie docelowych gałęzi pośrednich przydzielonych stosem, takich jak zmiana kolejności zmiennych lokalnych, takich jak umieszczenie buforów sąsiadujących z plikiem cookie zabezpieczeń w ramach /GS funkcji zabezpieczeń kompilatora.

Nieporozumienie typu spekulatywnego

Ta kategoria dotyczy wzorców kodowania, które mogą powodować zamieszanie typu spekulacyjnego. Dzieje się tak, gdy dostęp do pamięci jest uzyskiwany przy użyciu nieprawidłowego typu wzdłuż ścieżki innej niż architektura podczas wykonywania spekulatywnego. Zarówno błędnapredykcja gałęzi warunkowej, jak i obejście magazynu spekulatywnego mogą potencjalnie prowadzić do nieporozumień typu spekulacyjnego.

W przypadku obejścia magazynu spekulatywnego może to wystąpić w scenariuszach, w których kompilator ponownie używa lokalizacji stosu dla zmiennych wielu typów. Dzieje się tak, ponieważ magazyn architektury zmiennej typu A może zostać pominięty, co pozwala na spekulatywne wykonywanie obciążenia typu A przed przypisaniem zmiennej. Jeśli wcześniej przechowywana zmienna jest innego typu, może to spowodować powstanie warunków dla nieporozumień typu spekulatywnego.

W przypadku błędnego stosowania gałęzi warunkowej poniższy fragment kodu będzie używany do opisywania różnych warunków, które mogą powodować zamieszanie typu spekulacyjnego.

enum TypeName {
    Type1,
    Type2
};

class CBaseType {
public:
    CBaseType(TypeName type) : type(type) {}
    TypeName type;
};

class CType1 : public CBaseType {
public:
    CType1() : CBaseType(Type1) {}
    char field1[256];
    unsigned char field2;
};

class CType2 : public CBaseType {
public:
    CType2() : CBaseType(Type2) {}
    void (*dispatch_routine)();
    unsigned char field2;
};

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ProcessType(CBaseType *obj)
{
    if (obj->type == Type1) {
        // SPECULATION BARRIER
        CType1 *obj1 = static_cast<CType1 *>(obj);

        unsigned char value = obj1->field2;

        return shared_buffer[value * 4096];
    }
    else if (obj->type == Type2) {
        // SPECULATION BARRIER
        CType2 *obj2 = static_cast<CType2 *>(obj);

        obj2->dispatch_routine();

        return obj2->field2;
    }
}

Nieporozumienie typu spekulatywnego prowadzące do obciążenia poza granicami

Ten wzorzec kodowania obejmuje przypadek, w którym pomylenie typu spekulatywnego może skutkować brakiem granic lub dostępem pola zdezorientowanym typem, w którym załadowana wartość generuje kolejny adres ładowania. Jest to podobne do wzorca kodowania poza granicami tablicy, ale jest ono manifestowane za pośrednictwem alternatywnej sekwencji kodowania, jak pokazano powyżej. W tym przykładzie kontekst ataku może spowodować wielokrotne wykonanie ProcessType kontekstu ofiary z obiektem typu CType1 (type pole jest równe Type1). Będzie to miało wpływ na trenowanie gałęzi warunkowej dla pierwszej if instrukcji, aby przewidzieć, że nie zostanie podjęta. Kontekst ataku może następnie spowodować wykonanie ProcessType kontekstu ofiary za pomocą obiektu typu CType2. Może to spowodować zamieszanie typu spekulacyjnego, jeśli gałąź warunkowa dla pierwszej if instrukcji błędnie wyczytuje i wykonuje treść if instrukcji, w związku z tym rzutując obiekt typu CType2 na CType1. Ponieważ CType2 jest mniejszy niż CType1, dostęp do pamięci, aby spowodować CType1::field2 spekulacyjne poza granicami obciążenie danych, które mogą być tajne. Ta wartość jest następnie używana w obciążeniu, z shared_buffer którego można tworzyć zauważalne skutki uboczne, podobnie jak w przykładzie macierzy poza granicami opisanymi wcześniej.

Nieporozumienie typu spekulatywnego prowadzące do gałęzi pośredniej

Ten wzorzec kodowania obejmuje przypadek, w którym pomylenie typu spekulatywnego może spowodować niebezpieczną gałąź pośrednią podczas wykonywania spekulatywnego. W tym przykładzie kontekst ataku może spowodować wielokrotne wykonanie ProcessType kontekstu ofiary z obiektem typu CType2 (type pole jest równe Type2). Będzie to miało wpływ na trenowanie gałęzi warunkowej dla pierwszej if instrukcji, która ma zostać podjęta, a else if instrukcja nie zostanie podjęta. Kontekst ataku może następnie spowodować wykonanie ProcessType kontekstu ofiary za pomocą obiektu typu CType1. Może to spowodować zamieszanie typu spekulacyjnego, jeśli gałąź warunkowa dla pierwszej if instrukcji przewiduje, a else if instrukcja przewiduje, że nie zostanie podjęta, wykonując w ten sposób treść else if obiektu i rzutując obiekt typu CType1 na CType2. CType2::dispatch_routine Ponieważ pole nakłada się na tablicę charCType1::field1, może to spowodować spekulacyjną gałąź pośrednią do niezamierzonego obiektu docelowego gałęzi. Jeśli kontekst ataku może kontrolować wartości bajtów w CType1::field1 tablicy, może być w stanie kontrolować adres docelowy gałęzi.

Spekulacyjne niezainicjowane użycie

Ta kategoria wzorców kodowania obejmuje scenariusze, w których wykonywanie spekulatywne może uzyskiwać dostęp do niezainicjowanej pamięci i używać jej do podawania kolejnego obciążenia lub gałęzi pośredniej. Aby te wzorce kodowania były możliwe do wykorzystania, osoba atakująca musi mieć możliwość kontrolowania lub znaczącego wpływu na zawartość używanej pamięci bez inicjowania przez kontekst używany w programie.

Spekulacyjne niezainicjowane użycie prowadzi do obciążenia poza granicami

Spekulacyjne niezainicjowane użycie może potencjalnie prowadzić do obciążenia poza granicami przy użyciu kontrolowanej wartości przez osobę atakującą. W poniższym przykładzie wartość jest przypisywana indextrusted_index we wszystkich ścieżkach architektury i trusted_index przyjmuje się, że jest mniejsza lub równa buffer_size. Jednak w zależności od kodu utworzonego przez kompilator możliwe jest, że może wystąpić obejście magazynu spekulacyjnego, które umożliwia ładowanie z buffer[index] wyrażeń zależnych i przed przypisaniem do index. W takim przypadku niezainicjowana wartość index parametru zostanie użyta jako przesunięcie, w buffer którym osoba atakująca może odczytać poufne informacje poza granicami i przekazać je za pośrednictwem kanału bocznego przez zależne obciążenie shared_bufferelementu .

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

void InitializeIndex(unsigned int trusted_index, unsigned int *index) {
    *index = trusted_index;
}

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int trusted_index) {
    unsigned int index;

    InitializeIndex(trusted_index, &index); // not inlined

    // SPECULATION BARRIER
    unsigned char value = buffer[index];
    return shared_buffer[value * 4096];
}

Spekulatywne niezainicjowane użycie prowadzące do gałęzi pośredniej

Spekulacyjne niezainicjowane użycie może potencjalnie prowadzić do gałęzi pośredniej, w której obiekt docelowy gałęzi jest kontrolowany przez osobę atakującą. W poniższym routine przykładzie jest przypisywany do DefaultMessageRoutine1 elementu lub DefaultMessageRoutine w zależności od wartości mode. Na ścieżce architektonicznej spowoduje routine to, że zawsze będzie inicjowana przed gałęzią pośrednią. Jednak w zależności od kodu wygenerowanego przez kompilator może wystąpić spekulacyjne obejście magazynu, które umożliwia spekulatywne wykonywanie gałęzi routine pośredniej przed przypisaniem do routine. W takim przypadku osoba atakująca może być w stanie spekulacyjnie wykonać z dowolnego adresu, zakładając, że osoba atakująca może mieć wpływ na niezainicjowaną wartość routine.

#define MAX_MESSAGE_ID 16

typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);

const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
extern unsigned int mode;

void InitializeRoutine(MESSAGE_ROUTINE *routine) {
    if (mode == 1) {
        *routine = &DefaultMessageRoutine1;
    }
    else {
        *routine = &DefaultMessageRoutine;
    }
}

void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
    MESSAGE_ROUTINE routine;

    InitializeRoutine(&routine); // not inlined

    // SPECULATION BARRIER
    routine(buffer, buffer_size);
}

Opcje ograniczania ryzyka

Luki w zabezpieczeniach kanału po stronie wykonywania spekulatywnego można ograniczyć, wprowadzając zmiany w kodzie źródłowym. Te zmiany mogą obejmować ograniczenie określonych wystąpień luki w zabezpieczeniach, takich jak dodanie bariery spekulacji lub wprowadzenie zmian w projekcie aplikacji w celu udostępnienia poufnych informacji niedostępnych do wykonywania spekulacyjnego.

Bariera spekulacji poprzez instrumentację ręczną

Bariera spekulacji może zostać ręcznie wstawiona przez dewelopera, aby zapobiec kontynuowaniu wykonywania spekulatywnego wzdłuż ścieżki innej niż architektura. Na przykład deweloper może wstawić barierę spekulacji przed niebezpiecznym wzorcem kodowania w treści bloku warunkowego, na początku bloku (po gałęzi warunkowej) lub przed pierwszym obciążeniem, którego dotyczy problem. Zapobiegnie to błędnemu wykonaniu niebezpiecznego kodu na ścieżce innej niż architektura przez serializacji wykonywania gałęzi warunkowej. Sekwencja barier spekulacji różni się architekturą sprzętu zgodnie z opisem w poniższej tabeli:

Architektura Bariera spekulacji wewnętrzna cve-2017-5753 Bariera spekulacji wewnętrzna cve-2018-3639
x86/x64 _mm_lfence() _mm_lfence()
ARM obecnie niedostępne __dsb(0)
ARM64 obecnie niedostępne __dsb(0)

Na przykład poniższy wzorzec kodu można złagodzić, używając funkcji _mm_lfence wewnętrznej, jak pokazano poniżej.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        _mm_lfence();
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

Bariera spekulacji za pośrednictwem instrumentacji w czasie kompilatora

Kompilator Microsoft C++ w programie Visual Studio 2017 (począwszy od wersji 15.5.5) obejmuje obsługę /Qspectre przełącznika, który automatycznie wstawia barierę spekulacji dla ograniczonego zestawu potencjalnie narażonych wzorców kodowania związanych z CVE-2017-5753. Dokumentacja flagi /Qspectre zawiera więcej informacji na temat jego wpływu i użycia. Należy pamiętać, że ta flaga nie obejmuje wszystkich potencjalnie narażonych wzorców kodowania, a jako takie deweloperzy nie powinni polegać na niej jako kompleksowe środki zaradcze dla tej klasy luk w zabezpieczeniach.

Maskowanie indeksów tablicy

W przypadkach, gdy może wystąpić spekulacyjne obciążenie poza granicami, indeks tablicy może być silnie powiązany zarówno ze ścieżką architektury, jak i niekonseksacyjną, dodając logikę, aby jawnie powiązać indeks tablicy. Jeśli na przykład można przydzielić tablicę do rozmiaru wyrównanego do potęgi dwóch, można wprowadzić prostą maskę. Jest to pokazane w poniższym przykładzie, w którym zakłada się, że buffer_size jest wyrównany do potęgi dwóch. Gwarantuje to, że untrusted_index wartość jest zawsze mniejsza niż buffer_size, nawet jeśli wystąpi błędnapredykcja gałęzi warunkowej i untrusted_index została przekazana z wartością większą lub równą buffer_size.

Należy zauważyć, że maskowanie indeksów wykonywane tutaj może podlegać obejściu magazynu spekulacyjnego w zależności od kodu generowanego przez kompilator.

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        untrusted_index &= (buffer_size - 1);
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

Usuwanie poufnych informacji z pamięci

Inną techniką, która może służyć do eliminowania luk w zabezpieczeniach kanału po stronie wykonywania spekulacyjnego, jest usunięcie poufnych informacji z pamięci. Deweloperzy oprogramowania mogą szukać możliwości refaktoryzacji aplikacji, tak aby poufne informacje nie były dostępne podczas wykonywania spekulatywnego. Można to osiągnąć przez refaktoryzację projektu aplikacji w celu odizolowania poufnych informacji do oddzielnych procesów. Na przykład aplikacja przeglądarki internetowej może próbować odizolować dane skojarzone z każdym źródłem internetowym do oddzielnych procesów, co uniemożliwia jednemu procesowi dostęp do danych między źródłami za pośrednictwem wykonywania spekulacyjnego.

Zobacz też

Wskazówki dotyczące ograniczania luk w zabezpieczeniach kanału bocznego wykonywania spekulatywnego
Łagodzenie luk w zabezpieczeniach sprzętowych kanału po stronie wykonywania spekulatywnego