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:
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.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_buffer
verwiesen wird.
Aus architektonischer Sicht ist diese Codesequenz absolut sicher, da sie garantiert ist, dass untrusted_index
sie immer kleiner als buffer_size
sein 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_size
ist. 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 buffer
gelesen 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:
Rufen Sie mehrmals auf
ReadByte
, wobeiuntrusted_index
sie kleiner alsbuffer_size
ist. Der Angriffskontext kann dazu führen, dass der Opferkontext aufgerufenReadByte
wird (z. B. über RPC), sodass der Verzweigungsvorhersager so trainiert wird, dass er nicht genommen wird, alsuntrusted_index
kleiner alsbuffer_size
.Leeren aller Cachezeilen in
shared_buffer
. Der Angriffskontext muss alle Cachezeilen im freigegebenen Speicherbereich leeren, aufshared_buffer
den verwiesen wird. Da der Speicherbereich freigegeben wird, ist dies einfach und kann mithilfe systeminterner Werte wie z_mm_clflush
. B. durchgeführt werden.Aufruf
ReadByte
mituntrusted_index
größer alsbuffer_size
. Der Angriffskontext bewirkt, dass der Opferkontext aufgerufenReadByte
wird, sodass er falsch vorhersagt, dass die Verzweigung nicht genommen wird. Dies führt dazu, dass der Prozessor spekulativ den Textkörper des If-Blocksuntrusted_index
ausführt, der größer alsbuffer_size
ist und somit zu einem out-of-bounds-Lesevorgangbuffer
fü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.Lesen Sie jede Cachezeile,
shared_buffer
um zu sehen, auf welche Am häufigsten zugegriffen wird. Der Angriffskontext kann jede Cachezeileshared_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
, switch
oder 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_size
ist, 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_ID
ist, 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_ID
ist 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 ptr
steuern 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 CType2
ausgefü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 CType1
als 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 CType1
ausgefü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_size
ist. 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 index
zulä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 routine
ausgefü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_size
ist, 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