Leitfaden für C++-Entwickler für spekulative ausführungsseitige Kanäle

Dieser Artikel enthält Anleitungen für Entwickler zur Identifizierung und Milderung spekulativer Hardware-Hardwarerisiken in C++-Software. Diese Sicherheitsrisiken können vertrauliche Informationen über vertrauenswürdige Grenzen hinweg offenlegen und sich auf Software auswirken, die auf Prozessoren ausgeführt wird, die spekulative, out-of-order-Ausführung von Anweisungen unterstützen. Diese Klasse von Sicherheitsrisiken wurde erstmals im Januar 2018 beschrieben, und zusätzliche Hintergrundinformationen und Anleitungen finden Sie in der Sicherheitsempfehlung von Microsoft.

Die anleitungen in diesem Artikel beziehen sich auf die Klassen von Sicherheitsrisiken, die durch:

  1. CVE-2017-5753, auch bekannt als Spectre Variant 1. Diese Hardware-Sicherheitsrisikoklasse bezieht sich auf seitenseitige Kanäle, die aufgrund spekulativer Ausführung auftreten können, die aufgrund eines Fehlverhaltens einer bedingten Verzweigung auftritt. Der Microsoft C++-Compiler in Visual Studio 2017 (ab Version 15.5.5) enthält Unterstützung für den /Qspectre Switch, der eine Kompilierungszeitminderung für einen begrenzten Satz potenziell anfälliger Codierungsmuster im Zusammenhang mit CVE-2017-5753 bietet. Die /Qspectre Option ist auch in Visual Studio 2015 Update 3 bis KB 4338871 verfügbar. Die Dokumentation für das /Qspectre Kennzeichen enthält weitere Informationen zu ihren Auswirkungen und derEn Nutzung.

  2. CVE-2018-3639, auch als spekulative Speicherumgehung (SSB) bezeichnet. Diese Hardware-Sicherheitsrisikoklasse bezieht sich auf seitenseitige Kanäle, die aufgrund spekulativer Ausführung einer Last vor einem abhängigen Speicher auftreten können, da ein Speicherzugriff fehlschlegt.

Eine barrierefreie Einführung in spekulative Ausführungsseitenkanal-Sicherheitslücken finden Sie in der Präsentation mit dem Titel The Case of Spectre und Meltdown von einem der Forschungsteams, die diese Probleme entdeckt haben.

Was sind hardwareseitige Hardwarerisiken für spekulative Ausführung?

Moderne CPUs bieten höhere Leistungsgrade, indem spekulative und out-of-order-Ausführung von Anweisungen verwendet wird. Dies wird z. B. häufig erreicht, indem das Ziel von Verzweigungen (bedingt und indirekt) vorhergesagt wird, wodurch die CPU spekulative Anweisungen am vorhergesagten Verzweigungsziel ausführen kann, wodurch ein Stillstand vermieden wird, bis das tatsächliche Verzweigungsziel aufgelöst wird. Wenn die CPU später feststellt, dass ein Fehlverhalten aufgetreten ist, ist der gesamte Computerzustand, der spekulativ berechnet wurde, nicht Karte. Dadurch wird sichergestellt, dass es keine architektonisch sichtbaren Auswirkungen der falsch angenommenen Spekulationen gibt.

Die spekulative Ausführung wirkt sich zwar nicht auf den architektonisch sichtbaren Zustand aus, kann aber Restablaufverfolgungen im nicht architektonischen Zustand hinterlassen, z. B. die verschiedenen Caches, die von der CPU verwendet werden. Es handelt sich hierbei um die Restspuren der spekulativen Ausführung, die zu Seitenkanalrisiken führen können. Um dies besser zu verstehen, berücksichtigen Sie das folgende Codefragment, das ein Beispiel für CVE-2017-5753 (Bounds Check Bypass) bereitstellt:

// 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];
    }
}

In diesem Beispiel ReadByte wird ein Puffer, eine Puffergröße und ein Index in diesem Puffer bereitgestellt. Der Indexparameter, wie angegeben untrusted_index, wird von einem weniger privilegierten Kontext bereitgestellt, z. B. einem nicht administrativen Prozess. Ist untrusted_index der Wert kleiner als buffer_size, wird das Zeichen in diesem Index gelesen buffer und zum Indizieren in einem freigegebenen Speicherbereich verwendet, auf den shared_bufferverwiesen wird.

Aus architektonischer Sicht ist diese Codesequenz absolut sicher, da sie garantiert ist, dass untrusted_index sie immer kleiner als buffer_sizesein wird. In Anwesenheit von spekulativer Ausführung ist es jedoch möglich, dass die CPU die bedingte Verzweigung falsch angibt und den Textkörper der If-Anweisung ausführt, auch wenn untrusted_index die Anweisung größer oder gleich buffer_sizeist. Daher kann die CPU spekulativ ein Byte über die Grenzen buffer (die ein Geheimnis sein könnten) lesen und dann diesen Bytewert verwenden, um die Adresse einer nachfolgenden Last durchzurechnen shared_buffer.

Während die CPU diese Fehlschätzung schließlich erkennt, können restseitige Nebenwirkungen im CPU-Cache verbleiben, die Informationen über den Bytewert anzeigen, der außerhalb der Grenzen buffergelesen wurde. Diese Nebenwirkungen können durch einen weniger privilegierten Kontext erkannt werden, der auf dem System ausgeführt wird, indem sie probieren, wie schnell auf jede Cachezeile zugegriffen shared_buffer wird. Die schritte, die sie ausführen können, sind:

  1. Rufen Sie mehrmals auf ReadByte , wobei untrusted_index sie kleiner als buffer_sizeist. Der Angriffskontext kann dazu führen, dass der Opferkontext aufgerufen ReadByte wird (z. B. über RPC), sodass der Verzweigungsvorhersager so trainiert wird, dass er nicht genommen wird, als untrusted_index kleiner als buffer_size.

  2. Leeren aller Cachezeilen in shared_buffer. Der Angriffskontext muss alle Cachezeilen im freigegebenen Speicherbereich leeren, auf shared_bufferden verwiesen wird. Da der Speicherbereich freigegeben wird, ist dies einfach und kann mithilfe systeminterner Werte wie z _mm_clflush. B. durchgeführt werden.

  3. Aufruf ReadByte mit untrusted_index größer als buffer_size. Der Angriffskontext bewirkt, dass der Opferkontext aufgerufen ReadByte wird, sodass er falsch vorhersagt, dass die Verzweigung nicht genommen wird. Dies führt dazu, dass der Prozessor spekulativ den Textkörper des If-Blocks untrusted_index ausführt, der größer als buffer_sizeist und somit zu einem out-of-bounds-Lesevorgang bufferführt. Folglich wird ein potenziell geheimer Wert indiziert, shared_buffer der außerhalb der Grenzen gelesen wurde, wodurch die jeweilige Cachezeile von der CPU geladen wird.

  4. Lesen Sie jede Cachezeile, shared_buffer um zu sehen, auf welche Am häufigsten zugegriffen wird. Der Angriffskontext kann jede Cachezeile shared_buffer lesen und die Cachezeile erkennen, die deutlich schneller geladen wird als die anderen. Dies ist die Cachezeile, die wahrscheinlich in Schritt 3 eingebracht wurde. Da in diesem Beispiel eine 1:1-Beziehung zwischen Bytewert und Cachezeile besteht, kann der Angreifer den tatsächlichen Wert des Byte ableiten, der außerhalb der Grenzen gelesen wurde.

Die obigen Schritte enthalten ein Beispiel für die Verwendung einer Technik, die als FLUSH+RELOAD bezeichnet wird, zusammen mit dem Ausnutzen einer Instanz von CVE-2017-5753.

Welche Softwareszenarien können betroffen sein?

Wenn Sie sichere Software mithilfe eines Prozesses wie dem Security Development Lifecycle (SDL) entwickeln, müssen Entwickler in der Regel die Vertrauensgrenzen identifizieren, die in ihrer Anwendung vorhanden sind. Eine Vertrauensgrenze ist an Orten vorhanden, an denen eine Anwendung mit Daten interagieren kann, die von einem weniger vertrauenswürdigen Kontext bereitgestellt werden, z. B. einem anderen Prozess im System oder einem Prozess des nicht administrativen Benutzermodus im Falle eines Kernelmodusgerätetreibers. Die neue Klasse von Sicherheitsrisiken mit spekulativen Ausführungsseitenkanälen ist für viele der Vertrauensgrenzen in vorhandenen Softwaresicherheitsmodellen relevant, die Code und Daten auf einem Gerät isolieren.

Die folgende Tabelle enthält eine Zusammenfassung der Softwaresicherheitsmodelle, in denen Entwickler möglicherweise über diese Sicherheitsrisiken besorgt sein müssen:

Vertrauensgrenze Beschreibung
Begrenzung virtueller Computer Anwendungen, die Workloads auf separaten virtuellen Computern isolieren, die nicht vertrauenswürdige Daten von einem anderen virtuellen Computer empfangen, sind möglicherweise gefährdet.
Kernelbegrenzung Ein Kernelmodusgerätetreiber, der nicht vertrauenswürdige Daten von einem nicht administrativen Benutzermodusprozess empfängt, kann gefährdet sein.
Prozessbegrenzung Eine Anwendung, die nicht vertrauenswürdige Daten von einem anderen Prozess empfängt, der auf dem lokalen System ausgeführt wird, z. B. über einen Remoteprozeduraufruf (REMOTE Procedure Call, RPC), gemeinsam genutzten Speicher oder andere IPC-Mechanismen (Inter-Process Communication, Inter-Process Communication) kann gefährdet sein.
Enklavegrenze Eine Anwendung, die innerhalb einer sicheren Enklave (z. B. Intel SGX) ausgeführt wird, die nicht vertrauenswürdige Daten von außerhalb der Enklave empfängt, kann gefährdet sein.
Sprachgrenze Eine Anwendung, die Just-In-Time (JIT) interpretiert und ausgeführt wird, kompiliert und führt nicht vertrauenswürdigen Code aus, der in einer höheren Sprache geschrieben wurde, kann gefährdet sein.

Anwendungen mit Angriffsfläche, die einer der oben genannten Vertrauensgrenzen ausgesetzt sind, sollten Code auf der Angriffsfläche überprüfen, um mögliche Instanzen spekulativer Ausführungsseitenkanalrisiken zu identifizieren und zu mindern. Es sollte beachtet werden, dass Vertrauensgrenzen, die Remoteangriffsflächen wie Remotenetzwerkprotokolle ausgesetzt sind, nicht als Risiko für spekulative Ausführungsseitenkanalrisiken erwiesen wurden.

Potenziell anfällige Codierungsmuster

Spekulative Ausführungsseitige Kanalrisiken können als Folge mehrerer Codierungsmuster auftreten. In diesem Abschnitt werden potenziell anfällige Codierungsmuster beschrieben und Beispiele für die einzelnen Codemuster bereitgestellt, es sollte jedoch erkannt werden, dass Variationen zu diesen Designs vorhanden sein können. Entwickler werden daher empfohlen, diese Muster als Beispiele zu verwenden und nicht als vollständige Liste aller potenziell anfälligen Codierungsmuster. Die gleichen Klassen von Speichersicherheitsrisiken, die heute in der Software vorhanden sein können, können auch über spekulative und out-of-order-Pfade der Ausführung bestehen, einschließlich, aber nicht beschränkt auf Pufferüberläufe, außer grenzenlose Arrayzugriffe, nicht initialisierte Speichernutzung, Typverwechslungen usw. Dieselben Grundtypen, mit denen Angreifer Speichersicherheitsrisiken entlang von Architekturpfaden ausnutzen können, können auch auf spekulative Pfade angewendet werden.

Im Allgemeinen können spekulative Ausführungsseitenkanäle im Zusammenhang mit einer Fehleinschätzung bedingter Verzweigungen auftreten, wenn ein bedingter Ausdruck auf Daten arbeitet, die von einem weniger vertrauenswürdigen Kontext gesteuert oder beeinflusst werden können. Dies kann z. B. bedingte Ausdrücke enthalten, die in if, , for, while, switchoder ternären Anweisungen verwendet werden. Für jede dieser Anweisungen generiert der Compiler möglicherweise eine bedingte Verzweigung, für die die CPU dann das Verzweigungsziel zur Laufzeit vorhersagen kann.

Für jedes Beispiel wird ein Kommentar mit dem Ausdruck "SPEKULATIONSBARRIERE" eingefügt, in dem ein Entwickler eine Barriere als Gegenmaßnahme einführen könnte. Dies wird im Abschnitt zu Entschärfungen ausführlicher behandelt.

Spekulative out-of-bounds load

Bei dieser Kategorie von Codierungsmustern handelt es sich um eine bedingte Verzweigung, die zu einem spekulativen nicht gebundenen Speicherzugriff führt.

Laden einer Last durch Array außer Grenzen

Dieses Codierungsmuster ist das ursprünglich beschriebene anfällige Codierungsmuster für CVE-2017-5753 (Bounds Check Bypass). Im Hintergrundabschnitt dieses Artikels wird dieses Muster ausführlich erläutert.

// 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];
    }
}

In ähnlicher Weise kann eine außer Grenzen liegende Arraylast in Verbindung mit einer Schleife auftreten, die ihre Beendigungsbedingung aufgrund eines Fehlverhaltens überschreitet. In diesem Beispiel kann die dem x < buffer_size Ausdruck zugeordnete bedingte Verzweigung den Textkörper der for Schleife falsch festlegen und spekulativ ausführen, wenn x der Wert größer oder gleich buffer_sizeist, was zu einer spekulativen außergebundenen Last führt.

// 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];
    }
}

Out-of-Bounds-Arrayladevorgang einer indirekten Verzweigung

Bei diesem Codierungsmuster handelt es sich um den Fall, dass eine Fehlschätzung einer bedingten Verzweigung zu einem out-of-bounds-Zugriff auf ein Array von Funktionszeigern führen kann, das dann zu einer indirekten Verzweigung zu der Zieladresse führt, die außerhalb der Grenzen gelesen wurde. Der folgende Codeausschnitt enthält ein Beispiel, das dies veranschaulicht.

In diesem Beispiel wird über den untrusted_message_id Parameter ein nicht vertrauenswürdiger Nachrichtenbezeichner für DispatchMessage bereitgestellt. Wenn untrusted_message_id sie kleiner als MAX_MESSAGE_IDist, wird sie zum Indizieren in ein Array von Funktionszeigern und Verzweigung zum entsprechenden Verzweigungsziel verwendet. Dieser Code ist architektonisch sicher, aber wenn die CPU die bedingte Verzweigung falsch angibt, kann sie dazu führen DispatchTable , dass sie indiziert untrusted_message_id wird, wenn der Wert größer oder gleich MAX_MESSAGE_IDist und somit zu einem außergebundenen Zugriff führt. Dies kann zu spekulativer Ausführung von einer Verzweigungszieladresse führen, die über die Grenzen des Arrays hinaus abgeleitet wird, was je nach Code, der spekulativ ausgeführt wird, zu einer Offenlegung von Informationen führen könnte.

#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);
    }
}

Wie bei einer außergebundenen Arraylast kann diese Bedingung auch in Verbindung mit einer Schleife auftreten, die aufgrund eines Fehlverhaltens die Beendigungsbedingung überschreitet.

Gebundene Arrayspeicher für eine indirekte Verzweigung

Während im vorherigen Beispiel gezeigt wurde, wie eine spekulative out-of-bounds-Last ein indirektes Verzweigungsziel beeinflussen kann, ist es auch möglich, dass ein ungebundener Speicher ein indirektes Verzweigungsziel ändern kann, z. B. einen Funktionszeiger oder eine Absenderadresse. Dies kann möglicherweise zu spekulativer Ausführung von einer vom Angreifer angegebenen Adresse führen.

In diesem Beispiel wird ein nicht vertrauenswürdiger Index über den untrusted_index Parameter übergeben. Wenn untrusted_index die Elementanzahl des pointers Arrays kleiner ist (256 Elemente), wird der bereitgestellte Zeigerwert in ptr das pointers Array geschrieben. Dieser Code ist architektonisch sicher, aber wenn die CPU die bedingte Verzweigung falsch angibt, könnte es dazu führen ptr , dass sie spekulativ über die Grenzen des vom Stapel zugewiesenen pointers Arrays geschrieben wird. Dies könnte zu spekulativer Beschädigung der Absenderadresse für WriteSlot. Wenn ein Angreifer den Wert ptrsteuern kann, kann er möglicherweise spekulative Ausführung aus einer beliebigen Adresse verursachen, wenn WriteSlot er entlang des spekulativen Pfads zurückgegeben wird.

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

Ebenso kann eine lokale Funktionszeigervariable, die dem Stapel zugeordnet func wurde, spekulativ die Adresse func ändern, auf die verwiesen wird, wenn die bedingte Verzweigung falsch festgelegt wird. Dies kann zu spekulativer Ausführung von einer beliebigen Adresse führen, wenn der Funktionszeiger durch aufgerufen wird.

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();
}

Es ist zu beachten, dass beide Beispiele spekulative Änderungen von stapelverteilten indirekten Verzweigungszeigern beinhalten. Es ist möglich, dass spekulative Änderungen auch für globale Variablen, heap-zugewiesenen Speicher und sogar schreibgeschützten Speicher auf einigen CPUs auftreten können. Für stapelverteilten Arbeitsspeicher führt der Microsoft C++-Compiler bereits Schritte aus, um die spekulative Änderung von stapelverteilten indirekten Verzweigungszielen zu erschweren, z. B. durch Neuanordnen lokaler Variablen, sodass Puffer neben einem Sicherheitscookies als Teil des /GS Compilersicherheitsfeatures platziert werden.

Spekulative Typverwechslungen

Diese Kategorie befasst sich mit Codierungsmustern, die zu einer spekulativen Typverwechslung führen können. Dies tritt auf, wenn während der spekulativen Ausführung mithilfe eines falschen Typs auf einen nicht architekturfremden Pfad zugegriffen wird. Sowohl bedingte Verzweigungsfehler als auch spekulative Speicherumgehung können zu einer spekulativen Typverwechslung führen.

Bei spekulativer Speicherumgehung kann dies in Szenarien auftreten, in denen ein Compiler einen Stapelspeicherort für Variablen mehrerer Typen wiederverwendet. Dies liegt daran, dass der Architekturspeicher einer Variablen vom Typ A umgangen werden kann, sodass die Auslastung des Typs A spekulativ ausgeführt werden kann, bevor die Variable zugewiesen wird. Wenn die zuvor gespeicherte Variable einen anderen Typ aufweist, kann dies die Bedingungen für eine spekulative Typverwechslung erstellen.

Bei falscher Verzweigung wird der folgende Codeausschnitt verwendet, um verschiedene Bedingungen zu beschreiben, die spekulative Typverwechslungen verursachen können.

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;
    }
}

Spekulative Typverwechslungen, die zu einer ungebundenen Last führen

Dieses Codierungsmuster umfasst den Fall, in dem eine spekulative Typverwechslung zu einer ungebundenen oder typverwechselten Feldzugriff führen kann, bei dem der geladene Wert eine nachfolgende Ladeadresse einspeist. Dies ähnelt dem Codierungsmuster außerhalb der Grenzen des Arrays, aber es wird durch eine alternative Codierungssequenz manifestiert, wie oben gezeigt. In diesem Beispiel könnte ein Angriffskontext dazu führen, dass der Opferkontext ProcessType mehrmals mit einem Objekt vom Typ CType1 ausgeführt wird (type Feld ist gleich Type1). Dies hat die Auswirkung der Schulung der bedingten Verzweigung für die erste if Anweisung, die nicht vorhergesagt wird. Der Angriffskontext kann dann dazu führen, dass der Opferkontext mit einem Objekt vom Typ CType2ausgeführt wirdProcessType. Dies kann zu einer spekulativen Typverwechslung führen, wenn die bedingte Verzweigung für die erste if Anweisung falsch festgelegt und den Textkörper der if Anweisung ausführt, wodurch ein Objekt vom Typ CType2 in CType1. Da CType2 der Speicherzugriff kleiner CType1als ist, führt der Speicherzugriff CType1::field2 zu einer spekulativen, nicht gebundenen Last von Daten, die geheim sein können. Dieser Wert wird dann in einer Last verwendet, aus shared_buffer der beobachtbare Nebenwirkungen entstehen können, wie im zuvor beschriebenen Beispiel für out-of-bounds-Arrays.

Spekulative Typverwechslungen führen zu einer indirekten Verzweigung

Dieses Codierungsmuster umfasst den Fall, in dem eine spekulative Typverwechslung zu einer unsicheren indirekten Verzweigung während der spekulativen Ausführung führen kann. In diesem Beispiel könnte ein Angriffskontext dazu führen, dass der Opferkontext ProcessType mehrmals mit einem Objekt vom Typ CType2 ausgeführt wird (type Feld ist gleich Type2). Dies wirkt sich auf die Schulung der bedingungsbedingten Verzweigung für die erste if zu treffende Aussage und die else if Nichtausweisung aus. Der Angriffskontext kann dann dazu führen, dass der Opferkontext mit einem Objekt vom Typ CType1ausgeführt wirdProcessType. Dies kann zu einer spekulativen Typverwechslung führen, wenn die bedingte Verzweigung für die erste if Anweisung vorausgesagt wird und die else if Anweisung vorausgesagt wird, die nicht genommen wird, so dass der Textkörper des else if Typs ausgeführt und in ein Objekt des Typs CType1 umgeformt wird CType2. Da sich das CType2::dispatch_routine Feld mit dem char Array CType1::field1überlappt, kann dies zu einer spekulativen indirekten Verzweigung zu einem unbeabsichtigten Verzweigungsziel führen. Wenn der Angriffskontext die Bytewerte im CType1::field1 Array steuern kann, können sie möglicherweise die Verzweigungszieladresse steuern.

Spekulative nicht initialisierte Verwendung

Bei dieser Kategorie von Codierungsmustern handelt es sich um Szenarien, in denen spekulative Ausführung auf nicht initialisierten Arbeitsspeicher zugreifen und diese verwenden kann, um eine nachfolgende Last oder eine indirekte Verzweigung zu feeden. Damit diese Codierungsmuster ausgenutzt werden können, muss ein Angreifer in der Lage sein, den Inhalt des verwendeten Speichers zu steuern oder sinnvoll zu beeinflussen, ohne durch den Kontext initialisiert zu werden, in dem er verwendet wird.

Spekulative, nicht initialisierte Verwendung, die zu einer ungebundenen Last führt

Eine spekulative, nicht initialisierte Verwendung kann potenziell zu einer ungebundenen Last mit einem vom Angreifer gesteuerten Wert führen. Im folgenden Beispiel wird der Wert für index alle Architekturpfade zugewiesen trusted_index und trusted_index wird davon ausgegangen, dass er kleiner oder gleich buffer_sizeist. Je nach vom Compiler erzeugtem Code ist es jedoch möglich, dass eine spekulative Speicherumgehung auftreten kann, die das Laden von buffer[index] und abhängigen Ausdrücken vor der Zuordnung indexzulässt. Wenn dies der Fall ist, wird ein nicht initialisierter Wert als index Offset verwendet, in buffer den ein Angreifer vertrauliche Informationen außerhalb der Grenzen lesen und dies über einen Seitkanal durch die abhängige Last von shared_buffer.

// 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];
}

Spekulative nicht initialisierte Verwendung, die zu einer indirekten Verzweigung führt

Eine spekulative nicht initialisierte Verwendung kann potenziell zu einer indirekten Verzweigung führen, in der das Verzweigungsziel von einem Angreifer gesteuert wird. Im folgenden routine Beispiel wird entweder DefaultMessageRoutine1 oder DefaultMessageRoutine abhängig vom Wert von mode. Auf dem Architekturpfad wird dies dazu führen routine , dass sie immer vor der indirekten Verzweigung initialisiert wird. Je nach vom Compiler erzeugtem Code kann jedoch eine spekulative Speicherumgehung auftreten, die es der indirekten Verzweigung routine ermöglicht, spekulativ vor der Zuordnung routineausgeführt zu werden. Wenn dies geschieht, kann ein Angreifer spekulativ aus einer beliebigen Adresse ausgeführt werden, vorausgesetzt, der Angreifer kann den nicht initialisierten Wert beeinflussen oder steuern.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);
}

Abhilfeoptionen

Spekulative Ausführungsseitige Kanalrisiken können durch Änderungen am Quellcode abgemildert werden. Diese Änderungen können dazu führen, bestimmte Instanzen einer Sicherheitslücke zu verringern, z. B. durch Hinzufügen einer Spekulationsbarriere oder durch Änderungen am Entwurf einer Anwendung, um vertrauliche Informationen für spekulative Ausführung nicht zugänglich zu machen.

Spekulationsbarriere über manuelle Instrumentierung

Eine Spekulationsbarriere kann von einem Entwickler manuell eingefügt werden, um zu verhindern, dass spekulative Ausführung entlang eines nicht architektonischen Pfads fortgesetzt wird. Beispielsweise kann ein Entwickler eine Spekulationsbarriere vor einem gefährlichen Codierungsmuster im Textkörper eines bedingten Blocks einfügen, entweder am Anfang des Blocks (nach der bedingten Verzweigung) oder vor der ersten Last, die bedenklich ist. Dadurch wird verhindert, dass ein bedingter Verzweigungsfehler den gefährlichen Code auf einem nicht architektonischen Pfad ausführt, indem die Ausführung serialisiert wird. Die Sequenz der Spekulationsbarriere unterscheidet sich von der Hardwarearchitektur, wie in der folgenden Tabelle beschrieben:

Aufbau Spekulationsbarriere für CVE-2017-5753 Spekulationsbarriere für CVE-2018-3639
x86/x64 _mm_lfence() _mm_lfence()
ARM aktuell nicht verfügbar __dsb(0)
ARM64 aktuell nicht verfügbar __dsb(0)

Das folgende Codemuster kann z. B. mithilfe des _mm_lfence systeminternen Musters wie unten dargestellt abgemildert werden.

// 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];
    }
}

Spekulationsbarriere über Compilerzeitinstrumentation

Der Microsoft C++-Compiler in Visual Studio 2017 (ab Version 15.5.5) enthält Unterstützung für den Switch, der /Qspectre automatisch eine Spekulationsbarriere für einen begrenzten Satz potenziell anfälliger Codierungsmuster im Zusammenhang mit CVE-2017-5753 einfügt. Die Dokumentation für das /Qspectre Kennzeichen enthält weitere Informationen zu ihren Auswirkungen und derEn Nutzung. Es ist wichtig zu beachten, dass diese Kennzeichnung nicht alle potenziell anfälligen Codierungsmuster abdeckt, und da Entwickler sie nicht als umfassende Entschärfung für diese Klasse von Sicherheitsrisiken verwenden sollten.

Maskieren von Arrayindizes

In Fällen, in denen eine spekulative out-of-bounds-Last auftreten kann, kann der Arrayindex sowohl auf dem architektur- als auch nicht architektonischen Pfad stark gebunden werden, indem der Arrayindex explizit gebunden wird. Wenn beispielsweise ein Array einer Größe zugeordnet werden kann, die an einer Potenz von zwei ausgerichtet ist, kann eine einfache Maske eingeführt werden. Dies wird im folgenden Beispiel veranschaulicht, in dem davon ausgegangen wird, dass buffer_size sie an eine Potenz von zwei ausgerichtet ist. Dadurch wird sichergestellt, dass dies untrusted_index immer kleiner als buffer_sizeist, auch wenn ein Fehlverhalten einer bedingten Verzweigung auftritt und untrusted_index mit einem Wert übergeben wurde, der größer oder gleich ist buffer_size.

Beachten Sie, dass die hier ausgeführte Indexmaske abhängig vom vom Vom Compiler generierten Code der spekulativen Speicherumgehung unterliegen könnte.

// 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];
    }
}

Entfernen vertraulicher Informationen aus dem Arbeitsspeicher

Eine weitere Technik, die verwendet werden kann, um spekulative Ausführungsseitenkanalrisiken zu mindern, besteht darin, vertrauliche Informationen aus dem Arbeitsspeicher zu entfernen. Softwareentwickler können nach Möglichkeiten suchen, ihre Anwendung so umzugestalten, dass vertrauliche Informationen während der spekulativen Ausführung nicht zugänglich sind. Dies kann erreicht werden, indem der Entwurf einer Anwendung umgestaltt wird, um vertrauliche Informationen in separate Prozesse zu isolieren. Beispielsweise kann eine Webbrowseranwendung versuchen, die mit jedem Webursprung verbundenen Daten in separate Prozesse zu isolieren, wodurch verhindert wird, dass ein Prozess durch spekulative Ausführung auf ursprungsübergreifende Daten zugreifen kann.

Siehe auch

Leitfaden zur Verringerung spekulativer Ausführungs-Side-Channel-Sicherheitsrisiken
Milderung spekulativer Hardwarekanal-Hardwarerisiken für spekulative Ausführung