 von N-Space und Ghanimo. Wir sind Aiphen und Tribut. Falls ihr Feedback zur Übersetzung habt, meldet euch gerne bei hello at c3lingo.org oder benutzt den Hashtag C3T auf eurem liebsten sozialen Netzwerk. Eine große, grohende Applaus für unsere beiden Speaker. Ja, danke für die Einführung. Es ist schön hier zu sein, wie immer. Wir reden über verschiedene Art und Weisen, wie man das Fuzzing beschleunigen kann und Schwachstellen finden kann oder wie man ausführbare Dateien modifizieren kann. Ich bin Ghanimo auf Twitter. Ich bin ein Professor bei der EPFL. Ich arbeite an Software-Sicherheit, Fuzzing und Schutzmaßnahmen. Und Matthias arbeitet an seiner Masterarbeit, wo es darum geht, Binehrdateien umzuschreiben. Und wir werden heute auf eine Reise gehen, wie man sehr schnelles und effizientes Umschreiben von Binehrdateien machen kann, die Modifikationen an Binehrdateien erlaubt. Und womit man damit nicht gewünschte Features in einer Ausführbare Datei finden kann. Und das Fassen von Ausführbare Dateien ist wirklich, wirklich schwer. Es gibt sehr wenige Tools im Userspace. Es ist sehr schwer aufzusetzen. Es ist sehr schwer, das in einem performanten Weg aufzustellen. Man muss Setup ist sehr komplex. Man muss Tools, Werkzeuge bearbeiten. Und die Ergebnisse sind nicht wirklich zufriedenstellen. Und was den Kernel angeht, da gibt es überhaupt keine Werkzeuge. Es gibt sehr wenige Benutzer, die das überhaupt benutzen, die Binehrcode im Kernel modifizieren. Es ist ganz grässlich damit zu arbeiten. Und was wir heute zeigen, ist ein neuer Ansatz, der es erlaubt, jegliche Form von ausführbaren Dateien statisch umzuschreiben. Und das gibt die volle Performance. Und man kann sehr komplexe Transformationen damit machen. Wenn man ein modernes System anschaut, dann sieht das Bild so aus, ein modernes Setup. Also schauen wir mal hier, wir schauen uns Katzenbilder im Browser an. Chrome, Kernel, die CB Bibliothek, das grafische Oberfläche. Es sind ungefähr 100 Millionen Code-Zeilen zusammen. Das, zusammen zu instrumentieren, ist ein Albtraum. Wenn es vor allem eine sehr lange Menge von Software ist, Compiler, Linker, verschiedene Systeme, wo das kompiliert wurde, verschiedene Systeme und diese Instrumentierung da auf allen Systemen auszurollen, das ist im Wesentlichen unmöglich. Und wir wollen, dass ihr in der Lage seid, diese Teile auszuwählen, die euch tatsächlich interessieren, die zu modifizieren und dann das Fuzzing oder die Analyse auf diese kleinen Teile, das zu fokussieren, was sehr viel stärkere Möglichkeiten einräumt, die Teile des Systems zu testen, die euch wirklich interessieren. Wer hat Fuzzing schon mal gemacht? Zeig mal eure Hände. Wow, das sind wirklich einige von euch. Benutzt ihr AFL? Ja, die meisten benutzen AFL, Lipfuzzer. Okay, vielleicht 10, 15%, 30% AFL. Also es gibt hier schon eine ganze Menge Wissen, was Fuzzing angeht. Da werde ich also nicht allzu viel darüber reden, aber die Leute, die das noch nicht gemacht haben, das ist eine ganz einfache Möglichkeit, Software zu testen. Man nimmt also eine ausführbare Datei, zum Beispiel Chrome, als Ziel und man führt das in einem speziellen Umgebung auf. Und dann besteht das Fasen daraus, dass man Eingaben generiert, neue Testfälle generiert, auf das Programm wirft und dann nachschaut, was mit dem Programm passiert. Und entweder alles ist in Ordnung oder der Code wird ausgeführt und das Programm beendet sich ganz normal oder wir haben ein Fehlerbericht. Da kann man dann die Schwachstelle suchen, vielleicht kann man auch einen Proof-of-Konzept werfen und eventuell einen Exploit schreiben oder das ist im Wesentlichen, wie Fuzzing funktioniert. Wie kann man jetzt Fuzzing effektiv machen? Wie kann man große Menge von Code damit abdecken und in einem komplexen Umgebung? Es gibt ein paar einfache Schritte, die man da machen kann. Lasst uns da einmal durchgehen. Zunächst, ihr wollt in der Lage sein, Testfälle zu erzeugen, die tatsächlich auch Fehler auslösen. Und das ist eine sehr komplizierte Sache, denn ihr müsst eine Idee davon haben, welche Eingaben das Programm akzeptiert und wie wir verschiedene Teile des Programms untersuchen können, verschiedene Funktionalitäten untersuchen können. Wir könnten jetzt natürlich einen Entwickler die Testfälle schreiben lassen, aber auch ziemlich langweilig und würde sehr viel menschlichen Aufwand bedeuten, diese Eingaben zu erzeugen. Also Fuzzing, das auf Coverage basiert, also auf Abdeckung, wurde entwickelt, um diesen Prozess zu lenken, indem die verschiedenen Wege durch das Programm nachvollzogen werden und welche davon der Fuzzer kann dann dieses Feedback benutzen, um die Eingaben zu modifizieren, die dem Fuzzing-Prozess vorgesetzt werden. Und der zweite Schritt ist, der Fuzzer muss in der Lage sein, Fehler zu erkennen. Wenn man einfach ein Byte ans Ende eines Buffers schreibt, dann ist es sehr wahrscheinlich, dass die Software nicht abstürzt. Das ist immer noch ein Fehler, es kann immer noch ausnutzbar sein, aber wir wollen in der Lage sein, diese Verletzungen zu detektieren. Durch irgendeine Art von Instrumentierung, die wir dahin zufügen, die uns sagt, hey, hier ist die Speichersicherheit verletzt worden und dann können wir das dem Fuzzer als Feedback mitteilen. Und der letzte wichtige Teil ist, Geschwindigkeit ist wichtig. Wenn ihr Fuzzer ausführt, dann habt ihr eine gewisse Menge an Ressourcen, ihr habt eine Gewinke von Kernen, ihr wollt ihr 24 Stunden vielleicht ein paar Tage ausführen lassen, was auch immer eure Einschränkungen sind. Ihr habt einfach nur eine gewisse Menge von Instruktionen, die ihr ausführen könnt. Ihr müsst entscheiden, setze ich meine, erzeuge ich da neue Eingaben, untersuche ich die Einschränkungen, was oder benutze ich diese Sanitisierung in meinem Programm und ihr müsst da eine Balance finden und ihr müsst die Ressourcen so gut wie möglich nutzen. Und alles, was da dazukommt, macht euch nur langsamer und dann wird es ein Optimierungsproblem. Wie kann man möglichst effizient die Ressourcen nutzen, die man zur Verfügung hat? Wenn wir das mit Quake-Codes fassen, dann können wir das relativ einfach hinzufügen, diese Instrumentation. Wir können das einfach im Compiler drin machen. Dort können wir direkt im Compiler den Code instrumentieren, was dann während des Kompellierungsprozesses passiert. Zum Beispiel können moderne Compiler Coverage analysieren oder auch schauen, dass alle Speicherzugriffe sicher sind. Wenn man dann die instrumentierte Ausführbare Datei ausführt, dann kriegen wir das alles mit. Wenn wir jetzt Quake-Code für alles hätten, dann wäre das toll, aber das ist häufig nicht der Fall. Auf Linux können wir wahrscheinlich fast alles mit Source Code tracken, aber es gibt eventuell Anwendungen, die keinen Quake-Code haben oder auch auf Android. Da gibt es viele Treiber, die nicht Open Source sind oder manchmal gibt es vielleicht auch einen ganzen Software-Stack, der nicht Open Source ist. Man möchte trotzdem in diesen komplexen Software-Stacks Bugs finden mit mehreren 100 Millionen Zeilen Code. Das ist noch effizient. Der einzige Weg, das jetzt zu machen ist, ist, das neu zu schreiben und die Binarys neu zu schreiben. Die erste Möglichkeit ist, das als Blackbox zu betrachten, aber das ist sehr ineffizient. Der zweite Vorschlag ist, das dynamisch zu neu zu schreiben und da machen wir das so, dass wir während der Ausführung die Ausführerware Datei übersetzen und die Instrumentierung während der Laufzeit hinzufügen. Das funktioniert, aber es ist sehr, sehr langsam, sodass wir irgendwie ein Slowdown von 10-fachen Slowdown oder sogar 100-fachen haben. Das heißt, das wollen wir nicht wirklich. Wir wollen irgendwas haben, was effektiver ist. Also, was wir hier machen wollen, ist, das statisch zu machen. Das ist viel komplizierter zu analysieren, weil wir die Binary umschreiben müssen, bevor sie ausgeführt wird. Das heißt, wir müssen all den Kontrollfluss uns anschauen und so weiter, aber wir kriegen dadurch viel bessere Performance. Das heißt, wir kriegen mehr für unser Geld. Warum ist das Neuschreiben umschreiben so schwer? Na ja, wenn wir einfach Codes hinzufügen, dann geht es kaputt. Das heißt, wenn wir hier so eine Loop uns anschauen, so eine Schleife, dann sehen wir hier, es geht durch das Error durch und wenn wir nicht am Ende sind, dann schauen wir weiter. Und wenn wir jetzt uns die letzte Instruktion anschauen, dann ist es dort ein relatives Offset. Das heißt, das ist konstant, wenn man den Codes einfach ausführt, aber wenn man jetzt neuen Codes hinzufügt, dann sind die Offsets plötzlich anders. Das heißt, wenn man einfach neuen Codes hinzufügt, dann geht der Code kaputt. Das heißt, ein Kernproblem, was wir lösen müssen, ist, dass wir alle Referenzen finden müssen und sie entsprechend anpassen müssen. Wenn wir irgendetwas nicht sehen oder etwas falsch berechnen, dann geht alles kaputt. Das Problem ist, in der Binary, in der Ausführung der Tie, sind alles nur Bites. Das heißt, es gibt keinen Unterschied zwischen den Offsets und den konkreten Adressen. Und wenn wir irgendwo was da falsch machen, dann gibt es neue Crashes darin, die wir nicht diagnostizieren können einfach. Zum Beispiel, wenn wir das hier haben, dann sehen wir, es wird einen Wert genommen und es ist auf den Stack getan, das könnte von zwei verschiedenen Konstrukten kommen. Entweder könnte es die Adresse einer Funktion sein, die in den Stack getan wird oder es könnte ein Skalara-Wert sein, der auf den Stack getan wird. Und wenn es eine Funktion ist, dann müssten wir es modifizieren und wenn es eine Skala ist, dann müssten wir es gleich lassen. Und wie können wir jetzt diese beiden Fälle unterscheiden? Wie können wir statisch die Binarys umschreiben? Schauen wir mal in den Kernel, wie wir es dort machen können. Wir machen das erstmal einfach. Zum Beispiel schauen wir uns erstmal Binarys im User-Land an. Dann schauen wir uns an, dass wir den Kontrollfluss komplett ausnutzen. Und dann schauen wir an, wie wir die Bucks dann tatsächlich erkennen. Jetzt rede ich ein bisschen über Rhetorite, was das Tool ist, womit man statisch Binarys umschreiben kann. Das heißt, das funktioniert, indem wir die Binary in einen symbolischen Datei umschreiben. Wir schreiben die Werte in Symbolen um, sodass wir dieses Sammeln sozusagen und dann essamulieren wir es nachher wieder, sodass wir zwischendrin eine Abstraktion haben, wo wir diese Transformation machen können. Der wichtige Teil hier ist, dass wir Positionsunabhängigen Code haben. Das ist der Standard auf modernen Systemen. Und das bedeutet im Wesentlichen, dass Code auf jeder beliebige Adresse geladen werden kann, während das ausgeführt wird. Es ist eine notwendige Voraussetzung, wenn man Atro Space Randomization machen will oder Bibliotheken benutzen will. Seit ein paar Jahren wird der gesamte Code, der auf euren Telefonen, auf euren Rechnern ausgeführt wird, positionsunabhängig. Und die Idee, was Positionsunabhängigen Code angeht, ist, es kann überall in den Adressraum geladen werden und entsprechend können keine harkodierten statischen Adressen benutzt werden. Man muss das also als Relocation machen oder statische Adressen. Man muss Informationen bestellen, wie das System diese Adressen neu ändern kann. Was auf X64 Code benutzt, der dieser Code Adressen, die relativ zum Instruktionspointer sind. Also in dem Fall den aktuellen Instruktionspointer und dazu ein relatives Offset. Das ist eine einfache Art und Weise, für uns zu unterscheiden, Referenzen von Konstanten zu unterscheiden, in positionsunabhängigen Bineat erhalten. Wenn es relativ ist zum Rip Pointer, ist es eine Referenz, ansonsten ist es eine Konstante. Und wir können dieses fundamentale Erkenntnis benutzen, um die Referenzen alle zu erkennen. Das heißt, positionsunabhängigen Code, wir unterstützen nur positionsunabhängigen Code, aber wir geben euch dafür die Garantie, dass wir allen Code, der positionsunabhängig ist, der draußen existiert, umschreiben können. Also die Umwandlung im Symbole, da wird zuerst alle Referenzen mit Labels im Assembler ersetzt. Also wenn man zuerst die Call Instruktion hier benutzt und die ganz unten die Jump Not Zero Instruktion hüpft dann fünf Bytes zurück und die werden jetzt ausgetauscht durch Labels, durch Bezeichnungen. Und wenn wir die Funktion aufrufen, dann tauschen wir die aus durch dieses Label und tauschen das dann aus durch eine Rückwärtsreferenz. Und wenn man hier oben eine relative Adresse mitnimmt, zum Beispiel Datenladen, dann tauschen wir das aus durch den Namen der Daten, die wir tatsächlich rekonstruiert haben und können die dann durch den Direktreferenzieren. Und nach diesen drei Schritten können wir jetzt beliebigen neuen Code einfügen. Wir können also verschiedene Instrumentierungen mit einführen oder kompliziertere Analysen durchführen und dann das die Datei wieder zusammensetzen, wieder Assemblieren für Fasing oder was auch immer man damit machen möchte. Ich werde das jetzt an Matteo weiter übergeben, der die Abdeckung basierte Fasing besprechen wird. Also wir haben jetzt dieses super tolle Framework, um Binaries umzuschreiben. Und wie wir jetzt zu Fasing kommen, ist, wir benutzen die Abdeckung des Coverage und das ist eine Art und Weise, um den Fuzzer selbst interessante Eingaben zu finden und interessante Code-Pfade zu finden. Die Idee ist, dass der Fuzzer sich merkt, die Teile des Programms, die von verschiedenen Eingaben erfasst werden. Also wir haben jetzt hier zum Beispiel das Zielprogramm, das überprüft, ob die Eingabe einen String am Anfang PNG enthält. Und wenn nicht, dann macht das nichts Spannendes, sondern macht einfach nur eine Fehlerausgabe. Wenn wir jetzt uns also merken, wie viele, welche Teile des Programms ausgeführt werden, dann kann der Fuzzer jetzt rausfinden. Ah, also Eingaben, die mit P anfangen, werden andere Teile des Programms abdecken, wie Eingaben, die nicht P an der Stelle haben. Und daraus kann man quasi dann auch rausfinden, dass das Programm am Anfang der Eingabe PNG erwartet. Und das macht der Fuzzer so, dass er jedes Mal, wenn er einen neuen Pfad entdeckt, dann wird das quasi als interessant markiert und zu einer Menge von interessanten Eingaben hinzugefügt, seinem Corpus. Und jedes Mal, wenn er was ausprobieren will, dann nimmt er eine von diesen Corpus und mutiert das zufällig. Das ist relativ einfach, aber es funktioniert sehr, sehr gut. Und führt dazu, dass man Eingaben erzeugt, die das Programm erwartet in einem geführten Art und Weg. Also hier ist quasi ein AFL Beispiel, das Programm, hier wurde eine JPEG-Datei gefasst und hier wurde gestartet mit einem String, der einfach nur Hello hieß. Das ist erkennbar kein Bild. Aber der Fuzzer war trotzdem in der Lage, das korrekte Format zu generieren. Nach einer Weile hat das also graue Bilder erzeugt, links oben. Und als es immer mehr Eingaben erzeugt hat, hat er dann immer interessanter Eingaben erzeugt. Also so Gradienten oder auch Muster. Und das funktioniert wirklich. Es zeigt uns, dass wir hier in der Lage sind, ein Programm zu erfassen, ohne dem Fuzzer explizit beizubringen, wie dies Format aussehen muss. Und jetzt sprechen wir noch mal über Sanitisierung. Die Idee dahinter ist, dass wir nach, wenn wir nur nach Crashing suchen, dann werden wir einige Fehler nicht finden. Also wenn wir zum Beispiel auf eine Adresse schreiben, die nicht zulässig ist, dann wird es wahrscheinlich nicht Crashing, aber es ist halt trotzdem ein Bug. Und eine von den Methoden, die wir da benutzen können, ist Address Sanitizer. Der wird alle Speicherzugriffe prüfen und schauen, ob Speicherkorruption passiert. Das ist eine sehr gefährliche Art von Klassen, die leider immer noch sehr häufig ist in C und C++-Programmen. Und Ason, also der Address Sanitizer, versucht das zu finden, indem er das Ziel instrumentiert. Das ist sehr populär. Wurde für Tausende von Bugs zu finden benutzt in Chrome und Linux. Und es ist etwas langsamer. Slowdown ist ungefähr zweifach. Aber es wird trotzdem benutzt, weil es eben viele Bugs Fehler zeigt. Also die Idee ist, der macht hier rote Zonen um jeden Buffer im Speicher. Hier haben wir also eine Anweisung, die erzeugt einen 4-byte Array auf dem Stack. Und dann erzeugt Ason diesen Buffer und macht eine rote Zone davor und danach. Und jedes Mal, wenn ein Programm eines dieser roten Zonen zugreift, dann stürzt das Programm ab mit einer entsprechenden Fehlermeldung. Das kann man sehr gut benutzen, um Buffer Overflows oder Buffer Underflows zu detektieren. Also zum Beispiel hier versuchen wir, 5 Bytes in einen 4-byte Buffer zu kopieren. Dann prüft Ason jeden der Zugriffe. Und wenn es sieht, dass das letzte Byte in diese rote Zone schreibt, dann löst es ein Fehler aus und sorgt dafür, dass das Programm abstürzt. Den Bug hätte man vielleicht nicht gefunden, wenn man nur nach Crashus sucht, aber wenn man Ason benutzt, dann wird das eben gefunden. Also lass uns kurz über Ason sprechen. Und dann können wir darüber reden, wie man ausführbare Dateien im Kernel fassen kann. Also mit Retro-Ride können wir sowohl die Abdeckungen benutzen als auch Ason. Und um die Abdeckung basierte Variante zu benutzen, müssen wir die grundsätzlichen Blocks erkennen und dann ein Stück Code hinzufügen, das dem Fuzzer sagt hier, du bist jetzt in diesem Teil des Programms und der Fuzzer kann herausfinden, ist das ein neuer Teil des Programms oder nicht. Und Ason kann auch auf diese Art und Weise implementiert werden, indem alle Speicherzugriffe gefunden werden. Und dann benutzt man die Bibliotheklip Ason, die dafür sorgt, dass die roten Zonen hinzugefügt werden und dass die Metadaten aufgehoben werden, die gebraucht werden, um die roten Zonen zu finden und wo die Zugriffe passieren. Und wie können wir das jetzt auf den Kernel anwenden? Also im Kernel zu fassen ist nicht so einfach wie im User Space. Es gibt ein paar Probleme. Das erste ist Crash Handling, also wie man mit Crashes umgeht. Und wenn ein Programm im User Space crashed, dann wird das Programm einfach beendet. Das kann der Fuzzer detektieren und den Input speichern und so weiter. Das ist alles ganz einfach. Aber wenn man das beim Kernel macht, wenn man jetzt den Kernel benutzt, auf der Maschine, die man auch zum Fassen benutzt, dann wird die Maschine einfach irgendwann abstürzen, also nicht mehr zur Verfügung stehen. Und man kann, was viel wichtiger ist, die Crashes auch nicht benutzen, denn der Zugriff zum Fuzzer ist dann verloren. Man weiß nicht, wie der Zustand war, all diese Dinge. Und was die meisten Kernelfuzzer tun, ist, die benutzen eine Art von virtuellem Maschine und benutzen den Fuzzer dann außerhalb der dieser virtuellen Maschine. Und wenn man den Benutzer ein User Space Programm fassen will, da kann man einfach AFL runterladen oder irgendeines der anderen Programme, es gibt fertige Werkzeuge, muss man runterladen, kompellieren und starten. Und wenn man das für den Kernel machen will, dann wird es schon viel komplizierter. Also wenn man zum Beispiel Linux fassen will mit Syscaller, was ein sehr populäres Tool dafür ist, dann muss man Syscaller kompellieren, es gibt eine Spezialkonfiguration, man hat deutlich weniger Werkzeuge zur Verfügung und das ist einfach sehr viel komplexer, sehr viel weniger intuitiv. Und zuletzt geht es um die Determiniertheit. Also wenn man ein User Space Programm hat mit einem einzelnen Thread, dann ist es mehr oder weniger deterministisch. Also es gibt nichts, dass die Ausführung des Programms jetzt beeinflusst. Und das ist großartig, wenn man das reproduzieren will. Denn wenn man einen nicht deterministischen Testfall hat, dann ist es super schwierig rauszufinden, ist das wirklich ein Crash oder sollte man das vielleicht ignorieren? Und im Kernel ist das schwieriger, denn das gibt nicht nur gleichzeitig Ausführung, mehrere Prozesse, man auch interrupts und interrupts können zu jedem Zeitpunkt passieren. Und wenn das einmal hat, mein Interrupt gekriegt, während man den Testcase ausführt und beim zweiten Mal nicht, dann crasht es vielleicht nur einmal, man weiß es nicht so richtig, es ist nichts Schübsch. Also hier haben wir wieder mehrere Möglichkeiten, wie wir daran gehen können. Erst mal können wir das wieder als Blackbox behandeln. Das ist aber etwas, was wir nicht so gerne machen, vor allem weil der Kernel so komplex ist. Man könnte jetzt dynamisch es übersetzen, mit QEMO oder so, es funktioniert. Leute haben das gemacht, aber es ist leider sehr, sehr langsam. Es ist mindestens zehnmal langsamer. Und je mehr Testfälle man ausführen kann, desto mehr Bugs findet man natürlich. Deswegen gibt es, ja, deswegen ist es nicht so toll. Außerdem gibt es nicht so richtig einen Sanitizer für Kernelsachen. Im Userspace gibt es Wellgrind, aber im Kernelspace gibt es nicht so was. Dann könnte man den Intel-Processor-Trace benutzen. Es gibt ein paar Forschungspapers darüber. Das ist ganz praktisch, weil man die Coverage, die Überdeckung mitkriegen kann ohne Overhead quasi. Aber das Problem ist, es braucht Hardware-Support. Das heißt, auf Android oder am DSAPUs oder so funktioniert das nicht. Außerdem kann man das nicht für Sanitisierungen verwenden. Jedenfalls nicht so wie ASAN, das tut ... Also hier kann man keine Zugriffschecks machen. Das heißt, wir wollen wieder statisch das umschreiben. Wir hatten dieses tolle Framework, um ... ... Userspace-Software zu schreiben. Können wir es auch im Kernel machen, haben wir uns gefragt. Das heißt, wir haben das ursprüngliche Raterite genommen und haben das für den Kernel umgeschrieben, für Linux-Module. Und es funktioniert. Und das zeigt uns, dass der Ansatz nicht nur für ... ... Userspace-Programme, sondern auch für Module funktioniert. Das Gute über Kernel-Module ist, dass sie positionsunabhängig sind. Es gibt nicht so was wie Kernel-Module an einer festen Adresse. Aus dem sind Linux-Module auch eine besondere Art von Elf-Dateien. Aber sie sind ähnlich, nicht genau gleich, aber wir konnten trotzdem große Teile unseres Symbolisierers neu verwenden. Und damit konnten wir wieder Code-Überdeckungen implementieren und Adress-Sanitisierung. Die Idee war hier, dass wir uns mit integrierten ... ... uns mit bereits existierten Tools integrieren wollen. Wir wollten keinen eigenen Fasser schreiben. Im Userspace gab es irgendwie AFL-basierte Coverage Tracking. Und das Gleiche wollten wir jetzt für den Kernel machen. Und die Idee war jetzt, dass wir K-Cov benutzen, was direkt im Linux-Kernel eingebaut ist. Und wir wollten das jetzt selber benutzen. Wir haben unsere Überdeckungsinstrumentierung jetzt so gebaut, dass es mit K-Cov interagiert. Dafür bräuchten wir den Source, aber das ist ja der Linux-Kernel. Aber der ist ja Open Source, das ist kein Problem. Für Module wäre es ein Problem, aber wir wollten ja das gerade beim Linux-Kernel machen. Dann brauchten wir den Überdeckungssammler am Anfang jedes einfachen Blocks. Hier zum Beispiel sehen wir drei kleine Blöcke. Und jetzt müssen wir zu jedem dieser Blöcke einen kleinen Call hinzufügen, der den einfachen Block traced. Das ist relativ einfach, das funktioniert dann. Wie haben wir jetzt binären Assern implementiert? Hier haben wir erstmal, also erstmal mussten wir an die relevanten Stellen Daten schreiben, die man zur Laufzeit benutzen können. Im Kernel können wir dann nicht zu LibAsern linken. Das heißt, wir müssen das ein bisschen anders machen. Wir bauen den Kernel einfach mit K-Asern. Und damit haben wir den Vorteil, dass wir direkt mit existierenden Tools integrieren können. Und damit können wir mit Cyscaller oder so ähnlich integrieren. Und das sehen wir definitiv als ein großes Plus. So wie implementiert man jetzt Assern? Naja, wir müssen jeden Speicher zu Griff nehmen und ihn instrumentieren mit einer Abfrage. Falls ein Check failed, dann müssen wir einen Backreport schreiben und den Kernel crashen. Das kann gut mit K-Asern integrieren, das finden wir gut. Dieses umgeschriebene Modul kann dann mit dem Kernel geladen werden und mit einem Standard Kernel Faser gefasst werden. Also, wir haben es mit Cyscaller probiert, damit funktioniert es relativ gut. Also, das ist soweit unsere Geschichte. Und jetzt wollen wir ein paar Experimente zeigen, um zu zeigen, ob es wirklich funktioniert. Was Userspace angeht, wollten wir die Performance vergleichen mit Source Assern und mit existierenden Lösungen, die auch mit Bineer-Dateien arbeiten. Also, da kann man Walgrain benutzen. Das ist ein Memory Sanitisierer, der mit Bineries funktioniert und wir probieren es auch mit Source Assern und eben vergleichen das mit unserem Retro-Ride Assern. Und für den Kernel wollten wir ein paar Datei-Systeme und Treiber mit Cyscaller-Fasen, und zwar sowohl mit der Quelltext-basierten K-Assern als auch mit K-Coff und Retro-Ride. Und das sind die Ergebnisse für den Userspace. Der rote ist Walgrain. Man sieht, das ist die langsamste Variante. Das braucht drei, zehn, dreißig Mal Overhead. Grün ist unsere Assern, der Bineries benutzt. Orange ist der mit dem Quelltext. Und blau ist der Originale. Man sieht, dass man für die source-basierten Assern, also für Quelltext-basierten Assern zwei bis dreimal Biner ist etwas langsamer, aber es ist recht nah dran. Was den Userspace angeht, das sind die ersten Ergebnisse für den Kernel. Das sind Ergebnisse, die mache ich quasi aktuell für meine Masterarbeit. Das ist also noch in Arbeit. Und wir sehen, die Ergebnisse sind etwas langsamer. Das ist ein reiner CPU-Benchmark, der interagiert mit dem System gar nicht so viel. Und jegliche Instrumentatierung wird heftig einschlagen. Aber wenn man jetzt ein Datei-System anschaut, dann muss nicht jeder Systemcall vom System runter bis in das Haussystem. Und da müssen verschiedene Layer-Abstraktionen durchschritten werden. Das braucht eine ganze Menge Zeit. Also in Praxis ist der Overhead sehr machbar. Also, wir wissen, dass ihr Demos mögt. Deswegen haben wir eine Demo für K-Retro-Ride vorbereitet. Also schauen wir uns das mal an. Also wir haben ein kleines Kernelmodul vorbereitet. Und dieses Modul ist ganz, ganz einfach. Es enthält eine Schwachstelle. Und was es macht, ist, es erzeugt eine Character-Device. Also wenn ihr das nicht kennt, das ist eine Datei. Sieht aus wie eine Datei. Und man kann da lesen und schreiben. Und das geht aber nicht wirklich in eine Datei, sondern was ihr in dem Fall dahin schreiben könnt, geht an den Treiber und wird von dieser Datei, von dieser Funktion Demo-Ride hier gehandhabt. Und da wird ein 16-byte-Buffer allokiert. Und dann wird geprüft, ob die Daten den String 1337 enthält. Und wenn es das tut, dann hat man hier einen, schreibt er hier an das falsche Byte. Nämlich hier 16, das ist ein 16-byte-Buffer. Das ist also quasi ein Byte daneben. Und wenn nicht, dann schreibt er es an die richtige Stelle. Und wir können diesen Treiber jetzt also kompilieren. Also wir haben jetzt hier unser Modul. Und das wird jetzt mit K-Retro-Ride instrumentiert. Also das wurde jetzt hier bearbeitet. Und hat ein instrumentiertes Modul und ein Assembler-File vom Symbolizer. Das schauen wir uns jetzt gerade mal an, wie das aussieht. Ist das groß genug? Ich denke schon, hier können wir die ASIN-Implementierung sehen. Die Instrumentierung. Und der originale Code lädt Daten von dieser Adresse und die ASIN-Implementierung berechnet erst die Adresse und prüft dann Metadaten, die gespeichert wurden, um zu prüfen, ob die Adresse in der roten Zone sind oder nicht. Und wenn das fehlschlägt, dann wird diese Funktion ASIN-Report aufgerufen, wo man dann ein Stacktrace bekommt. Das ist also alles super. Wir können uns jetzt auch das Disassembly der beiden Module angucken. Also hier Demo. Nein, so geht es nicht. Okay, also links haben wir das Original-Modul ohne Instrumentierung und auf der rechten Seite haben wir das Modul instrumentiert mit ASIN. Und wie ihr seht, hier ist das Original-Modul, hat hier pushed R13 und dann den Speicher-Laden. Und auf der rechten Seite hat hier K-Rature-Write die ASIN-Implementierung. Der originale Load ist immer noch hier unten. Aber zwischen der ersten und der dritten Instruktion sehen wir jetzt eben diese Prüfung. Also wir können es jetzt testen und mal sehen, was es tatsächlich tut. Wir buten jetzt dieses minimalistische, ganz einfache Linux-System und schauen uns erst mal die Schwachstelle an. Und wir sehen zuerst mit dem nicht-instrumentierten Modul wird der Kernel nicht crashen, aber mit dem instrumentierten Modul wird der Kernel crashen und ein Back-Report generieren. Also das ist eine QEMO-VM. Ich weiß nicht, warum die so lange braucht, um zu buten. Die Demo-Götter sind da wohl schon dran. Wir müssen jetzt wohl leider einfach warten. Also, wir haben jetzt das Modul geladen. Das hat die Fake-Datei auf Dev-Demo erzeugt. Jawohl, da können wir jetzt also Daten reinschreiben. Jawohl. Und dieser Zugriff ist innerhalb der Grenzen, deswegen ist alles gut. Jetzt schreiben wir 1, 3, 3, 7 rein. Das wird jetzt also hier eine Byte daneben schreiben. Und da kommt jetzt ein schwachsinniger Wert raus. Jetzt laden wir das instrumentierte Modul stattdessen und machen dasselbe Experiment noch mal. Okay. Also ihr seht, Dev-Demo existiert noch. Das Modul funktioniert noch. Jetzt versuchen wir mal 1, 2, 3, 4 da reinzuschreiben. Das sollte nicht crashen, genau. Aber wenn wir jetzt 1, 3, 3, 7 reinschreiben, das erzeugt einen Fehler-Report. Das hat jetzt eine ganze Menge Informationen. Wir sehen an welcher Stelle das der Speicher alluziert wurde. Wo der Stacktrace ist, wurde nicht gefreet. Also man sieht keinen Stacktrace fürs Free. Wir sehen das die Cashgröße. Das Speicher sind 16 Byte alluzierung. Wir sehen die entsprechende Adresse des Speichers. Wir sehen hier die 2 Nullen. Da gibt es also 2, 8 Bytes. Und das FCFC, das ist die Rotenzonen, über die ich vorhin gesprochen habe. Jetzt gehen wir also wieder zurück. Ich hoffe, ihr hattet da Spaß dran. Also nachdem wir das auf dieses Demo-Modul angewendet hatten, wollten wir auch mal sehen, was passiert, wenn wir das auf ein echtes Datei-System anwenden. Und nach ein paar Stunden, als wir zurückgekommen sind, um die Ergebnisse anzuschauen, haben wir ein paar Probleme gesehen. Inklusive ein paar Use After Free-Lese-Zugriffe, Use After Free-Schreib-Zugriffe. Wir haben die Bug-Reporte uns angeschaut und haben eine ganze Menge von Linux-Kernel-Problemen gesehen. Eines nach dem anderen auf diesem Modul, von dem ich jetzt nicht sage, welches es war. Wir sind gerade dabei, dir das zu der Rückmeldung zu geben. Das ist noch dabei, das repariert zu werden. Deswegen haben wir das hier unkenntlich gemacht. Aber wie ihr seht, es gibt einige Gelegenheiten im Linux-Kernel, wo man gezielt das Fuzzing bei bestimmten Modulen benutzen kann. Und diese K-Ason-instrumentierten Kernel benutzen und dann da interessante Kernel-Zero-Debux finden. Ja, das könnt ihr dann weiterentwickeln oder wie auch immer ihr wollt benutzen. Jetzt haben wir euch gezeigt, wie man solche Binärmodule nehmen kann und wo man oder einfach irgendwelche Module, wo man bloß das Modul selber fassen möchte und nicht den ganzen Linux-Kernel. Und dann kann man, wie wir gezeigt haben, das dann darauf machen. Das funktioniert mit Überdeckungen, Instrumentierungen oder ASA-Instrumentierungen. Aber ihr könnt dort noch ganz viele andere Instrumentierungen hinzufügen. Wir haben hier bloß so eine Framework geschrieben. Gut, jetzt haben wir diese Reise gemacht von Instrumentieren, Überdeckungen, Analysation, Sanitisierung. Und am Ende haben wir Crashes im Kernel gefunden. Lass mich noch den Vortrag zusammenfassen. Das ist, was wir bei Hexhive machen, bei EPFL. Also falls ihr euren Doktor machen möchtet oder danach, dann redet ruhig mit mir. Die Framework ist schon Open Source. Es gibt noch ein paar Demos, an denen wir arbeiten, sodass ihr ein bisschen schneller loslegen könnt. Die Userspacearbeit ist schon verfügbar. Die Kernelarbeit werden wir auch in ein paar Wochen veröffentlichen. Das erlaubt euch, dieses Revo-Headzeug für Fuzzing zu benutzen, um schnelles effektives Fuzzing durchzuführen und Bugs zu finden. Das, was ihr mitnehmen sollt von dem Talk, ist, dass Rhetorite und K-Rhetorite statisch euch umschreiben lässt, sodass ihr wenig Kosten bei der Laufzeit habt. Wir machen das nur mit positionsunabhängigen Bineries. Das heißt, nicht alles. Aber damit kann man trotzdem große Anwendungen umschreiben. Das heißt, man kann nur bei großen Anwendungen nur kleine Teile fassen. Das heißt, wir können jetzt bekannte Tools neu benutzen, weil unser Tool sich sehr gut integriert mit anderen Tools. Unser Tool ist Open Source. Schaut es euch an. Es sagt uns, wenn es kaputt geht, wir finden Open Source toll. Zeit für Fragen. Das war die deutsche Übersetzung des Talks. Kein Quälttext, kein Problem. Schnelles Fuzzing von Ausführbahndateien von N-Space und Ganimo, eure Übersetzer waren Eibwen und Tribut. Wir haben jetzt noch Fragen aus dem Publikum. Falls ihr Feedback für die Übersetzer habt, dann schreibt uns eine E-Mail oder benutzt den Hashtag C3T. Vielen Dank für den Talk. Ich habe nicht ganz verstanden, was der Nutzfall von dem Kernel Fuzzing jetzt war. Wenn ihr keinen Source Code für den Kernel habt, wie Internet of Things oder Android, ihr habt dort einfach K-Cov oder KSN genommen in Android oder bei Internet of Things, hat man diesen Source Code nicht so, dass man es sich neu kompellieren hat. Das heißt, habt ihr Ideen, wie man den Kernel selber instrumentieren könnte? Also, darüber haben wir nachgedacht. Ich denke, es gibt noch ein paar zusätzliche Probleme, die wir da lösen müssen, um in der Lage zu sein, den kompletten Kernel zu instrumentieren. Von der Tatsache mal abgesehen, dass es uns Kompatibilitätsmöglichkeiten gibt, ist der Grund, warum wir K-ASIN und K-Cov benutzt haben. Du musst daran denken, wenn ihr den kompletten Speicheralluzikation implementieren will, was sehr kompliziert ist. Man muss die Ausnahmenhändler selbst instrumentieren. Man muss Fehler erkennen, die die Instrumentierung findet. Man muss Speicher zur Verfügung stellen für den Shadow-Bereich des ASINs. Ich denke, es wäre möglich, aber es braucht sehr viel zusätzliche Arbeit. Das war eine Arbeit von 4 Monaten. Deswegen haben wir gedacht, wir machen jetzt erst mal Module im Kernel für Module, und zukünftige Arbeit kann sich dann auf die kompletten Kernel konzentrieren. Und was Android angeht, was Linux angeht, der Kernel ist ja GPL. Das heißt, wenn der Hersteller einen Kernel manipuliert, dann sollte er auch den Quelltext zur Verfügung stellen. Nein, das tun Sie nie. Ja gut, deswegen frage ich ja, ob wir das in der wirklichen Welt machen. Dann will ich hier vielleicht die Perspektive mal aufzeichnen, was wir gemacht haben ist, wir haben existierende Tools wie K-ASIN oder K-Cov benutzt und da integriert. Aber jetzt, um Heap-Alluzierung zu machen, das ist relativ einfach, man kann auch zusätzliche Red Zones-Rote-Zonen einfügen. Das kann man selber auch gut machen, wenn man die verschiedenen Allokatoren sich hernimmt. Die zweite Sache ist, einfach einen Ups im Kernel zu erzeugen und den Stacktrace zu erzeugen. Das ist auch relativ einfach. Man muss das zusammenbauen, da muss man Ingenieursleistung erbringen, um das zu tun, um diese K-ASIN-kompilierten Kernel zu erzeugen. Aber wir denken, es ist wirklich sehr einfach, um Rücksicht zu nehmen auf die Zeit, haben wir uns auf diese K-ASIN-Kernel konzentriert. Eine gewisse ASIN-Implementierung ist hier schon aktiviert. Da muss man noch Ingenieursleistung erbringen, aber es gibt auch eine ganze Community, die da mal helfen kann. K-Retro-Rite und Retro-Rite sind diese Plattformen, die Binär-Dateien umschreiben kann, in ein Assembly-File, was man dann instrumentieren kann und auf andere Passes darauf auszumachen. Da könnte man auch einen kompletten ASIN-Passer aufbauen, der dann noch zur Verfügung gestellt wird. Es gibt noch eine Frage über die Folie mit dem Diagramm, wo die instrumentierte Version schneller war als die nicht instrumentierte. Warum war das so? Ja, Cash-Effekte. Danke für den Vortrag. Wie viele Architekturen unterstützt ihr? Aktuellen sind x86, 64. Also kein Arm und kein MIPS? Also Pläne gibt es schon. Es gibt nur eine gewisse Endliche Menge von Zeit. Wir haben uns auf die Technologie konzentriert. Arm ist ganz oben auf der Liste. Wenn jemand daran arbeiten will, dann würden wir uns freuen, davon zu hören. Unsere Liste ist Arm als allererstes, aber dann andere Sachen. Aber x86, 64 und Arm, da haben wir dann die meisten interessanten Plattformen abgedeckt. Zweite Frage. Habt ihr mal versucht, irgendwelche Close-Source-Programme gefasst? Im Datei-System habt ihr gefasst, aber das war ja Open Source. Das hätten wir mit Cisco machen können. Für die Auswertung wollten wir zwischen der source-basierten und der binär-basierten Implementierung vergleichen. Und deswegen haben wir uns hauptsächlich Open Source-Datei-Systeme und Treiber angeschaut, denn da konnten wir die auch mit dem Compiler instrumentieren. Wir haben das jetzt noch nicht gemacht, da auch ein Close-Source-Treiber uns anzugucken. Da gibt es ja sehr viele, für Grafikkarten oder so weiter. Vielleicht gucken wir uns das mal an und finden dann ein paar Probacks noch. Aber mit Cisco hat man immer noch ein Problem. Da muss man Regeln schreiben. Man muss wissen, wie man mit dem Treiberkommuniziert. Ja, das ist richtig. Und da gibt es Unterstützung für Close-Source-Datei-Systeme, die wir uns anschauen wollen. Vielen Dank für den Talk. Gibt es K-Cov oder KS-Sachen für Windows? Habt ihr mal versucht, das auf Windows zu machen mit dieser Framework? Ich könnte mir vorstellen, dass es relativ herausfordernd wäre, das zu machen. Aber ich habe mich gefragt, ob ihr das vielleicht mal probiert habt. Wir haben uns da Gedanken gemacht und haben uns dagegen entschieden. Windows ist echt schwierig. Wir sind Akademiker. Die Forschung, die ich mache, die wir machen bei uns, da geht es hauptsächlich um Open-Source-Software und soll Open-Source-Software stärker machen und Windows-Supporten. Das ist ein gewisserweise Out-of-Scope. Da konzentrieren wir uns nicht drauf. Wir arbeiten gerne mit Leuten zusammen, die das machen wollen, dass das großartige Wert für die Forschung bringt. Dann müssen wir ein gewisserweise ein Kompromiss finden, wenn ihr in der Lage seid, das zu bezahlen, dann gerne, aber ansonsten. Ihr redet sowohl über kernel als auch über userspace, oder? Ja, ganz genau. Vielen Dank für den Talk. Das sieht interessanter aus, wenn man über Bucks in Close-Softs kernel-Modulen sucht. Das heißt, das kann man benutzen, wenn man in einem eigenen Close-Softs... Was passiert, wenn die Leute das binäre kernel-Modul schreiben, was dagegen tun wollen? In dem man zum Beispiel zwei Funktionen voneinander absteht. Wir haben jetzt in dem Paper dieses erstmal ausgenommen, das Code, der explizit versucht, das zu umgehen. Das ist ein gewisserweise orthogonal dazu, dass man diese Obfuscation, diese Unkenntlichmachung da entfernen muss. Das ist natürlich eine Einschränkung. Aber nicht necessarily obfuscated, zumindest von dem, was wir gesehen haben, wenn wir die binary-only-only-Modul schreiben. Wie entscheidet ihr euch, wie ihr die RedZones baut? Ich habe gehört, ihr instrumentiert den Allocator, aber es passiert auch viel auf dem Stack. Wie macht ihr das? Das ist tatsächlich super cool. Ich verweise da auf unser Paper. Das ist auch im Github-Repo. Wenn ihr über moderne Compiler nachdenkt, die benutzen Kanarienvogel, wisst ihr, wie Canaries funktionieren auf dem Stack? Wenn der Compiler einen Puffer sieht, dann fügt ihr da einen Canary hinzu zwischen dem Puffer und irgendeinem anderen Daten. Und wir mit unserem Analysetool fügen diesen Stack-Canary hinzu und packen unsere RedZones an diese Stelle, wo der Stack-Canary war. Und hacken da in gewisser Weise die ASN-Rotenzonen an diese Stellen. Wir setzen da quasi darauf auf, was der Compiler schon für uns gemacht hat und können das benutzen, um zusätzlichen Vorteils auszuziehen. Noch eine Frage aus dem Internet? Ja. Habt ihr überlegt, das erstmal zu LLVM Intermediate Representation weiter zu machen anstatt Assembly? Ja, also etwas längerer Antwort ist, ja, wir haben uns darüber nachgedacht, das auf LLVM IR hochzuheben. Das ist super schwierig. Wir haben uns das angeschaut. Das ist sehr komplex. Es gibt keine direkte Abbildung und LLVM IR. Man müsste trotzdem die ganzen Typen reproduzieren. Also irgendwie dieser magische Traum, dass man den kompletten LLVM IR-Code zurückbekommt. Aber das ist halt sehr, sehr schwer, denn wenn man von IR nach Maschinencode kompiliert, dann wird sehr, sehr viel Information verloren und wir müssten eine Möglichkeit haben, diese ganze Information zurückzuholen. Und das ist im Wesentlichen unmöglich und unentscheidbar für sehr viele Fälle. Eine Notiz anzubringen. Wir kriegen nur Kontrollfluss zurück. Dafür Datenreferenzen haben wir keine Unterstützung für Instrumentierung, weil die sind ein unentscheidbares Problem, das wir sehen. Da kann ich auch noch ein bisschen, gerne offline noch ein bisschen darüber reden oder es gibt auch eine Anmerkung im Paper dazu. Es ist also nur ein kleines Problem, wenn ihr da auf Assemblerproblemen, wenn ihr zu IR erzeugen wollt, dann müsste man Ende zu Ende Typen rekonstruieren. Das wäre super toll und das ist leider unentscheidbar und sehr, sehr schwierig. Man kann eine ganze Menge Charakteristika rekonstruieren, aber es gibt keine Lösung, die das 100% korrekt macht. Noch eine weitere Frage von Mikrofon 6. Vielen Dank für den Vortrag. Was für Disassembler habt ihr für Retrovide benutzt? Habt ihr Probleme mit falschen Disassembly gehabt? Falls ja, wie habt ihr das, wie seid ihr damit umgegangen? Ja, also für Retrovide haben wir Capstone benutzt zum Disassembling. Das ist übrigens ein großartiges Tool. Und die Idee ist, dass man eine gewisse Menge an Informationen braucht, darüber, wo die Informationen sind. Für die Kernelmodule ist das relativ einfacher, denn Kernelmodule bringen diese Informationen mit, denn der Kernel muss in der Lage Stack Traces zu erzeugen. Für andere Binarys ist das weniger üblich, aber man kann ein anderes Tool benutzen, um Funktionen zu identifizieren und den Körper von einer kompletten Funktion zu finden. Wir hatten einige Probleme, was mit AT&T-Syntax zusammen hingen. Wir wollten den Knurassembler benutzen, um das wieder zu assemble am Ende. Und einige Instruktionen kann man, also die selbe Instruktion, zwei verschiedene Instruktionen, fünfbyte Knop und sechsbyte Knop kann man durch denselben String benutzen. Aber der Kernel mag das nicht und crusht dann. Der Kernel benutzt das dynamisches Binea Patchen und benutzt dafür feste Offset. Das heißt, wenn man einen fünfbyte Knop oder mit einem sechsbyte Knop ersetzt oder umgekehrt, dann ändern sich die Offsets und der Kernel explodiert euch ins Gesicht. Könnt ihr mir erzählen, was wir die verschiedenen ... Falls es irgendwie eine Instruktion gibt, die vom Disassembler nicht unterstützt ist, zum Beispiel wenn es vom Disassembler falsch disassembledeht wurde. Was habt ihr dann gemacht? Ich kann mich jetzt nicht erinnern, dass wir unbekannte Instruktionen hatten im Disassembly. Da hatten wir, glaube ich, kein Problem damit. Aber es war ansonsten ganz viel Ingenieursarbeit. Das Problem war also kein Bug im Disassembler, sondern ein Problem des Instruktionsformat, dass dieselbe Mnemonic in zwei verschiedene Instruktionen übersetzt werden kann. Eine fünf- oder sechsbyte Log, die ist exakt dieselbe Assembler Mnemonic, und das hat ein Problem, also mit Assembli zu tun. Aber ihr habt kein Problem mit ununterstützten Mnemonics gehabt? Nein, hatten wir nicht. Okay. Wie sieht es mit eurer binären Instrumentation aus? Ist es genauso mächtig wie KIS? Hat es auch Corruption auf Global, Stack und Heap erkannt? Keine Global, es schaut sich den Heap auch komplett an. Aber es gibt Variationen, was den Stack angeht. Wir müssen uns diese Canaries benutzen als Basis. Es gibt keine Reflowen oder komplette Rekonstruktion von Datenlayouts. Wir müssen uns hier schon existierende Compilerfeatures benutzen, wie Stack Canaries. Wir unterstützen keine Overflows innerhalb des Objekts auf dem Stack, aber wir schauen uns die Stack Canaries an, um einige Vorteile auf dem Stack zu kriegen. Das ist, ich weiß nicht, wie viel, 90, 95 Prozent auf dem Weg zur Lösung. Wir kriegen da auf dem Heap sehr präzisionen, aber für Globals haben wir ganz wenig Support. Noch viel Applaus für die Fronten.