Hacker: Warum Raspberry Pi sicher vor Meltdown und Spectre ist

Autor: Margit Kuther

Spectre und Meltdown attackieren moderne Intel-Prozessoren und – im Fall von Spectre – viele AMD-Prozessoren und ARM-Cores. Nicht aber Raspberry Pi. Raspberry-Pi-Entwickler Eben Upton verrät, warum.

Firmen zum Thema

Sicher: Spectre und Meltdown können Raspberry Pi nicht schaden.
Sicher: Spectre und Meltdown können Raspberry Pi nicht schaden.
(Bild: gemeinfrei, Pixabay / CC0 )

Mit Spectre kann ein Angreifer Softwareüberprüfungen umgehen, um Daten von beliebigen Speicherorten im aktuellen Adressraum zu lesen.

Meltdown ermöglicht es einem Angreifer, Daten von beliebigen Speicherorten im Adressraum des Betriebssystemkerns zu lesen, auf die Benutzerprogramme normalerweise keinen Zugriff haben.

Bildergalerie
Bildergalerie mit 8 Bildern

Beide Sicherheitslücken nutzen Technologien moderner Prozessoren, die diesen einen Geschwindigkeitsvorteil verschaffen. Dazu zählt etwa vorausschauendes Laden von Speicherinhalten in den schnelleren Cache. Allerdings hat dies den Nachteil, dass Daten über einen so genannten Seitenkanalangriff (Side-Channel-Attack) unerlaubterweise ausgelesen werden können.

„Glücklicherweise ist der Raspberry Pi aufgrund der speziellen ARM-Kerne (siehe Bildergalerie), die wir verwenden, nicht anfällig für diese Sicherheitsanfälligkeiten“, so Eben Upton, einer der Initiatoren des Raspberry Pis.

Warum das so ist, veranschaulicht Eben Upton in folgender Python-Syntax und gibt gleichzeitig einen Einblick in die allgemeine Prozessortechnologie:

t = a + b

u = c + d

v = e + f

w = v + g

x = h + i

y = j + k

Während der Prozessor im Computer Python nicht direkt ausführt, sind die Anweisungen hier so einfach, dass sie in etwa einem einzelnen Maschinenbefehl entsprechen. Wir werden einige Details (insbesondere das Pipelining und das Umbenennen von Registern), die für Prozessor-Designer sehr wichtig sind, aber die nicht notwendig sind, um zu verstehen, wie Spectre und Meltdown funktionieren, unter den Teppich kehren.

Skalar- und Superskalarprozessor

Die einfachste Art eines modernen Prozessors führt einen Befehl pro Zyklus aus; Wir nennen dies einen Skalarprozessor. Unser Beispiel oben wird in sechs Zyklen auf einem Skalarprozessor ausgeführt. Beispiele für Skalarprozessoren sind der Intel 486 und der ARM1176-Kern, der in Raspberry Pi 1 und Raspberry Pi Zero verwendet wird.

Die offensichtliche Möglichkeit, einen Skalarprozessor (oder einen Prozessor) schneller laufen zu lassen, ist die Erhöhung der Taktfrequenz. Wir erreichen jedoch bald Grenzen dafür, wie schnell die Logikgatter innerhalb des Prozessors zum Laufen gebracht werden können; Prozessor-Entwickler begannen daher nach Möglichkeiten zu suchen, mehrere Dinge gleichzeitig zu tun.

Ein superskalarer Prozessor untersucht in der Reihenfolge den eingehenden Befehlsstrom und versucht, mehr als eines gleichzeitig in einer von mehreren Pipelines (kurz Pipes) auszuführen, abhängig von den Abhängigkeiten zwischen den Anweisungen. Abhängigkeiten sind wichtig: Sie könnten denken, dass ein Zwei-Wege-Superskalar-Prozessor die sechs Anweisungen in unserem Beispiel einfach so paaren (oder zweifach ausgeben) kann:

t, u = a + b, c + d

v, w = e + f, v + g

x, y = h + i, j + k

Aber das macht keinen Sinn: Wir müssen v berechnen, bevor wir w berechnen können, damit die dritte und vierte Anweisung nicht gleichzeitig ausgeführt werden können. Unser Zwei-Wege-Superskalar-Prozessor wird tatsächlich nichts finden, um mit der dritten Anweisung zu koppeln, also wird unser Beispiel in vier Zyklen ausgeführt:

t, u = a + b, c + d

v = e + f # zweite Pipe tut hier nichts

w, x = v + g, h + i

y = j + k

Beispiele für superskalare Prozessoren sind der Intel Pentium und die ARM Cortex-A7- und Cortex-A53-Kerne, die in Raspberry Pi 2 bzw. Raspberry Pi 3 verwendet werden. Raspberry Pi 3 hat nur eine 33% höhere Taktrate als Raspberry Pi 2, aber hat ungefähr die doppelte Leistung: Die zusätzliche Leistung ist teilweise eine Folge der Fähigkeit von Cortex-A53, eine breitere Palette von Anweisungen als Cortex-A7 Dual-Ausgabe.

Was ist ein Out-of-Order-Prozessor?

Wenn wir zurück zu unserem Beispiel gehen, können wir sehen, dass, obwohl wir eine Abhängigkeit zwischen v und w haben, wir später im Programm andere unabhängige Anweisungen haben, die wir möglicherweise benutzt haben, um die leere Pipe während des zweiten Zyklus zu füllen. Ein außer Betrieb befindlicher superskalarer Prozessor hat die Fähigkeit, die Reihenfolge eingehender Anweisungen zu mischen (wiederum abhängig von Abhängigkeiten), um seine Leitungen beschäftigt zu halten.

Ein Out-of-Order-Prozessor könnte die Definitionen von w und x in unserem Beispiel folgendermaßen ersetzen:

t = a + b

u = c + d

v = e + f

x = h + i

w = v + g

y = j + k

erlaubt es in drei Zyklen auszuführen:

t, u = a + b, c + d

v, x = e + f, h + i

w, y = v + g, j + k

Beispiele für Out-of-Order-Prozessoren sind der Intel Pentium 2 (und die meisten nachfolgenden Intel- und AMD x86-Prozessoren mit Ausnahme einiger Atom- und Quark-Geräte) und viele neue ARM-Cores, darunter Cortex-A9, -A15, -A17, und -A57.

Was ist eine Verzweigungsvorhersage?

Unser Beispiel oben ist ein geradliniger Code. Echte Programme sind natürlich nicht so: Sie enthalten auch sowohl Vorwärtszweige (die zum Implementieren bedingter Operationen wie if-Anweisungen verwendet werden) als auch Rückwärtszweige (die zum Implementieren von Schleifen verwendet werden). Eine Verzweigung kann unbedingt (immer genommen) oder bedingt (abhängig von einem berechneten Wert) sein; es kann direkt sein (explizite Angabe einer Zieladresse) oder indirekt (wobei seine Zieladresse von einem Register, einer Speicherstelle oder dem Prozessorstapel genommen wird).

Während des Abrufens von Anweisungen kann ein Prozessor auf eine bedingte Verzweigung treffen, die von einem Wert abhängt, der noch zu berechnen ist. Um einen Stillstand zu vermeiden, muss er vorhersehen, welcher Befehl als nächstes zu holen ist: der nächste in der Speicherreihenfolge (entsprechend einem nicht genommenen Zweig) oder der am Verzweigungsziel (entsprechend einem genommenen Zweig). Ein Verzweigungsprädiktor hilft dem Prozessor, intelligent abzuschätzen, ob eine Verzweigung genommen wird oder nicht. Dies geschieht durch das Sammeln von Statistiken darüber, wie oft bestimmte Zweige in der Vergangenheit genommen wurden.

Moderne Branch Prädiktoren sind äußerst anspruchsvoll und können sehr genaue Vorhersagen generieren. Die zusätzliche Leistung von Raspberry Pi 3 ist teilweise eine Folge von Verbesserungen in der Verzweigungsvorhersage zwischen Cortex-A7 und Cortex-A53. Durch die Ausführung einer selbstgemachten Abfolge von Zweigen kann ein Angreifer einen Verzweigungsvorhersager jedoch falsch trainieren, um schlechte Vorhersagen zu treffen.

Warum vorausschauende Zugriffe so gefährlich sind

Das Umordnen sequentieller Anweisungen ist ein mächtiger Weg, um mehr Parallelität auf Befehlsebene wiederherzustellen, aber wenn die Prozessoren breiter werden (in der Lage, Befehle dreimal oder vierfach auszugeben), wird es schwieriger, all diese Pipes beschäftigt zu halten. Moderne Prozessoren haben daher die Fähigkeit ausgebildet, zu spekulieren. Durch die spekulative Ausführung können wir Anweisungen ausgeben, die sich als nicht erforderlich erweisen (weil sie verzweigt werden können): Dadurch bleibt eine Pipe beschäftigt (benutzen Sie sie oder verlieren Sie sie!), Und wenn sich herausstellt, dass die Anweisung nicht ausgeführt wird – können wir das Ergebnis einfach fallen lassen.

Das Ausführen unnötiger Anweisungen (und der Infrastruktur, die zur Unterstützung von vorausschauenden Zugriffen und Neuanordnungen erforderlich ist) verbraucht zusätzliche Energie. In vielen Fällen wird dies jedoch als lohnenswerte Abwägung betrachtet, um zusätzliche Single-Thread-Leistung zu erzielen. Der Verzweigungsprädiktor wird verwendet, um den wahrscheinlichsten Pfad durch das Programm zu wählen, wodurch die Wahrscheinlichkeit maximiert wird, so dass sich vorausschauende Zugriffe auszahlen.

Um die Vorteile der vorausschauenden Zugriffe zu demonstrieren, schauen wir uns ein anderes Beispiel an:

t = a + b

u = t + c

v = u + d

wenn v:

w = e + f

x = w + g

y = x + h

Jetzt haben wir Abhängigkeiten von t zu u zu v und von w zu x zu y, so dass ein Zwei-Wege-Out-of-Order-Prozessor ohne Spekulation seine zweite Pipe niemals füllen kann. Er gibt drei Zyklen aus, um t, u und v zu berechnen, nach denen es weiß, ob der Block der if-Anweisung ausgeführt wird, in welchem ​​Fall es dann drei Zyklen aufwendet, um w, x und y zu berechnen. Unter der Annahme, dass if (implementiert durch einen Verzweigungsbefehl) einen Zyklus benötigt, nimmt unser Beispiel entweder vier Zyklen (wenn v zu Null wird) oder sieben Zyklen (wenn v nicht null ist) an.

Wenn der Zweigprädiktor anzeigt, dass der Körper der if-Anweisung wahrscheinlich ausgeführt wird, mischt ein vorausschauendes Lesen das Programm folgendermaßen:

t = a + b

u = t + c

v = u + d

w_ = e + f

x_ = w_ + g

y_ = x_ + h

wenn v:

w, x, y = w_, x_, y_

So haben wir jetzt zusätzliche Parallelität auf Befehlsebene, um unsere Pipes beschäftigt zu halten:

t, w_ = a + b, e + f

u, x_ = t + c, w_ + g

v, y_ = u + d, x_ + h

wenn v:

w, x, y = w_, x_, y_

Die Zykluszählung wird in spekulativen Out-of-Order-Prozessoren weniger gut definiert, aber die Verzweigung und die bedingte Aktualisierung von w, x und y sind (ungefähr) frei, so dass unser Beispiel in (ungefähr) drei Zyklen ausgeführt wird.

Was ist ein Cache?

In früheren Zeiten war die Geschwindigkeit der Prozessoren gut mit der Geschwindigkeit des Speicherzugriffs abgestimmt. Mein BBC Micro mit seinem 2 MHz 6502 konnte ungefähr alle 2 μs (Mikrosekunden) einen Befehl ausführen und hatte eine Speicherzykluszeit von 0,25 μs. In den folgenden 35 Jahren sind die Prozessoren sehr viel schneller geworden, aber Speicher ist bescheiden: Ein einzelnes Cortex-A53 in einem Raspberry Pi 3 kann einen Befehl ungefähr alle 0,5 ns (Nanosekunden) ausführen, aber es kann bis zu 100 ns dauern, um Zugriff auf den Hauptspeicher zu erhalten.

Auf den ersten Blick klingt das wie eine Katastrophe: Jedes Mal, wenn wir auf Speicher zugreifen, warten wir bis zu 100 ns, um das Ergebnis zurück zu bekommen. In diesem Fall, dieses Beispiel:

a = mem [0]

b = mem [1]

würde 200 ns kosten.

In der Praxis tendieren Programme jedoch dazu, auf relativ vorhersehbare Weise auf den Speicher zuzugreifen, wobei sowohl zeitliche Lokalität (wenn ich auf einen Speicherplatz zugreife, werde ich wahrscheinlich bald wieder darauf zugreifen) als auch räumliche Lokalität (wenn ich auf einen Speicherplatz zugreife, greife ich in Kürze wahrscheinlich wahrscheinlich auch auf einen Speicherplatz in der Nähe zu). Caching nutzt diese Eigenschaften für den vorausschauenden Speicherzugriff.

Ein Cache ist ein kleiner On-Chip-Speicher in der Nähe des Prozessors, der Kopien der Inhalte kürzlich verwendeter Speicherorte (und ihrer Nachbarn) speichert, so dass sie bei nachfolgenden Zugriffen schnell verfügbar sind. Beim Caching wird das obige Beispiel in etwas über 100 ns ausgeführt:

a = mem [0] # 100ns Verzögerung, kopiert mem [0:15] in den Cache

b = mem [1] # mem [1] befindet sich im Cache

Aus der Sicht von Spectre und Meltdown ist es wichtig, dass Sie, wenn Sie die Speicherdauer eines Speichers zeitlich bestimmen können, feststellen können, ob die Adresse, auf die Sie zugegriffen haben, im Cache lag (kurze Zeit) oder nicht (lange Zeit).

Was ist eine Seitenkanalattacke?

"Quelle Wikipedia... Ein Seitenkanalangriff, bezeichnet eine kryptoanalytische Methode, die die physische Implementierung eines Kryptosystems in einem Gerät (z. B. einer Chipkarte, eines Security-Tokens oder eines Hardware-Sicherheitsmoduls) oder in einer Software ausnutzt. Dabei wird nicht das kryptographische Verfahren selbst, sondern nur eine bestimmte Implementierung angegriffen, d. h. andere Implementierungen können von dem Angriff unberührt bleiben. Zum Beispiel können Zeitinformationen, Stromverbrauch, elektromagnetische Lecks oder sogar Schall eine zusätzliche Informationsquelle darstellen, die ausgenutzt werden kann, um das System zu durchbrechen. "

Spectre und Meltdown sind Side-Channel-Attacken, die den Inhalt eines Speicherplatzes auslesen, auf den normalerweise nicht zugegriffen werden könnte. Dabei nutzen sie den Zeitraum, indem überprüft wird, ob ein anderer zugreifbarer Ort im Cache vorhanden ist.

Meltdown-Attacke auf den Prozessor

Betrachten wir nun, wie vorausschauendes Lesen und Caching einen Meltdown-artigen Angriff auf unseren Prozessor ermöglichen. Betrachten Sie das folgende Beispiel, bei dem es sich um ein Benutzerprogramm handelt, das manchmal von einer ungültigen (Kernel-) Adresse liest, was zu einem Fehler (Absturz) führt:

t = a + b

u = t + c

v = u + d

wenn v:

w = kern_mem [Adresse] # wenn wir hier sind, Fehler

x = w & 0x100

y = user_mem [x]

Jetzt, vorausgesetzt, wir können den Verzweigungs-Prädiktor so trainieren, dass wir glauben, dass v wahrscheinlich nicht Null ist, mischt unser außer der Reihe liegender Zweiweg-Superskalar-Prozessor das Programm folgendermaßen:

t, w_ = a + b, kern_mem [Adresse]

u, x_ = t + c, w_ & 0x100

v, y_ = u + d, user_mem [x_]

wenn v:

# Fehler

w, x, y = w_, x_, y_ # wir kommen nie hierher

Obwohl der Prozessor immer spekulativ von der Kerneladresse liest, muss er den resultierenden Fehler verschieben, bis er weiß, dass v nicht Null ist. Auf den ersten Blick scheint dies sicher zu sein, weil entweder:

v ist Null, so dass das Ergebnis des illegalen Lesens nicht auf w übergeben wird

v ist nicht Null, aber der Fehler tritt auf, bevor das Lesen an w übergeben wird

Nehmen wir jedoch an, wir leeren unseren Cache vor der Ausführung des Codes und ordnen a, b, c und d so an, dass v tatsächlich Null ist. Nun, das spekulative Lesen im dritten Zyklus:

v, y_ = u + d, user_mem [x_]

wird entweder auf die Benutzerlandadresse 0x000 oder die Adresse 0x100 zugreifen, abhängig von dem achten Bit des Ergebnisses des illegalen Lesens, wobei diese Adresse und ihre Nachbarn in den Cache geladen werden. Da v null ist, werden die Ergebnisse der spekulativen Anweisungen verworfen, und die Ausführung wird fortgesetzt. Wenn wir einen nachfolgenden Zugriff auf eine dieser Adressen zeitlich festlegen, können wir ermitteln, welche Adresse sich im Cache befindet. Herzlichen Glückwunsch: Sie haben gerade ein Bit aus dem Adressraum des Kernels gelesen!

Der echte Meltdown-Nutzen ist wesentlich komplexer als dieser (insbesondere, um zu vermeiden, den Verzweigungsprädiktor falsch zu trainieren, wird bevorzugt, das illegale Lesen bedingungslos auszuführen und die resultierende Ausnahme zu handhaben), aber das Prinzip ist das gleiche. Spectre verwendet einen ähnlichen Ansatz, um Software-Array-Grenzen-Prüfungen zu unterlaufen.

Fazit: Abstraktion und Realität moderner Prozessoren

Moderne Prozessoren tun viel, um die Abstraktion zu bewahren, dass sie Skalarmaschinen sind, die direkt auf den Speicher zugreifen, während sie tatsächlich eine Vielzahl von Techniken verwenden, darunter Caching, Neuordnung von Befehlen und vorausschauende Zugriffe, die viel mehr Leistung bieten als ein einfacher Prozessor erreicht. Meltdown und Spectre sind Beispiele dafür, was passiert, wenn wir im Kontext dieser Abstraktion über Sicherheit sprechen und dann auf kleine Diskrepanzen zwischen Abstraktion und Realität treffen.

Fazit: Das Fehlen von vorausschauenden Zugriffen in den ARM1176-, Cortex-A7- und Cortex-A53-Kernen, die im Raspberry Pi verwendet werden, machen die Raspberry Pis gegen Angriffe wie Meltdown und Spectre immun.

Die Ausführungen von Eben Upton finden Sie auch auf der Seite von raspberrypi.org.

(ID:45113225)

Über den Autor

 Margit Kuther

Margit Kuther

Redakteur, ELEKTRONIKPRAXIS - Wissen. Impulse. Kontakte.