Meltdown und Spectre, erklärt

Obwohl ich heutzutage hauptsächlich für Netzwerke auf Anwendungsebene und verteilte Systeme bekannt bin, habe ich den ersten Teil meiner Karriere mit Betriebssystemen und Hypervisoren verbracht. Ich halte eine tiefe Faszination für die niedrigen Details, wie moderne Prozessoren und Systemsoftware funktionieren. Als die jüngsten Sicherheitslücken Meltdown und Spectre bekannt gegeben wurden, Ich habe mich mit den verfügbaren Informationen befasst und wollte unbedingt mehr erfahren.

Die Schwachstellen sind erstaunlich; Ich würde argumentieren, dass sie eine der wichtigsten Entdeckungen in der Informatik in den letzten 10-20 Jahren sind. Die Abschwächungen sind auch schwer zu verstehen und genaue Informationen über sie sind schwer zu finden. Dies ist angesichts ihrer kritischen Natur nicht verwunderlich. Die Behebung der Sicherheitsanfälligkeiten erforderte monatelange Geheimarbeit aller großen CPU-, Betriebssystem- und Cloud-Anbieter. Die Tatsache, dass die Probleme 6 Monate lang unter Verschluss gehalten wurden, als buchstäblich Hunderte von Menschen wahrscheinlich daran arbeiteten, ist erstaunlich.

Obwohl seit ihrer Ankündigung viel über Meltdown und Spectre geschrieben wurde, habe ich keine gute Einführung in die Schwachstellen und Mitigationen auf mittlerer Ebene gesehen. In diesem Beitrag werde ich versuchen, dies zu korrigieren, indem ich eine sanfte Einführung in den Hardware- und Softwarehintergrund gebe, der zum Verständnis der Sicherheitsanfälligkeiten erforderlich ist, eine Diskussion der Sicherheitsanfälligkeiten selbst sowie eine Diskussion der aktuellen Maßnahmen.

Wichtiger Hinweis: Weil ich nicht direkt an den Abschwächungen gearbeitet habe und nicht bei Intel, Microsoft, Google, Amazon, Red Hat usw. arbeite. einige der Details, die ich zur Verfügung stellen werde, sind möglicherweise nicht ganz korrekt. Ich habe diesen Beitrag basierend auf meinem Wissen über die Funktionsweise dieser Systeme, öffentlich zugänglicher Dokumentation und Patches / Diskussionen in LKML und xen-devel zusammengestellt. Ich würde gerne korrigiert werden, wenn einer dieser Beiträge ungenau ist, obwohl ich bezweifle, dass dies in absehbarer Zeit passieren wird, wenn man bedenkt, wie viel von diesem Thema noch von NDA abgedeckt wird.

In diesem Abschnitt werde ich einige Hintergrundinformationen bereitstellen, die zum Verständnis der Sicherheitsanfälligkeiten erforderlich sind. Der Abschnitt beschönigt eine große Menge an Details und richtet sich an Leser mit einem begrenzten Verständnis von Computerhardware und Systemsoftware.

Virtueller Speicher

Virtueller Speicher ist eine Technik, die seit den 1970er Jahren von allen Betriebssystemen verwendet wird. Es bietet eine Abstraktionsschicht zwischen dem Speicheradresslayout, das die meiste Software sieht, und den physischen Geräten, die diesen Speicher unterstützen (RAM, Festplatten usw.). Auf einer hohen Ebene ermöglicht es Anwendungen, mehr Speicher zu nutzen, als die Maschine tatsächlich hat; Dies bietet eine leistungsstarke Abstraktion, die viele Programmieraufgaben erleichtert.

Abbildung 1: Virtueller Speicher

Abbildung 1 zeigt einen vereinfachten Computer mit 400 Byte Speicher, der in „Seiten“ von 100 Byte angeordnet ist (echte Computer verwenden Zweierpotenzen, typischerweise 4096). Der Computer verfügt über zwei Prozesse mit jeweils 200 Byte Speicher auf jeweils 2 Seiten. Die Prozesse führen möglicherweise denselben Code mit festen Adressen im Bereich von 0 bis 199 Byte aus, werden jedoch durch diskreten physischen Speicher unterstützt, sodass sie sich nicht gegenseitig beeinflussen. Obwohl moderne Betriebssysteme und Computer den virtuellen Speicher wesentlich komplizierter verwenden als in diesem Beispiel dargestellt, gilt die oben dargestellte Grundvoraussetzung in allen Fällen. Betriebssysteme abstrahieren die Adressen, die die Anwendung sieht, von den physischen Ressourcen, die sie unterstützen.

Das Übersetzen virtueller in physische Adressen ist in modernen Computern ein so üblicher Vorgang, dass der Computer unglaublich langsam wäre, wenn das Betriebssystem in allen Fällen beteiligt sein müsste. Moderne CPU-Hardware bietet ein Gerät namens Translation Lookaside Buffer (TLB), das kürzlich verwendete Zuordnungen zwischenspeichert. Auf diese Weise können CPUs die Adressübersetzung die meiste Zeit direkt in der Hardware durchführen.

Abbildung 2: Übersetzung des virtuellen Speichers

Abbildung 2 zeigt den Adressübersetzungsfluss:

  1. Ein Programm ruft eine virtuelle Adresse ab.
  2. Die CPU versucht, es mit dem TLB zu übersetzen. Wenn die Adresse gefunden wird, wird die Übersetzung verwendet.
  3. Wenn die Adresse nicht gefunden wird, konsultiert die CPU eine Reihe von „Seitentabellen“, um die Zuordnung zu bestimmen. Seitentabellen sind eine Reihe von physischen Speicherseiten, die vom Betriebssystem an einem Ort bereitgestellt werden, an dem die Hardware sie finden kann (z. B. das CR3-Register auf x86-Hardware). Seitentabellen ordnen virtuelle Adressen physischen Adressen zu und enthalten auch Metadaten wie Berechtigungen.
  4. Wenn die Seitentabelle eine Zuordnung enthält, wird sie zurückgegeben, im TLB zwischengespeichert und für die Suche verwendet. Wenn die Seitentabelle kein Mapping enthält, wird ein „Seitenfehler“ an das Betriebssystem ausgelöst. Ein Seitenfehler ist eine spezielle Art von Interrupt, der es dem Betriebssystem ermöglicht, die Kontrolle zu übernehmen und zu bestimmen, was zu tun ist, wenn eine fehlende oder ungültige Zuordnung vorliegt. Beispielsweise kann das Betriebssystem das Programm beenden. Möglicherweise wird auch physischer Speicher zugewiesen und dem Prozess zugeordnet. Wenn ein Seitenfehlerhandler die Ausführung fortsetzt, wird das neue Mapping vom TLB verwendet.

Abbildung 3: Virtuelle Benutzer-/Kernel—Speicherzuordnungen

Abbildung 3 zeigt eine etwas realistischere Ansicht des virtuellen Speichers in einem modernen Computer (Pre-Meltdown – mehr dazu weiter unten). In diesem Setup haben wir die folgenden Funktionen:

  • Kernelspeicher wird rot angezeigt. Es ist im physikalischen Adressbereich 0-99 enthalten. Kernelspeicher ist ein spezieller Speicher, auf den nur das Betriebssystem zugreifen kann. Benutzerprogramme sollten nicht darauf zugreifen können.
  • Der Benutzerspeicher wird grau dargestellt.
  • Nicht zugeordneter physischer Speicher wird blau angezeigt.

In diesem Beispiel sehen wir einige der nützlichen Funktionen des virtuellen Speichers. In erster Linie:

  • Der Benutzerspeicher in jedem Prozess liegt im virtuellen Bereich von 0 bis 99, wird jedoch von einem anderen physischen Speicher unterstützt.
  • Der Kernelspeicher in jedem Prozess liegt im virtuellen Bereich von 100 bis 199, wird jedoch vom selben physischen Speicher unterstützt.

Wie ich im vorherigen Abschnitt kurz erwähnt habe, sind jeder Seite Berechtigungsbits zugeordnet. Obwohl der Kernelspeicher jedem Benutzerprozess zugeordnet ist, kann der Prozess, wenn er im Benutzermodus ausgeführt wird, nicht auf den Kernelspeicher zugreifen. Wenn ein Prozess dies versucht, löst er einen Seitenfehler aus, an dessen Stelle das Betriebssystem ihn beendet. Wenn der Prozess jedoch im Kernelmodus ausgeführt wird (z. B. während eines Systemaufrufs), erlaubt der Prozessor den Zugriff.

An dieser Stelle möchte ich anmerken, dass diese Art der dualen Zuordnung (bei jedem Prozess wird der Kernel direkt zugeordnet) aus Leistungsgründen seit über dreißig Jahren im Betriebssystemdesign Standard ist (Systemaufrufe sind sehr häufig und es würde lange dauern, den Kernel oder den Benutzerbereich bei jedem Übergang neu zuzuordnen).

CPU-Cache-Topologie

Abbildung 4: CPU-Thread, Kern, Paket, und Cache-Topologie.

Die nächste Hintergrundinformation, die zum Verständnis der Sicherheitsanfälligkeiten erforderlich ist, ist die CPU- und Cache-Topologie moderner Prozessoren. Abbildung 4 zeigt eine generische Topologie, die den meisten modernen CPUs gemeinsam ist. Es besteht aus den folgenden Komponenten:

  • Die Grundeinheit der Ausführung ist der „CPU-Thread“ oder „Hardware-Thread“ oder „Hyper-Thread“.“ Jeder CPU-Thread enthält eine Reihe von Registern und die Fähigkeit, einen Strom von Maschinencode auszuführen, ähnlich wie ein Software-Thread.
  • CPU-Threads sind in einem „CPU-Kern“ enthalten.“ Die meisten modernen CPUs enthalten zwei Threads pro Kern.
  • Moderne CPUs enthalten im Allgemeinen mehrere Ebenen des Cache-Speichers. Die Cache-Ebenen, die näher am CPU-Thread liegen, sind kleiner, schneller und teurer. Je weiter weg von der CPU und näher am Hauptspeicher der Cache ist, desto größer, langsamer und kostengünstiger ist er.
  • Ein typisches modernes CPU-Design verwendet einen L1/L2-Cache pro Kern. Dies bedeutet, dass jeder CPU-Thread auf dem Kern dieselben Caches verwendet.
  • Mehrere CPU-Kerne sind in einem „CPU-Paket“ enthalten.“ Moderne CPUs enthalten möglicherweise mehr als 30 Kerne (60 Threads) oder mehr pro Paket.
  • Alle CPU-Kerne im Paket teilen sich normalerweise einen L3-Cache.
  • CPU-Pakete passen in „sockets.“ Die meisten Consumer-Computer sind Single-Socket, während viele Rechenzentrumsserver mehrere Sockets haben.

Spekulative Ausführung

Abbildung 5: Moderne CPU-Ausführung motor (Quelle: Google images)

Die letzte Hintergrundinformation, die zum Verständnis der Sicherheitsanfälligkeiten erforderlich ist, ist eine moderne CPU-Technik, die als „spekulative Ausführung“ bezeichnet wird.“ Abbildung 5 zeigt ein generisches Diagramm der Ausführungsengine in einer modernen CPU.

Das Wichtigste ist, dass moderne CPUs unglaublich kompliziert sind und Maschinenbefehle nicht einfach in der richtigen Reihenfolge ausführen. Jeder CPU-Thread verfügt über eine komplizierte Pipelining-Engine, die in der Lage ist, Anweisungen außerhalb der richtigen Reihenfolge auszuführen. Der Grund dafür hat mit Caching zu tun. Wie im vorherigen Abschnitt erläutert, verwendet jede CPU mehrere Caching-Ebenen. Jeder Cache-Fehler fügt der Programmausführung eine erhebliche Verzögerungszeit hinzu. Um dies zu mildern, sind Prozessoren in der Lage, voraus und außer Betrieb auszuführen, während sie auf Speicherlasten warten. Dies wird als spekulative Ausführung bezeichnet. Das folgende Code-Snippet zeigt dies.

if (x < array1_size) {
y = array2 * 256];
}

Stellen Sie sich im vorherigen Snippet vor, dass array1_size nicht im Cache verfügbar ist, aber die Adresse von array1 ist. Die CPU könnte vermuten (spekulieren), dass x kleiner ist als array1_size und die Berechnungen innerhalb der if-Anweisung durchführen. Sobald array1_size aus dem Speicher gelesen wird, kann die CPU feststellen, ob sie richtig geraten hat. Wenn ja, kann es weiterhin eine Menge Zeit gespart haben. Wenn nicht, kann es die spekulativen Berechnungen wegwerfen und von vorne beginnen. Dies ist nicht schlimmer, als wenn es an erster Stelle gewartet hätte.

Eine andere Art der spekulativen Ausführung wird als indirekte Verzweigungsvorhersage bezeichnet. Dies ist in modernen Programmen aufgrund des virtuellen Versands äußerst häufig.

class Base {
public:
virtual void Foo() = 0;
};class Derived : public Base {
public:
void Foo() override { … }
};Base* obj = new Derived;
obj->Foo();

(Die Quelle des vorherigen Snippets ist dieser Beitrag)

Die Art und Weise, wie das vorherige Snippet in Maschinencode implementiert ist, besteht darin, die „v-Tabelle“ oder „virtuelle Versandtabelle“ von dem Speicherort zu laden, auf den obj zeigt, und sie dann aufzurufen. Da diese Operation so häufig ist, haben moderne CPUs verschiedene interne Caches und werden oft erraten (spekulieren), wohin der indirekte Zweig gehen wird, und die Ausführung an diesem Punkt fortsetzen. Wenn die CPU richtig rät, kann sie weiterhin viel Zeit sparen. Wenn nicht, kann es die spekulativen Berechnungen wegwerfen und von vorne beginnen.

Meltdown vulnerability

Nachdem wir nun alle Hintergrundinformationen behandelt haben, können wir in die Schwachstellen eintauchen.

Rogue Data Cache load

Die erste Schwachstelle, bekannt als Meltdown, ist überraschend einfach zu erklären und fast trivial auszunutzen. Der Exploit-Code sieht ungefähr wie folgt aus:

1. uint8_t* probe_array = new uint8_t;
2. // ... Make sure probe_array is not cached
3. uint8_t kernel_memory = *(uint8_t*)(kernel_address);
4. uint64_t final_kernel_memory = kernel_memory * 4096;
5. uint8_t dummy = probe_array;
6. // ... catch page fault
7. // ... determine which of 256 slots in probe_array is cached

Lassen Sie uns jeden Schritt oben beschreiben, was er tut und wie er dazu führt, dass der Speicher des gesamten Computers aus einem Benutzerprogramm gelesen werden kann.

  1. In der ersten Zeile wird ein „Sondenarray“ zugewiesen. Dies ist Speicher in unserem Prozess, der als Seitenkanal zum Abrufen von Daten aus dem Kernel verwendet wird. Wie das gemacht wird, wird sich bald zeigen.
  2. Nach der Zuweisung stellt der Angreifer sicher, dass keiner der Speicher im Sondenarray zwischengespeichert wird. Es gibt verschiedene Möglichkeiten, dies zu erreichen, von denen die einfachste CPU-spezifische Anweisungen zum Löschen eines Speicherorts aus dem Cache enthält.
  3. Der Angreifer liest dann ein Byte aus dem Adressraum des Kernels. Denken Sie aus unserer vorherigen Diskussion über virtuellen Speicher und Seitentabellen daran, dass alle modernen Kernel normalerweise den gesamten virtuellen Kerneladressraum dem Benutzerprozess zuordnen. Betriebssysteme verlassen sich auf die Tatsache, dass jeder Seitentabelleneintrag Berechtigungseinstellungen hat und dass Programme im Benutzermodus nicht auf den Kernelspeicher zugreifen dürfen. Ein solcher Zugriff führt zu einem Seitenfehler. Das wird in der Tat in Schritt 3 passieren.
  4. Moderne Prozessoren führen jedoch auch eine spekulative Ausführung durch und werden vor der fehlerhaften Anweisung ausgeführt. Somit können die Schritte 3-5 in der Pipeline der CPU ausgeführt werden, bevor der Fehler ausgelöst wird. In diesem Schritt wird das Byte des Kernelspeichers (das zwischen 0 und 255 liegt) mit der Seitengröße des Systems multipliziert, die typischerweise 4096 beträgt.
  5. In diesem Schritt wird dann das multiplizierte Byte des Kernelspeichers verwendet, um aus dem Sondenarray in einen Dummy-Wert zu lesen. Die Multiplikation des Bytes mit 4096 soll verhindern, dass eine CPU-Funktion namens „Prefetcher“ mehr Daten als gewünscht in den Cache liest.
  6. Mit diesem Schritt hat die CPU ihren Fehler erkannt und zu Schritt 3 zurückgerollt. Die Ergebnisse der spekulierten Anweisungen sind jedoch weiterhin im Cache sichtbar. Der Angreifer verwendet Betriebssystemfunktionen, um die fehlerhafte Anweisung abzufangen und die Ausführung fortzusetzen (z. B. SIGFAULT ).
  7. In Schritt 7 iteriert der Angreifer durch und sieht, wie lange es dauert, jedes der 256 möglichen Bytes im Sondenarray zu lesen, die vom Kernelspeicher indiziert worden sein könnten. Die CPU hat einen der Speicherorte in den Cache geladen, und dieser Speicherort wird wesentlich schneller geladen als alle anderen Speicherorte (die aus dem Hauptspeicher gelesen werden müssen). Dieser Speicherort ist der Wert des Bytes im Kernelspeicher.

Mit der obigen Technik und der Tatsache, dass es für moderne Betriebssysteme üblich ist, den gesamten physischen Speicher in den virtuellen Adressraum des Kernels abzubilden, kann ein Angreifer den gesamten physischen Speicher des Computers lesen.

Jetzt fragen Sie sich vielleicht: „Sie sagten, dass Seitentabellen Berechtigungsbits haben. Wie kann es sein, dass der Benutzermoduscode spekulativ auf den Kernelspeicher zugreifen konnte?“ Der Grund ist, dass dies ein Fehler in Intel-Prozessoren ist. Meiner Meinung nach gibt es keinen guten Grund, Leistung oder anderweitig, dafür, dass dies möglich ist. Denken Sie daran, dass der gesamte virtuelle Speicherzugriff über den TLB erfolgen muss. Während der spekulativen Ausführung kann problemlos überprüft werden, ob ein zwischengespeichertes Mapping über Berechtigungen verfügt, die mit der aktuellen Ausführungsberechtigungsstufe kompatibel sind. Intel-Hardware macht das einfach nicht. Andere Prozessoranbieter führen eine Berechtigungsprüfung durch und blockieren die spekulative Ausführung. Soweit wir wissen, handelt es sich bei Meltdown also nur um eine Sicherheitsanfälligkeit von Intel.

Edit: Es scheint, dass mindestens ein ARM-Prozessor auch anfällig für Kernschmelzen ist, wie hier und hier angegeben .

Meltdown mitigations

Meltdown ist leicht zu verstehen, trivial zu nutzen und hat glücklicherweise auch eine relativ einfache Abschwächung (zumindest konzeptionell — Kernel-Entwickler sind sich möglicherweise nicht einig, dass es einfach zu implementieren ist).

Kernel Page Table Isolation (KPTI)

Denken Sie daran, dass ich im Abschnitt über den virtuellen Speicher beschrieben habe, dass alle modernen Betriebssysteme eine Technik verwenden, bei der der Kernelspeicher jedem Adressraum des virtuellen Speichers im Benutzermodus zugeordnet wird. Dies ist sowohl aus Gründen der Leistung und Einfachheit. Dies bedeutet, dass der Kernel, wenn ein Programm einen Systemaufruf ausführt, ohne weitere Arbeit verwendet werden kann. Die Lösung für Meltdown besteht darin, diese doppelte Zuordnung nicht mehr durchzuführen.

Abbildung 6: Isolierung der Kernel-Seitentabelle

Abbildung 6 zeigt eine Technik namens Kernel Page Table Isolation (KPTI). Dies läuft im Grunde darauf hinaus, den Kernelspeicher nicht in ein Programm abzubilden, wenn es im Benutzerbereich ausgeführt wird. Wenn kein Mapping vorhanden ist, ist eine spekulative Ausführung nicht mehr möglich und wird sofort fehlschlagen.

Zusätzlich dazu, dass der Virtual Memory Manager (VMM) des Betriebssystems komplizierter wird, wird diese Technik ohne Hardwareunterstützung auch Arbeitslasten erheblich verlangsamen, die eine große Anzahl von Übergängen zwischen Benutzermodus und Kernelmodus bewirken, da die Seitentabellen bei jedem Übergang geändert werden müssen und der TLB geleert werden muss (da der TLB möglicherweise veraltete Zuordnungen beibehält).Neuere x86-CPUs verfügen über eine Funktion namens ASID (Address Space ID) oder PCID (Process Context ID), mit der diese Aufgabe erheblich billiger werden kann (ARM und andere Mikroarchitekturen haben diese Funktion seit Jahren). Mit PCID kann eine ID einem TLB-Eintrag zugeordnet werden und dann nur TLB-Einträge mit dieser ID leeren. Die Verwendung von PCID macht KPTI billiger, aber immer noch nicht kostenlos.

Zusammenfassend ist Meltdown eine äußerst schwerwiegende und leicht auszunutzende Sicherheitsanfälligkeit. Glücklicherweise hat es eine relativ einfache Abschwächung, die bereits von allen großen Betriebssystemanbietern bereitgestellt wurde, mit der Einschränkung, dass bestimmte Workloads langsamer laufen, bis zukünftige Hardware explizit für die beschriebene Adressraumtrennung ausgelegt ist.

Spectre vulnerability

Spectre teilt einige Eigenschaften von Meltdown und besteht aus zwei Varianten. Im Gegensatz zu Meltdown ist Spectre wesentlich schwieriger zu nutzen, betrifft jedoch fast alle modernen Prozessoren, die in den letzten zwanzig Jahren hergestellt wurden. Im Wesentlichen handelt es sich bei Spectre um einen Angriff auf das moderne CPU- und Betriebssystemdesign im Vergleich zu einer bestimmten Sicherheitslücke.

Bounds Check Bypass (Spectre Variante 1)

Die erste Spectre Variante wird als „bounds check Bypass“ bezeichnet.“ Dies wird im folgenden Code-Snippet demonstriert (das ist das gleiche Code-Snippet, das ich verwendet habe, um spekulative Ausführung oben einzuführen).

if (x < array1_size) {
y = array2 * 256];
}

Nehmen Sie im vorherigen Beispiel die folgende Abfolge von Ereignissen an:

  1. Der Angreifer kontrolliert x.
  2. array1_size wird nicht zwischengespeichert.
  3. array1 wird zwischengespeichert.
  4. Die CPU vermutet, dass x kleiner als array1_size ist. (CPUs verwenden verschiedene proprietäre Algorithmen und Heuristiken, um zu bestimmen, ob spekuliert werden soll, weshalb die Angriffsdetails für Spectre zwischen Prozessorherstellern und -modellen variieren.)
  5. Die CPU führt den Hauptteil der if-Anweisung aus, während sie darauf wartet, dass array1_size geladen wird, was sich ähnlich wie Meltdown auf den Cache auswirkt.
  6. Der Angreifer kann dann den tatsächlichen Wert von array1 über eine von verschiedenen Methoden ermitteln. (Weitere Informationen zu Cache-Inferenzangriffen finden Sie in der Forschungsarbeit.)

Spectre ist erheblich schwieriger auszunutzen als Meltdown, da diese Sicherheitsanfälligkeit nicht von der Eskalation von Berechtigungen abhängt. Der Angreifer muss den Kernel überzeugen, Code auszuführen und falsch zu spekulieren. Normalerweise muss der Angreifer die Spekulationsmaschine vergiften und sie dazu bringen, falsch zu raten. Das heißt, Forscher haben mehrere Proof-of-Concept-Exploits gezeigt.

Ich möchte wiederholen, was für eine wirklich unglaubliche Entdeckung dieser Exploit ist. Ich persönlich halte dies nicht für einen CPU-Designfehler wie Meltdown an sich. Ich halte dies für eine grundlegende Offenbarung darüber, wie moderne Hardware und Software zusammenarbeiten. Die Tatsache, dass CPU-Caches indirekt genutzt werden können, um Zugriffsmuster zu erfahren, ist seit einiger Zeit bekannt. Die Tatsache, dass CPU-Caches als Seitenkanal zum Speichern von Computerspeicher verwendet werden können, ist sowohl konzeptionell als auch in ihren Auswirkungen erstaunlich.

Branch Target Injection (Spectre Variante 2)

Denken Sie daran, dass indirekte Verzweigung in modernen Programmen sehr verbreitet ist. Variante 2 von Spectre verwendet eine indirekte Verzweigungsvorhersage, um die CPU dazu zu bringen, spekulativ an einem Speicherort auszuführen, den sie sonst nie ausgeführt hätte. Wenn die Ausführung dieser Anweisungen einen Zustand im Cache hinterlassen kann, der mithilfe von Cache-Inferenzangriffen erkannt werden kann, kann der Angreifer den gesamten Kernelspeicher sichern. Wie Spectre Variante 1, Spectre Variante 2 ist viel schwieriger zu nutzen als Meltdown, Forscher haben jedoch gezeigt, dass Proof-of-Concept-Exploits von Variante funktionieren 2.

Spectre mitigations

Die Spectre Mitigations sind wesentlich interessanter als die Meltdown Mitigation. Tatsächlich schreibt das akademische Spectre-Papier, dass derzeit keine Milderungen bekannt sind. Es scheint, dass Intel (und wahrscheinlich auch andere CPU-Anbieter) und die großen Betriebssystem- und Cloud-Anbieter hinter den Kulissen und parallel zur akademischen Arbeit seit Monaten wütend daran arbeiten, Abhilfemaßnahmen zu entwickeln. In diesem Abschnitt werde ich die verschiedenen Mitigationen behandeln, die entwickelt und bereitgestellt wurden. Dies ist der Abschnitt, in dem ich am verschwommensten bin, da es unglaublich schwierig ist, genaue Informationen zu erhalten, daher setze ich Dinge aus verschiedenen Quellen zusammen.

Statische Analyse und Fencing (Variante 1 Mitigation)

Die einzige bekannte Variante 1 (Bounds Check Bypass) Mitigation ist die statische Analyse von Code, um Code-Sequenzen zu bestimmen, die vom Angreifer kontrolliert werden könnten, um Spekulationen zu stören. Anfällige Codesequenzen können eine Serialisierungsanweisung wie lfence eingefügt haben, die die spekulative Ausführung stoppt, bis alle Anweisungen bis zum Zaun ausgeführt wurden. Beim Einfügen von Zaunanweisungen ist Vorsicht geboten, da zu viele schwerwiegende Auswirkungen auf die Leistung haben können.

Retpoline (Variante 2 Mitigation)

Die erste Spectre Variante 2 (Branch Target Injection) Mitigation wurde von Google entwickelt und ist bekannt als „retpoline.“ Es ist mir unklar, ob es isoliert von Google oder von Google in Zusammenarbeit mit Intel entwickelt wurde. Ich würde spekulieren, dass es von Google experimentell entwickelt und dann von Intel-Hardware-Ingenieuren verifiziert wurde, aber ich bin mir nicht sicher. Details zum „retpoline“ -Ansatz finden Sie in Googles Paper zum Thema. Ich werde sie hier zusammenfassen (ich beschönige einige Details, einschließlich des Unterflusses, die in dem Papier behandelt werden).

Retpoline beruht auf der Tatsache, dass das Aufrufen und Zurückgeben von Funktionen und die damit verbundenen Stapelmanipulationen in Computerprogrammen so häufig sind, dass CPUs für ihre Ausführung stark optimiert sind. (Wenn Sie nicht wissen, wie der Stack in Bezug auf das Aufrufen und Zurückgeben von Funktionen funktioniert, ist dieser Beitrag eine gute Einführung.) Kurz gesagt, wenn ein „Anruf“ ausgeführt wird, wird die Rücksprungadresse auf den Stapel geschoben. „ret“ schaltet die Absenderadresse aus und setzt die Ausführung fort. Spekulative Ausführungshardware merkt sich die gepushte Absenderadresse und setzt die Ausführung an diesem Punkt spekulativ fort.

Die retpoline-Konstruktion ersetzt einen indirekten Sprung zu dem im Register r11 gespeicherten Speicherort:

jmp *%r11

mit:

call set_up_target; (1)
capture_spec: (4)
pause;
jmp capture_spec;
set_up_target:
mov %r11, (%rsp); (2)
ret; (3)

Mal sehen, was der vorherige Assemblycode Schritt für Schritt ausführt und wie er die Verzweigungszielinjektion abschwächt.

  1. In diesem Schritt ruft der Code einen Speicherort auf, der zur Kompilierungszeit bekannt ist, also ein hartcodierter Offset und nicht indirekt ist. Dies platziert die Rücksprungadresse von capture_spec auf dem Stapel.
  2. Die Rücksprungadresse aus dem Aufruf wird mit dem eigentlichen Sprungziel überschrieben.
  3. Für das reale Ziel wird ein Return ausgeführt.
  4. Wenn die CPU spekulativ ausgeführt wird, kehrt sie in eine Endlosschleife zurück! Denken Sie daran, dass die CPU spekuliert, bis die Speicherauslastung abgeschlossen ist. In diesem Fall wurde die Spekulation manipuliert, um in einer Endlosschleife erfasst zu werden, die keine für einen Angreifer beobachtbaren Nebenwirkungen aufweist. Wenn die CPU schließlich die reale Rückgabe ausführt, bricht sie die spekulative Ausführung ab, die keine Auswirkung hatte.

Meiner Meinung nach ist dies eine wirklich geniale Lösung. Ein großes Lob an die Ingenieure, die es entwickelt haben. Der Nachteil dieser Abschwächung besteht darin, dass die gesamte Software neu kompiliert werden muss, sodass indirekte Zweige in Retpoline-Zweige konvertiert werden. Für Cloud-Dienste wie Google, die den gesamten Stack besitzen, ist die Neukompilierung keine große Sache. Für andere kann es eine sehr große Sache oder unmöglich sein.

IBRS, STIBP und IBPB (Variante 2 Mitigation)

Es scheint, dass Intel (und AMD bis zu einem gewissen Grad) gleichzeitig mit der Entwicklung von Retpoline an Hardwareänderungen gearbeitet haben, um Branch Target Injection-Angriffe abzuschwächen. Die drei neuen Hardwarefunktionen, die als CPU-Mikrocode-Updates ausgeliefert werden, sind:

  • Indirect Branch Restricted Speculation (IBRS)
  • Single Thread Indirect Branch Predictors (STIBP)
  • Indirect Branch Predictor Barrier (IBPB)

Eingeschränkte Informationen zu den neuen Mikrocode-Funktionen erhalten Sie von Intel hier. Ich konnte grob zusammenstellen, was diese neuen Funktionen tun, indem ich die obige Dokumentation gelesen und mir die Patches für Linux-Kernel und Xen-Hypervisor angesehen habe. Aus meiner Analyse geht hervor, dass jedes Feature möglicherweise wie folgt verwendet wird:

  • IBRS leert sowohl den Verzweigungsvorhersagecache zwischen den Berechtigungsstufen (Benutzer zu Kernel) als auch deaktiviert die Verzweigungsvorhersage auf dem Geschwister-CPU-Thread. Denken Sie daran, dass jeder CPU-Kern normalerweise zwei CPU-Threads hat. Es scheint, dass auf modernen CPUs die Verzweigungsvorhersagehardware von den Threads gemeinsam genutzt wird. Dies bedeutet, dass nicht nur Benutzermoduscode den Verzweigungsprädiktor vor der Eingabe von Kernelcode vergiften kann, sondern auch Code, der auf dem Geschwister-CPU-Thread ausgeführt wird, ihn vergiften kann. Das Aktivieren von IBRS im Kernelmodus verhindert im Wesentlichen, dass jede vorherige Ausführung im Benutzermodus und jede Ausführung auf dem Geschwister-CPU-Thread die Verzweigungsvorhersage beeinflusst.
  • STIBP scheint eine Teilmenge von IBRS zu sein, die nur die Verzweigungsvorhersage für den Geschwister-CPU-Thread deaktiviert. Soweit ich das beurteilen kann, besteht der Hauptanwendungsfall für diese Funktion darin, zu verhindern, dass ein Geschwister-CPU-Thread den Verzweigungsprädiktor vergiftet, wenn zwei verschiedene Benutzermodusprozesse (oder virtuelle Maschinen) gleichzeitig auf demselben CPU-Kern ausgeführt werden. Es ist mir ehrlich gesagt nicht ganz klar, wann STIBP verwendet werden sollte.
  • IBPB scheint den Verzweigungsvorhersagecache für Code zu leeren, der auf derselben Berechtigungsstufe ausgeführt wird. Dies kann verwendet werden, wenn zwischen zwei Benutzermodusprogrammen oder zwei virtuellen Maschinen gewechselt wird, um sicherzustellen, dass der vorherige Code den Code, der gerade ausgeführt wird, nicht beeinträchtigt (obwohl ich ohne STIBP glaube, dass Code, der auf dem Geschwister-CPU-Thread ausgeführt wird, den Verzweigungsprädiktor immer noch vergiften kann).

Zum jetzigen Zeitpunkt scheinen die wichtigsten Mitigationen, die für die Branch Target Injection-Sicherheitsanfälligkeit implementiert werden, sowohl retpoline als auch IBRS zu sein. Vermutlich ist dies der schnellste Weg, um den Kernel vor Benutzermodusprogrammen oder den Hypervisor vor Gästen virtueller Maschinen zu schützen. In Zukunft würde ich erwarten, dass sowohl STIBP als auch IBPB abhängig von der Paranoia verschiedener Benutzermodusprogramme bereitgestellt werden, die sich gegenseitig stören.

Die Kosten für IBRS scheinen auch zwischen den CPU-Architekturen extrem stark zu variieren, wobei neuere Intel Skylake-Prozessoren im Vergleich zu älteren Prozessoren relativ günstig sind. Bei Lyft haben wir eine Verlangsamung bestimmter systemaufrufintensiver Workloads auf AWS C4-Instances um etwa 20% festgestellt, als die Mitigationen eingeführt wurden. Ich würde spekulieren, dass Amazon IBRS und möglicherweise auch Retpoline eingeführt hat, aber ich bin mir nicht sicher. Es scheint, dass Google retpoline möglicherweise nur in seiner Cloud eingeführt hat.

Im Laufe der Zeit würde ich erwarten, dass Prozessoren schließlich zu einem IBRS „always on“ -Modell wechseln, bei dem die Hardware standardmäßig nur die Trennung der Verzweigungsprädiktoren zwischen CPU-Threads bereinigt und den Status bei Änderungen der Berechtigungsstufe korrekt löscht. Der einzige Grund, warum dies heute nicht möglich wäre, sind die offensichtlichen Leistungskosten für die Nachrüstung dieser Funktionalität auf bereits veröffentlichte Mikroarchitekturen über Mikrocode-Updates.

Fazit

Es ist sehr selten, dass ein Forschungsergebnis die Art und Weise, wie Computer gebaut und betrieben werden, grundlegend verändert. Meltdown und Spectre haben genau das getan. Diese Erkenntnisse werden das Hardware- und Softwaredesign in den nächsten 7 bis 10 Jahren (dem nächsten CPU-Hardwarezyklus) erheblich verändern, da die Designer die neue Realität der Möglichkeiten von Datenlecks über Cache-Seitenkanäle berücksichtigen.

In der Zwischenzeit werden die Ergebnisse von Meltdown und Spectre und die damit verbundenen Abhilfemaßnahmen für Computerbenutzer in den kommenden Jahren erhebliche Auswirkungen haben. Kurzfristig werden die Minderungen Auswirkungen auf die Leistung haben, die je nach Workload und spezifischer Hardware erheblich sein können. Dies kann betriebliche Änderungen für einige Infrastrukturen erforderlich machen (beispielsweise verschieben wir bei Lyft einige Workloads aggressiv auf AWS C5-Instances, da IBRS auf Skylake-Prozessoren wesentlich schneller zu laufen scheint und der neue Nitro-Hypervisor Interrupts direkt an Gäste liefert, die SR-IOV und APICv verwenden, wodurch viele Exits virtueller Maschinen für E / A-schwere Workloads entfernt werden). Desktop-Computer-Benutzer sind auch nicht immun, aufgrund von Proof-of-Concept-Browser-Angriffen mit JavaScript, an deren Abschwächung OS- und Browser-Anbieter arbeiten. Darüber hinaus ist es aufgrund der Komplexität der Sicherheitsanfälligkeiten fast sicher, dass Sicherheitsforscher neue Exploits finden, die nicht von den aktuellen Mitigationen abgedeckt werden, die gepatcht werden müssen.Obwohl ich es liebe, bei Lyft zu arbeiten und das Gefühl habe, dass die Arbeit, die wir im Bereich der Infrastruktur für Mikroservice-Systeme leisten, zu den wirkungsvollsten Arbeiten gehört, die derzeit in der Branche geleistet werden, lassen mich Ereignisse wie diese die Arbeit an Betriebssystemen und Hypervisoren vermissen. Ich bin extrem neidisch auf die heldenhafte Arbeit, die in den letzten sechs Monaten von einer großen Anzahl von Menschen bei der Erforschung und Minderung der Schwachstellen geleistet wurde. Ich wäre gerne ein Teil davon gewesen!

Weiterführende Literatur

  • Meltdown und Spectre wissenschaftliche Arbeiten: https://spectreattack.com/
  • Google Project Zero Blogbeitrag: https://googleprojectzero.blogspot.com/2018/01/reading-privileged-memory-with-side.html
  • Intel Spectre Hardware mitigations: https://software.intel.com/sites/default/files/managed/c5/63/336996-Speculative-Execution-Side-Channel-Mitigations.pdf
  • Retpoline blog post: https://support.google.com/faqs/answer/7625886
  • Gute Zusammenfassung bekannter Informationen: https://github.com/marcan/speculation-bugs/blob/master/README.md



Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.