Wie Meltdown funktioniert

Tags: meltdown, intel cpu vulnerability
Last update: Aug 2020

1. Einleitung

Unsere heutigen gewöhnlichen Computersysteme funktionieren nach dem Prinzip der sog. Von-Neumann-Architektur nach welcher es im Computer einen gemeinsamen Speicher gibt, der sowohl Programmbefehle als auch Daten hält. Mit Daten sind unsere gewöhnlichen Nutzdaten gemeint, die wir als Nutzer speichern und verarbeiten. Da sich dies nur in dem für den Nutzer vorgesehenen Bereich des Betriebssystems abspielt, spricht man daher auch von User-Space. Programmbefehle hingehen liegen ausschließlich in der Hoheit des Betriebssystems, da sie das gesamte System steuern. Diesen Bereich nennt man Kernel-Space. Aufgabe eines guten Betriebssystems ist es diese beiden Speicherbereiche perfekt zu isolieren. Ein gewöhnlicher Benutzer sollte im Rechnerbetrieb niemals Zugriff auf Daten erlangen, die eigentümlich nur der Rechnersteuerung dienen. Das mag erstmal etwas unbedeutend klingen, aber sobald wir uns vorstellen, dass auf heutigen Betriebssystemen mehrere Benutzer oder Kunden gleichzeitig angemeldet sein können, wird das Problem offenbar: Der Kern des Betriebssystems (Kernel) speichert in seinem eigenen isolierten Speicherbereich (Kernel-Space) nicht nur potentiell sensitive Daten von uns als angemeldetem Benutzer, sondern auch von allen anderen Benutzern, die angemeldet sind. Wenn wir nun in der Lage wären den ganzen Kernel-Space auszulesen, dann würden uns sämtliche Informationen in die Hände fallen, die das Betriebssystem mit allen Mitteln der Isolation zu schützen versucht.

2. Problem

Mit der Meltdown genannten Technik ist es aber als Benutzer möglich den Speicherbereich des Kernels über einen Umweg auszulesen! Weil dieses Auslesen nicht direkt geschieht, sondern über einen Umweg realisiert wird, nennt man dies eine sog. Seitenkanal-Attacke. Diese Seitenkanal-Attacke wird erst durch die sog. Out of Order Execution und spekulative Ausführung ermöglicht.

3. Out of Order Execution und spekulative Ausführung

Moderne Prozessoren versuchen aus Gründen aus Auslastung und Performance so viele Befehle wie möglich auszuführen. Man muss sich das derart vorstellen, dass ein Prozessor die Programmbefehle nicht linear hintereinander bekommt und ausführt, sondern die Befehle in einem Bündel vom Betriebssystem erhält und dann „so gut es ihm gefällt“ sortiert. Es kann Befehle geben die sich für den Prozessor „gut“ sortieren lassen, weil sie sich unabhängig voneinander verarbeiten lassen wie beispielsweise:

a = 1 + 3;
b = 2 + 4;

Und es kann Befehle geben, die sich gegenseitig bedingen:

a = 1 + 3;
b = a + 4;

Hier muss der Prozessor auf die komplette Abarbeitung der ersten Zeile warten, weil die berechnete Variable a für die zweite Zeile benötigt wird.

Wir sehen also, dass der Prozessor die anstehenden Befehle nicht immer linear (In-Order) abarbeitet, sondern aus Gründen der Performance stets versucht unabhängige Befehle parallel (Out-of-Order) auszuführen. Diese Möglichkeit Befehle parallel zu verarbeiten ist die Grundlage dafür, dass der Prozessor sich fragt welche Befehle überhaupt als nächstes parallel ausgeführt werden können. Auf der Suche nach dem nächsten Befehl, welcher zur Abarbeitung geeignet sein könnte, bedient er sich seiner eigenen Vorhersage. Im Grunde versucht der Prozessor den nächsten wahrscheinlichen Befehl zu erraten und spekuliert somit auf dessen nächste Ausführung. Das bedeutet insgesamt, dass der Prozessor Befehle vorab ausführt, bevor sie eigentlich an der Reihe sind, ohne zu wissen, ob diese überhaupt benötigt sein werden. Daher der Name der spekulativen Ausführung. Das Kalkül für den Prozessor ist relativ einfach: Falls das Ergebnis des Befehls tatsächlich benötigt wird, so ist der Befehl bereits ausgeführt worden und das Ergebnis kann sehr schnell geladen werden. Im anderen Fall müssten die Befehle erst neu sortiert, die dafür nötigen Daten/Ressourcen geholt und der Befehl ausgeführt werden. Dies ist viel aufwändiger und kostet deshalb deutlich mehr Zeit.

Wir merken uns: Wenn wir irgendwie merken könnten, dass bestimmte Ergebnisse besonders schnell geliefert werden können, dann wüssten wir, dass diese vorher bereits abgearbeitet worden sind.

4. Ein konkretes Beispiel

Wir nehmen an wir haben einen gemeinsamen Speicherbereich von 0 bis 15 und davon sind dem Benutzer 0 bis 11 und dem Betriebssystem-Kern 12 bis 15 zugewiesen.

Adresse0123456789101112131415
Inhalt1337
Tabelle: Inhalt des Speicherbereichs

Wir als normaler Benutzer geben in einem Programm den Befehl aus, auf die Position Nr. 12 in diesem Speicherbereich zuzugreifen:

my_precious = read(12);

welcher jedoch zum Kernel-Space gehört und auf den wir deshalb keine Berechtigung haben zuzugreifen. Das Betriebssystem liefert uns daraufhin einen Fehler (Segmentation Fault) zurück, weil wir nicht berechtigt sind auf Speicherbereiche des Betriebssystems zuzugreifen und bricht unser Programm folgerichtig ab. Es ist für uns nicht möglich den Wert my_precious ausgeben zu lassen.

Da der Prozessor seine Befehle jedoch sortiert und diese spekulativ ausführt, führt dies dazu, dass der Speicherzugriff tatsächlich im System stattfindet. Nur mit der Einschränkung, dass das Betriebssystem uns diese Information nicht weitergeben darf. Diese Ausführung von spekulativen Befehlen ist Kern des Problems von Meltdown, denn sie findet statt, ohne dass vorher geprüft wird, ob der Benutzer dies überhaupt darf. Dadurch, dass der Befehl bereits ausgeführt wurde, befindet sich – und das ist besonders wichtig – das Ergebnis tatsächlich ab diesem Zeitpunkt im Zwischenspeicher (Cache) des Prozessors. Der Prozessor weiß zu diesem Zeitpunkt noch gar nicht, ob dieser Befehl gleich tatsächlich noch benötigt wird oder nicht, aber das Ergebnis befindet sich jetzt im Zwischenspeicher. Falls der Prozessor merkt, dass der Befehl falsch vorhergesagt wurde, dann werden zwar die fälschlich angenommenen Befehle aus der internen Befehlskette (Pipeline) entfernt, die errechneten Ergebniswerte bleiben jedoch weiterhin im Zwischenspeicher bestehen.

Kommen wir zu unserem Speicherbereich mit den 16 Positionen zurück. Wir wissen nun, dass unser Befehl des Zugriffs auf Adresse 12 stattgefunden hat und dass das Ergebnis im Zwischenspeicher des Prozessors liegt. Alle anderen 15 Adressen liegen nicht irgendwo im Zwischenspeicher, weil wir keine Operationen hierzu abgesetzt haben. Wir merken uns an dieser Stelle ergänzend im Hinterkopf: Ein Eintrag wurde bereits verarbeitet und dieser Wert befindet sich irgendwo im Zwischenspeicher des Prozessors.

5. Indirekter Zugriff auf den Kernel-Space

Wie kommen wir an diesen zwischengespeicherten Wert heran?

  • Im ersten Schritt allozieren wir uns als normaler Benutzer einen Speicherbereich A mit der Adresse 0.
  • Dann lassen wir einen Befehl spekulativ ausführen, welcher die Adresse 12 einliest. Wir wissen zwar noch nicht, dass dort die Zahl 1 gespeichert ist, aber wir nehmen das zum Verständnis einfach mal an. Das Ergebnis des Lesezugriffs (auf Adresse 12) soll nun an die Adresse A + B geschrieben werden (also an die Stelle 0 + 1 = 1).
  • Jetzt lesen wir nacheinander die uns erreichbaren Stellen 0 bis 11 aus und messen die Zeiten, die das System für den Lesezugriff braucht.
    Bei Adresse 0 ist der Zugriff langsam (z. B. 450ms).
    Bei Adresse 1 ist der Zugriff besonders schnell (z. B. 200ms).
    Bei Adresse 2 ist der Zugriff wieder langsam (z. B. 430ms)
    Usw.

Wir haben über den Umweg der Zeitmessung die Tatsache entdeckt, dass die Adresse 1 besonders schnell ausgelesen werden konnte. Und hier wird die Schönheit von Meltdown offenbar: Dadurch, dass wir bei Adresse 1 einen besonders schnellen Zugriff sehen, wissen wir, dass dieser Wert nicht erst wie die anderen Adressen berechnet/geholt werden musste, sondern dass dieser Wert sich bereits im Zwischenspeicher des Prozessors befunden haben muss. Anders ist diese besonders schnelle Lesezeit nicht möglich.

Das Elegante dabei ist, dass wir das Ergebnis an die Adresse A+B haben schreiben lassen. Da wir A ohnehin kennen, können wir nun direkt anhand des Abstands ablesen, welcher Wert bei Adresse 12 gestanden haben muss. Wir betrachten nämlich nun die Entfernung zwischen Adresse 0 und der Adresse die sich besonders schnell auslesen lässt (hier: 1) und wissen nun, dass der Wert bei Adresse 12 ebenfalls 1 ist.

  • Nachdem wir nun den Wert von Adresse 12 ermittelt haben, können wir im nächsten Durchgang einen Befehl spekulativ ausführen lassen, der die nächste Adresse 13 einliest usw.

In dieser Form lässt sich der gesamte (!) Kernel-Space komplett auslesen. Aus diesem Grunde wird Meltdown oft als eine der größten je gefunden Schwachstellen in Prozessorarchitekturen genannt.

6. Quellen & Weiterlesen

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *