 Schwimmer und Geochäscher, Sicherheitsforscher, Securityforscher, Professor an Purdue University in Indiana und wir hören jetzt Matthias Peuer über neue Memory Corruption Angriffe. Vielen Dank für die Einführung. Vielen Dank, bitte mit dem Applaus warten. Das ist nicht nur meine Arbeit, sondern zusammen mit einigen Studenten, denen ich auch danken möchte und vielen anderen, vielen Orten, auch bei Purdue University und Berkeley und eine Ethie als Zürich. Ich möchte dann noch Nicolas Carlini, der viel der Programmierarbeit gemacht hat und das gibt mir die Gelegenheit auch noch hinzuweisen auf ein Demo, worauf ich am Ende des Vortrags zeigen werde. Und es ist nicht nur über Memory Corruption, sondern eine ganze Reise, die wir unterm haben. Und am besten kann man sagen Dr. Strangelove oder wie ich lernte den Stack Vault zu lieben. In den letzten Jahren habe ich viel gearbeitet, mit Software gearbeitet und verschiedene Arten von Verwundbarkeiten, Memory Corruption und viele schlechte Dinge, die passieren können. Und ich habe gelernt, dass wir in der Welt leben, wo alle Software, die auf unserem System läuft, unsicher ist und Low Level Sprachen wie C und C++ haben keine Speichersicherheit und haben dafür mehr Performance. Und der Programmierer ist verantwortlich, aber man verliert halt die Sicherheit, den Speicher zu schützen und hat dafür mehr Performance. Und der Programmierer ist verantwortlich für alle Überprüfungen. Und wenn diese Überprüfungen fehlen, dann kann es einfach hochgehen. Und es ist nicht nur eine große Anzahl von alten Programmen, die bei uns laufen, sondern auch ganz viele neue Programme, die gerade jetzt noch geschrieben werden in C oder C++ und haben ganz viele Probleme mit Speichern. Und selbst Firmen wie Google oder so, die sehr streng auf Coding Standards achten und selbst die haben ganz viele Verwundbarkeiten, und zum Beispiel im Google Chrome Browser. Während es wichtig ist, solche Probleme zu finden und zu fixen, müssen wir auch die Integrität unserer Systeme wahren durch zusätzliche Sicherheitsmechanismen zur Laufzeit. Und wenn man die letzten zwei Jahre oder die letzten Monate auch nur anguckt, dann gibt es ganz viele Verwundbarkeiten. Und wir suchen nach stärkeren und besseren Verteidigungsmechanismen. Und einer der bekanntesten, der natürlich hart blieb, was einfach alle Schlüssel für Verschlüssel komprimenteert hat. Und Shellshock gab es die einfach die Ausführung von Commandos erlaubt auf verschiedenen Servern. Und dann gab es welche, wo einfach, einfach Memory Corruption relativ einfach erzeugt werden konnte. Und es gibt eine Riesenmenge von Speicherunsicherheit in den Programmen, mit denen wir uns befassen müssen. Viele verschiedene Sicherheitsgarantien und Mechanismen brauchen wir. Und ich möchte jetzt über die verschiedenen Speicherunsicherheitsprobleme reden und etwas Hintergrundinformationen. Im Grunde gibt es immer, bei diesen Verwundbarkeiten gibt es immer eine ungültige, einen ungültigen Verweis, zum Beispiel einen Dingling-Pointer, ein Pointer in einer Low-Level-Sprache zeigte auf ein Objekt und dann wurde das Objekt freigegeben vom Programmierer und dann wird anschließend noch auf diesen Pointer zugegriffen. Und die andere Möglichkeit ist ein Pointer, der außerhalb des Reichs ist. Irgendwann zeigte dieser Pointer auf ein gültiges Objekt und dann während der Ausführung des Programms wurde der Pointer verschoben und zeigte außerhalb des Objekts und zeigte auf eine ungültige Memory Adresse, Speicheradresse und er wurde außerhalb der zulässigen Grenzen bewegt. Und das ist nur eine Verletzung, wenn der Pointer anschließend benutzt wird, wenn er lesen oder geschrieben wird. Wenn man Dingling-Pointer hat oder Out-of-Boins-Pointer, ist kein Problem, wenn man sie nicht benutzt, anschließend. Und das macht es auch so schwer, diese Bugs zu finden, denn es wird nur dann wirklich ein Bug, wenn man diesen Pointer, der auf ein ungültiges Objekt zeigt, tatsächlich benutzt. Und in den meisten Programmen sind ganz, ganz viele, ganz viele ungültige Pointer in den Programmen, die nicht benutzt werden. Und deswegen findet man die Probleme nicht. Und viele Angriffe basieren auf diesen Memory-Unsicherheitsproblemen. Wenn der Hacker dann versucht, bestimmten Code auszuführen oder Code einzuschleusen und anderes Verhalten auszulösen will, was nicht vorgesehen war. Oder es gibt Angriffe, wo der Hacker der Angriffe nur Daten ändert, die dann benutzt werden von dem Programm und den internen Zustand des Programms ändern, um ein bestimmtes Verhalten auszulösen. Heute beschäftigen wir uns mit der Ausführung von Code, denn das ist normalerweise das, was ein Angreifer will. Er will Code ausführen auf einem Computer, um zusätzliche Möglichkeiten zu erlangen. So wie funktioniert das? Ein Kontrollflussangriff. Im Wesentlichen auf einer abstrakten Ebene können wir ein Programm darstellen als ein Kontrollflussgrafen und die Ausführung des Programms folgt diesem Kontrollflussgrafen und geht von einem Knoten zu einem anderen Knoten und führt dabei das Programm aus. In diesem Beispiel haben wir eine kleine Schleife mit einer Bedingung und da kann der Kontrollfluss aus der Schleife rausgehen. Während das ausgeführt wird, bewegt sich die Ausführung durch diesen Kontrollflussgrafen und es bewegt sich von einem Knoten zum anderen Knoten während das Programm ausgeführt wird. Ein Angreifer zu einem bestimmten Zeitpunkt könnte möglicherweise einen Pointer verändern, zum Beispiel den Rückkehr aus einer Funktion oder indirekte Sprünge oder indirekte Aufrufe und kontrolliert damit, wie der Kontrollfluss von einem Knoten, von einem Block zu einem anderen fortschreitet. Und wenn der Angreifer das beeinflussen kann, dann kann er die Ausführung des Programms in eine andere Stelle umbiegen. Um so einen Code-Pointer umzubiegen, verlässt der Kontrollfluss den gültigen Bereich und führt neue Befehle aus, sondern einen anderen Ort im Speicher und verlässt damit den gültigen Kontrollflussgrafen. Und der Angreifer kann verschiedene Teile des Existierencodes benutzen, um zusätzlichen Code einzufügen, um Rückkehrinstruktionen zu verändern oder zu benutzen, bestehenden Code zu benutzen, um andere Sprünge damit durchzuführen und anderen Rückkehr. Also wir haben hier eine kleine, verwundbare Funktion, einige Zeilen C-Code und nehmen wir einfach mal an, dass der Attacker die beiden Parameter kontrolliert, die dieser Funktion übergeben werden, User und User 2. Wir haben außerdem einen Funktionspointer, woan das irgendwo definiert wird. Aber der Angreifer kann diese Parameter benutzen, wie hier übergeben werden, an die Funktion, um ein Teil des Funktionspointers zu überschreiben, wie wir später werden sehen. Das zugrunde liegende Problem ist, dass der Angreifer es erzwingen kann, dass hier ein Speicherzugriff außerhalb der erlaubten Grenzen stattfindet und dann durch die Referenzierung des Pointers wieder so ein Pointer verschoben und dann geschrieben und dann wird dann aufgeschrieben. Wenn wir uns den Stack oder den Speicherlaut anschauen, dann sehen wir hier die Variable in Q, den Buffer, den Funktion-Pointer und wir haben hier auch ein Stück Code am unteren Ende. Also am Anfang, in einem gründlichen, in einer gültigen Ablauf wird der, mit Q irgendwie in den Buffer zeigen und dann wird anschließend ihn hineingeschrieben und kann auch legitimerweise benutzt werden. Aber der Pointer, dadurch, dass hier der Pointer Q verschoben wird, kann man dafür sorgen, dass das Q anschließend auf den Funktionspointer zeigt. Und während wir dann weitergehen, wenn QD referenziert wird, kann also der Funktionspointer überschrieben werden und kann dafür sorgen, dass er dann in den Code weiter unten zeigt. Und während das Programm sich ausgeführt wird und dann diesen Pointer dereferenziert, diesen überschriebenen Funktionspointer, kann der Angreifer dann Code ausführen, der vom Angreifer kontrolliert wird und dann alles mögliche machen. Das sind also die Grundbausteine, die wir in Angriffen haben heutzutage, was also Angreifer nutzen, um Kontrolle zu erhalten über ein Programm. Zuerst haben wir eine Speicherunsicherheit oder eine Verletzung von Speichersicherheit und den Angreifer benutzt dies, seinen Exploit, seinen Ausnutzung aufzubauen und der Kontrollfluss der Applikation verlässt dann den definierten Kontrollfluss und der Angreifer führt beliebigen Code aus. Wir haben aber Sicherheitsmaßnahmen, nicht wahr? Es gibt eine Vielzahl von Vorschlägen für Sicherheitsmechanismen, die in den letzten paar Jahren erschienen sind und es gibt so einen großen Strauß von Verteidigungsmechanismen für solche Angriffe. Lasst uns mal schnell einige von denen durchgehen, was hier die Begrenzungen sind, was noch möglich ist. Fangen wir an mit Data Execution Prevention, also die Verhinderung von Ausführungen von Daten, das wir also schreibt, Zugriff entfernen von Teilen des Speichers. Bevor es Data Execution Prevention gab, konnten wir unseren neuen Code einfach irgendwo injizieren auf dem Stack oder auf dem Heap und der Angreifer konnte einfach die Ausführung zu diesen Speicherorten umlenken und an diesen Code ausführen, aber seitdem es Data Execution Prevention gibt, müssen Angreifer sich beschränken, darauf vorhandenen, bereits vorher vorhandenen Code auf vorhandenen Code zu verweisen. Dies stellt also sicher, dass wir nicht mehr einfach irgendwelchen neuen Code wie Share Code injizieren können oder andere Code-Stücke, die durch den Angreifer benutzt werden könnten, um seine oder ihre Privilegien zu erhöhen. Das andere ist die Address Space Layout Randomization, also die zufällige Verwürflung des Layouts des Address-Speichers. Vorher gab es die Möglichkeit, dass man, vorher hatte man bekannt Speicherort für bestimmte Teilen der Applikation, die werden jetzt einfach durcheinander geworfen, was es schwieriger macht für den Angreifer zu raten, wo die genauen Orte sind, um die es geht. Dies ist natürlich verwundbar durch Informationslex. Wenn der Angreifer lernt, wo bestimmte Teile sind, kann er einen Exploit bauen, der gegen diese Verteidigungsmechanismen immun ist. Es gibt dann bestimmte Mechanismen, wie Warnwerte auf dem Stack oder sichere Behandlungen von Ausnahmesituationen, von Fehlern. Das sind Mechanismen, die uns von einigen dieser Verwundbarkeiten, vor einigen dieser Verwundbarkeiten schützen. Aber diese Verteidigungsmechanismen sind nicht vollständig, höchstens teilweise wirksam. Machen die Angriffe schwerer, aber stoppen sie nicht komplett. Address Space Leert, Randomization und Data Execution Prevention sind außerdem nur wirksam, wenn sie kombiniert werden, wenn sie zur gleichen Zeit auf einem System benutzt werden. Wenn man Data Execution Prevention durchbericht, kann man einen neuen Code ingezielen. Wenn man die Randomization des Adressespeichers durchbricht, dann kann man einfach, kann man mit Sicherheit neu existieren, Code neu ausführen. Ich sagte schon, dass Informationslex geht, um die Orte bestimmter Code Stücke zu, auf die zu schließen und dann kann man, die kann man dann nutzen und auf Desktop Systemen sind solche Informationslex ziemlich häufig. Es ist möglich, Informationslex in verschiedenen Stücken von Software zu finden. Auf Servern in den letzten paar Jahren sind Code Wiederbenutzungsangriffe weniger geworden, weil es einfach nicht so viele Informationslex auf Servern gibt. In diesem Sommer haben wir gearbeitet an einem Angriff, der die ASLR Informationen bekannt gibt für alle aktuell laufenden virtuellen Maschinen, für verschiedene Windows-Versionen und Linux-Versionen, in dem wir die Speicher-Detuplizierungsroutinen nutzen, die viele Cloud- Infrastrukturen haben. Dadurch konnten wir mehr oder weniger herausfinden, wo die Basisadressen für zufällig durcheinander gefüllten Speicher sind und konnten dann also, wir konnten durch die verfügbaren Entropy-Bits von ASLR iterieren. Für weitere Informationen verweise ich euch auf das Papier und einen weiteren Talk. Die anderen, zwei von den anderen, die hier daran gearbeitet haben, sind hier anwesend und können auch erreicht werden für Fragen. Also, der Stand für diese Verteidigungsmechnismen ist also unkomplett unvorständig und es gibt immer noch viele Möglichkeiten, aktuelle Systeme anzugreifen. Es muss doch irgendwo einen Geheimplan geben, der uns schützt gegen viele dieser neuen Angriffe schützt. Wir müssen uns irgendwie verteidigen gegen all diese neuen Exploits. Die Wissenschafts hat also hunderte von verschiedenen Vorschlägen generiert, wie wir uns gegen diese verschiedenen Speichersicherheitsverwundbarkeiten schützen können. Viele davon sind nicht praktikabel, weil es hohe Performance-Einbußen gibt oder in Kompassibilitäten mit existierender Software. Aber zwei Vorschläge, die mehr und mehr Impulse-Energie gewinnen, sind Stack-Intric Integrity und Control-Flow Integrity. Dieses sehen wir immer mehr und mehr in Benutzung. Pangen wir mit Stack-Integrität an. Wir wollen jetzt erzwingen, dass ein zusätzlicher Satz von von Restriktionen eingehalten wird auf den Return-Instructions. Das Zurückkehren von einer Funktion zum Aufrundenfunktion ist eine sehr gut definierte Funktion. Es ist normalerweise sehr eindeutig. Man kann nur zu einem ganz bestimmten Punkt Zurückkehren zu einer bestimmten Zeit während der Ausführung des Programms. Aber auf dem Hardware-Level ist das nicht eine beschränkte Operation, weil die Return-Instruktion implementiert ist auf allen existierenden Hardware existierenden Hardware-Architekturen. Dies läuft so ab, dass einfach der Corporate vom Stack abgelesen wird und dann zu dieser eigentlich willkürlichen Urlokation zurückgekehrt wird. Und was man jetzt macht ist, man nimmt die Semantik der höher gelegene, der abstrakteren Programmiersprache und bekommt dann Informationen, was sind die eigentlich die gültigen, die erlaubten Rücksprungziele. Man hat dann einen sogenannten Shuttle Stack und wenn man zum Beispiel diese Coach-Schnipsel haben hier, wo wir sehen, dass die Funktion FU entweder aus der Funktion A oder B aufgerufen werden kann. Daraus können wir dann herausfinden, dass der Rücksprung eben diese Beschränkungen hat. Auf dem Hardware-Level, während wir FU ausführen, könnten wir im Prinzip zu irgendeinem ausführbaren Beitelspeichers zurückkehren. In dem Image des ausführenden Prozesses, aber mit Stack-Interquete können wir nun beschränken, dass wenn A FU aufruft, wir nur in die Funktion A zurückkehren, wenn wir FU beenden. Und wenn wir andererseits FU aus B-Hairs aufrufen, können wir sicherstellen, dass FU nur in B zurückkehren kann zu dieser Zeit. Und dadurch können wir sicherstellen, dass die Garantien der abstrakten Programmiersprache und diese Mantik der Programmiersprache auch in dem darunterliegenden Maschinencode, der ausgefüllt wird, eingehalten werden. Und wir haben dadurch diesen statischen, welten, wohl definierten Graf. Das andere Prinzip über das ich gerne reden würde, ist Control Flow Interquete, also die Interität des Kontrollflusses. Auf dem Concept Level wird hier sichergestellt, dass die Ausführung des Programms niemals diesen statischen, definierten Kontrollflussgraf verlässt. Um dies zu sicherstellen, müssen wir zunächst mal diesen Graf konstruieren statisch und für jeden individuellen Kontrollflusstransfer müssen wir nun, dass die Menge der erlaubten Ziele herausfinden. Während der Laufzeit, während der Laufzeit machen wir dann einen Check. Sobald es ein transfertes Kontrollfluss es gibt, schauen wir nach, ob das Ziel irgendeines der erlaubten Ziele ist, das wir vorher herausgefunden haben. Und wenn das der Fall ist, kann das Fluss fortgesetzt werden und sonst beenden wir die Applikation. Um dies hier symbolisch zu zeigen, werden wir einen Funktionspunkter, die Referenzierende ausführen. Ohne Kontrollfluss Integrität können wir letztendlich jeden möglichen, jedes mögliche ausführbare Beit im Speicher erreichen. Es gibt keinerlei Restriktionen auf dem Maschinencode Level. Wenn wir dann zurückkehren, können wir zu jeder beliebigen Instruktion zurückkehren. Mit Kontrollfluss Integrität haben wir weitere Restriktionen, wir überprüfen den Funktionspreumenter, ist er in der Menge der erlaubten Sprungziele, wenn nicht beenden wir die Applikation. Das gleiche für die Rücksprung Instruktion. Wir überprüfen das aktuelle Rücksprungziel, vergleichen das mit dem statisch debattetiminierten Set, dem Satz von erlaubten Zielen für einen Rücksprung und erlauben es nur, wenn es in dieser Menge, dass das Rücksprungziel in dieser Menge sich befindet. Dies ist also ein wenig verschieden von der Stack Integrität, ich vorher vorgestellt habe. Und wir werden ein größeres Beispiel in kürzester Zeit sehen, aber was hier im Wesentlichen passiert ist, dass mit Kontrollfluss Integrität der Angreifer durchaus in den Speicher schreiben kann zu jeder Zeit und die darunter liegende Sicherheitsverletzungen darf passieren nur zu einem späteren Zeitpunkt, wenn der Code-Pointer benutzt wird, er verifiziert. Und es gibt eine gewisse Zeit, die vergeht zwischen der Speicherkorruption bis zu dem Zeitpunkt, zu dem der Code-Pointer überprüft wird und der Angreifer kann in diesem Zeitfenster einen Einfluss ausüben. Aber gehen wir zurück zu dem Beispiel mit den beiden Aufrufern für die Funktion FU mit einer Kontrollfluss Integrität auf dem Stack. Wenn A FU aufruft, dann kann FU später entweder zu A oder B zurückkehren. Beides sind gültige Ziele. Die Analyse, die durch den Code gesucht hat, hat sowohl A und B als aufrufende Funktionen von FU gefunden. Deswegen sind A und B natürlich auch gültige Rücksprungziele, wenn wir aus FU zurückkehren. Und vielleicht kann der Angreifer dies benutzen, um für seinen oder ihren Vorteil, um den Kontrollfluss umzuleiten, entweder zu A oder zu B, weil beide Ziele grundsätzlich erlaubt sind während der Laufzeit. Verglichen mit dem statisch festgestellten Satz von Zielen. So, jetzt kommen wir zu neuartigen Code-Wiederbenutzungsangriffen. Einer von denen ist Kontrollflow Bending, also Kontrollfluss verbiegen. Und das ist, die haben, andere haben den größten Teil dieser Arbeit gemacht und einer von denen ist bei University of California in Berkeley. Und die Idee ist, dass man nicht den Kontrollfluss übernimmt und auf einen völlig neuen Ziele umbiegt, sondern wir verbiegen den Kontrollfluss nur ein ganz bisschen anhand des gültigen Kontrollflussgrafen. Das heißt, es ist nicht sichtbar, dass es zu neuen Zielen umgelenkt wird. Also wenn wir eine Ausführung angucken, das heißt, jeder einzelne Kontrollflussübergabe ist gültig, aber die gesamte Abfolge von Kontrollflussübergängen ist dann nicht mehr gültig. Aber wenn wir einen einzelnen Kontrollflussübergang angucken, dann kann der unter Umständen nicht möglich sein, wegen der Constraints, die dort gelten. Aber jeder Kontrollfluss selbst an sich allein ist gültig. Und dies kann uns erlauben, Kontrollflow Integrity zu umgehen, wie wir gleich sehen werden. Die zugrunde liegende Beschränkung von Kontrollflow Limitation ist, dass es zustandslos ist. Und es wird alles ohne Zustand überprüft, ohne den Kontext überprüft. Das heißt, jede Verbiegung des Kontrollflusses entlang von gültigen Kontrollflussübergängen ist gültig anhand von dem, was die Kontrollflow Integrity überprüfen kann. Und in diesem abstrakten Kontrollflow Grafen passt es zu dem gültigen Verhalten. Und ich rede nicht über schwache Formen von Kontrollflussintegrität, sondern schwache Kontrollflussintegrität ist schon lange kaputt und das ist schon bekannt. Es gibt ganz viele Papers dazu, die auf verschiedenen Security-Konferenzen gezeigt wurden im letzten Jahr, dass schwache Kontrollflussintegrität schon kaputt ist. Microsoft hat einen Kontrollfluss-Controlflow-Guard, die Kontrollflow Integrity ist und eine schwache Kontrollflow Integrity und kann mit allen den hier gezeigten Methoden überwunden werden. Das macht den Angriff vielleicht ein bisschen schwerer oder in manchen Fällen macht es den Angriff schwerer, aber es verhindert ihn nicht. Das heißt, jetzt gehen wir über zu starker Kontrollflussintegrität. Und die Annahme hier ist, dass wir definieren ganz genau so genau wie möglich den Kontrollfluss-Diagrafen. Das heißt, wir nehmen an, dass es keine Annäherung gibt und jede mögliche Analyse zur Kompilations-Time hat immer eine zu hohe Annäherung und wird deswegen ungenau. Aber wenn wir annehmen, dass es keine übermäßig Annäherung gibt und dann zeigen können, dass der Kontrollflussgraf kaputt ist, dann können wir zeigen, dass jeder möglich, jede mögliche Kontrollfluss-Integritätsimplementation gewunden werden kann. Und wenn wir annehmen, dass es eine Stack-Integrität gibt und dann haben wir vollständig präzise Kontrollfluss-Integrität und ein Transfer ist nur erlaubt, wenn wir einen ganz genau die möglichen Ziele bekannt sind. Wir gucken mal auf zwei verschiedene Arten von Kontrollfluss-Integrität an, die erst mit und dann ohne Stack-Integrity. Der einfach hat halber, gucken wir mal auf Kontrollfluss-Integrität mit und ohne Stack-Integrität. Wir haben nur ganz kleine Mengen von möglichen Zielen innerhalb von dem Kontrollfluss-Transfer. Und was ist der beste Angriff, den wir unter diesem Bedingungen machen können? Idealerweise haben wir eine Art von return of program. Wenn das Ziel des Angreifers ist, einen Pfad zu finden, zum Beispiel zu dem Funktionssystem im Kontrollflussgrafen, dann müssen wir einen Anzahl von Custrains finden und Speicherzielen, die wir beschreiben müssen, um den Kontrollfluss des Programms an diesem Pfad entlanglaufen zu lassen. Und als zweiten Schritt müssen wir dann die Argumente, die den System-Physen-Funktion übergeben werden kontrollieren. Wir sind immer beschränkt durch die Anzahl von Verletzlichkeiten, Verwundbarkeiten, die wir überhaupt nutzen können. Wie sieht so ein Graph nun aus? Lange dachten wir, dass dieser Kontrollfluss-Graf, dieser super, dieser super, super komplexer Graph-Seite sehr kompliziert ist und wo es fast unmöglich sein würde, für einen Angreifer einen Pfad zu finden, von der eigentlichen Verwundbarkeit durch den komplexen Graph all die verschiedenen Argumente und Einschränkungen zu nutzen, um dann wirklich den System-Call am anderen Ende zu erreichen. Die Idee ist also, dass es sehr unwahrscheinlich ist, dass ein Angreifer so einen Pfad finden kann. Aber was, wie sieht ein Kontrollfluss-Graf wirklich aus? Tatsächlich haben wir gefunden, dass es eine große Menge von Funktionen gibt, die grundsätzlich verbunden sind in so einem Kontrollfluss-Graf zwischen all diesen verschiedenen Orten. Es gibt all diese Funktionen wie Memcopy, malloc und ein Haufen von anderen Print-F, die mehr oder weniger jeden Punkt des Kontrollfluss-Graf mit jedem anderen beliebigen Punkt verbinden. Diese Funktionen werden von überall aufgerufen und viele dieser Funktionen können ihre eigene Rücksprung-Adresse überschreiben oder später eine Funktion aufrufen, die die Rücksprung-Adresse überschreibt. Und unter der Policy des Kontrollfluss-Intequerities ist es so, dass wir sobald wir eine solche Funktion finden, wir zu jeder möglichen Aufruf dieser Funktion zurückkehren können und dadurch beliebige Punkte des Kontrollfluss-Grafs direkt miteinander verbinden können. Und dann können wir einfach ein Pfad finden durch den Kontrollfluss-Grafen von der Verwundbarkeit zu der Aufruf von System und der Argumente kontrollieren und der Angreifer gewinnt. Diese sogenannten Dispatcher-Funktionen werden oft aufgerufen. Die Argumente der Funktionen können vom Angreifer kontrolliert werden durch verschiedene Formen von Speicherkorruptionen, die wir diskutiert haben. Die Dispatcher-Funktionen überschreiben vielleicht ihre eigene Rücksprung-Adresse, wenn wir uns also Mem-Copy anschauen, wo der Angreifer diese drei Argumente bereitstellen können. Wir nutzen hier diese Mem-Copy-Funktion, um den eigenen Rücksprung-Wert zu überschreiben und zwei beliebige Punkte des Kontrollfluss-Grafs zu verbinden. Wir machen eigentlich eine Abkürzung durch diesen Grafen und legen dann den Kontrollfluss durch diese Abkürzung zu unserem Ziel. Wir können im Wesentlichen die Daten des Angreifers dann übergeben, die Rücksprung-Adresse überschreiben, eine Abkürzung finden, in der Nähe von System heraus kommen im Kontrollfluss-Grafen und dann System ausführen. Wir können also mit Kontrollfluss-Integrität ohne Stark-Integrität als gebrochen ansehen, als kaputt ansehen. Die zustandslosen Verkarteilungs-Mechanismen sind nicht ausreichend für Stark-Angriffe. Diese Dispatcher-Funktionen sind recht häufig zu finden und wir können verschiedene davon finden, die mehr oder weniger von überall aufgerufen werden. In den Standard-Libraries werden sie überall verwendet, aber auch in dem eigentlichen Code des Programms. Der Angriff ist jetzt abhängig von dem Programm, während also die Angriffe den Angriff nicht unmöglich machen, machen sie ihn wenigstens schwerer. Der Angreifer muss zumindest herausfinden, welche Dispatcher-Funktionen da sind und dann die entsprechenden Abkürzungen finden, aber der Angriff ist immer noch möglich. Also es sieht so aus, als ob wir jetzt ein Problem haben. Wir haben immer noch den Angriff schwieriger gemacht. Immerhin ein anderer Angriff, der vorgestellt wurde vor recht kurzer Zeit, ist gefälschtes, objektorientiertes Programmieren. Nicht meine Arbeit, auch nicht die von meinen Kollaboratoren, aber ich fand, das war ein sehr, sehr schönes Beispiel, ein sehr schöner Angriff, der hier erwähnt werden sollte. Die grundlegende Idee ist, dass eine Funktion ein Gadget für sich selbst sein kann. Man kann sie verbinden und verschiedene Teile zusammenfügen, falsche Objekte und dann interessantes Verhalten bekommen. Wenn wir uns diesen C++-Code anschauen, wo wir eine kleine Klasse definieren und dann hier einen kleinen Destruktur haben, der natürlich virtuell ist. Das Virtual-Geschlüssewort in C++ sagt dem Compiler, hey, ich möchte, dass dies ein indirekter Aufruf wird. Das gibt uns die Möglichkeit, die Ausführung umzuleiten, denn wir haben gerade gehört, dass indirekte Calls, indirekte Aufrufe, das zur Grundeliegende Prinzip sind für all diese Kontrollflussangriffe. Und dann wiederum in diesem Beispiel, in diesem Destruktur, da iterieren wir einfach durch eine Liste von Studenten, die dann alle deallokiert werden und wir führen alle möglichen virtuellen Funktionen auf all diesen Objekten auf, die wir gesammelt haben. Nun stellen wir uns vor, dass der Angreifer die Liste der Studenten kontrolliert, die wir haben oder das Feld von Studenten, dann kann er auf einmal einen Satz von virtuellen Zielen generieren und diese dann wiederverwenden, die die virtuellen Tabellen wiederverwenden, um beliebigen Codes auszuführen. Als zweiten Schritt können diese verschiedenen Formen virtueller Tabellen benutzt werden und neuerweise zusammengefügt werden. Es gibt verschiedene Formen von arithmetischen Gadgets, anderen Gadgets, zum Beispiel in diesem einfachen Update Score haben wir ein arithmetisches Gadget, das wir kombinieren können. Hier ein Mem-Copy Gadget der Attacker kann, also diese Dinge einfach zusammen flicken, zusammen nähen und es wurde hier eine solche Technik vorgestellt, die es erlaubt, Objekte übereinander zu legen. Wir dürfen nicht vergessen, dass das Speicher unter der Kontrolle des Angreifer ist. Also anstatt dass wir jedes Objekt hier für sich selbst haben, können diese Objekte sich überlagern und das Memory Layout, das Speicher Layout für zum Beispiel das Examenobjekt, sieht vielleicht so aus wie auf der rechten Seite jetzt zu sehen ist in der Folie und der Angreifer kann nun verschiedene Felder, existierende Felder benutzen und dort andere Objekte hineinlegen und verschiedene Arithmetik Gadgets benutzen, um zum Beispiel die virtuelle Pointer Tabelle zu aktualisieren und etwas anderes Pointerziel hier einzutragen. Durch diese Objekt-Overlays kann man also verschiedene Gadgets zusammenflicken und verschiedene beliebige Codes ausführungen erhalten. Dies sieht also ziemlich schlecht aus, wenn wir diese beiden Angriffe kombinieren, das Verbiegen vom Kontrollfluss und das benutzen von gefälschten Objekten, aber wir haben eine große Menge von Kontrollfluss, Verteidigungsmechanismen, die wir als Vorschlag haben. Wie widerstehen Sie diese Art von Angriff? Wir haben hier verschiedene Kontrollfluss-Integritätsmechanismen, die vorgeschlagen worden sind entweder in der Wissenschaft oder durch Google oder Microsoft und andere. Also es gibt Lockdown. Dies erzwingt eine dynamische Kontrollfluss-Policy, in dem eine Form von binäre Analyse verwendet wird, man braucht keinen Source Code dafür. Es gibt MCFI und PICE-CFI verschiedene Kontrollfluss-Integritätsmechanismen, die eine volle Analyse zur Compile-Zeit erfordern. Es gibt LLWM-CFI von Google, verkürzt und veröffentlicht und ist jetzt dabei, in neue Releases von LLWM hineingefügt zu werden. Dann gibt es EFCC von Google letztes Jahr prosettiert. Dies wurde allerdings inzwischen verändert und wieder zurückgezogen. Wir werden bald sehen, warum das geschehen ist. Und dann gibt es von Microsoft Control Flow Guard. Es gibt viele, viele andere, die verschiedene Subregeln, Subpolicies implementieren. Also nochmal einen Zusammenhang von CFI zu zeigen, um die Stärke der Verteidigung hängt ab von den Klassen, die wir haben. Und wenn wir ein bestimmtes Stück Programmcode haben mit Kontrollfluss-Transfer, wie es auf der linken Seite stehen, dann können alle diese Instructions benutzt werden, um den Kontrollfluss umzubiegen auf eine andere Stelle. Und alle Control Flow Integrity Analysen geben eine Menge von Equivalenzklassen zurück, die eine Menge von Zielen zurückgeben und in diesem Beispiel zeige ich drei verschiedene Equivalenzklassen. Und die letzten beiden Instruktionen benutzen dieselbe Equivalenzklasse. Und mehrfacher Indirektorkontrollfluss-Transfer kann dieselben Ziele erlauben. Und hier ist noch die Größe dieser Equivalenzklasse, die zeigt, wie viele Möglichkeiten der Angreifer hat, um den Kontrollfluss umzubiegen auf eine andere Stelle. Und idealerweise wollen wir ganz viele Equivalenzklassen haben, die jeweils ganz klein sind. Je kleiner die Größe ist, idealerweise wäre die Größe einer Equivalenzklasse 1 und dann gibt es gar keine Möglichkeit, den Kontrollfluss umzubiegen. Aber am besten, idealerweise sollten die Equivalenzklassen nicht so klein wie möglich sein. Wir haben die Präzision angeguckt von der Forward Edge und wir haben die Größe der Equivalenzklassen angeguckt und haben hier Grafen, wo wir die den Medien zeigen, mit dem roten Pfeil von der Größe der Equivalenzklassen und 25 und 27 mit den kleinen Pluszeichen werden verschiedene Ausreißer angezeigt. Die verschiedenen CFI Policies auf der X-Achse und die dann Dynamic zeigt die Anzahl der möglichen Ziele und wenn wir diese Programme alle ausgeführt und haben gemessen, wie viele verschiedene Ziele genutzt wurden und wenn wir diese verschiedenen Locations angucken, dann sehen wir eine große Menge von indirekten Kontrollflusstransfer und diese Policies erlauben alle verschiedene Ziele. Unter der dynamischen Policie sind nur ganz wenige Ziele erlaubt und diese ist eine logarithmische Skala, sollte ich vielleicht noch sagen, das heißt je höher das ist, desto mehr Transfers sind erlaubt. Ich wollte noch auf einige interessante Sachen hinweisen. Für IFCC in vielen Stellen kollabieren die Equivalenzklassen oft in eine einzige Menge, das heißt alle möglichen Klassen können dieselben Klassen benutzen, das heißt es ist eine sehr schwache Form des CFI, die erlaubt alle Tages wieder zu verwenden und das ist vielleicht auch der Grund warum IFCC zurückgezogen wurde von Google und hier sehen wir bei Lockdown, dass der Sprit sehr groß ist und es gibt ganz viele, ein Outlier hier, der ganz, ganz viele Transfers ermöglicht und es liegt an einem Problem mit der Binäranalyse, wo zusätzliche Symbole erforderlich sind und es gibt normalerweise immer eine Library, die keine Symbole zur Verfügung stellt und dann sind ganz viele Möglichkeiten des Transfers gegeben und man kann nicht alle Informationen wieder erlangen von allen möglichen Zielen in dem Code. Hier ist noch MCFI und PiCFI und für die meisten Programme liefern sie ungefähr die gleiche Präzision, aber PiCFI beschränkt manchmal die Anzahl der erlaubten Transfers aufgrund einer zusätzlichen Laufzeitanalyse, die dort gemacht wird und hier ist auch noch ein neues LCFI, was im Wesentlichen die zweite Version von IFCC ist, von Google kürzlich freigegeben hat und in neuen LVM Releases und es sieht aus als wäre es viel näher an dem Limit oder an dem minimal möglichen Anzahl von Transfers verglichen mit der alten Version und wir gucken mal auf eine Zusammenfassung von allen diesen verschiedenen CFI Mechanismen. In dem Grafen eben haben wir gesehen, nur auf der vorderen Kante, was ist die Präzision für indirekte Sprünge und ist die hintere Kante und Backward Edge. Und die Wichtigkeit, diese auch zu schützen gegen Control Flow Bending. Wir sehen, dass die ersten drei IFCC Microsoft Control Flow klar und LLVM CFI haben im Wesentlichen keinen Schutz verglichen und das macht Control Flow Bending trivial und es einfach wieder zusammenzubauen um den Control Flow umzubiegen. Bei MCFI und Pi CFI gibt es etwas Schutz hier und es macht Control Flow Bending viel schwerer und diese Dispatcher Funktionen für Lockdown wegen der starken Schutz an der Backward Edge müssen wir Control Flow Bending an der Backward Edge machen, was den Angriff viel schwieriger macht, aber wegen der Beschränkungen der statischen Binary Analyse können wir trotzdem immer noch mögliche Angriffsziele finden. Das heißt es sieht so aus als wäre die vorgeschlagenen Schutzmechanismen, dass sie die Angriffe schwerer machen, sobald wir Stack Integrity auch noch hinzufügen, machen sie die Angriffe sehr viel schwerer. Das heißt wenn wir Stack Integrity haben, dann können wir den Schutz von aktuellen Systemen sehr weit erhöhen, aber viele Systeme haben keine Stack Integrity. Also wir nehmen jetzt mal an, dass wir Stack Integrity haben und dann Return Oriented Programming kann man dann nicht mehr machen und der Angriff wird wesentlich schwerer. Der Angreifer muss einen Pfad finden durch die virtuellen Aufrufe und muss sich auf einige und der Angriff wird einfach sehr schwer. Dadurch ein Interpreter würde Angriffe viel einfacher machen, wenn wir uns überlegen, was wir zum Beispiel ein Stück Code haben, was wir einfach einfügen können in das Image, was ausgeführt wird und selbst unter den Beschränkungen von voller Control Flow Integrity erlaubt, immer noch anderen Code auszuführen. Und es würde wahrscheinlich überraschen, wie viele Touring vollständige Möglichkeiten es hier gibt. Es gibt zum Beispiel printf orientierte Programmierung. Printf ist Touring vollständig und wir können jedes beliebige Programm in einen Format String überführen. Wir können Speicher lesen und schreiben und wir können auch Bedingungen implementieren. Der Programm Zähler wird der Counter im Format String und wir können einfach den Program Counter bewegen. Wenn man Schleifen machen will, können wir den Program Zähler, diesen Format String Zähler überschreiben und verschiedene Teile des Format Strings werden einfach auf dem Bildschirm geschrieben und wir können darin herum springen und unseren Program Counter verschieben und an verschiedene Stellen springen. Und dies ist eine Touring vollständige Domain spezifische Sprache. Um es nochmal interessanter zu machen, hat jemand schon von Brainfuck gehört. Das ist auch eine spaßige Touring vollständige Sprache, die nur acht verschiedene Symbole hat. Man kann den Datenzeiger vorwärts und rückwärts bewegen. Man kann in der aktuellen Zelle den Wert erhöhen und erniedrigen. Man kann einen Buchstaben auf dem Bildschirm schreiben und man kann einen Charakter holen. Und das ist ein Statement, um den Datenzeiger zu erhöhen, zu niedrigen, addieren, subtrahieren und vorwärts und rückwärts bewegen. Und je nach der Implementation von dem Drucker kann man auch diesen Format String verändern und kann in dem Format String hin und her springen. Warum das von der Person abhängt, die man gerade benutzt, nehmen wir an, dass wir Printef in einer Schleife aufrufen. Das heißt, entweder müssen wir auf die aktuelle Implementation gucken von Printef in der Lippsee und die internen Daten überschreiben von Printef oder wir haben einen Printef in einer Schleife im aktuellen Programm und das können wir normalerweise machen in vielen von den Programmen, die wir angeguckt haben. Und wir können den Rainfuck Interpreter aufbrechen in eine einfache Schleife, die einfach einen Printef nach dem anderen aufruft. Und hier ist eine kurze Demo, wie wir beliebigen Printfuck Code in eine Printef-Sequenz übertragen können und dann Printfuck ausführen können durch einfach eine Serie von Printef. Ich habe ja eine Menge von kleinen Printfuck-Programmen und wir fangen mal an mit dem Fibonacci. Das ist ein kurzes Brainfuck-Programm, das einfach nur Fibonacci darin berechnet. Und einmal kompilieren, das Programm kompilieren und wir haben ein kleiner, ja der Bildschirm ist etwas klein, aber wir können auf der GitHub Seite gucken und dasselbe runterladen und ausprobieren. Also es läuft aber das Printef-Fibonacci-Programm in einem einzigen Printef-Statement und wir erzeugen hier die verschiedenen Fibonacci-Zahlen und das ist nur ein Printef-Statement, das einfach nur immer wieder ausgeführt wird. Das ist eigentlich ein Brainfuck-Programm, was diese Zahlen berechnet. Und es gibt noch ein anderes lustiges Programm. Ein anderes lustiges Programm habt ihr schon mal gehört von 99 Bottles of Beer, 99 Bierflaschen und das ist etwas länger, ein etwas längeres Programm und wir starten das auch und gucken mal was es macht. Interessanter Weise wird es schneller, je mehr Biermann getrunken hat, desto schneller wird es schneller, die Zahlen hinzuschreiben. Es gibt auch Game of Life hier und es gibt ein Mandelbrot-Fraktal-Programm und ein Serpinski-Dreik und das sind alle, alle in einem einzigen Printef-Statement und es gibt, man kann ganz viel Spaß damit haben mit diesem Interpreter. Es ist Open Source, ihr könnt es einfach runterladen und benutzen und damit rumspielen und hier ist die Adresse auf GitHub, wo man das runterladen kann und spielt mit der Touring-Vollständigkeit und guckt was passiert. So, die Schlussfolgerung. Low Level Languages werden unzerhalten bleiben und diese sind voller, haben, bieten ganz viel Potenzial und ich habe gezeigt, dass nur ein einziger Printef Call einfach in einer Schleife ausgeführt werden kann oder indem man einfach nur die interne Implementationen von Printef anguckt, kann man das voller Potenzial dieser Low Level Programmiersprachen ausschöpfen und seltsame Maschinen verstecken sich überall. Es ist nur eine einzige Memory Corruption, das heißt, wenn man eine Format String Vulnerability hat oder Memory Corruption, dann kann man das erste Argument von Printef kontrollieren und dann hat man einen vollständigen, einen Touring vollständigen Interpreter, der mit dem man beliebiges ausführen kann. Ohne Stack Integrity sind die Verteidigungsmechanismen einfach schon gebrochen, schon kaputt und mit Stack Integrity gibt es immer noch die Möglichkeit, dass der Angreifer viele lustige Sachen machen kann und ich ermutige euch mit dem Code rumzuspielen, den wir haben ein einfaches Python Script, das die BrainFuck Programme übersetzt in lange Format Strings und die Format Strings gibt es da und habt einfach Spaß damit. Ja, wir haben noch ein paar Minuten für Fragen, haben wir hier Mikrofone, gibt es Fragen? Also geht nach Hause, spielt mit Printef. Ich bin noch, ich bin noch eine Zeit da, ja, da ist eine Frage, bitte. Hallo. Ja, vielen Dank, eine sehr gute Präsentation. Ich habe eine Kurzfrage. In den Printef, in einigen Implementationen ist das aber deaktiviert, die nur Prozent N gibt der Speicher zu, Schreibzugriffe. In all das, was wir in allen Implementierungen, die ich gesehen habe, sind sie implementiert. Und in VisualSea, Microsoft VisualSea ist es bei default deaktiviert, und man kann es aktivieren. Danke. Bitte geht oder bleibt ruhig? Ich habe mich gefragt, ich habe, ja, ich habe eine Diskussion gesehen neulich auf der Mellingliste über SafeStack und ich würde gerne, es ist ganz der Meinung Fragen. Wir haben eine frühere Version bei OSDI veröffentlicht. Die aktuelle Version in LNWM ist ein bisschen beschränkt, hauptsächlich als die Bugging-Tool ausgerichtet, aber die zugrunde liegende Idee ist, euch einen Tool zu geben, das Stack Frames schützen kann und Stack Frame Integretate ohne jeden Overhead zu bekommen. Ich denke, es ist großartig ein sehr gutes Werkzeug und ich denke, es sollte in LNWM eingebaut werden ohne weiteren Overhead. Ja, vielen Dank Matthias.