Über die Hardware-basierten Sicherheitslücken Spectre 1 und Spectre 2 holen sich Angreifer per Javascript Passwörter aus dem Browsercache. Mit einem Trampolinsprung versucht Linux die Anwender davor zu schützen – so gut es eben geht.
Pablo Picassos Aphorismus “Computer sind nutzlos, sie können uns nur Antworten geben” lässt sich seit Ende 2017 ergänzen um “… und sie geben uns bereitwillig Antworten auf Fragen, die ein anderer gestellt hat”. Denn seither sind die drei hübsch fest in Hardware gemeißelten Sicherheitslücken Spectre 1, Spectre 2 und Meltdown (Tabelle 1) kaum weniger bekannt als Picassos Frauenporträts – kein Wunder: Fast jeder halbwegs moderne Rechner zeigt sich anfällig für die drei Attacken.
Während Meltdown – abgesehen von einer spürbaren Performance-Einbuße – überschaubar zu behandeln ist [1], sind die Auswirkungen von Spectre nur sehr mühsam in Grenzen zu halten. Ganz grob gesprochen gehen alle drei Sicherheitslücken auf eine Eigenschaft aktueller und leidlich moderner Prozessoren zurück: die spekulative Code-Ausführung.
Während die CPU etwa auf das Ende eines Speichertransfers wartet, arbeitet sie zeitgleich die im Cache befindlichen Befehle in vorauseilendem Gehorsam spekulativ ab. Stehen diese Befehle nach Abschluss des Speichertransfers tatsächlich zur Ausführung an, geht es mit den bereits berechneten Zwischenergebnissen weiter – der Anwender hat einen Performance-Vorteil verbucht.
Sollten am Ende des Speichertransfers und nach Auswertung des zugehörigen Speicherinhalts andere Befehle abzuarbeiten sein, verwirft die CPU die Zwischenergebnisse. In vielen Fällen spekuliert die CPU jedoch erfolgreich und verwendet nun das vorausberechnete Ergebnis – das spart CPU-Zyklen und erhöht die Verarbeitungsleistung im Ganzen signifikant. Am Unterprogramm-Aufruf und insbesondere dem Maschinenbefehl Ret
lässt sich die spekulative Befehlsausführung einfach veranschaulichen – Abbildung 1 tut dies.
| Name | Englische Bezeichnung | CVE-Nummer | Abwehr |
|---|---|---|---|
| Spectre 1 | Bounds Check Bypass | CVE-2017-5753 | Index-Makro |
| Spectre 2 | Branch Target Injection | CVE-2017-5715 | Retpoline |
| Meltdown | Rogue Data Cache Load | CVE-2017-5754 | KPTI |
Erfolgreich spekulieren
Technisch gesehen lädt »ret« eine auf dem Stack liegende Adresse in den Befehlszähler (Instruction Pointer). Folglich arbeitet die CPU als Nächstes den Code an dieser Adresse ab. Ret ist also ein Sprungbefehl, bei dem die Zieladresse auf dem Stack liegt. Typischerweise ist »ret« das Gegenstück zu einem »call«, also einem Unterprogrammaufruf.
Call legt nämlich den Wert des Befehlszählers (der auf den Befehl nach dem Call zeigt) auf dem Stack ab und belegt den Befehlszähler mit der Adresse des abzuarbeitenden Unterprogramms. Call fungiert demnach als Unterprogramm beziehungsweise Funktionsaufruf und Ret als Rücksprung. Da Ret fast ausschließlich in Kombination mit Call auftritt, gibt es hier einen Ansatzpunkt zur effektiven Beschleunigung.
Performante Prozessoren legen bei einem Call (Schritt [1]+ in Abbildung 1) den Inhalt des Instruction Pointers (im Beispiel A000 0004) nicht nur auf den Stack [2]+, sondern zugleich eine Kopie davon in das CPU-Register RSB (Return Stack Buffer, [3]). Dieses Register – genau genommen sind es mehrere – ist Teil der Spekulationslogik.
Den Inhalt des RAX-Registers (im Beispiel Abbildung 1: A000 1000) kopiert die CPU in den Instruction Pointer [4]+. Damit erfolgt die Abarbeitung ab Adresse A000 1000 (mit dem Befehl »subl $4, %esp«, [5]+). Der Befehl »ret« ([6]+, Adresse A000 10E0) bewirkt, dass die CPU die auf dem Stack befindliche Adresse (A000?0004) in den Instruction Pointer lädt. Da dies dauert, führt sie spekulativ zeitgleich den Schritt [7] aus.
Die Spekulations-Engine arbeitet bereits ab der im RSB abgelegten Adresse (hier A000 0004) weiter [8]. Ist die vom Stack geholte Adresse identisch mit der im RSB, übernimmt die CPU die spekulativ gewonnenen Ergebnisse. Andernfalls verwirft sie diese und die Abarbeitung beginnt ab der im Instruction Pointer (über den Stack) abgelegten Adresse.
Bedingte Indirektionen
Als Spectre 1 und 2 kennt die IT-Welt zwei Sicherheitslücken, die das spekulative Abarbeiten von Code bei Sprungbefehlen ausnutzen. Bei Spectre 1 geht es um bedingte Sprünge (Conditional Branch), wie sie bei If-Abfragen vorkommen. Spectre 2 nutzt indirekte Sprünge (Indirect Branch), die bei objektorientierter Programmierung oder beim Zugriff über Funktionszeiger auftreten.
Tatsächlich kann ein pfiffiger Angreifer dafür sorgen, dass eine CPU den zu einer Bedingung gehörenden Code aufruft, obwohl die Bedingung nicht erfüllt ist, eben spekulativ. Dabei darf er sogar Einfluss auf die innerhalb der Bedingung verwendeten Variablen nehmen, sodass er gewünschte Informationen ausliest und über den von Meltdown bereits bekannten Seitenkanal-Angriff identifiziert. Das gelingt sogar, wenn die CPU die Fehlspekulation bemerkt hat (siehe Kasten “Cache-basierter Seitenkanal-Angriff”).
Cache-basierter Seitenkanal-Angriff
Den für Meltdown und Spectre verwendeten Seitenkanal-Angriff hatte bereits die Kern-Technik 97 [2] vorgestellt: Macht eine anfällige CPU die Ergebnisse einer spekulativ ausgeführten Codesequenz rückgängig, da sie sich verspekuliert hat, gelingt ihr dies unglücklicherweise nicht vollständig. So verbleiben beispielsweise Daten im Cache, die während der Spekulation dort angefallen sind. Dies nutzen die findigen Angreifer aus.
Sie lassen eine Codesequenz spekulativ ablaufen, die eine Speicherzelle ausliest, auf die normalerweise kein Zugriff möglich ist. Mit dem Wert als Index greift die CPU auf einen vorbereiteten Speicherblock zu (Abbildung 2). Dadurch landet dieser Block im Cache. Die Daten im Block sind für den Angreifer uninteressant. Es zählt nur die Blocknummer, denn sie entspricht dem ausgelesenen Wert.
Um diese Blocknummer später herauszukriegen, misst der Angreifer mit einem separaten Prozess die Zugriffszeit auf seine Speicherblöcke aus. Der Block mit der kurzen Zugriffszeit war bei der spekulativen Abarbeitung im Cache zwischengespeichert; die Blocknummer entspricht dem gesuchten Wert.
Ein schützendes Makro für den Kernel
So kann ein gewiefter Angreifer mit Hilfe der spekulativen Befehlsausführung beim Zugriff auf Arrays eine eventuell vorhandene Überprüfung von Feldgrenzen (Listing 1, Zeile 3) überwinden. Per Seitenkanal-Angriff liest er damit die für ihn auf konventionellem Wege unzugänglichen Speicherinhalte aus [3].
Zur Abwehr haben die Kernelentwickler das neue Makro »array_index_nospec()« eingeführt (Listing 2, Zeile 6). Es stellt sicher, dass sich der Feldindex innerhalb des legitimen Wertebereichs befindet. Gerät der Index außerhalb des Feldes, wird als Index 0 verwendet. Allerdings müssen die Entwickler jetzt alle relevanten Stellen im Code identifizieren und ergänzen – eine Sisyphusarbeit. Daher versuchen Torvalds’ Mannen diese Codestellen-Suche zu automatisieren.
Listing 1
Verwundbare Codesequenz
01 int load_array(int *array, unsigned int index)
02 {
03 if (index >= MAX_ARRAY_ELEMS)
04 return 0;
05 else {
06 return array[index];
07 }
08 }
Listing 2
Geschützte Codesequenz
01 int load_array(int *array, unsigned int index)
02 {
03 if (index >= MAX_ARRAY_ELEMS)
04 return 0;
05 else {
06 index = array_index_nospec(index, MAX_ARRAY_ELEMS);
07 return array[index];
08 }
09 }
Beeinflusste Spekulation
Beim Spectre 2 ist sogar noch mehr möglich. Die über diese Lücke ausnutzbaren Sprungbefehle sind »jmp *%rax« und »call *%rax«, bei denen das Sprungziel nicht direkt angegeben ist. Der Maschinenbefehl »jmp *%rax« beispielsweise sorgt dafür, dass die CPU die Abarbeitung ab der im Register RAX abgelegten Adresse fortgesetzt. Befindet sich der Inhalt von Register RAX jedoch nicht im internen Sprungregister, spekuliert die CPU bezüglich des Registerinhalts und arbeitet an der vermuteten Adresse in vorauseilendem Gehorsam weiter.
Sobald der Prozessor nun merkt, dass die Vermutung falsch war, will er die bei der spekulativen Ausführung vorgenommenen Veränderungen verwerfen. Tatsächlich – und das haben die Entdecker von Spectre gezeigt – darf ein Angreifer den Spekulationswert beeinflussen!
Da sich zwei Threads, sobald sie eine SMT-CPU (siehe Kasten “SMT-CPU”) abarbeitet, interne Register der Spekulations-Engine teilen, kann der Angreifer der ahnungslosen Codesequenz sogar seine Wunschadresse unterjubeln und die Auswirkungen seiner (Schand-)Tat über den Seitenkanal-Angriff ablesen.
Klar, Spectre-Angriffe – egal ob in Version 1 oder Version 2 – sind keine trivialen Spielchen, man muss den auszutricksenden Code kennen und Zugriff auf die CPU haben. Aber die Hacker haben bereits bewiesen: Das ist kein theoretischer, sondern ein praktisch auszuführender Angriff. Die Instruktionen dazu lassen sich sogar bequem als Javascript einschleusen. Im Proof of Concept lesen die Hacker damit die im Webbrowser abgelegten Passwörter aus.
SMT-CPU
SMT steht für Symmetrisches Multithreading und qualifiziert CPU-Architekturen, die pro Verarbeitungseinheit die doppelte Anzahl Registersätze und Pipelines besitzen. Während die Verarbeitungseinheit mit dem einen Registersatz arbeitet, wird die zweite Pipeline mit den nächsten Befehlen gefüllt. Dies bringt einen Performancegewinn von grob fünf Prozent.
Durch den doppelten Registersatz sieht es für Linux aus, als hätte die CPU die doppelte Anzahl Kerne. So zeigt »cat /proc/cpuinfo« in Abbildung 3 vier »siblings« (Geschwister) an, obwohl de facto nur ein Dualcore-Prozessor (»cpu cores«) verbaut ist.

Abbildung 3: CPUs, die dank ihres doppelten Registersatzes symmetrisches Multithreading unterstützen, sind durch Spectre-Angriffe besonders gefährdet.
Angetäuschter Angriff
Die Entwickler haben lange gegrübelt, wie sich ein Spectre-2-Angriff abwehren lässt. Denn die spekulative Abarbeitung ist nicht einfach abzuschalten und die kritischen Sprungbefehle sind nicht einfach durch eine unkritische Codevariante zu ersetzen. Gemäß dem Motto “Angriff ist die beste Verteidigung” täuschen die Entwickler die CPU nun mit einer eigenen, unkritischen Spekulationsroutine, sodass Angreifer mit ihrem gefährlichen Spekulationscode außen vor bleiben.
Trampolinsprung und glückliche Rückkehr
Die favorisierte und in Linux implementierte Lösung hat den Namen Retpoline bekommen – ein Kunstwort aus Return und Trampolin [4]. Diese Abwehr ist ein Schutz, der auf veränderter Codegenerierung fußt. Dazu ist leider ein aufgefrischter Compiler notwendig, der die unkritische Spekulationsroutine an den in Frage kommenden Stellen einbaut.
Der schlaue (neue) Compiler setzt den indirekten Sprungbefehl »jmp *%rax« in den in Listing 3 dargestellten Code um, der auf einer Manipulation des Stacks beruht und letztendlich die Spekulationslogik hinters Licht führt. Die Kernelprogrammierer ersetzen den Jump zunächst durch einen Unterprogrammaufruf (»call set_up_target«, Zeile 1). Die Spekulationslogik rechnet derweil damit, nach dem »ret« mit der nachfolgenden Codesequenz einfach weitermachen zu müssen.
Der »call« belegt auch in der internen Spekulationsmaschinerie das RSP-Register mit der dem Call nachfolgenden Adresse (Inhalt des Instruction Pointers). Im ersten Befehl des (Pseudo-)Unterprogramms überschreibt der Befehl »mov« ganz tricky die vom »call« auf dem Stack abgelegte Rücksprungadresse mit dem Inhalt des Registers RAX, sodass der nachfolgende »ret«-Befehl zur tatsächlich gewünschten Codezeile springt.
Das Unterprogramm und vor allem das »ret« am Ende, das die auf dem Stack lagernde Rücksprungadresse in die CPU laden muss, arbeiten mit lahmen Hauptspeicherzugriffen. Deswegen arbeitet die CPU den nun anstehenden Code nach dem Call schon mal spekulativ ab, also ab »capture_spec« (Zeile 5). Die Endlosschleife dort ist unbedenklich.
Sobald »ret« die Daten vom Stack gelesen hat, merkt die Spekulationsmaschine der CPU, dass sie ausgetrickst wurde. Folglich verwirft sie die Änderungen, die die spekulative Abarbeitung hinterlassen hat, und setzt die Abarbeitung an der vom Programmierer gewünschten Adresse fort. Aus dem Stack-manipulierten Unterprogrammsprung ist somit ein normaler Sprung geworden.
Erkennbar ist aber auch, dass der einzelne Maschinenbefehl zu einer Kette von Befehlen mutiert; ganz ohne Performance-Einbußen geht es also nicht. Außerdem ist es notwendig, die gesamte Software mit einem aktuellen Compiler neu zu kompilieren.
Listing 3
Trampolinsprung
01 call set_up_target 02 capture_spec: 03 pause 04 jmp capture_spec 05 set_up_target: 06 mov [rsp],rax 07 ret
Drei desaströse Herstellerlösungen
Nicht nur die Kernelentwickler waren zum Schutz vor Spectre aktiv. Auch Prozessorhersteller wie Intel, AMD und auch ARM [5] haben Überstunden gemacht. Dabei sind auf der x86-Plattform drei neue Microcode-Funktionen hinzugekommen [6].
Die IBPB (Indirect Branch Prediction Barrier) verhindert die Verwendung spekulativer Sprungziele. STIBP (Single Thread Indirect Branch Predictors) verhindert, dass auf einem Hyperthreading-Prozessor parallel laufende Geschwisterthreads die Sprungziele des ersten Thread verwenden. Die dritte Funktion nennt sich IBRS (Indirect Branch Restricted Speculation). Ähnlich wie die zweite unterbindet auch sie die gemeinsame Nutzung von erlernten Sprungzielen. Sie verhindert im privilegierten (Kernel-)Mode die Nutzung von Sprungzielen, die die CPU im unprivilegierten Mode trainiert hat.
Insbesondere die erste Funktion hört sich zunächst sehr nützlich an und könnte Retpoline ersetzen. Kernelentwickler Woodhouse schreibt jedoch, dass dieser Maschinenbefehl nicht vier, sondern 4000 (!) CPU-Zyklen benötigt [4] – in Sachen Performance also ein Desaster. Da bleibt Linus naheliegend lieber bei Retpoline.
Andererseits ist Retpoline sehr auf die innere Architektur der anfälligen Hardware abgestimmt und schützt dabei nicht zu 100 Prozent. Denn auf manchen Prozessoren (wie Intel Skylake) ist der erwähnte indirekte Sprungbefehl »ret« ebenfalls anfällig – die Adresse liegt ja auf dem Stack. Die Sache ist kompliziert und Linus setzt zurzeit auf eine Kombination von Retpoline und den neuen Microcode-Funktionen. Ein Kompromiss also zwischen Performance und Sicherheit.
Sorgenkind Prozessor
Das Thema der Hardware-verursachten Sicherheitslücken bleibt ohnehin sowohl Linus als auch allen Computeranwendern noch lange erhalten. Gerade im Rampenlicht stehend haben IT-Sicherheitsforscher weitere Hardware-Lücken entdeckt. Auch die neuen, Spectre-NG getauften Sicherheitsprobleme [7] setzen vermutlich keinen Schlusspunkt. Pablo Picassos Abneigung gegen Computer erhält späte Bestätigung.
Infos
- Quade, Kunst, “Kern-Technik”, Folge 97 zu Meltdown und Linux’ Gegenmaßnahmen: Linux-Magazin 05/18, S. 72
- Paul Turner, “Retpoline: a software construct for preventing branch-target-injection”: https://support.google.com/faqs/answer/7625886
- CPU-Spekulation in der Kernel-Dokumentation: »linux/Documentation/speculation«
- David Woodhouse auf der Linux-Kernel-ML: https://lkml.org/lkml/2018/1/22/598
- ARM – Vulnerability of Speculative Processors to Cache Timing Side-Channel Mechanism: https://developer.arm.com/support/security-update
- Kristian Kißling, “Linux-Entwickler diskutieren weiter über Spectre-Patches”: https://www.golem.de/news/ibrs-und-retpoline-linux-entwickler-diskutieren-weiter-ueber-spectre-patches-1801-132364.html
- Jürgen Schmidt, “Super-GAU für Intel: Weitere Spectre-Lücken im Anflug”: https://www.heise.de/ct/artikel/Super-GAU-fuer-Intel-Weitere-Spectre-Luecken-im-Anflug-4039134.html








