 Hallo, ihr hört den Vortrag Inside the AMD Microcode ROM in der Übersetzung von Stefan und Albin. Danke. Wie gesagt, wir waren in der Lage, den Microcode des AMD Prozessors zurückzuentwickeln, reverse zu engenieren und wir wollen euch erzählen, wie wir das gemacht haben. Hier ist eine kleine Übersicht, wie genau funktioniert ein Microcode, wie haben wir das geschafft, das auseinanderzunehmen und ein paar Beispiele, was wir alles so gelernt haben dabei. Und ich werde erklären, welches Frameboard wir verwendet haben, um das durchzuführen und man findet das auf GitHub und wie ihr damit arbeiten könnt. Microcode ist sowas wie eine Firmware für den Prozessor. Man kann das benutzen, um CPU-Box zu fixen, welche, die man sonst nur durch die Systemänderungen schaffen könnte. Es wird benutzt für Instruktionsdecoding, für die Fehlerbehandlungen, für Interrupts und der Code wird ausgeführt, bevor das Betriebssystem an die Reihe kommt. Es wird auch für Power Management benutzt und auch für andere Features. Microcode Updates werden benutzt, um Sicherheitslücken zu fixen, wie zum Beispiel Spectre oder Meltdown. In ein X86 CPU gibt es mehrere Schritte für eine Instruktion. Als erstes dekodieren wir sie, da wird es in wenige kleinere Microcodes übersetzt und dann wird es weitergeleitet und in die verschiedene Einheiten führen das dann aus. Der dekodierstep ist der interessanteste, der wird die Instruktion zu verschiedenen Decodern weitergegeben und dann wird das weitergegeben und es kommt dann irgendwann zum Vectordecoder und die Microcode-Einheit besteht aus einem Rom, in dem der Microcode gespeichert ist. Es gibt auch ein beschreibbaren Speicher, das RAM, da kommen die Microcode Updates mit rein und natürlich gibt es dazu eine ganze Reihe Sachen, die den Code eigentlich ausführen. Da gibt es entsprechende Breakpoint Register und wenn man ein solches Match Register entsprechend befüllt, dann wird der Befehl aus dem RAM und nicht aus dem Rom geholt. Microcodes Updates werden üblicherweise über das BIOS geladen oder durch den Kernel, die sind relativ simpel in der Struktur, sie haben einen Header und werden dann von den tatsächlichen Microcode Updates gefolgt. Diese Triaden bestehen aus drei Operationen, es sind sehr x86 Instruktionen und am Ende kommen wir zu einem Sequence Word und dort steht drin, was als nächstes ausgeführt werden soll, eine neue Triade oder etwas anderes und dann heißt es einfach, führe diese Operation aus und führe danach diesen x86 Code aus. Updates werden authentiziert und wir können das aber uns anschauen, aber leider kann immer nur ein Update gleichzeitig geladen werden können. Wenn man den Computer neu startet, dann ist das Update wieder weg. Für diesen Vortrag schauen wir auch an etwas Microcode angeführt. Hier ist es bei x86 und ich schaue einfach mal die Unterschiede an. Der Microcode kann bis zu drei Operanten haben, während x86 maximal zwei hat. Also man kann zwei Quellen und zwei Einzieloperanten haben. Der Microcode hat auch bestimmte Bitflex, die behandelt werden müssen und je nachdem, was in der Instruktion gesetzt ist, wird zum Beispiel das Carry-Flag abgedatet oder nicht. Der erste Operant zeigt hier, was für einen Vergleich ausgeführt werden soll und der zweite Operant zeigt den Offset an, zu dem gesprungen werden soll. Es gibt zusätzlich eine Sequenz-Annotation, die interne Microcode-Architektur ist Load-Star und das bedeutet, dass man Speicherzugriffe immer explizit mit angeben muss. Es ist also keine Register gibt. Wie kann man jetzt den Microcode rekonstruieren? Das Ganze ist ein Rom, es ist hart in CPU-Transistor und anderen Strukturen abgelegt und das wird während der Herstellung des Chips entsprechend hart in den Chip eingebracht. Dieses Bild ist ein Elektronen-Mikroskop-Bild und man kann hier erkennen, wo die Bits gespeichert sind. Diese Bereiche bestehen aus vier einzelnen Unterbereichen und das Interessante hier ist das RA2, das bestimmte Strukturen über sich hat und das ist das statische RAM, in dem die Updates gespeichert werden. Das RAM ist also hier in dem Bild über dem Rom zu sehen und das macht auch total Sinn, weil die beiden natürlich genau gleich schnell abrufbar sein müssen. Wir haben also das Bild genommen und haben daraus die Bits des Roms rekonstruiert und wir haben die dann gleich in die Triaden zerlegt und die Abhängigkeiten zwischen den Triaden und die Reihenfolge der Triaden war nicht linear, sondern da gab es Abhängigkeiten, die wir nicht unmittelbar erkennen konnten und wir haben dann es geschafft, indem wir den Microcode ausgelesen haben über die CPU selber und das mit unseren Daten verglichen haben, um dann zu ermitteln, wie die Anordnung tatsächlich ist. Das war allerdings relativ schwierig, weil wir die Umsetzung zwischen physikalischen und virtuellen Adressen finden mussten und da die Updates jeweils immer nur zwei Paare definieren, ist es sehr schwer, die entsprechende Triade zu identifizieren. Wir mussten aber noch mehr Datenpunkte kriegen kommen, aber das haben wir gemacht, in die wir die Semantik, dass der Triaden uns angeschaut haben und mit den Semantiken haben wir einen Microcode Emulator geschrieben, der dann die Semantik extrahiert. Das funktioniert auf dem Triaden, der auf Triaden Ebene und es berechnet den Ausgabezustand gegeben eines gewissen Eingabezustand, gegeben x86 und Microcode Register, die x86 Register können von den Instruktionen modifiziert werden, aber natürlich auch von Microcode. Das hat natürlich ein paar arithmetische Operationen, aber es gibt auch ein paar, die nicht so. Insgesamt haben wir 54 neue Datenpunkte bekommen, damit haben wir das komplette Mapping herausbekommen. Wir haben hier vier verschiedene Arrays, die auf diese Art und Weise rüber mapen und die Triaden innerhalb der Blöcke haben diese Permutation. Das ist keine Verschleierung, sondern das ist einfach, weil es vom Hardware-Design her Sinn ergeben kann. Außerdem können wir bestimmte Adressen dem ROM Readout zuweisen und wir können tatsächlich die Instruktionsimplementierung anschauen. Schauen wir uns zunächst eine Instruktion an und die Schiftet, es gibt hier eine Rechtsschiebe-Operation und die hat entweder einen Rack MD6 oder Rack MD4 haben, das sind Platzhalter und das hier könnte zum Beispiel dieses Rack MD4, könnte zum Beispiel durch IX oder IDX ersetzt werden. Hier können wir noch mehr Informationen über den Mikrokod herausfinden. Das hier ist eine Quelle, das hier ist eine Quelle, das ist ein Ziel und das hier ist eine Quelle, was die Verschiebe-Wert herausfindet. Das ist ein Mikrokodregister und das sind verschiedene, die haben verschiedene Zwecke. Manchmal, wenn man da hinschreibt, crashed es manchmal, macht das gar nichts und so weiter. Aber in dem Fall ist es das Schift der Wert und der Schiftwert ist durch eine andere Instruktion gegeben. Also falls wir die Quelle-Operationen lesen wollen, müssen wir T41 lesen. Der Rest der Implementierung beschäftigt sich jetzt wirklich mit der spezifischen Semantik dieses X86 Befehls. Lass uns einen Befehl angucken, der ein bisschen interessanter ist. Dieser Befehl gibt den internen Cycle-Counter in zwei X86 Registern zurück und die Daten müssen aber eingesammelt werden. Hier gibt es eine Lado-Operation. Der Befehl hier oben, den kennen wir noch nicht. Wir wissen aber, dass das Ergebnis in T9 landet und dass dann der zweite Wert in T10 landet. Und was wir später herausgefunden haben, diese Bits hier bestimmen, dass es ein spezifisches internes Mikrokodregister geladen wird. Und genau diese drei Bits bedeuten, dass das interne Register CR4 adressiert wird. Und wenn man weiter guckt, dann sehen wir, dass T10 erweitert wird, um eine zusätzliche Bitmaske. Und dieses Bit, um das es hier geht, das entscheidet genau, ob CR4 aus dem User-Mode des Prozessors zugänglich sein soll oder nicht. Und damit ist dann klar, dass T9 einen anderen Wert aus einem anderen internen Register enthält. Da kommen wir später noch mal drauf. Hier unten sehen wir ein typisches Muster für die Implementierung. Und hier haben wir einen bedingten Sprung. Und wenn wir hier oben gucken und dann sehen wir, dass da ein Fleck aktualisiert wurde. Und hier sehen wir einen bedingten Sprung, dass der genau auf dieses Bit referenziert, auf dieses Prüfbild. Und hier sehen wir den Ende, das Ende der Mikrokodausführung. Und hier wird der Wert nach links geschoben. Und das ist genau das, was man erwarten würde, wenn dieser Timestamp in die oberen 32-Bittes-Registers geschrieben werden soll. Und dann sehen wir, die niedligen 32-Bitt werden dann in RX geschrieben. So, und wir können uns nochmal angucken, was passiert, wenn der Befehl aufgerufen wird, ohne dass die Berechtigung besteht, also die Federbehandlung. Und dann sehen wir, dass dieser wird ein General Protection Fault kodiert, der dann in das Register T19 geschrieben wird. Und etwas später sehen wir dann einen Sprung zu dieser Adresse. Und wenn wir gucken, wer das noch aufruft, dann sehen wir, dass da auch Interrats mit ausgelöst werden. Und damit können wir also Mikrokod schreiben, der eine eigene Interrupte auslöst. Jetzt haben wir eine Menge darüber gelernt, wie wir Mikrokod schreiben können, aber es ist auch interessant zu sehen, wie Instruktion implementiert ist. Und jetzt schauen wir uns eine Komplizite an. Es ist ein Befehl, der zu einem bestimmten Adresse schreibt. Das wird benutzt um Extensions und so weiter zu implementieren gemacht, zum Beispiel für Mikrokods, Updates und so weiter. Hier ist die Adresse in hcx. Und wir sehen, hcx wird gelesen. Es wird nach links geschoben und in t10 geschrieben. Wir schauen an, wo t10 gelesen wird. Es wird xOrt mit 400000. Das ist ein Namespace von einem modulspezifischen Adresse. Und das setzt wieder ein paar Flex. Hier sehen wir einen Sprung, einen konditionellen. Und wir sehen, wir springen zum Handler für diesen Namespace. Hier unten haben wir noch ein weiteres xOrt mit einer anderen Bitmask. Das ist 001. Das ist die Position, wo die Update Routine drin ist. Und wir gehen wieder zu dem Handler für diese Mikrokod. Hier ist mehr mit rcx, mehr Branches. Das macht weiter, bis alles beim richtigen Handler angekommen ist. Und das ist, wie das alles implementiert ist. Und das wird auch auf ähnlichen Systemen ähnlich implementiert sein. Jetzt zeige ich euch, wie wir das tatsächlich zur Anwendung gebracht haben. Was man damit machen kann. Also, wir haben einen relativ einfachen Timecode. Wir haben den Timecode-Counter in der Genauigkeit reduziert, was manchmal hilfreich sein kann. Und hier sind noch zwei weitere Beispiele, zu denen ich gleich noch komme. Das bedeutet Microcode Assisted Instrumentation bedeutet, dass man einen Filter schreiben kann für den Microcode selbst. Und statt den zu emulieren oder zu debaggen, dann kann man x86 Code aufrufen, wenn bestimmter Microcode ausgeführt wird. Wir haben auch implementiert, dass Microcode Updates authentifiziert werden. Der Standardmechanismus ist ziemlich schwach und das haben wir ein bisschen verbessert. Und Microcode hat eine entlarven Artikel Ausführung zum Gebung. Und da jede Statusänderung, die von außen sichtbar sein soll, explizit ausgeführt werden muss, kann man deswegen eine relativ einfache Entlave entwickeln, weil von außen der Microcode nicht unterbrochen werden kann. Das erste Beispiel ist ein Adressreiniger, der sicherstellt, dass ungültige Speicherzugriffe richtig behandelt werden. Die Autoren der ursprünglichen Idee haben Hardware vorgeschlagen, um das umzusetzen. Und diese ausgeführten Befehle sollten dann ein entsprechenden Fault, ein Exception werfen, wenn auf ungültigen Speicher zugegriffen wird. Und was diese Operation durchführt, wenn man das in Hardware umsetzt, dann hat man bessere Performance. Das funktioniert logischerweise besser als wenn man dafür x86code schreibt. Der eigene Code wird natürlich kleiner, wenn man das mit einem Befehl ausführen kann. Wir haben diesen Hardware-Adress Sanitizer umgesetzt mit einer Instruktion umgesetzt, namens Bound, die von normalen Compilern heutzutage nicht mehr verwendet wird. Und wir haben das Interface dieser Funktion, dieses Befehls verändert mit dem Register, das die Adresse enthält und dann die Größe des Zugriffs, also Wort oder Beid oder ähnliches. Wenn die Instruktion aufgerufen wird und alles in Ordnung ist, dann verhält es sich wie Norb, also tut gar nichts. Das Programm läuft einfach weiter. Und wenn ein Fehler auftritt, wenn Zugriff erfolgt, der nicht erlaubt ist, dann kann man konfigurieren, was dann passieren soll und das kann zum Beispiel sein, dass man bestimmte x86code aufruft, der dann weitere Prüfungen zum Beispiel durchführen kann. Das ist eine einzelne Instruktion, die alle Register bleiben komplett erhalten und wenn man also normalerweise Zwischenergebnisse irgendwo speichern muss, dann braucht man halt Register dafür und die x86 Register werde ich hier nicht benötigt. Und das Interessante ist, wenn wir das tatsächlich mit dem Mikrocode machen, dann ist das schneller, als wenn man dieselbe Logik als x86 Befehle implementiert. Und natürlich kriegt man bessere Cash-Auslastung dazu, weil das Ganze sehr viel kompakter ist. Und zusätzlich ist es sehr viel einfacher zu unterscheiden, was ist Testcode und was ist Produktionscode. Als Letztes zeige ich euch noch unsere Framework, die wir für die Entwicklung benutzt haben, die ist auf GitHub. Am Anfang haben wir herausgefunden, dass wir eine Menge Mikrocode-Updates testen müssen. Am Anfang haben wir einfach ganz viele Sachen auf die CPU geworfen und geschaut, was passiert. Also haben wir ein kleines Betriebssystem geschrieben, Angry OS, Fitness OS und haben das einfach mal darauf getestet. All diese Mainboards, die wir benutzt haben, wurden über ein Raspberry Pi gesteuert, über GPIO Pins und damit haben wir die von der Ferne kontrolliert. Dann haben wir das über das Internet gesteuert. In der ersten Version wussten wir nicht so viel über Elektronik, also haben wir einen Raspberry Pi pro Mainboard benutzt. Die Raspberry Pi es waren teurer als die Mainboards. Und jetzt haben wir das verbessert und wir haben bloß noch einen Raspberry Pi für fünf Aufbau. Jetzt brauchen wir bloß noch drei GPIO Ports. Man verbindet das zu Octocopperlern und dann sehen wir, wie wir die Octocopper zum Reset Pin und anderen Dingen verwenden. Unter anderem der PowerLED. Damit kann man eine Menge Platz und Geld sparen. Wenn man wirklich keinen Platz hat, dann kann man natürlich zum Beispiel die PowerLED einfach weglassen. Wir haben unser eigenes, kleines, minimales Betriebssystem geschrieben. Das Entscheidende, was wir haben wollten, war 100% die Kontrolle über die Ausführungsumgebung. Damit wird tatsächlich die ausgeführten Instruktionen kontrollieren können. Wir könnten die CPU selber crashen und da wollten wir natürlich volle Kontrolle haben. Das Betriebssystem hört einfach nur auf den seriellen Port und nimmt darüber Updates entgegen für Testcode aus. Man kann direkt x86 Code ausführen über diese Schnittstelle und man braucht dann zum Beispiel nicht ein USB Stick abzudaten und natürlich die Fehler werden zum Raspberry Pi zurück über die Schnittstelle reported, um es auszuwerten. Das Framework hat einen Microcode Assembler und einen Disassembler, der das sehr ausführlich darstellt und damit kann man seinen eigenen Microcode schreiben. Wir haben auch einen x86 Assembler, weil wir natürlich Testcode schreiben wollten und mit diesem Framework konnten wir die vorhandenen Updates disassemblieren und auch das ROM nach der Extraktion dann disassemblieren. Mit dem Framework können wir neue Updates erzeugen, die dann mit dem Linux Update Driver auch geladen werden können. Der Linux Driver erlaubt das Microcode ohne weitere Prüfungen tatsächlich reinzuladen, egal ob die CPU überhaupt stimmt oder ähnlich ist. Unser Angry OS wird dann über serielle Schnittstelle und GPIO kontrolliert. Mit einem kleinen Rapper kann man das Ganze auch remote ausführen. Zusammengefasst mit dem disassemblieren des ROMs ergeben sich viele neue Möglichkeiten und es gibt sehr viel mehr Möglichkeiten als wir hier heute zeigen konnten und man kann mehr machen als nur einfach irgendwelche zufälligen Bitstrings an die CPU zu schicken. Das Framework, unser Angry OS, die Beispiele und eine ganze Reihe weiterer Sachen stehen auf GitHub und vielen Dank und ich stehe bereit für eure Fragen. Vielen Dank. Zehn Minuten für Fragen, bitte an den Mikrofonen. Danke für den schönen Vortrag. Ein paar Fragen bezüglich des Hardware Address Sennetizers, soweit ich das verstehe. Braucht man keinen Source Code, keine Veränderung des Source Codes, weil man den Microcode einfach direkt anpassen kann? Die originale Instrumentierung basiert auf einer Compiler Erweiterung und die instrumentiert eine Shadow Map und weitere Datenstrukturen. Aber wir brauchen den Code nicht, der da zusätzlich eingefügt wird, der zusätzliche x86 Code, der da eingefügt wird. Das Ganze ist aber immer noch Source Code basiert. Das eigentliche Programm muss weiterhin angepasst werden. Ich habe es nicht gesehen, vielleicht habe ich es verpasst. Wie viel schneller ist es denn, das Hardware-Based-Ding? Wie ist es denn mit dem Address Sennetizer in Software versus den Hardware, den ihr gemacht habt? Wir haben nur einen Micro-Benchmark gemacht. An der Instrumentierung haben wir prinzipiell nichts geändert. Wir haben also tatsächlich nur den eigentlichen Aufruf der Prüfung selber in unserem Micro-Benchmark überprüft. Wir haben nur eine Implementierung gemacht und es ist eigentlich nur ein Prototyp. Danke für den Vortrag. Habt ihr irgendwelche komischen Mikrokode Implementierung gefunden? Ich meine jetzt nicht sicherheitstechnisch, sondern einfach so einfach irgendwelche komischen Sachen, die ihr nicht geglaubt habt zu sehen. Irgendwie, dass es komisch implementiert war oder so. Das Problem ist, es gibt sehr viel Micro-Code. Es gibt F000, das sind ... und die Analyse ist relativ schwierig. Ein einzelnes Bit kann schon ganz andere Sachen bewirken. Wir haben uns nur einen ganz kleinen Teil des gesamten Micro-Codes angeguckt. Es ist einfach nicht möglich, sich alles anzugucken. Und uns ist nichts Besonderes aufgefallen, aber wir würden auch gar nicht wissen, wo wir eigentlich gucken sollen. Eine Frage aus dem Internet. Welche AMD-CPU-Generation war das? Das Ganze basiert auf unserem Vortrag vom letzten Mal. Das Ganze funktioniert nur mit den CPUs, die bis ungefähr 20.000, 20.13 hergestellt wurden, also K8 bis K10. Und die neueren CPUs verwenden ordentliche Krypto, um die Mikrokode-Updates zu sichern. Und da haben wir einfach noch keinen Fuß in die Tür bekommen. Ich würde gerne wissen, wie komplex der Micro-Code-Programme sein könnten. Was ist die Komplexität von neuen Operationen, die man implementieren könnte? Also der einzige begrenzende Faktor ist die Größe des Micro-Code-Update-Rams. Auf dem K8, mit dem wir die meisten unserer Versuche durchgeführt haben, ist man auf 32.000 Triaden begrenzt und 96 Befehle. Und da gibt es noch weitere Einschränkungen, bestimmte Operationen können nur in bestimmten Slots ausgeführt werden, bestimmte Befehle für ein Automatisch, dass der nächste Triade ausgeführt wird. Eine ganze Reihe weiterer Einschränkungen. Wenn man was sehr Komplexes durchführen will, die authentifizierte Micro-Code-Update-Geschichte, die wir implementiert haben, ist das Komplexe, was wir implementiert haben. Und da waren wir sehr eingeschränkt und am Ende ist das sehr klein. Du hast gesagt, dass der Micro-Code für alle Instruktionen benutzt wird. Im Wesentlichen führen wir nicht wirklich Code in der Micro-Code-Engine aus. Die Micro-Code-Engine ist so eine Art Software-Rezept, wie am Ende die Pipelines was ausführen sollen. Und wegen der verschiedenen Branchmöglichkeiten zum Beispiel wird das nicht direkt ausgeführt, sondern man sagt den Pipelines der Pipeline und den Auswahlungseinheiten, was sie machen sollen. Wie habt ihr das Foto von das Internet-Pick-Bild der CPU genommen? Wir haben mit einem Kollegen gearbeitet, der ein Hardware-Guy ist, der Chips aus dem Gehäuse entfernt und der hat für uns das Foto gemacht. Kennt ihr die Forschung von Christopher Domes, der das Instruktions-Set von X86 erforscht hat? Ja, wir haben mit ihm gesprochen. Wir wissen, dass es da im Prinzip eine Karte, der aller Befehle gibt. Am Anfang haben wir bestimmte X86 Befehle auseinandergenommen und es gibt Teile des ROMs, die nicht von Instruktionen, von X86 Instruktionen ausgelöst wurden, sondern von anderen Quellen gespeist werden. Und diese Geschichte lässt sich nicht so einfach rekonstruieren, weil da sehr große Mengen an Coat eine Rolle spielen. Vielen Dank.