*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!