35C3 Vorspannmusik Herald: Gut dann wollen jetzt beginnen. Wer von euch hat schon mal eine E-Mail gekriegt mit einem Anhang? Wer von euch hat eine E-Mail bekommen mit einem Anhang von einer euch unbekannten Person? Das sind erstaunlich wenige, so 20 Prozent. Wer von euch hat den schon mal aufgemacht? 3, 4. Wer von euch kennt jemanden, der schon mal einen Anhang aufgemacht hat? Ja deutlich mehr. Wer von euch musste die Probleme beseitigen, die dadurch entstanden sind? Ja auch so 10 20 Prozent würde ich sagen. Ja es ist natürlich eine ganz gute Idee, sich zu überlegen, ob man denn jetzt eine Rechnung oder ein Angebot auf machen möchte, was man von einer völlig unbekannten Person aus dem Internet bekommen hat. Und was da passiert, wenn man es tatsächlich täte, das wird uns jetzt haggl erläutern. Applaus. Applaus Haggl: Was? Das sollte so nicht aussehen, Moment, warum sagt mir das keiner? So, hallo, dann können wir jetzt ja anfangen. Ja also danke für die schöne Einleitung. Schön, dass ihr alle da seid, dass ihr so zahlreich erschienen seid. Wie in jedem Jahr eigentlich, gab es auch in 2018 - ich schiebe mal eben die Maus da weg - 2018 wieder ein paar schöne Beispiele, oder schön ist in Anführungszeichen zu setzen, Beispiele von Sicherheitslücken, die ziemlich gravierend sind und die geschlossen werden, glücklicherweise, gingen direkt am Anfang des Jahres los. Im Januar wurden im Firefox und mithin auch im tor-Browser, der ja drauf basiert, ein paar Sicherheitslücken geschlossen, für die man nur eine Webseite besuchen musste und dann sind da intern irgendwelche Buffer Overflows passiert und im schlimmsten Fall konnten Angreifer damit halt beliebigen Code ausführen. Ein weiteres Ding aus diesem Jahr, da habe ich leider keine schöne Pressemitteilung zu gefunden, nur so einen CVE-Artikel. Der Adobe Reader, mal wieder, übrigens guckt mal wie viele Sicherheitslücken der hat, das ist enorm. Da war auch was drin mit einem Buffer Overflow und ebenfalls arbitrary code execution. Heißt, ein Angreifer kann beliebigen Code auf dem Rechner ausführen. Drittes Beispiel, gar nicht so lange her, ist eigentlich im letzten Jahr gewesen, und zwar die beiden Würmer Petya und WannaCry. Das waren so sympathische Ransomwares, die die Festplatte verschlüsseln und den Besitzer dann auffordern, Bitcoins zu überweisen, an eine bestimmte Adresse, damit der Rechner wieder frei geschaltet werden kann. Auch die waren beide, also Teil der Angriffe, die die genutzt haben, funktionierten über Buffer Overflows. Man sieht also, es ist ein Thema mit dem man sehr viel machen kann und deswegen reden wir da jetzt ein bisschen darüber. Das ist der grobe Fahrplan, es gibt jetzt so eine kleine Vorstellungsrunde, ich sage etwas zu mir, frage was über euch, sag was zum Vortrag. Danach machen wir ein paar Grundlagen, die zum Teil sehr technisch und zäh sind, ich bitte das zu entschuldigen, aber ganz ohne geht es leider nicht. Und dann erkläre ich, wie halt der Exploit funktioniert, so ein Stack Buffer Overflow Exploit und zum Schluss gibt es, wenn die Demo-Götter uns weiter gewogen sind, eine Demonstration, wo ich meinen eigenen Rechner halt mit einem verwundbaren Programm übernehme. Also über mich: Ich bin im Chaos und Hackspace Siegen aktiv und treibe da so ein bisschen mein Unwesen, habe so halb den Hut für "Chaos macht Schule" und die Cryptoparties auf. Wenn es sich nicht vermeiden lässt, mache ich noch Infrastruktur und ähnliche Geschichten. In meinem sonstigen Leben bin ich Informatiker, ich programmiere kleine Mikrocontroller für Soundsysteme, ich mache Judo und bin Musiker. Meine Lieblingsspeise ist Dal. An dieser stelle herzlichen Dank an das Küchenteam, die uns während dem Aufbau sehr gut verpflegt haben, unter anderem mit Dal. So, bisschen was über euch. Ein kurzes Funktionscheck: Wer hört mir zu und ist in der Lage und gewillt, seinen Arm zu heben bei Fragen, die zutreffen? Das sind ungefähr alle, cool. Dann: Wer weiß, wie so ein Rechner aufgebaut ist, Stichwort Rechnerarchitektur, Rechenwerk, Steuerwerk, so was? Das sind auch erstaunlich viele, okay. Wer hat schon mal programmiert? Im Idealfall mit C, das sind ungefähr alle. Wer hat schon mal gehört, was ein Stack ist? Was ein Buffer Overflow ist? Wer hat sich bei allem gemeldet? Ihr werdet euch langweilen! lacht Der Vortrag ist nämlich, ich habe versucht, ihn möglichst einfach zu halten, weil inspiziert inspiriert, entschuldigung, inspiriert ist er von einem Arbeitskollegen, den ich eigentlich für sehr kompetent halte, und der sich auch auskennt mit Mikrocontrollern, mit Assemblycode und der mich irgendwann mal fragte, oder mehr im Nebensatz beiläufig erwähnte, dass er nicht verstünde, wie denn eigentlich sein kann, dass ein PDF einen Rechner übernehmen kann. Challenge accepted habe ich mir gedacht, das muss man doch irgendwie erklären können. Und dann habe ich den Vortrag gemacht, habe hier in den Talk eingereicht in Pretalx, der wurde dann halt paar Wochen später auch angenommen, dann habe ich mir den Vortrag, den ich zwei Wochen vorher zusammengeschraubt hatte, noch einmal angeschaut und geprüft, ob er den Anforderungen entspricht, die ich mir selber gesetzt habe, festgestellt oh mein Gott, er ist viel zu technisch. Deswegen habe ich den weiter abgespeckt und versucht, alle nicht nötigen Details, alle nicht nötigen Fachwörter, alles raus zu streichen, was man nicht unbedingt braucht, um das Prinzip eines Stack Buffer Overflows oder ein Exploit dessen zu verstehen. Der Vortrag ist vor 20 Minuten fertig geworden, ich habe ihn noch nie Probe gehalten, ich habe gestern bei der Demo zweimal meinen Rechner abgeschossen, insofern bin ich selber sehr gespannt, was jetzt passiert. Also fangen wir an. Grundlagen: Wie funktioniert ein Rechner? Wenn man es ganz ganz ganz ganz grob vereinfacht ausdrückt, gibt es irgendwo einen Prozessor, da kommen auf der einen Seite Daten rein, nämlich Programmdaten, und auf der anderen Seite kommen Daten rein und raus, nämlich die Nutzdaten, die ich verarbeiten möchte. Das kann, weiß ich, ein Office-Dokument sein, ein PDF oder halt auch Daten aus dem Internet wie HTML-Seiten, bei Web-Browsern sind das halt die Daten die rein und raus gehen. Daten werden intern im Rechner in dem Speicher abgelegt und zwar in Paketen von acht 1en und 0en. 1en und 0en sind Bits, das wissen ja wahrscheinlich alle. So ein Paket von acht davon nennt man ein Byte. Jedes Byte hat eine Adresse im Speicher. Und der Witz ist, dass das hier so, dass was da gezeichnet ist, nicht ganz korrekt ist, denn Programme und Daten liegen im gleichen Speicher. Das heißt, ich kann es theoretisch schaffen, den Prozessor dazu zu bringen, Programme auszuführen aus dem Speicherbereich, wo eigentlich Daten liegen sollten. Das ist schon eigentlich wie letztendlich so ein Buffer Overflow Exploit funktioniert. Wenn ich den Prozessor da in der Mitte doch noch mal ein bisschen aufbohre, dann sehe ich, dass er da drin so einen Registersatz hat. Man nennt das so, Register. Das ist so ein bisschen vergleichbar mit dem, man könnte sagen, das Kurzzeitgedächtnis von Menschen, man sagt Menschen ja nach, sie könnten sich so größenordnungsmäßig 7 Dinge gleichzeitig merken, kurzzeitig. So ähnlich ist das bei einem Prozessor auch, der hat einen sehr begrenzten Satz von Registern, also Speicherzellen, die sehr schnell sind, aber halt auch sehr teuer sind. Deswegen baut man da nicht so viele von, das sind die Register. Und dann merkt er sich halt zum Beispiel, wo er im Programm gerade steht. Also wo im Programmspeicher er gerade steht oder wo der Stack gerade steht. Der Stack ist ein besonderer Speicherbereich im Datenspeicher, also eigentlich kein Programmspeicher, der verwendet wird um Dinge längerfristig abzulegen, also man könnte sagen Langzeitgedächtnis. Naja es stimmt nicht ganz, aber grob. Und der Stack ist wirklich, der Name ist Programm, ist wie ein Stapel, ich lege unten was drauf, dann lege ich was oben drauf, lege etwas weiteres oben drauf, und ich lege auch Dinge oder hole Dinge genau in der umgekehrten Reihenfolge wieder runter. Also wie ein Stapel Papier eigentlich. Der wird gebraucht, um bestimmte Features von Programmiersprachen zu implementieren, das werden wir gleich noch sehen. Wenn jetzt also so ein Programm abläuft, also links sieht man einen Ausschnitt aus dem Programmspeicher. Also ich habe da jetzt byteweise mal irgendwelche random Zahlen hingemalt. Naja es sind keine random Zahlen, es ist tatsächlich ein Teil eines Shell-Codes. Also von dem Ding, was ich gleich benutze, um meinen Rechner aufzumachen. Die sind jetzt von oben nach unten byteweise aufgelistet und man würde jetzt sehen, also Speicheradressen gehen jetzt hier von oben nach unten. Das heißt, oben sind die niedrigen Speicheradressen, unten sind die hohen Speicheradressen. Und die werden einfach der Reihe nach durchgezählt, jedes Byte hat halt so eine Adresse. Und so weiß der Prozessor genau, wenn er gesagt bekommt, macht mal hier an der und der Stelle im Programmcode weiter, dann springt er halt dahin und liest weiter. Und zwar sieht das so aus, da werden halt Dinge, also dass der Opcode oder der Befehl, der gerade ausgeführt werden soll, wird eingelesen und wird ausgeführt und der könnte dann zum Beispiel so etwas machen wie ein Datum aus dem Registersatz in den Stack legen oder auf den Stack oben drauf legen. Im nächsten Schritt wird dann etwas anderes oben draufgelegt und dann wird da wieder was runtergeholt und wieder zurück in das Register geschrieben, möglicherweise noch irgendwomit verrechnet und so, hangelt er sich halt durch den Code durch. Von oben nach unten. Man sieht auch, es muss nicht unbedingt immer alles, jedes Ding muss nicht auch ein Befehl sein, da sind auch Daten dazwischen. In diesem Fall halt das, was auf den Stack gelegt wurde. Aber im Prinzip funktioniert das so. Ein Rechner geht einfach der Reihe nach die Befehle durch. Es gibt dann halt auch Sprungbefehle, die sagen können: springe mal irgendwie zurück oder springe mal vor, aber im Prinzip ist das alles was ein Rechner macht. Okay, der Shell Code, den ich gleich benutzen werde, sieht so aus. Das ist offensichtlich ein Programm, was den laufenden Prozess beendet und eine Shell mit dem Namen "sh" startet. Nun ist das natürlich nicht so offensichtlich, so will ja keiner Code schreiben. Deswegen haben sich Leute einfallen lassen: Hey, wir müssen irgendwie es schaffen, was lesbareres zu bekommen und außerdem würden wir ganz gerne, wenn wir ein Programm geschrieben haben, das nicht für jeden Prozessor dieser Welt neu schreiben müssen. Weil das da ist Code, der nur auf diesem Prozessor, in diesem Fall ein x86 Prozessor ,läuft. Wenn ich versuche, den auf einem ARM-Prozesor laufen zu lassen, dann sagt er: Kenne ich nicht den Befehl, mache ich nicht. Beides erreicht man mit sogenannten höheren Programmiersprachen. Eine davon ist C. Das gleiche Programm sieht in C so aus. Und hier kann man schon einigermaßen erkennen, okay hier ist irgendwie ein String drin, offensichtlich also eine Zeichenkette die auf /bin/sh zeigt. Das ist für Leute, die Unix kennen, die Standard Shell. Also ein Kommandozeileninterpreter. Und darunter ist eine Zeile, naja die ist wirklich sehr kryptisch, die muss man schon kennen. Es ruft halt das Programm auf. Aber wie gesagt, Menschen, die sich damit ein bisschen auskennen, die können so etwas direkt auf Anhieb lesen, das da oben natürlich nicht. Dieser Code wird jetzt in einen sogenannten Compiler reingeschmissen und der Compiler macht dann aus dem Unteren ungefähr das Obere wieder. Zudem gibt es dann, wenn man schon so eine schöne Abstraktion hat, noch Programmbibliotheken, dass man nicht jeden Scheiß neu machen muss. So was wie: öffne mir eine Datei und lies den Inhalt. Das will ich ja nicht jedes mal auf der Ebene neu programmieren. Deswegen lege ich mir dafür schöne Bibliotheken an, mit Funktionen. Funktionen sind Teile also wieder verwertbare Programmteile, die einen definierten Satz von Eingabeparametern haben. Also ich kann einer Datei-öffnen-Funktionen beispielsweise mitgeben, wo die Datei sich befindet im Dateisystem und ob ich sie lesend schreiben (öffnen) möchte oder schreibend oder beides. Dann haben die einen Funktionsrumpf. Das ist der Teil der Funktionen, der was macht. Der also das tut, was die Funktion tun soll mit den Eingangsparametern und dann gibt es noch einen Return-Wert, dass ist also ein Rückgabewert, den die Funktion dann zurück gibt, wenn sie fertig ist mit was immer sie getan hat. Und auf diese Weise baut man sich halt Bibliotheken und verwendet die halt immer wieder. Dieses execve ganz unten ist beispielsweise eine Bibliotheksfunktion. Und das war im Prinzip schon alles, was wir für den Exploit wissen müssen. Wenn nämlich jetzt so eine Funktionen aufgerufen wird. Nehmen wir mal an, wir hätten eine Funktion, die drei Parameter hat, einer davon ist, ach Quatsch. Zwei Parameter hat, entschuldigung, und drei lokale Variablen. Achso, Variable habe ich gar nicht erklärt. Variablen ist auch ein Konzept von Programmiersprachen. Da kann ich halt Speicheradressen sprechende Namen geben. Dass ich halt als Programmierer sehen kann, wo ich was abgelegt habe. Und Funktionen können lokale Variablen haben. Davon beliebig viele und die werden eben über einen Stack abgebildet, genauso wie die Funktionsparameter über einen Stack abgebildet werden und der Rückgabewert weiß ich jetzt gerade nicht, da will ich nichts falsches sagen. Könnte sein, dass der auch über einen Stack läuft, bin ich mir aber gerade nicht sicher. Nein, ich glaube, läuft er nicht, whatever. Wenn ich jetzt mir eine Funktion vorstelle, die zwei Parameter hat und drei lokale Variablen. Davon soll eine ein Buffer sein, dann passiert, wenn die Funktion aufgerufen wird, folgendes: Zuerst werden die Funktionsargumente oder Parameter auf den Stack gelegt, und zwar in umgekehrter Reihenfolge. Fragt nicht, ist so. Danach wird die momentane Adresse oder die nächste Adresse auf den Stack gelegt, damit das Programm weiß, wenn es aus der Funktion zurückkommt, wo es weitermachen muss, weil es folgt ja ein Sprung. Dann kommt noch was, das uns nicht interessiert und dann kommen die lokalen Variablen der Funktion selber. In diesem Fall eine Variable, dann kommt ein Buffer und dann kommt noch eine Variable. An dieser Stelle ist sehr schön zu erklären, was ein Buffer ist. Ein Buffer ist eigentlich nichts anderes als eine Variable, die aber mehr Speicher hat. Also das ist jetzt nicht eine Zahl, sondern es sind beispielsweise 512 Zahlen der gleichen Größe. Das ist ein Buffer. Das hier wäre zum Beispiel jetzt ein Buffer der Größe 5. 5 Bytes groß. Die können beliebig groß sein, wobei beliebig ist nicht ganz richtig. Hängt von der Speichergröße ab, wie ich gestern gelernt habe, bei der Demo. lacht Ja, aber im Prinzip sind die nicht begrenzt und das Schlimme ist, man muss sich wenn man so low level programmiert, selber darum kümmern, dass man die Grenzen einhält und nicht zu viel da rein schreibt. Und dann sind wir schon beim Programm. Das Rechte ist das C Program, was ich exploite. Das ist relativ überschaubar, oben diese Zeile "int main", das kann man ignorieren, dass braucht man halt, um C zu sagen: hier beginnt das Programm, hier geht es los und es ist aber wichtig zu wissen, dass main bereits eine Funktion ist. Also vorher passieren noch andere Dinge, die wir nicht genauer betrachten, die mir aber sehr viele Steine in den Weg gelegt haben gestern. lacht Die rufen am Ende dann halt diese Funktion main auf. Es ist also ein ganz normaler Funktionsaufruf. Die Funktion main hat jetzt eine lokale Variable, nämlich den Buffer der Größe 256. Das ist nicht mehr ganz aktuell, in der Demo, die ich gleich zeige, ist er ein bisschen größer, spielt aber keine Rolle. Danach gibt es eine Bibliotheksfunktion string copy, strcpy. Die kopiert, was immer in argv[1] liegt. Da muss man jetzt dazu sagen, das ist ein Kommandozeilenparameter. Wenn ich ein Programm aufrufe auf der Kommandozeile, kann ich dem noch Argumente mitgeben und das erste Argument, was ich dem mitgebe in diesem Fall, würde halt in den Buffer kopiert. Danach wird eine nette Nachricht ausgegeben "Hallo, was immer in dem Buffer steht" und dann folgt das return. Sprich, die Funktionen kehrt zurück und dieser ganze Stack wird abgeräumt und der Prozessor springt dahin, an die Stelle, deren Adresse jetzt in return links rot markiert steht. Also weil vorher hat sich ja das Programm gemerkt, wo es nachher hin zurück muss, indem es die Adresse extra da hingelegt hat. Und wenn ich jetzt zu viele Daten in diesen Buffer rein schreibe, der ist links verkürzt dargestellt, also jetzt die letzten 5 Bytes von diesem 256 Bytes Buffer, wenn ich jetzt zu viele Daten da reinschreibe, dann kommt irgendwann der Punkt, wo ich halt die letzten paar Bytes überschreibe. Dann überschreibe ich das Ding, was uns nicht interessiert und irgendwann überschreibe ich die Return-Adresse. Was jetzt passiert, erst mal noch nichts. Dann kommt das return 0 und was das return macht: Es lädt was immer an dieser stelle steht zurück in den instruction pointer, also an die Stelle, wo sich der Prozessor intern merkt, wo das Programm gerade steht und macht an der Stelle weiter. Wenn ich es jetzt schaffe, in diesen Buffer meinen Shell Code reinzuladen, also das kleine Programmstück, was ihr eben gesehen habt, was das laufende Programm beendet und ein neues Programm, nämlich einen Kommandozeileninterpreter startet, wenn ich das jetzt schaffe das da oben rein zu schreiben und zudem noch es schaffe, an die Return-Adresse den Anfang von diesem Code reinzuschreiben, dann passiert genau, was nicht passieren darf: Nämlich das Programm macht, was ich ihm vorher gesagt habe und was ich vorher in diesem Buffer reingeschrieben habe. Und jetzt kommt der Punkt, an dem es interessant wird. Jetzt muss ich erst mal meine Maus wiederfinden, dass habe ich mir alles anders vorgestellt. Sieht man das? Das ist ein bisschen klein. Könnt ihr das lesen, nein oder? Da hinten, könnt ihr das da hinten lesen? Nein. Bis gerade eben konnte ich auch noch die Größe von meinem Terminal verstellen. Das scheint jetzt nicht mehr zu gehen. Aha, geht nur auf dem linken Bildschirm, aus Gründen! Könnt ihr das jetzt einigermaßen lesen? Gut. Also ich habe hier dieses Programm, ich habe das mal vorbereitet. Es macht, was es soll, es liest halt den ersten Parameter ein, also "vuln" ist das Programm, das verwundbar ist. Ich habe ihm den Parameter "haggl" mit gegeben, also sagt das "Hallo, haggl", alles cool. Wenn ich jetzt aber Dinge da rein schreibe, die ein bisschen länger sind, beispielsweise so etwas. Ich hoffe, das ist die richtige Syntax um in Python Dinge auf der Kommandozeile direkt auszuführen. Ja. Dann kriege ich einen segmentation fault. Ein segmentation fault ist der nette Hinweis vom Betriebssystemen: Kollege, du hast gerade Speicher lesen und/oder schreiben wollen, auf den du keinen Zugriff hast. Wenn Leute die Exploits schreiben, so etwas kriegen, also das ist das was normalerweise das kennt wahrscheinlich jeder, was passiert wenn ein Programm einfach so ohne irgendetwas zu sagen abstürzt, dann ist das oft ein segmentation fault. Das ist für die meisten Menschen sehr nervig, weil man dann das Programm neu starten muss, gegebenenfalls Daten verloren gegangen sind. für Leute, die Exploits schreiben, ist das der heilige Gral, weil segmentation faults sind oft ein Indiz dafür, dass es da gerade einen Buffer Overflow gegeben hat. Und ein Buffer Overflow, damit kann man eine Menge anfangen. Man kann beispielsweise einen Shell Code reinladen. xxd -p shellcode, das ist das Ding, was wir eben in den Slides gesehen haben. Das ist also dieses Programm in Maschinencode, was das laufende Programm beendet und eine Shell startet. Und wenn ich die jetzt da rein injecte, also anstatt jetzt diesem Pythonding oder dem einfachen String haggl, dieses Teil jetzt da rein pipe, dann bekomme ich eine Shell. Und zwar eine Shell, die nicht die andere Shell ist, sondern eine neue Shell ist. An der Stelle habe ich es geschafft. Jetzt bin ich halt drin und kann hier auf dem System beliebige Dinge machen. Das ist erstmal so noch nicht so schlimm und das ist ja auch ein sehr akademisches Beispiel, weil ich es alles selber geschrieben habe. Ich muss auf dem eigenen Rechner sein, ich bin eh schon der Benutzer, aber interessant wird es natürlich, wenn so eine Verwundbarkeit irgendwo an der Stelle sitzt, die auf das Netzwerk horcht. Wenn man von außen irgendwie Pakete einschleusen kann, die so etwas hervorrufen. Und ich dann irgendwie eine Shell oder ähnliches bekomme. Auch interessant wird es, wenn das ein Dienst ist, der aus Gründen mit root-Rechten laufen muss. Dann habe ich nämlich auf einmal eine root-Shell. Und so weiter und so fort. Also man kann da eine Menge lustige Sachen mit machen. Ich habe noch etwas anderes vorbereitet. Und zwar, wenn ich jetzt einen Debugger über das Ding laufen lassen. Ein Debugger ist ein Programm, mit dem ich zur Laufzeit rein gucken kann, in den Speicher, und schauen kann, was denn der Prozess so gerade da macht, und was wo an welcher Stelle im Speicher geschrieben wird. Hier sehe ich jetzt noch mal den Quellcode von dem Programm. Das ist das gleiche wie eben, bis auf das der Buffer ein bisschen größer ist. Das spielt aber keine große Rolle und ich habe zwei breakpoints gesetzt. Breakpoint heißt, das Programm oder der Debugger vielmehr, hält das Programm an der Stelle an, damit man halt schauen kann, was an bestimmten Speicheradressen steht. Und einer ist hier auf dieser Zeile stringcopy, Zeile 6. Also der macht, hält an bevor das reinkopiert wurde, und der nächste breakpoint hält kurz vor dem return an. Und da müsste ich halt dann schon sehen, was im Buffer passiert ist. Wenn ich das Programm jetzt laufen lasse, mit den richtigen Argumenten, das ist alles schon hoffentlich konfiguriert, ja, dann sehe ich hier, das es ist ein Ausschnitt aus dem Buffer, und zwar die letzten, irgendwo in den letzten paar hundert Bytes, da steht ziemlich zufälliger Quatsch drin. Keine Ahnung, was das ist das. Das hat irgend ein Prozess vorher, oder vielleicht möglicherweise auch der init Prozess, also das was vor meinem eigentlichen main Programm läuft, da hingelegt, keine Ahnung, weiß ich nicht. Und die return Adresse steht im Moment noch auf dieser lustigen, kryptischen Zahl. Das wird irgendwas sein, was, nachdem main beendet wurde, also die Funktion main beendet wurde, wahrscheinlich noch irgendwie Speicher aufräumt oder so etwas. So, jetzt sind wir also an der Stelle stringcopy, also es ist noch nichts passiert. Wenn ich jetzt zum nächsten breakpoint weiterlaufe, dann seht ihr? Hier oben sind ganz viele 0x90 Instruktionen. Das sind die Anweisungen für den Prozessor: mach mal nichts, warte mal einen Takt lang. Und davon habe ich ganz viele da rein geschrieben und irgendwann kommt dann hier dieser Shell Code. Das war das Ding, was eben das Programm beendet und die Shell startet. Und ich kann auch schon sehen, die return Adresse ist jetzt überschrieben worden mit einer Adresse, die ich selber gewählt habe. Und diese Adresse, die zielt irgendwo oben vor den Shell Code in diese ganzen no-operation Instruktionen, das nennt man eine NOP-slide. Das macht man, um ein bisschen zu puffern und ein bisschen Platz zu haben und ein bisschen Freiheit zu haben, weil wenn so ein Prozess gestartet wird, dann hängen da oben über dem Stack noch ganz viele andere Sachen und die können sich, nicht zur Laufzeit, die können sich aber auch von Programmausführung zu Programmausführung ändern. Wenn ich zum Beispiel eine neue Variable definiere oder whatever. Jedenfalls habe ich die Sachen nicht immer exakt an der gleichen Speicheradresse und deswegen macht man gerne so einen NOP- slide. Also man nimmt seinen Shell Code, packt den irgendwo in die Mitte, packt davor ganz viele NOPs, wo der Rechner einfach so durchtingelt und nichts tut, und dann irgendwann an dem Shell Code ankommt und dahinter packt man ganz viele return Adressen, die oben in diese NOP- Slide reinspringen. Das heißt, egal ob ich den Bereich vor dem Shell Code treffe oder den Bereich hinter dem Shell Code treffe, es passiert immer was passieren muss, nämlich er springt im ungünstigsten Fall von hinterm Shell Code zu vor dem Shell Code und hangelt sich dann durch bis zum Shell Code, der dann das tut, was er tut. Und das ist der Buffer Overflow Exploit. Demo Time! Ja wobei, wie ich schon sagte, das ist ein sehr akademisches Beispiel, weil ich das alles halt sehr, um es einfach zu halten, sehr klein gehalten habe. In der Realität würde man sagen, es ist ja keiner so blöd, wenn da irgendwas in den Puffer rein schreibt, nicht zu checken, wie groß denn der Puffer ist und mal zu gucken, ob man überhaupt noch da rein schreiben darf. Das stimmt auch, meistens. lacht Das Problem ist: selbst, wenn es immer gemacht würde, habe ich oft das Problem, dass ich das zu der Zeit, wo ich das Programm schreibe, noch gar nicht wissen kann, wie groß dieser Buffer sein muss, weil ich es erst zur Laufzeit mitbekomme, wie viele Pakete da jetzt gleich ankommen, wenn es zum Beispiel ein Netzwerkdienst ist. Das heißt, die Größe von so einem Buffer wird oft berechnet und wenn ich jetzt bei der Berechnung der Größe irgend einen Fehler habe, der dazu führt, dass ich eine falsche Größe raus bekomme, die dann dazu führt, dass der Rechner, oder das Programm vielmehr, denkt, der Buffer ist riesig groß, dann schreibt das Ding halt weiter. Das ist eine Sache, die oft ausgenutzt wird und Daten kommen halt eben, let's face it, meistens nicht von der Kommandozeile, aber stattdessen kommen die aus Dateien, die ich irgendwie manipulieren kann oder sogar aus dem Netzwerk. Da habe ich natürlich dann beliebige Angriffsvektoren. Das heißt, was in der Realität passieren würde, also jetzt beispielsweise bei diesen drei Dingern die wir da hatten: Das Erste war ja der Firefox Browser. Da würde man vermutlich ein Bild beispielsweise sich zusammen bauen, was irgendwie den Bild Standard nicht ganz erfüllt oder wo irgendwo ein Byte falsch ist, so dass, wenn der Browser das Bild lädt, sich irgendwo verhaspelt und irgendwo es zu einem Buffer Overflow kommt, weil z. B. ein Puffer zu klein ausgelegt wird. Oder ich kann versuchen, es über Javascript Dinge zu machen, aber das ist eigentlich noch was anderes. Im zweiten Fall, von dem Adobe Reader, sind es auch oft tatsächlich irgendwelche Bilder. Das Problem bei dem Adobe Reader ist halt so ein bisschen das der so eine eierlegende Wollmilchsau ist. Ich kann mit dem Teil ja beliebige Bildformate anzeigen lassen, Text mit eigenen Schriften, dass wäre auch ein Angriffsvektor, wenn ich da eine eigene Schrift einbauen und die Schrift halt irgendwie so baue, dass beim Einlesen und beim Parsen und Bauen dieser Schrift der Reader irgendwie einen Buffer Overflow kriegt. Bis hin zu 3D Elemente, ich habe gehört, sie haben teile von Flash in den Adobe Reader eingebaut, damit man damit dann 3D Modelle irgendwie anzeigen und auch schön drehen kann. Was man ja alles braucht, alles im gleichen Programm, gute Idee! Naja jedenfalls, auch da bieten sich verschiedenste Angriffsvektoren und was man machen würde ist halt eine Datei bauen, die halten den Fehler ausnutzt und einen Buffer Overflow erzeugt. Der Angriff den ich hier gezeigt habe, der ist heute aus verschiedenen Gründen nicht mehr möglich. Ich musste drei Dinge abschalten um den überhaupt laufen lassen zu können. Das Eine ist, wie ihr euch wahrscheinlich jetzt schon gedacht habt, naja dann könnte ich doch einfach sagen ich habe einen bestimmten Programmspeicher, ich habe einen bestimmten Datenspeicher und was im Datenspeicher liegt, darf niemals nicht vom Prozessor ausgeführt werden. Das gibt es. Das nennt sich "write XOR execute" und ist seit einigen Jahren im Kernel und sogar in Hardware drin. Wenn es angeschaltet ist. Das musste ich ausschalten, dann gibt es noch die Möglichkeit sogenannte Stack Canaries, also Kanarienvögel, in den Stack einzubauen. Das ist so ein Magic Byte, was da reingeschrieben wird, und ein bisschen Code, was vor dem rückkehren der Funktion noch einmal testet, ob dieses Byte, was ich da reingeschrieben habe, also zwischen den lokalen Variablen und der Returnadresse, moment ich mache das mal eben grafisch, damit man das sehen kann, genau, also an diese Stelle, die jetzt da schwarz markiert ist, zwischen dem Return und dem Buffer, würde man ein zur Laufzeit gewähltes beliebiges Byte reinschreiben und bevor man aus der Funktion zurückkehrt, würde man testen, ob dieses Byte, was man da vorher reingeschrieben hat, noch das gleiche ist wie vorher. Wenn nicht, ist ein Buffer Overflow passiert und dann lässt man das Programm besser sofort abstürzen, anstatt abzuwarten, was jetzt kommt. Das ist ein Stack Canary. Auch das musste ich ausschalten. Und das Dritte ist bei modernen Betriebssystemen, mittlerweile haben es glaube ich alle drin, ist das so, dass jeder Prozess mit einem zufälligen Speicherlayout läuft. Das heißt, ich kann nicht mehr wirklich sagen wo meine Dinge im Speicher liegen und das macht es sehr sehr schwierig, weil ich ja irgendwo an den Shell Code, an das Ende vom Shell Code, eine Adresse schreiben muss, die vorne in diese NOP slide reinführt. Und wenn ich absolut keine Ahnung habe, wo diese Adresse ist, also ich kann nicht mehr durch Vergrößern des Buffers oder durch mehr NOPs einfügen kann ich das irgendwann nicht mehr schaffen, weil es einfach so zufällig ist, dass ich keine Chance mehr habe herauszufinden, wo zur Laufzeit denn dieser Shell Code liegt, und wenn ich nicht weiß wo der liegt, kann ich da nicht rein springen, kann ich also keinen Buffer Overflow Exploit machen. Diese drei Dinge gibt es heute, die sind in der Regel, also zwei davon macht der Compiler, das Dritte macht das Betriebssystem, und die musste ich jetzt alle umgehen, um das überhaupt demonstrieren zu können. Aber die zugrunde liegenden Probleme bestehen weiterhin, die sind nämlich: Speichermanagement ist fehleranfällig, Menschen machen immer Fehler, ist oft auch nicht so übersichtlich, Datenformate sind zu komplex, Programme müssen zu viel gleichzeitig können und das führt alles dazu, dass halt immer mehr Fehler passieren. Je höher die Komplexität ist, desto höher ist natürlich die Wahrscheinlichkeit, das ich Fehler mache. Und das ist nicht gefixt und die Programmiersprachen, die man dafür verwendet, sind eigentlich einer Zeit entstammend, in der das Internet noch ein freundlicher Ort war. Man kannte sich und da hat man über Security nicht so richtig nachgedacht. Es gibt außerdem Nachfolger von dieser Praxis, also diese Praxis, die ich jetzt hier gezeigt habe, geht halt nicht, aber es gibt Nachfolger davon, also Modifikationen davon, die sind ein bisschen gewiefter, ein bisschen trickreicher und die schaffen es dann, unter Umständen eben doch noch so etwas nicht zu bekommen. Also das funktioniert heute immer noch, wie ihr halt an den Nachrichten gesehen habt. Oft werden auch nicht alle Gegenmaßnahmen ergriffen, das ist halt Teil der Betriebssystemhersteller bzw. Benutzer. An der Stelle danke ich und Fragen? Applaus Herald: Herzlichen Dank haggl für diesen wundervollen Vortrag! So, Frage-Antwort- Runde, es gibt zwei Mikrofone, die sind dort und dort, beleuchtet, wer eine Frage hat, stellt sich bitte hinter die Mikrofone und ich rufe dann auf. Keine Fragen? Das scheint ein ganz schön umfangreicher Vortrag gewesen zu sein. Haggl: Keine Fragen, oder seid ihr bedient? Keine Fragen, weil es zu viele gäbe, oder keine Fragen, weil es gar keine mehr gibt? Achso, eine Entweder-Oder-Frage kann man nicht mit Handzeichen beantworten, das ist schwierig. lacht Herald: Dort steht jemand, bitte. Frage: Ich hätte mal eine Frage und zwar: Ich habe die Stelle nicht gefunden, wo die Return-Adresse ausgerechnet wird und reingeschrieben wird, also der Return- Wert. Haggl: Ja, die konntest du auch nicht finden, weil ich die garnicht gezeigt habe. Ich habe dazu noch ein kleines Pythonskript, das das alles macht. Das ist diese hier. Also der der Shellcode, den ich vorher gezeigt habe, das ist wirklich nur der Part, der halt execve aufruft mit /bin/sh und der wird halt hier eingelesen in diesem Pythonskript und dann baue ich mit dem Pythonskript halt eben ganz viele NOPs davor, das ist an der Stelle, also hier berechne ich erstmal wie lange diese NOP-slide sein soll und packe halt den NOP-slide davor, dann kommt noch ein bisschen Padding und Alignment, damit das alles richtig hinten rauskommt, dass die Rücksprungadresse auch an der richtigen Stelle liegt im stack. Genau und hier oben habe ich halt diese Target- Adresse und die kriegst du halt nur raus, wenn du das Programm einmal laufen lässt und mit dem Debugger schaust, na wo ist denn der Buffer. Und dann kannst du halt irgendwie eine Adresse in der Mitte von dem Buffer aussuchen, den Shellcode dahinter packen, ein par NOPs davor, dann tut es das schon. So weit die Theorie, die Praxis, habe ich schmerzhaft gelernt, ist ein bisschen komplizierter. Genau, also mehr macht dieses Skript auch nicht, es nimmt den Shellcode, packt NOPs davor, return-Adresse dahinter, fertig. Frage: Ist die Adresse eine relative oder eine absolute Adresse? Haggl: Das ist eine absolute Adresse. Und das ist halt das Ding, an der Stelle greift halt dieses address space layout randomization, also dieser Abwehrmechanismus vom Betriebssystem, der dafür sorgt, dass jedes mal, wenn ich das Programm aufrufe, diese Adresse nicht mehr stimmt. Das kann ich euch demonstrieren. Wenn ich jetzt dieses Ding, was nämlich der make Befehl macht, weil, wenn ich dieses make exploit mache, was ich euch eben gezeigt habe, da wird vorher noch mit diesem Befehl hier für den nächsten Prozess, dieses address space layout randomization ausgeschaltet. Standardmäßig ist das an im Linux Kernel und wenn ich das nicht mache, sondern einfach nur das Programm aufrufe und da meine payload reinschiebe, dann gibt es zwar auch einen segmentation fault, natürlich, aber es gibt halt, ich kriege halt keine shell. Und das ist genau aus dem Grund, weil der buffer nicht an der Stelle liegt, wo ich ihn vermutet habe und deswegen meine Rücksprungadresse irgendwo im Speicher zeigt, da springt er dann hin und, ups, gibts einen segmentation fault. Die beiden anderen Dinge könnte ich auch noch eben zeigen, ich weiß nicht, wir haben noch Zeit oder? Die beiden an Dinge könnte ich eben auch noch zeigen, also was man machen muss, um die beiden anderen Sicherheitsmechanismen abzuschalten. Da muss man das Programm nämlich mit diesen beiden "-z execstack", das ist dieses was den Softwarecheck ausschaltet, ob ich gerade im stack versuche Programme, also generell im Datenspeicher, nein im stack, versuche, Programme auszuführen. Das muss man ausschalten. Und dann gibt es noch diesen stack protector, dass ist ein stack canarie, dass wird auch standardmäßig eingebaut. Muss man also auch explizit ausschalten, um das möglich zu machen. Das heißt , Programme, die mit modernen compilern und modernen compiler flags kompiliert wurden, sollten trade mark eigentlich nicht so leicht verwundbar sein. Und wenn dann noch address space layout randomization dazu kommt, dann ist das schon relativ gut, da muss man schon echt eine Menge Zeug machen, um dann noch irgendwie reinzukommen. Aber es geht halt trotzdem. Herald: Gut ich sehe da drüben an dem Mikrofon noch jemanden, der wartet, bitte. Frage: Kann man nicht einfach relative Adressen verwenden, um ASLR auszuhebeln und wenn nein, warum nicht? Haggl: Was meinst du mit relativ, relativ wozu? Frage: Naja, also ich meine, ah nein, ich habe es gerafft, okay. Haggl: Aber im Prinzip bist du auf dem richtigen Weg, also was man halt machen kann, ist, man kann herausfinden, wo die libc zum Beispiel anfängt. Und wenn ich weiß, wo die libc anfängt, dann weiß ich auch, wenn ich zudem noch die Version von der libc kenne, die da reingelinkt wurde, weiß ich dann auch, wo bestimmte Dinge aus der libc im Speicher liegen. Und sobald ich das habe, kann ich halt dann anfangen, mir wieder Dinge zusammenzubauen. Das geht aber dann eher in Richtung return oriented programming. Herald: Gut, sieht aus, als sei die Frage beantwortet, gibt es noch weitere Fragen? Das scheint nicht der Fall zu sein, aus dem Internet scheint auch keine Frage zu kommen. Dann würde ich hiermit den Vortrag schließen, herzlichen Dank haggl! Applaus 35c3 Abspannmusik Untertitel erstellt von c3subtitles.de im Jahr 2020. Mach mit und hilf uns!