Buffer Overflow Angriffe, Teil 1 - Ruhr

Transcrição

Buffer Overflow Angriffe, Teil 1 - Ruhr
Grundpraktikum Netz- und Datensicherheit
Thema:
Buffer Overflow Angriffe, Teil 1
Lehrstuhl für Netz- und Datensicherheit
Ruhr-Universität Bochum
Versuchdurchführung: Raum ID 2/168
Zusammengestellt von: Sebastian Gajek, Tim Werthmann, Florian Bache, Patrick Meier
Stand: 8. Januar 2014
Version: 1.2
1 Vorwort
1 Vorwort
Ziel des Praktikums soll sein, ihnen grundlegendes Wissen der Netz- und Datensicherheit praktisch
darzustellen. Neben dem didaktischen Erfolg soll der Spaß an Kryptographie, Internetsicherheit und
Programmierung im Vordergrund stehen. Trotzdem sollten sie den Aufwand dieser Veranstaltung nicht
unterschätzen! Sie werden in diesem Praktikum einer Auswahl an Themen begegnen, die in solch einem
Umfang den Rahmen einer einzigen Vorlesung überschreiten würden. Viel mehr wird ihnen Wissen
vermittelt, das Bestandteil einiger Grundlagenvorlesungen ist, oder Basis für vertiefende Vorlesungen
sein wird.
Aus diesem Grund ist ihre Vorbereitung entscheidend für den Erfolg des Praktikums. Das Studium
der angegebenen Literaturreferenzen ist Voraussetzung für einen erfolgreichen Praktikumsversuch.
Durch das Studium der Referenzen eignen sie sich theoretisches Wissen an, das Grundlage für die
Durchführung eines Versuchs ist und welches anschließend in einem Versuch praktisch untermauert
werden soll. Die Aufgabe eines Betreuers ist somit nicht die Vermittlung des Grundlagenwissens,
sondern die Unterstützung bei der Durchführung ihres Versuchs.
Vor Beginn eines Versuchs wird in einem Vortestat überprüft, ob sie die Referenzen ausreichend
studiert haben. Damit wird sichergestellt, dass sie in der vorgegeben Zeit die gestellten Aufgaben
lösen können. Sollte vom Betreuer festgestellt werden, dass sie nachweislich nicht vorbereitet sind,
werden sie von dem Versuch ausgeschlossen und müssen zu einem Nachholtermin erscheinen. Ihr Ziel
sollte es demnach sein, das Testat auf den direkten Weg zu erhalten.
2 Hinweise
Lesen sie sich zuerst das Grundlagenkapitel durch. Recherchieren sie bei Unklarheiten im Internet,
diskutieren sie mit Kommilitonen oder kontaktieren sie bei schwerwiegenden Problemen ihren Betreuer. Nehmen sie bei ihrer Recherche die angegebenen Quellen zur Hilfe, und versuchen sie sich an den
Hilfsfragen zu orientieren. Sie sollten unter allen Umständen auch Versuchen die Aufgaben so weit
wie möglich zu bearbeiten. Es ist ebenfalls möglich die Aufgaben vollständig in Heimarbeit zu lösen,
sofern ihnen alle Materialien zur Verfügung stehen. Ihre Lösungen werden vom Betreuer während
des Praktikums kontrolliert und bei nachweislich selbstständiger Erarbeitung erhalten sie vorab das
Testat. Nach einem Versuch muss jede Gruppe ein Protokoll anfertigen, in dem die Herleitung, die
Lösung der Aufgaben, und vor allem deren Begründung unter Ausnutzung des gesammelten Wissens
erörtert werden. Bei der Begründung können Zeichnun- gen helfen! Das Protokoll kann wahlweise in
deutscher oder englischer Sprache erstellt werden. Es sollte den orthographischen und grammatischen
Anforderungen der Sprache genügen. Sie haben bis zu einer Woche Zeit, um ihr computergefertigtes
Protokoll in ausgedruckter Form beim Betreuer abzugeben, ansonsten erhalten sie ihr Endtestat nicht.
Sollte ihre schriftliche Ausarbeitung nicht den Hinweisen in [1] genügen, so ist dies ein Grund ihnen
kein Testat zu erteilen.
Bei offenen Fragen richten sie sich immer an den jeweiligen Betreuer!
Viel Spaß!
Grundpraktikum NDS - Buffer Overflows 1
2
8. Januar 2014
3 Einleitung
3 Einleitung
1988 verursachte der sog. Morris Wurm die erste, groß angelegte Buffer Overflow Attacke. Seit diesem
Zeitpunkt vergeht kaum ein Tag, an dem in der Fachpresse keine Meldungen von neuen Verwundbarkeiten in Betriebssystemen und Applikationen zu lesen sind, welche auch wirtschaftliche Schäden zur
Folge haben. 2001 verursachte zum Beispiel der Code Red Wurm einen Schaden von schätzungs- weise
2.5 Milliarden US Dollar.
Gewöhnlich sind schlechte Implementierungen Ursachen für Buffer Overflow Attacken. Sie treten vorallem in Zusammenhang mit Programmen auf, die mit maschinennahen Sprachen wie z.B. Assembler,
C oder C++ geschrieben wurden. Diese Sprachen erwarten vom Programmierer, dass er die Speicheradressen eines Programms manuell verwaltet (alloziieren/allokieren), diese Aufgabe wird bei Hochsprachen jedoch nicht vollständig durch den Programmierer, sondern vielmehr durch den Compiler
(Übersetzer) übernommen.
Im Gegensatz dazu verfügen modernere Programmiersprachen wie Java, PHP oder Perl Verwaltungsmechanismen, die den benötigten Speicher automatisch verwalten (z.B. garbage collection), vor der
Kompilierung nach fehlerhaften (Rücksprung-)Adressen prüfen oder überdimensionierte Puffer auf die
erlaubte Größe reduzieren. Nichtsdestotrotz reichen diese Mechanismen für einen prinzipiellen Schutz
vor Buffer Overflows nicht aus, denn diese Schutzmaßnahmen (z.B. durch Compiler durchgesetzt) sind
in der Regel selbst mit maschinennahen Sprachen entwickelt worden.
Die Folgen eines Buffer Overflows sind, dass ein Angreifer eigenen Code einzuschleusen kann, um z.B
den Programmfluss zu verändern oder einen Denial of Service (DoS) Angriff auszuführen.
Abbildung 1: Einige unsichere Funktionen der standard C Bibliothek
4 Grundlagen
4.1 Die IA-32 Architektur
Heutige Computer basieren zumeist auf der Intel IA-32 Architektur (auch als x86 Architektur bezeichnet). Diese Architektur beinhaltet ein Speichermanagement, dass ein Text/Data/BSS/Heap und
Stack Segment aufweist (Abbildung 3). Seit Einführung der 64Bit Prozessoren existiert auch eine IA-64
Architektur, diese ist jedoch zur IA-32 Architektur inkompatibel und wird in den Itanium Prozessoren von Intel verwendet. Die heutigen ”Heim-CPUs” auf 64Bit Basis gehören zu den Architekturen
AMD64 (auch x86-64 genannt) bzw. Intel64, wobei die Architekturen zu einander kompatibel sind sich
jedoch auch in einigen Punkten unterscheiden. Der interessierte Leser sei hier auf weitere Literatur
verwiesen.
Mit einem Programm, dass auf der IA32-Architektur gestartet wird, lassen sich mehrere Speicherbe-
Grundpraktikum NDS - Buffer Overflows 1
3
8. Januar 2014
4 Grundlagen
reiche assoziieren. Das Text Segment ist schreibgeschützt und beinhaltet die sog. ”opcodes” (operation
codes), welche das Programm definieren (sie werden ausgeführt, wenn das Programm gestartet wird).
Das Data/BSS (Block Started by Symbol) Segment beinhaltet globale und statische Variablen, wobei
die initialisierten Variablen im Data Segment und die uninitialisierten Variablen im BSS Segment
gespeichert werden.
Das Heap Segment wird zur dynamischen Zuweisung von Datenstrukturen benötigt (malloc Routinen),
wobei der Programmierer für die Verwaltung des Speicherplatzes verantwortlich ist (free).
Das letzte Segment ist das Stack Segment. Dies ist eine abstrakte Datenstruktur, basierend auf dem
LIFO (Last In, First Out) Prinzip, welche lokale Variablen speichert (gültig innerhalb einer Funktion
siehe [2, 3, 4, 8, 9]). Objekte werden per push auf den sog. top of stack (das obere Ende des Stacks)
gelegt und mit pop kann das letzte Objekt des Stacks zurückgeladen werden.
Wichtig zu verstehen ist auch, dass das Stack Segment in der von uns betrachteten Architektur dynamisch von hohen zu niedrigen und das Heap Segment von niedrigen zu hohen Adressen wächst
(Abbildung 2).
Abbildung 2: IA-32 Speicherlayout
Intern nutzt die CPU1 Register zur Datenverarbeitung. Die für dieses Praktikum wichtigen Register
sind:
• EIP (Extended Instruction Pointer)
• EBP (Extended Base Pointer)
• ESP (Extended Stack Pointer)
• Generelle Verwendung finden die Register EAX/EBX/ECX/EDX2
1
2
Central Processing Unit, der sog. Prozessor
EAX wird z.B. immer für Rückgabewerte verwendet und ECX wird immer als Zähler für Schleifen benutzt. Übergabewerte
für Funktions-/API-Aufrufe werden mit Hilfe dieser Register übergeben (weitere Verwendungszwecke können sie z.B.
aus [3, 4] entnehmen).
Grundpraktikum NDS - Buffer Overflows 1
4
8. Januar 2014
4 Grundlagen
Der EIP beinhaltet immer eine Adresse, die auf den nächsten auszuführenden Befehl zeigt. Das EIP
Register wird von der CPU verwaltet und ist für alle anderen Vorgänge schreibgeschützt. Das EIP
Register wird jedoch von der CPU bei einer ret (return) Anweisung mit 4 Byte aus dem Stack
überschrieben (32 Bit bei einer 32-Bit Architektur). Der ESP zeigt vor dem Return auf diese 4 Byte.
Der ESP zeigt auf den ”top of Stack”, somit benötigt die CPU nur zwei Befehle (push und pop) um
mit dem Stack zu arbeiten.
Es ist jedoch notwendig innerhalb einer Funktion indirekt (relativ zu dem mit der laufenden Funktion
assoziierten Stacksegment) zu adressieren. Da sich der Wert von ESP oft ändert (bei jeder Stackoperation), ist der ESP denkbar ungeeignet für diese Aufgabe. Darum wird für diesen Zweck der EBP
(auch Framepointer genannt) genutzt.
Wenn eine Funktion mit dem Befehl call aufgerufen wird, wird ein neuer Stackframe für diese Funktion erstellt (Abbildung 3). Die Grenzen dieses Abschnittes sind der ESP (Ende des Stackframes) und
der EBP (Anfang des Stackframes). Die ”call” Direktive sichert zuerst den EIP mit dem Befehl ”push”
auf dem Stack und läd die neue Adresse in das EIP Register. Dann beginnt der sog. Funktionsprolog.
Der vorherige EBP wird per ”push” gesichert und der ESP wird mit ”mov” (move) in den EBP transferiert. Nun wird Platz für die lokalen Variablen reserviert, indem deren Größe vom ESP subtrahiert
wird (Abbildung 4 zeigt einen exemplarischen Funktionsaufruf).
Am Ende einer Funktion wird der EBP wieder in den ESP transferiert und der ursprüngliche EBP
wird per ”pop” zurückgeladen. Damit wird also der Stackframe der aufrufenden Funktion wieder hergestellt. Am Ende eines jeden Funktionsaufrufes steht dann letztendlich eine ret-Anweisung , die den
gesicherten EIP wiederherstellt. Somit wird das Programm in der aufrufenden Funktion fortgesetzt.
Abbildung 3: Stackframe
Grundpraktikum NDS - Buffer Overflows 1
5
8. Januar 2014
4 Grundlagen
Abbildung 4: C Sourcecode und korrespondierender Assemblercode (vereinfacht)
Zu erwähnen ist noch der Aufbau eines Registers: bisher haben die Register immer das Format E*X
gehabt. E bedeutet dabei extended und sagt aus, dass es sich um ein 32Bit Regsiter handelt und
somit 4Byte groß ist. Ein 32Bit Register ist unterteilt in zwei 16Bit Register, wovon jedoch nur das
unterste direkt angesprochen werden kann (Bsp.:AX, BX, SP). Die 16Bit general purpose Regsiter
sind wiederum in zwei acht Bit Register unterteilt, diese sind einzeln ansprechbar (Bsp.: AH, AL, BH,
BL, SH, SL).
4.2 Buffer Overflow Varianten
Heutzutage sind einige Buffer Overflow Attacken bekannt. Generell kann jeder Puffer, der durch
unsichere Funktionen angesprochen wird, kompromitiert werden. In der Fachliteratur werden die
gängigsten Attacken wie folgt unterteilt:
• ”Stack smashing” wird benutzt um eigenen, bösartigen Code einzubringen (Shellcode) oder um
DoS (denial of service) Attacken durchzuführen
• ”Variable Attack” wird benutzt um den Programmzustand zu modifizieren (verwand mit Stack
smashing)
• ”Heap Overflow” wird benutzt um beliebigen Code auszuführen oder um Variablen zu modifizieren
• ”Off-By-One” ist ein klassischer Programmierfehler, bei dem genau ein Byte überschrieben wird
(z.B. durch falsche Schleifengrenzen wie: wiederhole zehn mal“ realisiert als for(i=0;i<=10;i++),
”
wobei hier elf Iterationen durchgeführt werden)
• ”BSS Overflow” ist verwand mit Heap Overflows/Variable Attack
• ”Signed/Unsigned Overflows” treten aufgrund von Fehlinterpretationen auf. Negative Zahlen
werden mit Hilfe des Zweierkomplements dargestellt und sind riesig, sofern sie als positive Zahlen
interpretiert werden (das höchste, most significant Bit ist dabei immer gesetzt)
• ”Frame Pointer Overflow” ist verwand mit Stack smashing und missbraucht den EBP zur indirekten Adressierung oder um den Stackframe zu schädigen (DoS).
Grundpraktikum NDS - Buffer Overflows 1
6
8. Januar 2014
4 Grundlagen
4.3 Das Prinzip von Buffer Overflow Attacken
Zur Veranschaulichung eines Buffer Overflows soll uns das Stack smashing als Beispiel dienen (die
anderen Attacken sind ähnlich3 ). Beim Stack smashing werden alle Variablen über der angegriffenen
Variable überschrieben (da der Stack von oben nach unten, Variablen, insbesondere Strings (Array
of Char), aber von unten nach oben geschrieben werden). Da der EBP überschrieben wird, ist der
Stackframe nach einer solchen Attacke, ohne weitere Schutzmaßnahmen getroffen zu haben [5], irreparabel beschädigt, so dass eine weitere Ausführung des Programmes oder der Unterfunktion zu einem
Segmentierungsfehler (Segmentation Fault) führt (Abbildung 5).
Abbildung 5: Stack smashing durch die Funktion strcpy
Ein interessanter Aspekt an der Attacke ist, dass der gesicherte EIP der einzige Weg ist, um den
Ausführungspfad des Programmes zu verändern (da der EIP von der CPU verwaltet wird, kann der
EIP nie durch einen Prozess/Benutzer manipu liert werden).
Stack smashing kann dazu benutzt werden, um beliebigen Code in den Puffer einzubringen, um dann
später den EIP auf den Start dieses Puffers zeigen zu lassen. Hierfür wird der gesicherte EIP verändert
(er liegt im Stack über dem EBP), welcher später von der CPU wiederhergestellt wird (weiterführende
Informationen in [6, 7]).
3
Die Attacken sind i.d.R komplexer, da einfache Fehler meist schon in der Entwicklung behoben werden. Unser Beispiel
ist in eine einfache Form, welche jedoch auf einige Programme direkt anwendbar ist.
Grundpraktikum NDS - Buffer Overflows 1
7
8. Januar 2014
4 Grundlagen
4.4 Code Injection
Wenn ein Angreifer eine Buffer Overflow Schwäche in einem Programm gefunden hat, kann er versuchen eigenen Code einzuschleusen, den sog. Shellcode. Dieser soll dem Angreifer ermöglichen das
Zielsystem zu übernehmen.
Ein solcher Shellcode unterliegt einigen Regeln, welche es erschweren Shellcodes zu schreiben. Zum
einen muss der Code klein sein (je kleiner desto besser) und er darf unter keinen Umständen sog.
Terminatoren enthalten (z.B. ist 0x00 der Stringterminator, er markiert das Ende eines Strings).
Verarbeitet die betroffene Funktion (z.B. strcpy) einen String und der Shellcode enthält 0x00, bricht
strcpy das Kopieren ab dem Terminator ab (dies ist natürlich für den Angreifer nicht wünschenswert).
Es gibt daher einige spezielle Funktionen, die man zum Schreiben von Shellcodes einsetzt [6]. Der
Platz, der für einen Shellcode zur Verfügung steht, ist definiert durch den reservierten Speicher auf
dem Stack (die Größe der angegriffenen Variable plus alle Variablen über dieser Variable plus dem
EBP; nach 32 Bit Grenzen gerundet).
Der Code selbst wird in Assembler geschrieben und in das Format Flat- bzw. Raw- Binary kompiliert. Das heißt, die Assemblerbefehle werden nur eins zu eins“ übersetzt, ohne Optimierungen oder
”
Einfügen von weiteren, zur Laufzeit benötigten Informationen (z.B Startup-Code). Dadurch kann
man den Code zwar einschleusen, ihn aber nicht direkt (z.B. als exe“-Datei) ausführen, da er keinem
”
ausführbaren Format entspricht.
Abbildung 6: Shellcode geschrieben in Assembler, mit XOR Code Obfusciation
Als Code Obfuscation bezeichnet man alle Möglichkeiten, ein Programm so zu verändern, dass Analysen des Programmcodes schwierig werden. Die eigentliche Funktion des Programms wird dabei aber
nicht verändert.
• Encryption (Verschlüsselung). Dazu verwendet man aufgrund des Platzmangels meist XOR Techniken (One-Time-Pad) oder andere Techniken, sofern sie von Platz realisierbar sind, um den Code
zu tarnen
• Splitting. Dabei werden Variablen und Arrays (Felder) verteilt, um einen Analysten zu verwirren
und um (automatische) Flussanalysen zu erschweren bzw. zu verhindern
• Einfügen von überflüssigen Daten, verhindert einige Disassemblierungsversuche, verwirrt Leser
des Quelltextes
Grundpraktikum NDS - Buffer Overflows 1
8
8. Januar 2014
4 Grundlagen
• Einfügen von überflüssigen Sprungroutinen, erzeugt riesige Referenztabellen bei Analysen, ist
schwer zu lesen und erzeugt bei Flussgraphen z.T. nicht unterscheidbare Abläufe
Alle Verschleierungen können rückgängig gemacht werden, jedoch verhindern polymorphe (sich selbst
verändernde) Shellcodes die überprüfung durch Signaturen.
Als letztes soll noch auf eine Technik hingewiesen werden, die von Angreifern sehr oft eingesetzt wird,
dem sog. NOP-Sliding. Bei einem NOP handelt es sich um den hexadezimalen Wert 0x90. Dieser
bewirkt, dass die CPU einen Takt lang ”pausiert” (NOP = No Operation).
Wenn ein Shellcode eingeschleust wird, muss immer an den Anfang des Shellcodes gesprungen“ werden
”
(ohne Abweichungen!), damit dieser korrekt ausgeführt wird. Wird der Shellcode vor dem neuen EIP
platziert (der sich noch im Stack befindet) und der Puffer mit NOPs aufgefüllt, kann irgendwo in den
mit 0x90 gefüllten Bereich gesprungen werden (also irgendwo vor dem Shellcode). Die NOPs werden
die Ausführung zum Anfang des Shellcodes tragen“ (man gleitet (to slide) förmlich über die NOPs
”
hinweg).
4.5 Auffrischung beim Umgang mit Pointern
Sie sollten im Laufe ihres bisherigen Studiums bereits eine Vorlesung über Programmiersprachen
gehört haben in der Pointer (Zeiger) verwendet werden. Da in diesem Praktikum besonders die Programmiersprache C verwendet wird und der Zusammenhang zwischen Pointern und Adressen eines
Computers gut sichtbar ist, soll hier eine kurze Wiederholung stattfinden.
Ein Pointer wird in C wie folgt beschrieben:
TYPE * NAME (z.B. int * Zahlen)
Dies ist ein Zeiger auf eine dynamische Datenstruktur welche im Heap liegt. Um diese Variable zu
verwenden muss noch erklärt werden auf welchen Speicherbereich sie zeigen soll. Dafür muss dieser
Speicherplatz reserviert werden. Dies geschieht mit:
(TYPE *) malloc(size) (z.B. (int *) malloc(sizeof(int)*Anzahl)
Dabei reserviert malloc size viele Bytes, diese sind noch typenlos, daher wird mittels Casting (hier
(int *)) der Speicherplatz als Array of integer deklariert. Um Fehler zu vermeiden wird darüber
hinaus size i.d.R. als y*sizeof(TYPE) geschrieben, dabei wird y viel Speicherplatz der Größe TYPE
reserviert, wobei TYPE i.d.R. zwischen einem und vier Byte beansprucht4 .
Die ähnlichen Deklarationen:
int * Zahlen_p = (int *) malloc(sizeof(int)*4);
und
int Zahlen_a[4];
haben die selbe Struktur, mit dem Unterschied, dass die zweite Form statisch, die erste jedoch dynamisch ist. Damit ist deren Größe zur Laufzeit veränderbar. Außerdem muss der Programmierer bei
dieser Form den Speicherplatz selbst freigeben falls die Daten nicht mehr benötigt werden.
free(Zahlen);
4
Auf einem 32Bit-System. Beachten Sie: 4 Byte = 32 Bit
Grundpraktikum NDS - Buffer Overflows 1
9
8. Januar 2014
4 Grundlagen
Bei beiden betrachteten Möglichkeiten ist Zahlen_p und Zahlen_a der Zeiger auf den Anfang der
Struktur. Außerdem lassen sich beide Konstrukte mit dem [ ]-Operator verwenden. So ist Zahlen[0]
und Zahlen[0] jeweils der erste Integer-Wert in der Struktur. Eine weitere Möglichkeit um zu erfahren
auf welchen Wert der Pointer zeigt ist der sog. Derefferenzierungsoperator * :
int eine_Zahl = * Zahlen_p;
Umgekehrt kann mit dem Referenzierungsoperator & bestimmt werden, auf welchen Speicherbereich
der Pointer zeigt:
Zahlen_p = &eine_Zahl;
Anders als das Array kann der Pointer seinen Wert ändern. Damit ist nicht der Inhalt des Speichers
gemeint, auf den der Pointer zeigt sondern die Adresse des Speichers. Zahlen_p++; ist also ein gültiges
(und gängiges) Statement. Dieses bewirkt, dass der Pointer nun auf das nächste (zweite) Element des
alloziierten Speichers zeigt.
Grundpraktikum NDS - Buffer Overflows 1
10
8. Januar 2014
5 Kontrollfragen
5 Kontrollfragen
1. Was ist ein Puffer?
2. Wie ist der Speicher aufgebaut?
3. Wie ist ein Stack aufgebaut?
4. Wie ist ein Stackframe aufgebaut?
5. Was ist ein Stack Pointer? Wofür brauche ich ihn? Wie ist er aufgebaut?
6. Was ist ein Base Pointer? Wofür brauche ich ihn? Wie ist er aufgebaut?
7. Was ist ein Pointer? Gibt es Zeiger in Java?
8. Was braucht man, um einen Buffer-Overflow auszunutzen?
9. Welche Varianten von Buffer Overflows gibt es?
10. Warum sind Buffer Overflows (ohne Weiteres) nicht mit Java möglich?
11. Wie können Buffer Overflows verhindert werden?
12. Skizzieren Sie den Ablauf eines Buffer Overflows!
13. Wie sieht der Speicher aus, wenn eine Funktion main() eine lokale Variable auf 100 zählt?
14. Wozu benutzt man Shellcodes?
15. Was ist NOP-Sliding?
16. Wie findet man Buffer Overflows (generell, keine Details)?
17. Was ist ein Jumpcode?
18. Nennen sie mindestens zwei Verschleierungsmethoden (obfuscation)!
19. Was ist der Unterschied zwischen mov eax, 0 und xor eax, eax?
20. Beschreiben sie, wie sie ein dynamisches Array of char der Größe 100 erzeugen!
21. Sehen Sie sich folgenden Aufruf an:
char** strArray = (char**) malloc (anz*sizeof(char*))
Was tut dieser? Können sie nach diesem Aufruf mit strArray arbeiten? Wenn ja, wie arbeiten
sie damit? Wenn nein, was muss als nächstes geschehen und wie arbeiten sie dann mit strArray?
Gibt es noch mehr Methoden um mit strArray zu arbeiten?
22. Wie lautet die little endian Darstellung von 0x7C239088?
23. Warum wählt man mögliche Sprungadressen in Exploits in Betriebssystemmodulen? Gibt es
eine/mehrere weitere gute Möglichkeit solche Sprungadressen zu finden? Wenn ja, nennen sie
mindestens eine!
Grundpraktikum NDS - Buffer Overflows 1
11
8. Januar 2014
6 Verwendete Programme
6 Verwendete Programme
Im Praktikum werden sie folgende Programme verwenden:
• Dev-C++ Ver. 4.9.9.0 : Kompiler mit IDE für C/C++. Installiert, zu finden über das Startmenü
• Netwide Assembler (NASM) Ver. 0.98.39 : Assemblierer. Aufruf zum assemblieren eines Shellcodes: "nasmw -f bin shellcode.asm -o shellcode"
C:\BO\Programme\NASM\nasmw.exe
Aufruf zum assemblieren eines Shellcodes: "nasmw -f bin shellcode.asm -o shellcode"
• OllyDbg Ver. 1.10 : Ein 32 Bit Debugger zum Analysieren von Programmen (insbesondere benötigt
wenn der Quelltext nicht verfübar ist).
C:\BO\Programme\Ollydbg\OLLYDBG.EXE
• XVI32 : 32 Bit Hexeditor zum betrachten des Binärcodes im hexadezimalen Format.
C:\BO\Programme\xvi32\XVI32.exe
• Das verwendete Betriebssystem ist Windows XP mit Service Pack 2.
Grundpraktikum NDS - Buffer Overflows 1
12
8. Januar 2014
7 Aufgaben
7 Aufgaben
Im Nachfolgenden sollen sie nacheinander die einige Programme auf die Anfälligkeit für Buffer Overflow
Attacken testen und diese Anfälligkeit ausnutzen, sowie ein sog. Exploit5 für einige der Programme
schreiben.
Des Weiteren sind in den letzten beiden Programmen eine geheime“ Funktion enthalten, die sie zuerst
”
manuell und später dann automatisch aufrufen sollen. Dies ist als vereinfachendes Beispiel zu sehen.
Natürlich können Sie nicht erwarten in Programmen geheime“ Funktionen anzutreffen, die nur darauf
”
warten von ihnen benutzt zu werden. Jedoch wird Ihnen in einem späteren Versuch die Möglichkeit
gegeben einen realistischeren Angriff zu entwickeln.
7.1 Passwortabfrage
Starten Sie das Programm passwort.exe. Sie erkennen, dass dieses Sie zur Eingabe eines Passwortes
auffordert.
Damit Sie sich mit OllyDbg vertraut machen können ist ihre erste Aufgabe das Herausfinden des
Passwortes.
Dafür starten Sie Ollydbg und öffnen mit Datei→öffnen das anzugreifende Programm. Im nun
sichtbaren Programmcode sollte es für Sie ein Leichtes sein, das gesuchte Passwort zu finden6 .
7.2 Bessere Passwortabfrage
Der Programmierer von passwort.exe hat die Sicherheitslücke ebenfalls erkannt. Er hat das Passwort
nun mit einer Hashfunktion geschützt.
Starten Sie das Programm passwort_sicher.exe. Sie werden wieder zur Eingabe eines Passwortes
auffordert.
Umgehen Sie diese Sicherung der Datei mit Hilfe einer Variable-Attack!
Ihnen steht diesmal neben der ausführbaren Datei auch deren Quellcode zur Verfügung. Sehen Sie sich
zunächst den Quellcode an. Sie werden feststellen, dass ihre Eingabe mit der unsicheren Funnktion
scanf() eingelesen wird und die Gewährung des Zugangs mit der Variable pass geregelt wird. Benutzen Sie Ollydbg um eine Möglichkeit zu finden pass mit Hilfe des Eingabepuffers zu überschreiben.
Dafür müssen herausfinden an welcher Adresse jeweils der Puffer und pass auf dem Stack liegen.
Dafür setzen Sie einen Breakpoint hinter dem scanf-Aufruf. Versuchen Sie nun den folgenden Code
zu verstehen.
Sie sollten die Zeilen finden, in denen pass getestet wird. Finden Sie die Adresse der Variablen im
Stack. Suchen Sie auch die Adresse des Eingabepuffers.
Aus diesen beiden Adressen können Sie nun die benötigte Pufferlänge ermitteln. Geben Sie also einen
String der benötigten Länge als Passwort ein um Zugang zu erhalten.
5
Ein Exploit ist ein automatisierter Angriff auf ein Programm (to exploit = ausbeuten), welches i.d.R. dazu verwendet
wird Computer zu kompromittieren (um z.B. Administratorrechte zu erlangen).
6
Eventuell müssen Sie einmal F9 drücken, damit der Programmcode angezeigt wird.
Grundpraktikum NDS - Buffer Overflows 1
13
8. Januar 2014
7 Aufgaben
7.3 Fuzzing
Benutzen sie das Fuzzer7 -Programm
C:\BO\Aufgaben\1\strcpy_test.exe
um das Programm
C:\BO\Aufgaben\1\strcpy.exe
auf die für den Angriff benötigte Puffergröße zu testen. Die Aufrufsyntax ist dabei wie folgt:
C:\BO\Aufgaben\1\strcpy_test.exe strcpy.exe PUFFERGRÖßE
Da Ollydbg im Just In Time“-Modus läuft, wird Ollydbg bei einem Ausnahmefehler den Programm”
fluss unterbrechen. Daraufhin sollten sie sich in Ollydbg die EIP Adresse anschauen. Lautet diese
41414141 (AAAA), haben sie die richtige Puffergröße getroffen. Ansonsten müssen sie die Puffergröße
variieren.
Geben sie die Größe des Puffers an, der den EIP zu überschreibt.
Erklären Sie warum der EIP 41414141 sein sollte.
Erläutern Sie auch, wie der Rest des Puffers aussieht und überlegen Sie weshalb er in dieser Form
gewählt wurde.
Tipp: Die Größe des benötigten Puffers liegt zwischen 520 und 530 Byte.
7.4 Manueller Angriff
Nachdem sie nun wissen, wie groß der Puffer ist, modifizieren sie den Fuzzer
C:\BO\Aufgaben\1\strcpy_test.c
so, dass dieser nicht mehr strcpy.exe aufruft (Tipp: Den execve-Aufruf auskommentieren), sondern
den Puffer in eine Datei ausgibt (Tipp: Im Quelltext sind vier auskommentierte Befehle/Befehlsfolgen.
Wenn sie die Kommentare entfernen und das Programm erneut kompilieren (in DevCpp Strg+F9 ),
wird der Fuzzer den Puffer in die Datei out.txt ausgeben).
Führen sie den Fuzzer danach erneut aus und benutzen sie den ausgegebenen Puffer (Kopieren und
Einfügen) als Parameter in Ollydbg. Dazu öffnen sie Ollydbg und klicken auf Datei→Öffnen. Die zu
öffnende Datei ist strcpy.exe und das Argument ist der von ihnen kopierte Puffer.
Nachdem Ollydbg die Datei geladen hat sehen sie dessen Opcodes in disassemblierter Form. Suchen
sie zuerst nach der ”geheimen” Funktion (in unserem Fall führt diese ”calc.exe” aus) und merken sie
sich die Adresse (Tipp: Die Adresse muss der Anfang einer Funktion sein (in Ollydbg links durch einen
schwarzen Strich gekennzeichnet, der sich über die gesamte Funktion zieht)). Danach suchen sie den
Aufruf von strcpy und markieren sie den Aufruf mit einem Breakpoint (F2 ).
Nun führen sie das Programm aus (F9 ). Ollydbg sollte an der strcpy Funktion halten. Nun tracen8 sie
(F8 ) bis zum nächsten ”Return” (RETN) und schauen sie sich den Stack am ESP an (Rechtsclick→follow
in stack ). Dieser sollte nun 41414141 aufweisen. Modifizieren sie diesen Wert mit der Adresse der
geheimen Funktion (Rechtsklick auf den Stackwert → Modify ) und lassen sie das Programm
weiterlaufen (F9).
Dokumentieren sie ihre Sprungadresse und erläutern sie ihre Beobachtungen.
7
Ein Fuzzer testet ein Programm auf Buffer Overflows, indem ein beliebig großer Puffer generiert wird um einen
Ausnahmefehler in dem getesteten Programm zu verursachen.
8
Es gibt zwei Möglichkeiten manuell Befehle in einem Programm mitzuverfolgen: step in mit (F8 ) und step over
(F8 ). Dabei überspringt“ step in einen call, step over springt zur Call-Adresse.
”
Grundpraktikum NDS - Buffer Overflows 1
14
8. Januar 2014
7 Aufgaben
7.5 Exploit
Nun sollen sie strcpy.exe mit einem Exploit angreifen, dazu öffnen sie
C:\BO\Aufgaben\3\strcpy_exploit.c
und tragen ihre gefundenen Werte an die richtigen Stellen ein (BUF_LEN sowie teststr benötigen noch
Werte).
BUF_LEN sollte ihre gefundene Puffergröße sein. Innerhalb des Exploits wird dann die exakte Puffergröße berechnet, denn sie können mehr Daten in den Puffer schreiben als sie herausgefunden haben9 .
Dadurch können sie einen sog. Jumpcode10 einschleusen (vorgegeben = stage1) und diesen mit einem
Sprung zur Adresse von ESP ausführen (jmp esp). Wir bedienen uns dabei der Tatsache, dass der
Anfang der Daten die nach dem gesicherten EIP auf dem Stack liegen, durch den ESP markiert wird.
Dazu müssen sie jedoch einen Weg finden, um ESP ausführen zu können Tipp: jmp esp hat den
Opcode FFE4
Sie können diesen Code mit Hilfe von Ollydbg in geladenen Modulen suchen (sie könen dazu strcpy.exe
innerhalb von Ollydbg). In Ollydbg bietet sich hierfür die Datei ntdll.dll an11 . Wenn sie diese Datei
in Ollydbg laden (ALT+E→Doppelklick auf ntdll.dll ), können sie mit Strg+B diesen Opcode
suchen. Die gefundene Adresse müssen sie dann als Array of Char“ (teststr ist als Array of Char
”
deklariert worden) in den Quelltext einbringen
Hinweis: der Computer benutzt das sog. little endian Format, d.h. sie müssen die Adresse byteweise
umdrehen (z.B. 12345678→78563412). Orientieren sie sich an dem Format von stage1, so sollte auch
das Array für teststr aussehen). Der Shellcode ist vorgegeben und öffnet einen Telnetserver auf Port
4444 (zu testen mit netstat -ano)12 . Sie können nach dem erfolgreichen Kompilieren testen, ob sie
sich einloggen können (telnet 127.0.0.1 4444).
Erläutern sie, welche Adresse sie gewählt haben, um ESP anzuspringen und welche Werte sie im
Exploit verwendet haben.
7.6 Fuzzing 2
Benutzen sie nun das Fuzzer Programm
C:\BO\Aufgaben\4\gets_test.exe
um das Programm
C:\BO\Aufgaben\4\gets.exe
auf die benötigte Puffergröße zu testen. Die Aufrufsyntax ist dabei wie folgt:
C:\BO\Aufgaben\4\gets_test.exe gets.exe PUFFERGRÖßE
9
Wenn sie zu viele Daten hineinschreiben, überschreiben sie ggf. sog. Exceptionhandler des Betriebssystems (SEH Structured Exception Handler). Dadurch wird der Prozess vom Betriebs- system abgefangen und i.d.R. beendet.
Sie können es selbst testen, indem sie einen Breakpoint auf den Return-Befehl (siehe Aufgabe 2) setzen und als
Puffergröße z.B. 20 Byte mehr nehemen und sich die Adresse des ESP anschauen.
10
Dem Shellcode nicht unähnlich, jedoch ist seine Aufgabe den Programmfluss zum Shellcode umzuleiten. Wenn für
diesem nach dem EIP nicht genug Platz ist
11
Eine Adresse in einem Modul des Betriebssystems bietet sich an,da diese für jeden Computer mit der selben Betriebssystemversion und -sprache gültig!
12
Ein Screenshot von netstat -ano“ unterstreicht ihre Versuchsbeschreibung in der Ausarbeitung.
”
Grundpraktikum NDS - Buffer Overflows 1
15
8. Januar 2014
7 Aufgaben
Da Ollydbg im Just-In-Time“ Modus läuft, wird Ollydbg bei einem Ausnahmefehler den Programm”
fluss unterbrechen. Daraufhin sollten sie sich in Ollydbg die EIP Adresse anschauen. Ist diese 41414141
(AAAA), haben sie die richtige Puffergröße getroffen.
Geben sie die Größe des Puffers an, der den EIP zu überschreibt.
Tipp: Die Größe des Puffers liegt zwischen 40 und 50 Byte.
7.7 Jumpcode
Auch hier gilt wieder, dass Sie mehr Daten in den Puffer schreiben köonnen als sie zum Ausnahmefehler
brauchen. Darum können Sie wieder den ESP anspringen, um eigenen Code auszuführen. Diesmal
sollen Sie jedoch keinen Shellcode einbringen, sondern nur einen Jumpcode der die ”geheime” Funktion
anspringt (gehen Sie zum Finden des Offsets vor wie in der Aufgabe Manueller Angriff“. Es ist der
”
selbe Aufruf).
Nachdem Sie die Adresse gefunden haben, schreiben sie einen eigenen Jumpcode und kompilieren
diesen mit NASM (nasmw -f bin jumpcode2.asm -o jumpcode2, wobei die ASM Datei mit einem
Texteditor erstellt wird).
Tipp: Schauen sie sich die Datei C:\BO\Aufgaben\4\jumpcode1.asm an und erstellen sie die Datei
nach dem selben Syntax (es existiert bereits eine Datei Namens ”jumpcode2.asm” im Aufgabenverzeichnis, sie können/sollten diese verwenden).
Tipp: Verwenden Sie das EBX Register und füllen Sie es mit 0x90909090 unter der Verwendung der
MOV Anweisung. Subtrahieren Sie dann die Differenz zum Offset der geheimen Funktion in hexadezimaler Schreibweise (0x...) mit der SUB Direktive. Zu guter Letzt springen“ sie nach EBX mit der
”
JMP Anweisung. Überprüfen sie nach dem Kompilieren die Ausgabedatei mit dem Hexeditor
C:\BO\Programme\xvi32\XVI32.exe
und denken sie daran, dass keine 0-Bytes (z.B. 00, 0000 o.ä.) enthalten sein dürfen. Ist dies doch der
Fall, müssen sie andere Operationen in ihrem Jumpcode durchführen.
7.8 Exploit 2
Benutzen sie nun ihren Jumpcode als Wert für shellcode (in XVI32 können sie den Jumpcode
markieren und als String kopieren) und die Adresse des EIP aus der Aufgabe Exploit“ als Parameter
”
für jmpesp um gets_exploit.c anzupassen. Sollten sie alles korrekt durchgeführt und kompiliert
haben, sollte das Expoit die Programmausführung umleiten.
Dokumentieren sie ihren Jumpcode und ihre anderen Werte. Was konnten sie nach dem Ausführen
beobachten?
Grundpraktikum NDS - Buffer Overflows 1
16
8. Januar 2014
8 Literatur
8 Literatur
Literatur
[1] http://www.nds.rub.de/teaching/lectures/225/.
[2] Ritchie Kernighan. Programmieren in C, 1990. 2. Auflage.
[3] R. Hyde.
The Art of Assembly Language Programming.
\url{~}thiebaut/ArtOfAssembly/artofasm.html.
[4] Iczelion.
Win32 Assembly Tutorials.
tutorials.html.
http://cs.smith.edu/
http://win32assembly.programminghorizon.com/
[5] T. Werthmann. Survey on Buffer Overflow Attacks and Countermeasures, June 2006.
[6] F. Opatz. Buffer Overflows füur Jedermann, 2005.
[7] T. Klein. Buffer Overflows und Format-String-Schwachstellen, 2004.
[8] J. Schwenk. Vorlesung Programmiersprachen. http://www.ruhr-uni-bochum.de/nds/lehre/
vorlesungen/programmiersprachen/.
[9] K. Ackermann and P. Klingenbiel. Programmieren in C - Eine Einführung.
hs-fulda.de/\url{~}klingebiel/c-vorlesung/.
http://www2.
Grundpraktikum NDS - Buffer Overflows 1
8. Januar 2014
17

Documentos relacionados