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!