 Unser heutiger Redner ist Emil. Er würde uns etwas erzählen, also eine kleine Einführung geben in die Binary-Exportation, denn er kommt doch aus dem Sicherheitsbereich. So, ja, hallo. Also erst mal, ich erzähle euch jetzt was zum Buffer-Wuffalo und wie wir von einem Buffer-Wuffalo zur Nachschel kommen. Und wir hatten das ja eben in dem Vortrag von Liam und Martin, dass sie auch irgendwie so eine kleine Demo gezeigt haben, zu wie so eine Challenge aussehen kann. Und das Ganze, das geht natürlich irgendwie nicht unglaublich gut in so einem 30-Minuten-Slot, weil es zeitlich begrenzt, deshalb habe ich jetzt einen wunderbar großen 30-Minuten-Slot, indem ich mich komplett darauf fokussiere. Das Ganze geht in die Richtung Binary-Exportation. Das heißt, wir wollen tatsächlich ein Programm kaputt machen, bzw. nicht kaputt machen, sondern aktiv ausnutzen. Kaputtusprogramm hilft uns nicht, wir wollen das nutzen. Und dafür nutzen wir einen sogenannten Buffer-Wuffalo. Der komplette Talkie ist sehr, sehr für Einsteiger bestimmt. Das heißt, wenn ihr noch nie irgendwas in die Richtung gemacht habt und solltet ihr am Ende wenigstens aus diesem Talk rausgehen können und sagen können, okay, ich habe jetzt grundlegend verstanden, wie das funktioniert und was da eigentlich abgeht. Denn es passiert immer wieder, dass ich mit Leuten auf Veranstaltungen sitze und genau dieses Thema den Leuten erkläre. Und deshalb erzähle ich euch das jetzt allen einmal gemeinsam. Denn dann muss ich das nicht von euch einmal einzeln erklären. Also, ich habe das Ganze so ein bisschen unterteilt in die ganzen grundlegenden kleinen Aspekte, die einfach benötigt werden. Weil wir wollen nicht direkt einfach einsteigen und nicht zwar stehen, wir wollen erstmal so ein bisschen Wissen aufbauen. Deshalb wollen wir uns zum Beispiel erstmal angucken, was eigentlich die Grundlage dafür sind. Ich habe für diese vielen kleinen Teilbereiche viele kleine Sachen weggelassen. Wir wollen uns erstmal auf das Wesentliche konzentrieren. Wenn ihr schon mal irgendwie sowas gemacht habt und feststellt, hey, da fehlt ja irgendwie ganz viel, nicht wundern, das ist beabsichtigt. Insgesamt, wenn ihr am Ende feststellt, ihr seid komplett verwirrt, redet mit den Leuten. Es gibt hier genug Leute, die sich relativ intensiv damit beschäftigt haben und euch dabei weiterhelfen können. So, also Speicher. Irgendwie haben wir Arbeitsspeicher in unserem Computer. Da liegen die ganzen Programme drin und auf denen wird irgendwie Aktion ausgeführt, wenn man das so grob sagen kann. So, Speicher ist irgendwie nummeriert. Wir wollen irgendwie sagen können, an dieser Stelle liegt diese Datei im Speicher, an dieses 0x, fff, irgendwas. Ich habe jetzt vier F-Star hingeschrieben, einfach mal das Beispiel. Das kann deutlich mehr sein, abhängig davon, wie viel Speicher in diesem System entsprechend vorhanden ist. So, in diesem Speicher liegt irgendwo unser kleines Programm, was wir ausführen. Das habe ich jetzt mal hier so reingemalt, das ist erstmal egal wo. Aber wir können uns vorstellen, da liegt das Programm, das fängt irgendwo an und das geht bis irgendwo. Erstmal egal wo. So ein Programm besteht aus vielen kleinen Segmenten. In diesen kleinen Segmenten stehen die verschiedensten Sachen drin. Zum Beispiel der eigentliche Code, der ausgeführt wird. Dann haben wir zum Beispiel einen Hieb für, falls wir irgendwie Speicher alluzieren wollen und sagen, okay, ich habe jetzt zum Beispiel Input von einem Nutzer bekommen und möchte den irgendwo speichern. Aber das ist noch nicht einfach per Default in den Programmen quasi eingebacken. Und dann haben wir zum Beispiel hier unten unseren Stack. Da kommen wir gleich drauf zu so sprechen. Und noch ein paar andere Sektionen. Also das Ganze ist so in Sektionen aufgebaut. Und wir wollen uns für heute einfach erstmal den Stack anschauen. Weil so ein Stack den brauchen wir, wenn wir zum Beispiel Funktionen aufrufen wollen. Da kommen wir auch gleich drauf zu sprechen, wie genau das eigentlich funktioniert, weil das ist tatsächlich relativ relevant. Ich habe das jetzt hier mal so aufgebaut, das heißt auf so einem Stack, das ist einfach quasi wie so ein Haufen Teller. Und da können wir entsprechend zwei Operationen aufführen, nämlich Sachen draufschieben und Sachen runternehmen. Da stehen dann ganz viele Informationen drin. Ich habe jetzt hier so ein paar reingemalt. Zum Beispiel könnte man jetzt sagen, okay, wir wollen jetzt ein Vier-Eck malen und um dieses Vier-Eck zu malen, müssen wir erst eine Linie malen, dann die nächste Linie, dann die nächste Linie, dann die nächste Linie. Und so kann das aussehen. Das heißt, wir haben mehrere Schichten. Wir haben eine Schicht mal das Vier-Eck und eine Schicht mal irgendwie eine Linie. Das ist jetzt sehr abstrakt, aber ihr werdet wahrscheinlich gleich irgendwie so ein bisschen besser sehen, warum das eigentlich so ist bzw. wofür wir das jetzt eigentlich brauchen. Als nächstes haben wir Register. Speicher ist wunderschön, aber Register sind a quickly accessible location available to a computer's processor. Und Quote, was ich irgendwo gefunden hatte. Es gibt einen großen Haufen Register, aber viele dieser Register sind erstmal für uns irrelevant. Für den Anfang reicht es erstmal zu merken, wir haben so ein paar Register, die genutzt werden, um die Argumente für Funktionen zu übergeben. Das heißt, wenn wir zum Beispiel ein wiederes Beispiel von so einem Rechteck aufgreifen, könnten wir sagen, okay, wir wollen ein Rechteck malen mit irgendwie einer bestimmten Kantenlänge, dann müssen wir diese Kantenlänge irgendwie, irgendwo hin übergeben, zum Beispiel in die Funktion, hey, wir wollen eine Linie malen. Und das können wir entsprechend über die ersten Argumente machen. Die ganzen Calling Conventions nutzen als erstes die Argumente, aber sie sind auch teilweise komplett verschieden, habe ich nicht davon, wo ihr seid. Am Ende für eine Funktion wollen wir irgendwas zurückgeben, zum Beispiel irgendwann Wert. Und dafür gibt es das RAX Register, das RAX Register, das ist erstmal unser Return Value, das haben Leute einfach erstmal so definiert. Und dann haben wir noch den REP, also das REP Register, das ist da sogar um unseren Instruction Point zu nutzen. Denn wir haben ja gesagt, wir haben irgendwie unsere Programme in so Speicher. Und irgendwie müssen wir noch irgendwie tracken, wo denn wir jetzt in diesem Speicher sind und was denn als nächstes ausgeführt werden soll, weil ansonsten hätten wir ja Speicher und keiner weiß genau, wie das passiert. Das heißt, wir haben ein Register, das REP, das ist der Instruction Point, der zeigt da irgendwo hin und sagt, ab hier wird ausgeführt, beziehungsweise das ist unsere nächste Instruktion. So, Computer haben sich über die letzten Jahre weiterentwickelt. Das ist das, was jetzt hier in diesem rechten Block zu sehen ist. Und zwar früher hatten wir weniger Speicher und dann haben Leute festgestellt, hey, wir können irgendwie aus 8 Bit, 16 Bit machen, aus 16 Bit, 32 Bit und aus 32 Bit, 64 Bit. Dafür gibt es verschiedene Konventionen, um diese Register zu benennen. Zum Beispiel seht ihr hier unten einmal das AH und AL Register. Das ist von dem A Register, der High Teil und der Low Teil. Und zum Beispiel vom AX Register irgendwie, das heißt dann einfach AX, da hat man das High und Low erstmal weggelassen. Und dann gibt es das EAX, das E steht dann für 32 Bit und das Gleiche für 64 Bit mit R. Das heißt, wenn ihr irgendwo seht, irgendwie so ein R, dann steht das dafür, dass das 64 Bit Register sind. Die ausentastend 32 Bit von dem 64 Bit Register, also von dem RAX Teil da ganz oben, das ist das EAX Register, deshalb steht es da unten. Und die zwei wesentlichen Register, die wir uns gleich noch anschauen wollen, sind das Base Pointer Register, das ist der, der unten auf den Stack zeigt und der Stack Pointer, das ist RSP, der zeigt oben auf den Stack. Damit tracken wir, wo auf dem Stack wir denn jetzt eigentlich Elemente haben und auf das Oberstelement im Stack irgendwie verweisen wollen, können wir sagen, das ist das Element, wo der Stack Pointer drauf zeigt. Das heißt, in diesem Register steht, hey, an dieser Adresse, das ist das Oberste im Stack. Mit diesen beiden Sachen können wir uns ein Stack irgendwo definieren. Das heißt, wir haben ja eben hier in Memory gesehen, dass der Stack irgendwo in dieser Beine liegt. Das Ganze ist durch den Base Pointer und den Stack Pointer definiert. So, wo wir jetzt beim Stack sind, können wir uns eigentlich mal kurz angucken, wie so ein Stack funktioniert. Wir haben zum Beispiel das, was ihr da in der Mitte seht, wo das A drin ist. Das ist ein Wert auf dem Stack. Aktuell zeigt unser Base Pointer darauf und unser Stack Pointer. So, der Base Pointer sagt einfach, das ist der unterste Wert auf dem Stack. Der Stack Pointer sagt, hey, das ist gleichzeitig der Oberste Wert, weil wir aktuell nur einen Wert haben. Unten habe ich euch noch so ein paar Register drunter gemalt. Zum Beispiel ganz links haben wir das RAX Register, dann das RWX und RCX Register. Die sind aktuell erstmal leer, beziehungsweise vielleicht steht da irgendwas drin, aber das ist erstmal irrelevant. Das heißt, wir haben die Sachen auf den Stack pushen. Heißt, wir haben Werte und die wollen wir da draufpacken. Zum Beispiel können wir sagen Push A, dann ist das, was ihr hier gerade seht, nämlich dann ist da der Wert A. Die nächste Instruktion Push B, würde einfach den Wert B auf den Stack drauf pushen. Das heißt, ja, jetzt ist der da. Der Stack Pointer zeigt jetzt, ob der Oberste Wert der Base Pointer auf den untersten. Wenn wir noch einen weiteren Wert drauf pushen, geht der Stack für ihr heute immer eins höher. Also könnt ihr euch das vorstellen, wie so ein Haufen Teller. Jetzt können wir da genau das Gegenteil machen. Wir können eben sagen, nimm mal wieder Werte vom Stack runter, das geht mit Pop. Das heißt, wir können Pop RAX sagen und dann popt er den Obersten Wert auf dem Stack, da wo der Stack Pointer gerade hin zeigt, in das RAX Register und verniedrig den RSP Register um eins, dann zeigt er auf RBP, auf das B. So. Das Gleiche können wir jetzt mit dem nächsten Wert machen. Zum Beispiel wollen wir jetzt den Wert B in RBX poppen. Dann wird der Oberste Wert auf dem Stack in das entsprechende RAX Stack poppen und auf gleiche können wir auch mit A machen. Am Ende haben wir keine Werte mehr auf dem Stack und das ist etwas doof. Also ich glaube, der Stack ist gar nicht so, dass es komplizierter an dem Ganzen ist, aber es ist gut, sich nochmal zu verbildlichen, wie das Ganze funktioniert. Wenn wir so Programme haben und das werdet ihr irgendwie feststellen, wenn ihr irgendwie das Kit CTF spielt bzw. das GPN CTF jetzt hier, dann am Ende so ganz wilde Grafen und so zu euch und es sieht so super verrückt aus und ich glaube, das schreckt sehr viele Leute ab. Genau deshalb möchte ich euch eigentlich zeigen, dass das alles gar nicht so wild ist. Das sieht jetzt super kompliziert aus, aber letztendlich, das sind auch einfach nur so ganz kleine Instruktionen, die uns sagen, an dieser Stelle irgendwie im Speicher wollen wir jetzt zum Beispiel diesen Einwert dahin verschieben oder irgendwie diesen Wert inkrementieren und so weiter. Dafür gibt es so ein paar kleine, sogenannte Memonics, damit wir das auch lesen können. Die können wir halt schlecht lesen, also die wenigsten Leute irgendwie können sagen, oh, da steht jetzt C3, das ist jetzt irgendwie ein Rett oder was auch immer. Deshalb haben wir so eine schöne kleine Programme, die dann irgendwie so fancy Texte ausgeben und uns sagen können, okay, das Ganze ist jetzt diese Instruktion. Ganz unten noch angemerkt, es gibt so zwei verschiedene Arten von irgendwie Möglichkeiten, das darzustellen, nämlich die Angel-Syntax und die AT&T-Syntax. Sie haben sich mal gedacht, komm, wir machen mal zwei verschiedene und das eine ist halt eher umgekehrt zu anderen. Wir beschäftigen uns hier hauptsächlich mit der Angel-Syntax, einfach, weil ich weiß nicht, das sieht man eigentlich überall mehr. So, wir wollen aber nicht zu stark in das ganze Assembly einsteigen. Das könnt ihr tatsächlich nachher machen, wenn ihr euch das CTF selber mal irgendwie zu genügend führt. Wir wollen jetzt jetzt einmal damit beschäftigen, wie funktioniert das jetzt eigentlich mit diesem Buffer-Overflow und irgendwie wie kommen wir von diesem Buffer-Overflow eigentlich zu der Shell. Dafür brauchen wir noch eine weitere Primitive, nämlich Funktionen. Wie funktioniert so eine Funktion? So eine Funktion könnt ihr sich so vorstellen, ihr habt irgendwie Code und theoretisch könntet ihr sagen, der ganze Code im Computer wird einfach auf 1v1 nach unten irgendwie ausgeführt, immer weiter und weiter und weiter, aber irgendwann geht euch der Speicher aus. Deshalb definiert der kleine Blöcke, die ihr immer wieder aufrufen könnt. Das heißt, ihr führt irgendwie euer Programm aus und müsst dem Programm irgendwann sagen, springen wir bitte zu so diesem Blog, für da das ganze aus und dann springt es am Ende wieder zurück. So, damit das Ganze funktioniert, haben wir diese drei kleine Instruktionen hier, die wir uns jetzt mal ganz genau angucken werden, und zwar das PushRBP, das PopRBP, RSP und dann MOVRBP, RSP. Das war jetzt super schnell, aber das gucken wir uns jetzt, wie gesagt, im Design an. Hier muss ich anmerken, das mit dem Move, das ist wahrscheinlich das Komischste jetzt erstmal, der verschiebt den Wert von RSP nach RBP, also nach links, müssen wir ein bisschen umdenken, aber das gucken wir uns jetzt nochmal an. Wir gehen das jetzt mal Stück für Stück durch. Also ich habe jetzt rechts einmal so ein bisschen Speicher aufgemalt, der geht oben von 0x, was steht da, 99 bis 99, das heißt, wir können auf die einzelnen Feldern Speicher irgendwie referenzieren und auf der rechten Seite steht kurz, wo der Stack jetzt ist. Das heißt, wir können jetzt sagen, wir führen irgendwie Code aus und irgendwann wollen wir eine Funktion aufrufen. Das heißt, wir sagen erstmal, Speicher bitte, was die nächste Adresse wäre, die wir ausführen würden, mit PushRBP. PushRBP sorgt dafür, dass der Instruction Point, also das Snacks, was wir ausgeführt werden, einmal auf den Stack gepusht wird. Das haben wir jetzt hier mal gemacht. Wir hatten mal 0x55 genommen. Nehmen wir einfach an, dass das Snacks ausgeführt worden wäre. Der Stack Point hat sich jetzt um eins im Prinzip verniedrigt, dadurch, dass es zu den niedrigen Adressen geht. Da muss man mal so ein bisschen umdenken. In den meisten Fällen, die ihr sehen werdet, sind die niedrigen Adressen oben, die hohen Adressen unten und dann kann man da reinschreiben wie so ein Buch. Aber der Stack wächst tatsächlich in Richtung der niedrigen Adressen. Das ist an vielen Ecken und enden irgendwie anders dargestellt und das kann zu sehr viel Verwirrung führen. Wir können weiter wie dieser Teller hoffen und dann ist das eigentlich ganz gut. Als Nächstes wollen wir einmal den Base-Pointer speichern, weil wir wollen eine neue Funktion aufrufen. Diesen Strich, den ihr hier seht, das ist jetzt der alte Stack-Frame, das heißt, die alte Funktion, die wir haben. Wir wollen jetzt nur komplett neu erstellen. Damit wir diese neu erstellen können, wollen wir natürlich auch speichern, wo die alte Funktion denn war. Das heißt, wir pushen den alten Base-Pointer. Der alte Base-Pointer 0x99, das war der Wert, der da unten steht, der liegt jetzt da oben auf dem Stack und das neue Stack-Frame zu erzeugen, ist, wir müssen sagen, dass der Base-Pointer, also unser Base-Pointer, der jetzt hier unten hin zeigt, einfach oben hin zeigt und zwar auf den Wert von dem Stack-Pointer. Die zeigen jetzt auf denselben Wert, nämlich die Adresse von dem Base-Pointer. Alles Weitere, was wir jetzt auf den Stack draufschieben, wird oben draufgelegt. Das heißt, es könnte jetzt sein, wir haben irgendwas in dieser Funktion gemacht und wollen das ganz rückgängig machen, dann passieren wirklich diese drei Instruktionen rückwärts. Das heißt, was wir hier sehen, dann ist einfach der State, nachdem wir irgendwas in dieser Funktion getan haben. Und der Stack-Pointer zeigt oben irgendwo hin. Vielleicht haben wir noch da so ein paar Sachen auf den Stack gemacht, irgendwie ein bisschen rumgespielt, ein bisschen Mathe gemacht und jetzt sagen wir, okay, wir wollen jetzt aber irgendwie wieder zurückgehen. Um wieder zurückzugehen, machen wir das ganz rückwärts. Die erste Instruktion ist MOVRSPRBP. Das heißt, wir verschieben den Stack-Pointer, das Oberste, was da irgendwo hin zeigt, wieder auf den Base-Pointer. Das sieht dann so aus. Als Nächstes wollen wir den alten Base-Pointer wieder herstellen. Das heißt, diese 0.93, die wir da haben, wird jetzt entsprechend wieder nach unten geschoben, indem wir sagen, pop mal den obersten Wert auf dem Stack, also da, wo der Stack-Pointer aktuell hin zeigt, in den Base-Pointer. Dann zeigt der Base-Pointer wieder da unten hin. Und als Nächstes haben wir da die Adresse, wo der Code weiter ausgeführt werden sollte, nämlich der Instruction-Pointer. Der Instruction-Pointer, der ist das, was ausgeführt wird. Das heißt, vielleicht könnt ihr ja schon so ein bisschen überlegen, was schiefgehen könnte. Aber der zeigt auf 0.55, was jetzt passiert ist. Wir haben den Stack-Pointer, der zeigt auf 0.55, wir poppen das Ganze in den Instruction-Pointer und der ist Instruction-Pointer gepoppt. Das RSP zeigt wieder auf den Wert, der Base-Pointer zeigt wieder auf den Wert und das ist genau das Gleiche, was wir vorhin hatten, nämlich, als wir angefangen haben, mit diesem ganzen, wir erstellen eine Funktion. So, das war jetzt recht schnell. Wir haben jetzt Buffer. Buffer sind, wo der Spaß richtig anfängt, loszugehen. Insgesamt, wir haben Computerprogramme. Über alle Computerprogramme schreiben wir irgendwas rein. Ob es jetzt ein Webbrowser ist oder irgendwie Excel, Outlook, irgendwo müssen Daten gespeichert werden. Das heißt, irgendwie, es werden Buffer erstellt und in diese Buffer könnt ihr dann irgendwie reinschreiben. Es gibt jetzt verschiedene Arten von Buffern. Das heißt, es könnte natürlich sein, dass ihr genau wisst, okay, ich möchte jetzt zum Beispiel in meinem Fall 16 bytes an den Speicher irgendwo aluzieren. Das kann aber auch jetzt sein, dass ihr sagt, ah, ich weiß nicht, wie viel Buffer ich brauche. Ich kann mal so und so viel aluzieren und vielleicht brauche ich nochmal mehr. In diesem Fall erstelle ich einen Char Buffer in den 16 Strings rein, also, ja, bytes reinfassen. Was genau diese bytes jetzt sind, ist es uns erst mal egal. Das ist jetzt auch irgendwie komisch dargestellt. Eigentlich hat jedes dieser einzelnen Unterstriche, die ihr da seht, irgendwie eine eigene Adresse, auf die wir verweisen können. Ich habe das jetzt mal so ein bisschen komprimiert, damit das irgendwie auf diese Folien passt. Auf jeden Fall, wir haben jetzt irgendwie ein Buffer, in den wir reinschreiben können. Wir haben jetzt hier einen Kort, den ihr links seht. Also, dieses Gats verwendet das bitte niemals. Das ist einfach nur als Beispiel, damit das Ganze einfach ist. Sollte ihr je Gats sehen, könnt ihr die Manpage zu Gats aufmachen und euch die mal durchlesen. Und da werdet ihr relativ fett sehen, so dieses niemals Gats nutzen. Gibt es gute Alternativen für, zum Beispiel, irgendwie so FSS Gats und so. Also, Leute haben festgestellt, wenn wir uns die Gats aufmachen, dann können wir uns die Gats aufmachen. Wir haben festgestellt, wenn wir unbegrenzt in einen Buffer reinschreiben können, kann das zum Problem führen. Aber ich glaube, das ist der Part, wo viele Leute sagen, ja, es ist halt ein Bufferoverflow. Aber wie kommen wir da eigentlich von diesem Bufferoverflow dann zu nach Schell? Und es ist eigentlich super einfach, weil wenn wir uns das jetzt hier angucken, haben wir eine Funktion, beziehungsweise ein Stackframe, den sehen wir mit dem RBP und dem RSP. Und der definiert, wo unsere Funktion ist. Und wir haben ja eben gesehen, wenn wir den EP-Look auswühren, um wieder aus dieser Funktion zu return, haben wir Daten auf dem Stack, die einfach da sind und uns sagen, was war denn vorher da? In diesem Fall haben wir dann den alten RBP und den alten Instruction pointer. Und wenn wir es jetzt hinbekommen, diese Daten zu korrupten, dann können wir damit irgendwas machen. In unserem Fall haben wir jetzt ein Bufferoverflow. Wir haben jetzt gesagt, wir haben irgendwie ein Array an Charles erstellt. Das heißt String. Und da schreiben wir jetzt zum Beispiel mal 16 As rein. Das wäre jetzt einfach mal so ein kleines Beispiel. Irgendwie habt ihr irgendwie ein Programm, da könnt ihr jetzt reinschreiben. Und 16 As ist intended. Also ich habe jetzt gesagt, das klingt 16 groß, dann schreibe ich halt 16 rein. Es hindert uns aber durch Getz niemand daran, einmalfach mehr reinzuschreiben. Und das ist tatsächlich der Spaß hinter der ganzen Sache. Wenn wir jetzt hier ein Stackframe haben und einfach weiter reinschreiben, ich will noch ein paar weitere B's reinschreiben oder weitere Buchstaben zahlen und in diesem Fall haben wir jetzt den Basepoint überschrieben. Jetzt könnt ihr euch mal überlegen, was passiert denn jetzt, wenn wir 16 As reingeschrieben haben, den Basepoint überschreiben und noch mal das Ganze ausführen. Das heißt, wenn wir in den Epilogue gehen und ihr könnt euch jetzt mal überlegen, was passiert, wenn ich 0x99 habe und erstmal auf RBP nach RSP mache, das ist eigentlich ganz normal, aber wenn ihr da jetzt nicht 0x99 steht, sondern in der nächste Werte wird in den Basepointer gepoppt. Ich könnte jetzt hier theoretisch sagen, der Basepointer ist 0x123 dadurch, dass ich den mit diesem Buffer-Warflow überschreiben kann. Und was dann passiert ist, der Basepointer zeigt nicht so schön hier da unten, wie in den alten Stackframes, sondern der Basepointer kann irgendwo hinzeigen. Das können wir uns aussuchen, weil wir können es überschreiben in diesem einfachen Szenario. Und als nächstes der Instruction-Pointer. Also wenn wir uns das hier, nochmal wieder, das war zu schnell, wenn wir uns das hier anschauen, können wir, wenn wir noch mehr schreiben, erstmal den Basepointer überschreiben und dann den Instruction-Pointer. Wir haben uns ja eben gemerkt, so der Instruction-Pointer ist das, was speichert, wo wir als nächstes auswühlen wollen, nachdem die Funktion ausgeführt wurde. Und das ist eigentlich die Magie hinter der ganzen Sache. Das heißt, wenn wir hier diesen Epilogue haben und dann den Basepointer poppen, der Basepointer kann dann irgendwo hinzeigen, das ist vielleicht sogar erstmal irrelevant, aber dann wollen wir natürlich irgendwie den Instruction-Pointer überschreiben. So, das haben wir jetzt hier gemacht. Der Instruction-Pointer wird gepoppt. Wenn wir jetzt vorhin bekommen, diesen Instruction-Pointer, der aktuell auf 0x55 zeigt, mit einer arbiträreren Adresse zu überschreiben, können wir dem Programm sagen, Moment, nachdem die Funktion returned, wollen wir nicht einfach da weitermachen, wo wir vorher waren, sondern wir können weitermachen, wo auch immer wir sonst weitermachen wollen. So, und das ist eigentlich die Magie hinter der ganzen Sache. Also, ich hoffe, das war jetzt so, so weit so ungefähr klar. Da sind jetzt natürlich irgendwie sehr, sehr, sehr viele Annahmen getroffen worden, so die sehr einfach sind. Beispiel, wir haben keine Security-Mechanismen. Wenn das Ganze jetzt so wirklich laufen würde, das wäre ja irgendwie ziemlich verrückt, weil dann könnte die wirklich irgendwo hingehen und dafür sorgen, dass das Programm einfach ausführt, was ihr wollt. Jetzt fragt man sich, Moment, wir haben ja gesagt, vom Buffer-Overflow zur Shell. Und wir haben jetzt irgendwie gesagt, wir können den Instruction-Pointer überschreiben, aber wie kommen wir denn jetzt eigentlich zur Shell? Also, das geht jetzt noch so ein bisschen weiter, und zwar, wir haben unser Programm. Unser Programm liegt im Speicher, kann Sachen ausführen und wenn wir sagen können, wir können den Instruction-Pointer überschreiben, können wir dem natürlich sagen, springen wir bitte irgendwo anders in dieser Binary. Das heißt, wir haben zum Beispiel ein Programm, was sagt, wir führen jetzt einfach mal aus, fragen den Nutzer nach irgendwie Daten, und zwar einmal und dann machen wir irgendwas mit diesen Daten und sagen, ja, nein. Dann könnten wir sagen, okay, sobald die Funktion returned, springst du einfach wieder zum Anfang zurück. Und wir haben unendlich viele Versuche, weil dieser Buffer irgendwie immer wieder überschrieben wird, die Instruction-Pointer überschrieben wird und wieder zurückkommt. Jetzt haben wir aber das leichte Problem, da gehen wir davon aus, dass wir wissen, wohin wir springen wollen. Wir könnten jetzt sagen, okay, ich schreibe jetzt CCCCC rein, woher weiß ich, was ich da reinschreibe. Und das ist tatsächlich eine gute Frage und das haben Leute tatsächlich irgendwie mitigated. Also, wir haben das nicht immer so, dass ein Programm einfach normal im Speicher liegt. Das wäre cool, weil dann könnten wir das Programm ausführen und es einfach mal zum kleinen Debug anschauen, was passiert denn jetzt mit diesem ganzen Speicher? Was liegt denn jetzt irgendwie in diesen ganzen Instruktion, in den ganzen Registern? Und wenn das Programm jedes Mal in derselben Stelle im Speicher wäre, könnten wir das Programm ausführen und sagen gucken, okay, was steht denn da jetzt und dann das Programm einfach nochmal ausführen und die Adressen entsprechend nehmen. In der Welt des Binary-Explotations, zum Beispiel zum CTF, haben wir das so, wir bekommen eine Binary lokal und können uns diese Binary dann entsprechend in unserem Disassembly angucken, müssen dann entsprechend hier herausfinden, was sind da für Funktionen, kann ich da eventuell so ein Buffer irgendwie nutzen und kann ich da vielleicht mehr reinschreiben und das Problem ist, der Server der führt ja auch die Binary aus und der Server führt zwar dieselbe Binary aus, aber das heißt nicht, dass die Binary an derselben Stelle im Speicher liegt. Das heißt, es kann sein, dass er auf unserem PC, wenn wir das hier lokal ausführen, kann es sein, dass der Server dann sagt, Moment, das kenne ich gar nicht, weil die Binary komplett anders im Speicher liegt. Das hatten wir hier vorne einmal. Hier, ich habe ja irgendwie keine Zahlen dran geschrieben und das mache ich tatsächlich explizit so, wenn ich mir so was vorstelle und zwar, ich schreibe da meistens ein A oder ein B hin so, denn dann habe ich einmal A und A kann nämlich der lokale Offset der Binary im Speicher sein. Das heißt, es kann sein, dass in meinem Computer die Binary an einen bestimmten Punkt geladen wird und dann auf dem Server an einen anderen Punkt. Das heißt, wenn ihr jetzt hingeht und dem CTFS stellt, ich habe jetzt die Binary lokal bei mir geladen und jedes Mal, wenn ich diese Binary Neues ausführe, lädt ihr die woanders in den Speicher, dann haben wir das Problem, dass A SLR an ist. Das ist so ein Mechanismus. Ich hatte eigentlich irgendwie in die Torgebeschreibung geschrieben, dass wir nicht zu viele in diese Mechanismen reingehen, aber das wollte ich euch einfach mal warnen, weil das kann sehr förderin sein, wenn ihr das Programm neu ausführt und auf einmal da komplett andere Werte stehen. Insgesamt, mit diesen ganzen Informationen solltet ihr euch jetzt zu pimal Daumen an die ersten Challenges in diesem CTFS stürzen können. Das KCTF beziehungsweise das GPN CTF hat tatsächlich relativ viele Challenges. Hier Liam und Martin haben euch das eben wunderbar vorgestellt und die ersten Challenges dürften tatsächlich so welche Aufgaben beinhalten. Das heißt, ihr könnt jetzt einmal hingehen, euch die Challenges angucken, überlegen, was haben wir denn jetzt für Challenges? Sind das Binary Exploitation Challenges? Dann nehmt ihr euch so eine Binary Exploitation Challenge. Dann könnt ihr euch überlegen, wo kommt die ganzen Daten her? Das heißt, ich kann Daten eingeben und wo gehen die ganzen Daten hin? In diesem Fall landen sie im Speicher. Dann könnt ihr euch überlegen, Moment, werden diese Daten vielleicht danach anderweitig verwendet? Überschreibe ich da eventuell irgendwelche Strukturen? Kann ich damit irgendwelche Bounds umgehen? Kann ich damit irgendwas tun, was nicht erwartet ist? Ich behaupte mal, das macht den ganzen Reiz an der ganzen Sache aus, denn das hier ist nicht erwartet. Du kannst einfach an 16 Bounds und die ganzen befüllen, aber du kannst trotzdem mehr reinschreiben. Das ist nicht erwartet und dadurch könnt unintended Probleme auftreten. Oder ganz viel Spaß. In unserem Fall, wenn wir CTF spielen oder so, ist das halt super witzig. Wenn ihr dann irgendwie hingeht und eine Challenge seht und ich denke, das funktioniert so, das würde ich eigentlich so selber deployen. Das sieht es eigentlich wunderbar und dann auf einmal feststellt, am Moment, dann kann ich irgendwie irgendwas überschreiben. Und an dem Punkt könnt ihr einfach mal selber hingehen und das Ganze mal selber auszuprobieren. Damit rumzuspielen, ist die einfachste Möglichkeit, das Ganze zu lernen. Insgesamt, ihr seid super viele Leute hier im Raum. Setz euch alle nochmal zusammen hin und wenn ihr nicht wisst, mit wem, strecht mich gerne an, ich laufe hier den ganzen Tag lang rum und irgendwie mach so Zeug und dann können wir gerne mal einfach gemeinsam darauf schauen. Ja, und das war's. Allgemein, wir haben jetzt noch so fünf Minuten. Vielleicht habt ihr so ein, zwei Fragen oder so? Komplett gerüstet. Da haben wir eine Frage. Ihr meint, dieses Get sollte man nicht verwenden, wenn man selber schreibt. Und ich denke mal, da gibt's eine Alternative. Kann man auch die Alternativen dann umgehen? Also es kommt natürlich ganz drauf an, was diese Alternativen machen. Get in diesem Fall ist einfach eine Funktion, die sagt, gib mir bitte einen Speicherbereich und dann gebe ich dir die Möglichkeit, in diesen Speicherbereich reinzuschreiben. Das ist natürlich komplett dämlich. Leute haben dann irgendwie festgestellt, okay, dann schreiben wir weitere von diesen Funktionen. Zum Beispiel, wenn wir sagen, wir haben eine Funktion fgets oder so, dann geben wir dir ein Speicherbereich und dann geben wir aber noch explizit einen Wert an, wie viel du in diesen Speicherbereich reinschreiben kannst. Das klingt ja schon mal irgendwie ein bisschen sinnvoller. Beispiel, ich aluziere ein Buffer, der hat eine Größe von 16 bytes und dann sage ich der Funktion, du darf maximal 16 bytes in diesen Buffer reinschreiben. Problem, wir sind immer noch alle Menschen und Menschenbau und Fehler. Das heißt, es kann sein, dass die Menge an bytes, die wir da reinschreiben können, noch mal irgendwie berechnet wird und der Kotmann 3 Jahre liegen bleibt, noch mal ausgegraben wird, von irgendwie mangepasst wird und schubst die Wups, habt ja auch einmal so einen kleinen Rechenfehler drin und dann stellt ihr fest, moment, wir haben einen Buffer von 16 bytes aluziert und irgendwer möchte 16 Doubles reinschreiben lassen und auf einmal kann man da deutlich mehr reinschreiben, als geht. Das heißt, nicht alle Funktionen sind perfekt, aber es gibt viele Funktionen, die dafür umgebaut wurden, dass das Ganze nicht ganz so schlimm ist wie Gets. Also Gets ist wirklich, wenn ihr Gets seht, genau. Und da drüben haben wir noch eine Frage. Ja, ich habe mich gefragt, mir ist noch nicht klar geworden, wie man am Ende zur Shell kommt. Ja, ich merke auch gerade, das habe ich gar nicht so richtig angerissen. Das Ding ist so ein Programm, komplett alle getiesert, aber nichts dazu gesagt. Also letzten Endes haben wir ein Programm und wir können natürlich nicht alles in unser kleines Programm reinpacken. Das heißt, es gibt sogenannte Libraries. Großer Zusammenschluss, wo ganz, ganz, ganz viel Zeug drinsteht. Zum Beispiel gibt es irgendwie Funktionen, um irgendwie Speicher zu lesen und whatever. Und in der LIBC gibt es Funktionen, die dir zum Beispiel sagen, ich möchte jetzt ein Programm ausführen. Zum Beispiel kann sie sagen, hier execute mal bitte LS oder so Zeug oder irgendwas. Und was wir machen können ist, wir können die Adresse von der LIBC liegen. Das geht noch ein gutes Stückchen weiter. Und dann können wir sagen, spring doch bitte einfach zu der Adresse von der LIBC. Aber wir wissen, welche LIBC das ist, dann springen wir dahin, wo diese Schelle aufgerufen wird. Das springt leider hier komplett den Rahmen. Das würde ich einfach mal in einem Anschlussvortrag nochmal deutlich mehr in die Tiefe gehen, weil da gibt es noch einen Haufen Sachen, die wir da machen müssen. Aber letzten Endes, wir springen auch einfach dahin. Nur wir müssen wissen, wohin. Und ich glaube, das ist der ganze Spaß in der ganzen Sache herauszufinden, wohin wir eigentlich wollen. Das klingt vielleicht so verschalt, als es eigentlich ist. Ja, also wenn ich jetzt aber mir Programme so an sich anguckt, dann haben die doch meistens ihren virtuellen Speicher, wo die Adressen einfach bei 0 wieder anfangen. Wie komme ich dann von diesen virtuellen Adressen auf die tatsächlichen Adressen? Also du brauchst letztendlich irgendeine Art von Info League, um herauszufinden, wo du überhaupt bist. Das meine ich halt mit, wir wissen halt am Anfang nicht genau, wo wir sind und was wir machen. Das ist ASLR, also dieses Adress Space Layout Randomization. Das heißt mehr oder weniger, dass die ganze Binary und der ganze Adress Space immer irgendwie so ein bisschen durch die Gegend verschoben ist. Und diese ganzen Binarys und so sind auch entsprechend verschoben, weil das ist dieser Security Mechanismus, der dafür sorgen soll, dass wir nicht wissen, wo wir sind. Das heißt, wenn du eine Binary hast, wird die ausgeführt und die ganzen Adressen sind immer anders. Das heißt, das was wir in vielen Binarys irgendwie als erstes brauchen, es gibt jetzt zum Beispiel CTF Challengers, die sagen dir dann irgendwie als kleine Hilfe. Hey, bevor wir jetzt anfangen, gebe ich dir mal zum Beispiel ein Adress League, der dir sagt, die Main Funktion liegt da in der Binary und alles andere kann relativ zur Main Funktion ausgerechnet werden. Das heißt, entweder müssen wir selber herausfinden, wie wir Adressen liegen oder irgendwo werden Adressen gelegt und helfen uns dabei. Ja, gibt es weitere Fragen? Wenn nicht, würde ich sagen Hector Plant.