Material zum Praktikumsversuch Buffer Overflows - Ruhr

Transcription

Material zum Praktikumsversuch Buffer Overflows - Ruhr
Material zum Praktikumsversuch Buffer Overflows
Betreut von: Dipl. Ing. Sebastian Gajek und Tim Werthmann
Stand: 22. April 2008
Lehrstuhl für Netz- und Datensicherheit
Ruhr-Universität Bochum
Inhaltsverzeichnis
1 Vorwort
1
2 Hinweise
1
3 Einleitung
2
4 Grundlagen
4.1 Die IA-32 Architektur . . . . . . . . . . . .
4.2 Buffer Overflow Varianten . . . . . . . . . .
4.3 Das Prinzip von Buffer Overflow Attacken .
4.4 Das Prinzip von Code Injection (Shellcode)
schleierung) . . . . . . . . . . . . . . . . . .
4.5 Auffrischung beim Umgang mit Pointern . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
und Obfuscation
. . . . . . . . . .
. . . . . . . . . .
. . . .
. . . .
. . . .
(Ver. . . .
. . . .
2
2
5
6
7
9
5 Vorbereitung/Durchführung
11
6 Hilfsfragen
12
7 Verwendete Programme
13
8 Aufgaben
14
i
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.
Nichtsdestotrotz 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. Vielmehr 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 Zeichnungen 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ß
1
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ätzungsweise 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 proprietären 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
proprietären Sprachen entwickelt worden (Abbildung 1 zeigt einen Auszug unsicherer Funktionen in der C Standardbibliothek).
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
4.1
Grundlagen
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 2). 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.
2
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 noch, 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).
Hi
gh
addr
es
s
es
Dy
nami
c
gr
owt
h
Low
addr
es
s
es
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
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 (auch wenn dies u.U. erwünscht wird) schreibgeschützt. Das EIP Register
wird jedoch von der CPU bei einer ”ret” (return) Anweisung mit 4 Byte aus dem
Stack überschrieben (normalerweise eine Adresse bei 32 Bit Adressierung; 8 Byte
1 Central
Processing Unit (der sog. Prozessor).
wird z.B. immer für Rückgabewerte verwendet und ECX wird immer als counter für
”loop” Aufrufe benutzt. Übergabewerte für Funktions-/API-Aufrufe werden mit Hilfe dieser Register übergeben (weitere Verwendungszwecke können sie z.B. aus [3, 4] entnehmen).
2 EAX
3
in nativer 64 Bit Adressierung). 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 zu adressieren. Da sich der
Wert von ESP oft ändert (bei jeder Stackoperation), ist der ESP denkbar ungeeignet für diese Aufgabe3 . 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 wird4 (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. Am Ende eines jeden Funktionsaufrufes steht dann letztendlich eine ”ret” Anweisung, die den EIP wiederherstellt
und an dessen Position springt.
EBP(
St
ar
toft
hes
t
ac
k
f
r
ame)
St
ac
k
f
r
ame
POP
ESP(
Endoft
hes
t
ac
k
f
r
ame)
PUSH
Abbildung 3: Stackframe
3 Der ESP könnte als Referenz benutzt werden, jedoch würde so mehr Overhead entstehen, da
die Änderungen des ESP nachgehalten werden müssten.
4 Wenn deren Größe nicht durch Vier (4 Bytes → 32 bit) teilbar ist, wird die Größe zum nächsten
Vielfachen von Vier aufgerundet
4
Abbildung 4: C Sourcecode und das Assembler Derivat
Zu erwähnen gilt noch der Aufbau eines Registers, bisher haben die Register
immer das Format E X gehabt. E bedeutet dabei extanded und sagt aus, dass
es sich um ein 32Bit Regsiter handelt und somit 4Byte groß ist (Hinweis: 32Bit
bedeutet eigentlich 232 , bei dieser Bezeichung bezieht sich 32Bit jedoch nur auf
32 = 4 ∗ 8, wobei 8Bit ein Byte bilden). Ein 32Bit Register ist unterteilt in zwei
16Bit Register, wovon jedoch nur das unterste direkt angesprochen werden kann (für
uns interessant: AX, BX, CX, DX, SP, BP). Die 16Bit general purpose Regsiter sind
wiederum in zwei acht Bit Register unterteilt, diese sind einzeln ansprechbar (AH,
AL, BH, BL, CH, CL, DH, DL). Zu erwähnen sei noch, dass bei Codeoptimierungen
diese Gegebenheiten oft mittels Logikverknüpfungen ausgenutzt werden (z.B. AX
and 0xFF löscht AH, AX and 0xFF00 löscht AL usw.).
4.2
Buffer Overflow Varianten
Heutzutage sind einige Buffer Overflow Attacken bekannt und man versteht auch
die Ursachen, die zu Buffer Overflows führen. Generell kann jeder Puffer, der durch
unsichere Funktionen angesprochen wird, kompromitiert werden5 . In der Fachliteratur werden die gängigsten Attacken wie folgt unterteilt (siehe dazu [5]):
• ”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: zähle von 1 bis
10 realisiert als for(i=0;i<=10;i++), wobei hier elf Iterationen durchgeführt
werden)
5 Es existieren Möglichkeiten die Unsicherheit durch Ausnahmefehler oder Vorabprüfungen zu
reduzieren oder aufzuheben.
5
• ”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)
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 ähnlich6 ). 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
6 Die Attacken sind i.d.R komplexer, da einfache Fehler meist schon in der Entwicklung behoben
werden, unser Beispiel soll auch nur das Schema darstellung und ist desshalb in einer einfachen
Form gewählt. Wie sie aber im Verlauf des Praktikums bemerken werden, existieren Programme
auf denen genau dieses Schema zutrifft.
6
der CPU verwaltet wird, kann der EIP nie durch einen Prozess/Benutzer manipuliert 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]).
4.4
Das Prinzip von Code Injection (Shellcode) und Obfuscation (Verschleierung)
Wenn ein Angreifer eine Buffer Overflow Schwäche in einem Programm gefunden
hat, kann er ein Programm einschleusen, den sog. Shellcode7 . Dieser Angriff ist für
das Opfer sehr gefährlich, denn wenn es dem Angreifer gelingt eine solche Shell zu
öffnen, hat dieser die volle Kontrolle über den Computer8 .
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]. Die Größe für einen Shellcode, die 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 und das
ganze nach 32 Bit Grenzen (4 Byte) gerundet).
Der Code selbst wird in Assembler geschrieben und in das Format Flat- bzw. RawBinary kompiliert, d.h. die Assemblerbefehle werden nur ”eins zu eins” übersetzt,
ohne Optimierungen oder Einfügen von weiteren (zur Laufzeit benötigten) Informationen. Dadurch können sie den Code zwar einschleusen, ihn aber nicht direkt
ausführen, da er keinem ausführbaren Format entspricht (Abbildung 6 zeigt einen
Shellcode der ”Obfuscation” Techniken anwendet [6]). Warum dieser Code nicht
direkt ausführbar ist, wird erst klar, wenn man sich die Umgebung ansieht, in der
dieser Code ausgeführt werden soll, also das Betriebssystem (Operating System,
OS). Da wir uns in diesem Praktikum auf das OS Windows beschränken, sei hier
darauf hingewiesen, dass alle ausführbaren Dateien (stand-alone, also nicht zu verwechseln mit Batch Dateien) dem Portable Executeable (PE) Format unterliegen.
Dieses Format besitzt eine Praeamble, den sog. PE Header, welcher dem Loader
alle wichtigen Informationen über die Datei liefert. Da wir wirklich nur den Code
haben, ohne weitere Angaben ist diese Datei nicht im geringsten ausführbar. Der
interessierte Leser sei hier auf weiterführende Literatur zum PE Header verwiesen.
7 Benannt nach seinem ursprünglichem Ziel eine Root-Shell (Administratorkonsole im Unix/Linux Bereich) zu öffnen.
8 Unter der Annahme, dass der Prozess zum starten der Shell mit Administrator-/Root-Rechten
ausgeführt wurde
7
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. Das eigentliche
Programm wird dabei aber nicht verändert. Es gibt Methoden zur Verschleierung,
die polynomiell bei der Erzeugung sind, für die man aber exponentielle Zeit benötigt,
um die Verschleierung rückgängig zu machen. Einige wichtige Methden sind:
• 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
• 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 (derzeitig) rückgängig gemacht werden, jedoch verhindern polymorphe (sich selbst verändernde) Shellcodes die Überprüfung durch
Signaturen. Wenn ein Shellcode mit der XOR Methode verschlüsselt wird, existieren nach dem ASCII Zeichensatz 255 Signaturen pro Shellcode (dies erfordert eine
Menge Speicherplatz).
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
8
(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.
Eine Architektur mit x Adressleitungen wird auch xBit Architektur genannt und
kann, da Bits verwendet werden, 2x Datentypen adressieren. Der kleinste adressierbare Datentyp in der IA-32 Architektur ist ein Byte, zwei Byte bezeichnet man
auch als Word und vier Byte als DWord oder Double Word. Es existieren jedoch
auch Bezeichnungen wie Nibble (vier Bit, also ein halbes Byte) und Quad Word
(acht Byte). Die IA-32 Architektur ist eine 32Bit Architektur, d.h. es existieren 32
Adressleitungen, wobei jede Leitung binär angesteuert wird. Aus der Kombinatorik
kennt man das ungeordnete Ziehen mit Zurücklegen von k Elementen aus einer n
elementigen Menge mit nk , hieraus folgt, dass 232 Byte (= 210 ∗210 ∗210 ∗22 = 4Giga
Byte) existieren bzw. adressierbar sind.
Ein Pointer wird in C wie folgt beschrieben:
TYPE * NAME (z.B. int * Zahlen)
Dies ist eine dynamische Datenstruktur welche im Heap liegt, und bisher nur eine Adresse aus dem oben beschriebenen Adressraum darstellt. Um diese Variable
als Speicherplatz zu verwenden, muss der Adresse noch Speicherplatz zugewiesen
werden, dies geschieht mit
malloc (z.B. (int *) malloc(size))
Dabei reserviert malloc ”size” viele Bytes, diese sind noch typenlos (wie z.B. Integer), 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 x/8 Byte (wobei x die Anzahl
der Adressleitungen darstellt) beansprucht.
Es ist eigentlich klar, dass
int * Zahlen = (int *) malloc (4*sizeof(int))
und
int Zahlen[4]
die selbe Struktur haben, nur ist die zweite Form statisch, die erste dynamisch
und damit auch während der Laufzeit in der Größe veränderbar. Dafür muss der
Programmierer den Speicherplatz für die erste Form am Ende selber wieder frei
geben
free(Zahlen)
Nach der Alloziierung bezeichnet die Variable ”Zahlen” das erste Element des Feldes, es ist also äquivalent mit ”Zahlen[0] ”. Einer Adresse bzw. einem Pointer kann
man nun Offsets (Verschiebungen) hinzufügen, wie z.B. Zahlen++ oder Zahlen+v,
9
mit v eine Ganze Zahl UND innerhalb des definierten Arrays9 . Um auf den derzeitigen Wert der Adresse/des Pointers zuzugreifen, muss der Pointer dereferenziert
werden (z.B. *Zahlen), der Compiler sorgt in diesem Fall dafür, dass die richtige
Anzahl an Bytes in der richtigen Reihenfolge zurückgeliefert wird. Soll ein fester
Wert hingegen als Pointer übergeben werden, also die Adresse des Wertes, so muss
er referenziert werden (&Wert). Somit kann man einer Funktion
int test (int * a)
die Variable c = 3 übergeben, indem
test(&c)
aufgerufen wird (für das Arbeiten mit diesem Pointer innerhalb der Funktion gelten
wieder alle Aussagen wie oben erläutert).
Zu Sagen bleibt noch, dass eine dynamisch erzeugte Struktur, da sie äquivalent zu
einem Array ist, auch wie ein Array genutzt werden kann. Auf int * Zahlen =
(int *) malloc (4*sizeof(int))kann statt mit
Zahlen, Zahlen+1, Zahlen+2, Zahlen+3
auch mit
Zahlen[0], Zahlen[1], Zahlen[2] und Zahlen[3]
zugegriffen werden (dies wird im Laufe dieses Versuchs auch noch Anwendung finden). Die Werte können aber wiederum referenziert werden, z.B. ist
&Zahlen[0]
das selbe wie
Zahlen bzw. Zahlen+0
(siehe Fußnote).
Wichtig ist also zu verstehen, dass eine Adresse wie 0x7C586912 im Computer ein
Pointer auf genau ein Byte darstellt. Sollte nun ein Typ größer als ein Byte referenziert werden, so müssen wir zur Erklärung kurz auf die Low-Level Ebene wechseln
und etwas Assembler besprechen. Ein Compiler macht an sich nichts weiter als die
Hochsprache in Assembler zu übersetzen, wobei dies nicht ganz korrekt ist. Der
Compiler übersetzt in Opcodes, diese können jedoch direkt als Assembler interpretiert werden (daher kann man kompilierte Dateien auch disassemblieren). Wenn
ein Wert aus dem Speicher gelesen wird, wird der Wert in ein Register geschrieben (es existieren auch direct memory Befehle, der interessierte Leser sei hier auf
weiterführende Literatur verwiesen). Wird als Ziel ein 32Bit Register angegeben,
so werden 4 Byte gelesen. Wird ein 16Bit Register angegeben, so werden 2 Byte
gelesen. Wird ein 8Bit Register angegeben, so wird ein Byte gelesen. Die Organisation dieser Vorgänge liegt im CPU und interessiert den Programmierer daher nicht.
Es existieren auf CISC CPUs wie bei der IA-32 Architektur auch Befehle um kleinere Datentypen in größere Register zu laden und um mit Registerkombinationen
Zahlen doppelter Breite zu handhaben (64 Bit verteilt auf zwei Register, z.B. bei
Multiplikation zweier 32Bit Zahlen), diese sind hier aber nicht wichtig. Wichtig ist,
dass der Compiler anhand des Datentyps das Zielregister und den entsprechenden
Ladebefehl (z.B. ”mov”) wählt und somit die korrekte Anzahl an Bytes gelesen
9 Wenn Zahlen nach malloc nicht verändert wurde, so sind Zahlen+v und Zahlen[v] äquivalent,
dies ist nicht mehr der Fall, nachdem etwas wie Zahlen = Zahlen+v ausgeführt wurde, dies
sollte klar sein, da man sich nun beim Aufruf von Zahlen nicht mehr an Position Null sondern an
Position v befindet.
10
wird. Der Programmierer hat dann aber die Aufgabe die nächsten Daten korrekt
zu adressieren. Der Befehl mov ebx, [eax]10 , wobei eax eine Adresse enthält
(”[]” bedeutet Referenzierung, also in C &eax) kopiert die vier Byte ab der Position eax nach ebx. Das nächste DWord bekommt man nur, wenn als nächstes mov
ebx, [eax+4]adressiert wird. Da eax das erste, eax+1 das zweite usw. Byte
adressiert, werden bei mov ebx, [eax]die Bytes
eax, eax+1, eax+2 und eax+3
kopiert, also startet das nächste DWord bei eax+4. Alle anderen Adressierungen
folgen diesem Schema.
5
Vorbereitung/Durchführung
1. Lesen sie die Einleitung durch. Benutzen sie ggf. die Referenzen zur Vertiefung
ihres Wissens.
2. Untersuchen sie die C/Assembler Quellcodes aus den Aufgaben. Welche Funktion
haben sie? Hinweis: Es ist nicht notwendig die Bedeutung jedes Befehls zu kennen,
sondern sie sollen den Sinn der Skripte verstehen (Fuzzer/Exploit/etc.).
3. Gehen sie die Aufgaben Schritt für Schritt durch.
4. Schreiben sie eine ca. 10-seitige Auswertung. Gehen sie (außer in Aufgabe 10)
davon aus, dass sie ein Angreifer sind und beschreiben sie:
a. Welche Software haben sie benutzt?
b. Wie sind sie vorgegangen?
c. Was haben sie (für Probleme) bemerkt?
d. Warum sind die Probleme entstanden?
e. Wie haben sie diese Probleme gelöst?
f. Wo enstand ein Buffer Overflow?
g. Wie haben sie ihn ausgenutzt?
h. Wie sahen die verwendeten (Angriffs-)Parameter (z.B. Puffer) aus?
5. Geben sie ihre Dokumentation binnen einer Woche beim Betreuer ab.
10 Der Standardsyntax in Assembler lautet Befehl Destination, Source , Befehl ist dabei
ein Assembler Befehl (Mnemonic), Destination ist der Ort für das Ergebnis der Operation, Source
ist der zweite Operand, sofern vorhanden (abhängig vom Befehl).
11
6
Hilfsfragen
• Was ist ein Puffer?
• Wie ist der Speicher aufgebaut?
• Wie ist ein Stack aufgebaut?
• Wie ist ein Stackframe aufgebaut?
• Was ist ein Stack Pointer? Wofür brauche ich ihn? Wie ist er aufgebaut?
• Was ist ein Base Pointer? Wofür brauche ich ihn? Wie ist er aufgebaut?
• Was ist ein Pointer? Gibt es Zeiger in Java?
• Was braucht man, um einen Buffer-Overflow auszunutzen?
• Welche Varianten von Buffer Overflows gibt es?
• Warum sind Buffer Overflows (in der Theorie) nicht mit Java möglich?
• Wie können Buffer Overflows verhindert werden?
• Skizzieren Sie den Ablauf eines Buffer Overflows!
• Wie sieht der Speicher aus, wenn eine Funktion main() eine lokale Variable
auf 100 zählt?
• Wozu benutzt man Shellcodes?
• Was ist NOP-Sliding?
• Wie findet man Buffer Overflows (generell, keine Details)?
• Was ist ein Jumpcode?
• Nennen sie mindestens zwei Verschleierungsmethoden (obfuscation)!
• Was ist der Unterschied zwischen mov eax, 0 und xor eax, eax?
• Beschreiben sie, wie sie ein dynamisches Array of char der Größe 100 erzeugen!
• Was macht folgender Aufruf:
char** strArray = (char**) malloc (anz*sizeof(char*))?
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?
• Wie lautet die little endian Darstellung von 0x7C239088?
• 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!
12
7
Verwendete Programme
Im Praktikum werden sie folgende Programme verwenden (Programmname/Funktion/Pfad):
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: Kompiler für Assembler.
C:\BO\Programme\NASM\nasmw.exe Aufruf zum Kompilieren 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ügbar 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.
13
8
Aufgaben
Im Nachfolgenden sollen sie nacheinander die drei Programme strcpy.exe, lame gets.exe
und Winamp (Ver. 5.12) auf die Anfälligkeit bei Buffer Overflow Attacken testen
und diese Anfälligkeit ausnutzen, sowie ein sog. Exploit11 für jedes der drei Programme schreiben. Die ersten beiden Programme (strcpy.exe und lame gets.exe)
bestehen nur aus einer Routine, dem Einlesen eines Strings. Dazu werden die unsicheren Funktionen strcpy und gets verwendet. Des Weiteren sind in den beiden
Programmen eine ”geheime” Funktion enthalten, die sie zuerst manuell und später
dann automatisch aufrufen sollen.
In Aufgabe 3 werden sie ein reales Beispiel kennenlernen. Dazu werden sie einen
Buffer in dem Multimedia Programm Winamp ausnutzen, um beliebigen Code einzuschleusen und auszuführen. Nachdem sie auch für dieses Beispiel ein Exploit geschrieben haben, sollen sie das Exploit von einem local in ein remote12 Exploit
umwandeln. Sollte danach noch genügend Zeit vorhanden sein, werden sie den Blickwinkel eines Security Advisors einnehmen und sie werden die beiden Offsets zu den
beiden Schleifen herausfinden, die für den Buffer Overflow verantwortlich sind.
Hinweis: Achten sie darauf, dass Ollydbg im JIT (Just In Time) Debugger Modus
läuft (Options → Just-in-time debugging → Make OllyDbg just-in-time Debugger).
Dies konfiguriert Ollydbg dazu bei Ausnahmefehlern zu reagieren (sonst reagiert
DrWatson, eine Windows Applikation).
Aufgabe 1.1
Benutzen sie das Fuzzer13 Programm
C:\BO\Aufgaben\1\strcpy_test.exe
um das Programm
C:\BO\Aufgaben\1\strcpy.exe
auf die benötigte Puffergröße zu testen. Der Aufrufsyntax ist dabei wie folgt:
C:\BO\Aufgaben\1\strcpy_test.exe strcpy.exe <PUFFERGRÖßE>
Da Ollydbg im JIT Modus läuft, wird Ollydbg bei einem Ausnahmefehler den
Programmfluss unterbrechen. Daraufhin sollten sie sich in Ollydbg die EIP Adresse
anschauen. Lautet diese 41414141 (AAAA), haben sie die richtige Puffergröße
getroffen14 . Ansonsten müssen sie die Puffergröße variieren.
Geben sie die Größe des Puffers an, der den EIP zu überschreibt.
Tipp: Die Größe des Puffers liegt zwischen 520 und 530 Byte.
11 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).
12 Local Exploits funktionieren nur, wenn man auf dem angegriffenen Computer eingeloggt ist,
remote Exploits funktionieren über das Netzwerk.
13 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.
14 In ihrer Ausarbeitung sollten sie kurz erklären, warum der EIP 41414141 seien sollte, wie der
Rest des Puffers aussieht und warum der Fuzzer nur vier mal 41 in den Puffer schreibt (Tipp: Es
hat etwas mit der Adressierung zu tun).
14
Aufgabe 1.2
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 DevC++ Strg+F9), wird der Fuzzer den Puffer in die Datei out.txt (im selben Verzeichnis) 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 File → Open. 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 tracen15 sie (F8) bis zum nächsten ”Return” (RETN) und schauen sie sich
den Stack am ESP an (→follow in dump). Dieser sollte nun 41414141 aufweisen.
Modifizieren sie diesen Wert mit der Adresse der geheimen Funktion (Rechtsklick
mit der Maus auf den Stackwert→ Modify) und lassen sie das Programm weiterlaufen (F9).
Dokumentieren sie ihre Sprungadresse und erläutern sie ihre Beobachtungen.
Aufgabe 1.3
Nun sollen sie strcpy.exe mit einem Exploit angreifen, dazu öffnen sie
C:\BO\Aufgaben\1\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 es herausgefunden haben16 . Dadurch können sie einen sog. Jumpcode17
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 öffnen dazu strcpy.exe innerhalb von
15 Es gibt zwei Möglichkeiten manuell Befehle in einem Programm mitzuverfolgen: execute
(F7) und step over (F8). Dabei ”überspringt” F8 einen call, F7 springt zur Call-Adresse (die
sich ggf. in einem anderen Modul befindet) und man muss bis zum nächsten return tracen um zur
Augangsposition + eine Adresse zu kommen.
16 Wenn sie zu viele Daten hineinschreiben, überschreiben sie ggf. sog. Exceptionhandler des
Betriebssystems (SEH (Structured Exception Handler)). Dadurch wird der Prozess vom Betriebssystem 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.
17 Ein Jumpcode ist dem Shellcode nicht unähnlich, jedoch ist seine Aufgabe den Programmfluss
zum Shellcode umzuleiten.
15
Ollydbg). In Ollydbg bietet sich hierfür die Datei ntdll.dll an18 . Wenn sie diese Datei in Ollydbg laden (ALT+E → Doppelklick auf den Eintrag von 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 ”verkehrt herum” schreiben (z.B. 12345678 → 78563412)19 ).
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)20 . 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.
Aufgabe 2.1
Benutzen sie nun das Fuzzer Programm
C:\BO\Aufgaben\2\gets_test.exe
um das Programm
C:\BO\Aufgaben\2\gets.exe
auf die benötigte Puffergröße zu testen. Die Aufrufsyntax ist dabei wie folgt:
C:\BO\Aufgaben\2\gets_test.exe gets.exe <PUFFERGRÖßE>
Da Ollydbg im JIT Modus läuft, wird Ollydbg bei einem Ausnahmefehler den Programmfluss 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, um den EIP zu überschreiben.
Tipp: Die Größe des Puffers liegt zwischen 40 und 50 Byte.
18 Man sollte diese Adresse in einem Modul des Betriebssystems finden, damit ist diese für jeden
Computer mit der selben Betriebssystemversion und -sprache gültig → nahezu universell einsetzbar
und muss nur bei Änderungen des Betriebssystems neu ermittelt werden.
19 Ein unsigned Char hat den Wertebereich 0 bis 255 oder hexadezimal 0x00 bis 0xFF (genau ein Byte), es werden also die Bytes gedreht; Das most significant byte wird also das least
significant byte, die anderen wechseln entsprechnd die Position. Anmerkung: ein Nibble (4 Bit)
kann genau als ein Hexadezimalzeichen geschrieben werden (4 bit hat den WerteBereich 0 bis 15,
ein Hexadezimalzeichen den Wertebereich von 0 bis F, wobei F = 15). Bsp.: 1010 = A. Mehrere
aneinanderergereihte Nibble können dann wie aneinandergereihte Hexadezimalzeichen geschrieben
werden; Ein Byte = zwei Nibble = zwei Hexadezimalstellen.
20 Ein Screenshot von ”netstat -ano” unterstreicht ihre Versuchsbeschreibung in der Ausarbeitung.
16
Aufgabe 2.2
Auch hier gilt wieder, dass sie mehr Daten in den Puffer schreiben können 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 zur
Findung des Offsets vor wie in Aufgabe 1.2. 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\1\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 löschen sie dieses unter Verwendung
des XOR Befehls. Füllen sie es danach mit 0x90909090 unter der Verwendung
der MOV Anweisung und subtrahieren sie die Differenz zum Offset21 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-Folgen (z.B. 00, 0000 o.ä.) enthalten sein dürfen.
Ist dies doch der Fall, müssen sie andere Operationen in ihrem Jumpcode durchführen.
Aufgabe 2.3
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
Aufgabe 1.3 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?
Aufgabe 3.1
Benutzen sie nun das Fuzzer Programm
C:\BO\Aufgaben\3\test.exe
um das Programm Winamp auf die benötigte Puffergröße zu testen. Der Aufrufsyntax ist dabei wie folgt:
C:\BO\Aufgaben\3\test.exe <PUFFERGRÖßE>
Da Ollydbg im JIT Modus läuft, wird Ollydbg bei einem Ausnahmefehler den Programmfluss unterbrechen. Daraufhin sollten sie sich in Ollydbg die EIP Adresse
anschauen. Ist diese 41414141 (AAAA), haben sie die richtige Puffergröße getroffen.
Hier ist jedoch eines zu beachten, wenn der von ihnen gewählte Puffer zu klein ist,
geschieht nichts (normaler Programmablauf). Ist der von ihnen gewählte Puffer zu
21 Der Taschenrechner von Windows (calc.exe) kann im wissenschaftlichen Modus mit hexadezimalen Zahlen rechnen.
17
groß, wird der Ausnahmefehler abgefangen (Ollydbg reagiert nicht). Das ”Fenster”,
welches ihnen zur Verfügung steht, um den Puffer zu testen ist 7 Byte, wobei das
4. Byte ähnlich reagiert wie als wenn der Puffer zu groß ist.
Geben sie die Größe des Puffers an, um den EIP zu überschreiben.
Tipp: Die benötigte Puffergröße liegt zwischen 1015 und 1025 Byte.
Aufgabe 3.2
Das hier betroffene Modul ist ”in mp3.dll”, wenn sie sich die Adressierung in Ollydbg anschauen, werden sie bemerken das diese keine vorangestellte 0-Folge besitzt
und Adressen somit direkt ”anspringbar” sind (sie benötigen jmp esp nicht mehr
aus der ntdll.dll). Darum werden sie ”call esp (FFD4)” aus der in mp3.dll statt
dem vorherigen EIP benutzen (das Vorgehen zur Findung der Adresse ist das Selbe wie in Aufgabe 1.3, darum wird ihnen die Adresse vorgegeben). Des Weiteren
müssten sie einen neuen Jumpcode erstellen, auch dieser wird ihnen vorgegeben.
Schauen sie sich dessen Quelltext
C:\BO\Aufgaben\3\jumpcode3.asm
an und probieren sie einmal die ”BYTE” Parameter wegzulassen (danach müssen
sie den Quelltext neu kompilieren und mit XVI32 betrachten). Was bemerken sie
und wie können sie sich das erklären (sollten sie das nicht erklären können, lassen sie die Erklärung weg. Schildern sie jedoch ihre Beobachtung und warum der
Jumpcode nicht funktionieren würde. Sie müssen mindestens eine Hochsprache der
Programmierung beherrschen (die Casting unterstützt) um dieses Phänomen zu beschreiben)?
Modifizieren sie
C:\BO\Aufgaben\3\exploit.c
mit Hilfe der Puffergröße und kompilieren sie den Quelltext (Hinweis: Sie müssen
BUF LEN und drei memcpy Funktionen verändern. Orientieren sie sich an den vorherigen Exploits um die memcpy Funktionen anzugleichen).
Was können sie beobachten, wenn sie das Exploit ausführen?
Tipp: Sollten sie etwas selbst nachprüfen wollen (Sprungweiten o.ä.), die Returnanweisung der in mp3.dll, nach welcher der Ausnahmefehler eintritt, liegt bei Offset
02005CB7. Außerdem besteht die Möglichkeit den ESP in Stack/Dump zu verfolgen (Rechtsklick mit der Maus auf das Register), damit sollten sie in der Lage sein
die Parameter auch selbst zu finden.
Aufgabe 3.3
Modifizieren sie nun die Datei
C:\BO\Aufgaben\3\Internetseite\attack.html
so, dass das local Exploit zu einem remote Exploit wird (Tipp: schauen sie sich den
Quellcode genau an, es ist sehr einfach.).
Beschreiben sie was sie geändert haben, und schlagen sie mind. eine Gegenmaßnahme zum Schutz gegen diese Attacke vor.
18
Aufgabe 3.4 (Optional sofern genügend Zeit vorhanden)
Nun wechseln sie die Betrachtungsweise. Sie arbeiten nun (erfolgreich) als Angestellter bzw. sind selbstständig in der ITS Branche. Sie haben erfahren, dass in der
Software einer ihrer Auftraggeber eine Sicherheitslücke besteht (Winamp). Sie haben bereits getestet, unter welchen Bedingungen das Programm abstürzt (Aufgabe
3.1).
Benutzen sie die Testplayliste (sie liegt in dem Aufgabenverzeichnis (test.pls)) um
die zwei Funktionen (dies wüssten sie unter realen Bedingungen nicht) herauszufinden, die in dem Modul in mp3.dll (auch das müssten sie erst herausfinden) die
Rücksprungadresse überschreiben (eine Funktion überschreibt nicht den EIP selbst
sondern schreibt ohne Überprüfung eine beliebige Menge an Daten auf den Stack).
Das Vorgehen ist nun (eigentlich) wie folgt: sie gehen in das Modul in mp3.dll und
lassen alle ”intermodularen calls” anzeigen (Rechtsklick in den Code → Search for
→ All intermodular calls), dann setzen sie alle Aufrufe als Breakpoint und lassen das
Programm ausführen (F9) (ausgenommen sie wissen welcher Call am wahrscheinlichsten ist, dann neben sie nur diese/n Call/s). Nach jedem Breakpoint untersuchen
sie den Stack (klicken sie in das Stackfenster und drücken sie Strg+B, danach geben
sie als Suchstring 41414141 oder etwas anderes aus ihrem Puffer ein). Wird ihre
Suchabfrage gefunden, geschieht das Überschreiben vor dem jetzigen Breakpoint
(sie müssen das Debuggen neu starten). Wurde der Suchwert nicht gefunden, lassen
sie das Programm weiter ausführen (F9).
Bei jedem neuen Breakpoint müssen sie den Stack untersuchen. Sie sollten dabei
auch die Register beobachten (diese geben manchmal Hinweise darauf, wann etwas
passiert. Man benötigt dazu aber i.d.R. meist Erfahrung mit den API Calls von
Windows). Sie können also nun zählen wie oft sie F9 drücken müssen, bis der Stack
überschrieben wird. Es ist jedoch so, dass sog. Threads22 erstellt werden und verursachen, dass es variiert wie oft sie weiter tracen müssen (dann können die Register
wiederum helfen).
Wenn sie herausgefunden haben, wann sie F9 nicht mehr drücken sollten, gehen
sie von dort an mit F8 weiter vor. Sollten sie dabei über einen ”call” kommen,
müssen sie den Stack wieder durchsuchen und diesen ”call” als Breakpoint setzen
(Hinweis: Mit ”Minus” können sie einen Schritt zurück gehen. Außerdem empfiehlt
es sich die anderen Breakpoints zu deaktivieren, um das Tracen zu beschleunigen),
falls der Stack überschrieben wurde. Nach einem Neustart des Debuggingprozesses,
wird beim Ausführen der neue Breakpoint ausgeführt. Nun gehen sie mit F7 in
den ”call” hinein und fahren mit F8 fort. Dies wiederholt sich solange, bis sie (in
unserem Beispiel) in eine Schleife kommen. In dieser Schleife zeigt Ollydbg ihnen
Informationen über jeden Befehl innerhalb der Schleife an, sobald beim Tracen die
Position des Befehls erreicht wird. Somit können sie die Stackpositionen leicht finden und beobachten wie der Stack überschrieben wird.
Da dies sehr zeitaufwändig ist, bekommen sie einige Breakpoints und API Calls
(intermodular calls) vorgegeben: Die erste Routine wird leichter gefunden, wenn sie
nur Breakpoints auf
CreateFileA
setzen. Sie sollten dadurch auf die Adresse 02009836 stoßen (probieren sie trotzdem einmal das Tracen aus und geben sie an, wie oft sie F9 drücken mussten). Wenn
sie nun den Stack durchsuchen, sollte dieser schon überschrieben worden sein (ein
Mal). Daher scrollen sie etwas nach oben, dort finden sie einen Funktionsprolog (s.
22 Unterprozesse,
die (hier) zur Verwaltung gestartet werden)
19
Kapitel 4.1). Wenn sie dort alle Sprungreferenzen anzeigen lassen (Find References
To), sollten sie neun calls finden. Starten sie den Debugprozess neu und setzen sie
alle Calls der Form 02019xxx als Breakpoints. Sollten sie feststellen, dass auch
bei diesen Breakpoints der Stack überschrieben wurde, nehmen sie den ersten call
VOR ihrem Breakpoint und testen sie dies erneut. Wiederholen sie dies, bis sie
einen Breakpoint gefunden haben andem folgendes gilt: vor dem call ist der Stack
ok, danach ist er überschrieben. Tracen sie in diesen call und suchen sie dort genauso weiter. So sollten sie die erste Funktion schnell finden.
Wenn sie die erste Funktion gefunden haben, lassen sie das Programm bis zum
nächsten Return ausführen und kehren sie zum Aufrufer zurück. Danach lassen sie
das Programm wieder bis zum Return ausführen und sollen so nahe der zweiten
Funktion sein (sie können dies immer bis zu einem Return wiederholen und den
Stack überprüfen, diese Methode ist ähnlich der mit den Calls). Nun tracen sie weiter und beobachten wieder das Programm (nach jedem Call den Stack untersuchen,
denn er wird zweimal überschrieben). Sie sollten nun die zweite Funktion schnell
finden können.
Tipp: Öffnen sie vor der Untersuchung einmal Winamp und laden sie die Testplayliste. Danach schließen sie Winamp wieder. Damit müssen sie die Playliste nicht
immer neu laden, wenn sie die Untersuchung neu beginnen müssen.
Tipp: Wenn sie eine Funktion gefunden haben, bei der sie vermuten das diese den
Stack überscheibt, benutzen sie die Option ”Lock Stack” um den Stack an der jetzigen Position im Auge zu behalten (Rechtsklick mit der Maus auf das Stackfenster.
Es ist dabei immernoch möglich sich andere Teile des Stacks anzuschauen (Scrollen)).
Geben sie die beiden Offsets an, die sie herausgefunden haben. Beschreiben sie
den von ihnen eingeschlagenen Weg unter der Benutzung von F7/F8/F9 und den
von ihnen verwendeten Breakpoints23 . Damit können sie ihrem Auftraggeber eine
detailierte Auflistung der von ihnen gefundenen Ergebnisse liefern (Hinweis: Dieser
Fehler wurde ab Version 5.13 in Winamp behoben.).
Sollten sie diese Aufgabe erfolgreich bewältigt haben, haben sie eine gute Grundlage
im Umgang mit Dateianalysen erworben. Interessierte Teilnehmer seien darauf hingewiesen, dass die letzte Aufgabe zum sog. Reverse Engineering gehört. Zu diesem
Themenkomplex existieren sehr viele, sehr interessante Anwendungen.
23 Es gibt sehr viele Möglichkeiten diese Aufgabe zu erfüllen, die obige Vorgehensweise ist nur
ein Vorschlag.
20
Literatur
[1] http://www.nds.rub.de/lehre/praktika/grundpraktikum its/index.html
[2] Kernighan, Ritchie, Programmieren in C, 2. Auflage, Hanser 1990.
[3] Hyde, R. The Art of Assembly Language Programming.
http://webster.cs.ucr.edu/AoA/index.html
[4] Iczelion, Win32 Assembly Tutorials.
http://win32assembly.online.fr/tutorials.html
[5] Werthmann, T. Survey on Buffer Overflow Attacks and Countermeasures,
Horst Görtz Institute for IT-Security, Ruhr-University Bochum, Germany, June 2006.
[6] Opatz, F. Buffer Overflows für Jedermann, July/August 2005.
[7] Klein, T. Buffer Overflows und Format-String-Schwachstellen. Dpunkt Heidelberg, 2004, ISBN 3-89864-192-9.
[8] Schwenk, J. Vorlesung Programmiersprachen.
http://www.nds.rub.de/lehre/vorlesungen/programmiersprachen/index.html
[9] Ackermann, K. Programmieren in C - Eine Einführung.
http://www.uni-giessen.de/hrz/software/programmiersprachen/C/c teil1.html
21

Documents pareils