E-Mail: f a l k e n s t e i n e r @ g m x . a t
Ich erkläre hiermit an Eides Statt, dass ich die vorliegende Arbeit selbständig angefertigt
habe. Die aus fremden Quellen direkt oder indirekt übernommenen Gedanken sind als
solche kenntlich gemacht. Die Arbeit wurde bisher weder in gleicher noch in ähnlicher
Form einer anderen Prüfungsbehörde vorgelegt und auch noch nicht veröentlicht.
Zu einem der am häugsten registrierten Angrien auf IT-Systeme zählen die Buer-
Overow-Attacken, die gleichzeitig auch ein groÿes Schadens-Potential in sich bergen.
Deshalb wurden in den letzten Jahren verschiedene Technologien realisiert, um Angris-
möglichkeiten aufzudecken oder die erfolgreiche Durchführung der Attacken abzuwehren.
Bei den verfügbaren Techniken zur Verhinderung von Buer-Overow-Angrien wurde
besonderes Augenmerk auf die Sicherheit der Systeme vor unbefugtem Zugri gelegt
- allerdings zielen nicht alle Attacken auf den Zugri auf das System ab. Es kann
bei Angrien der Umstand genutzt werden, dass Ausfallsicherheit, im Gegensatz zu
Zugrissicherheit, oft nicht gewährleistet wird. So ist es möglich, dass selbst geschütz-
te Dienste durch Puerüberläufe angegrien werden und nicht mehr zu Verfügung stehen.
One of the most widely used class of attacks against IT-systems are buer-overow-
attacks. As the eects of such an attack may be fatal, there has been research into
methods that allow the detection of buer-overow vulnerabilities or prevent successful
exploitation.
The main techniques used today are designed to prevent unauthorized access to the
system through a buer-overow. But not all attacks are designed to gain access to a
system, for example a denial-of-service attack. These attacks may still be successful
with most of these protections enabled. Therefore, seeing each of these technologies as
a full protection against buer-overows does not cover the availability of the protected
services and a successful attack may still be possible.
3. Dynamische Methoden 16
3.1. Nicht-ausführbare Speicherbereiche . . . . . . . . . . . . . . . . . . . . . . 16
3.2. Canary-Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.3. Explizite Erkennung von Speichergrenzen . . . . . . . . . . . . . . . . . . 19
3.4. Verbesserte Heap-Implementation . . . . . . . . . . . . . . . . . . . . . . . 20
3.5. Address Space Layout Randomization . . . . . . . . . . . . . . . . . . . . 21
Ziel dieser Arbeit ist es, die Auswirkung von dynamischen Methoden zur Verhinderung
von erfolgreichen Buer-Overow-Attacken auf die Ausfallsicherheit von Diensten zu un-
tersuchen und derzeit verbreitete Technologien zu vergleichen.
1
2. Angris-Grundlagen
2.1. Buer-Overow-Schwachstellen
Wie Vallentin feststellte, werden sicherheitskritische Fehler in Software nicht einfach ver-
schwinden: Exploitable software aws produced by software developers will not cease to
exist. ([Vallentin2007], S.18)
Buer-Overow-Schwachstellen entstehen durch simple - oft marginale - Fehler in
Software-Algorithmen. Es ist für Programmierer durchaus gebräuchlich, einen Puer mit
einer frei gewählten Gröÿe anzulegen - im guten Glauben, dass niemals mehr Speicher
in diesem Puer benötigt wird. Weiters wird meist die Überschreitung der maximalen
Gröÿe nicht explizit behandelt ([RussellCunningham2000], S.204). Diese Vorgangsweise
kann unter Anderem dazu führen, dass Angrie durch Puerüberläufe möglich werden.
Ein Angreifer kann etwa unberechtigt die Kontrolle über Systeme übernehmen oder ein
verwundbares Service manipulieren.
Die verfügbaren statischen Werkzeuge zum Aufspüren von Schwachstellen neigen aller-
dings einerseits zu false-positives, also falschen Treern, andererseits ist es diesen Tools
oft nicht möglich, ein Risiko bei Überlagerung von Speicherstrukturen und komplexer
Pointer-Arithmetik zu erkennen ([Johns2005], S.10).
Schaden von Buer-Overow-Attacken dynamisch begrenzen zu können, ist das Ziel von
anderen - heute weit verbreiteten - Techniken. Es wird versucht, allen heute bekannten
Angris-Techniken auf Buer-Overow-Schwachstellen mit unterschiedlichen Vorgehens-
weisen entgegenzuwirken und damit erfolgreiche Angrie zu verhindern ([Johns2005],
S.43f ). Wie unter anderem Klein ([Klein2003a], S.9, S.187f ) beschreibt, können diese
Technologien Attacken nicht immer unterbinden, jedoch sind sie in der Lage, deren Aus-
wirkung einzudämmen.
Dynamische Ansätze wurden in der Vergangenheit stark weiterentwickelt, um Systeme
vor einer Veränderung der Programm-Algorithmen und vor der Ausführung von frem-
dem Code zu schützen, und daher auf Angrie zur Laufzeit reagieren zu können. Dabei
kann sich jedoch die Reaktion auf einen Puerüberlauf auf einen Programm-Abbruch
beschränken, was einem Ausfall des Programms gleichkommt.
Eine Charakteristik von Buer-Overows ist, dass Daten eines Prozesses überschrieben
und Information zerstört wird, die für den gewünschten Programm-Ablauf notwendig ist.
Durch diesen Umstand bleiben wenige Möglichkeiten, das betroene Programm regulär
fortsetzen zu können, selbst wenn der Puerüberlauf bereits während der Ausführung
erkannt wird. Daher untersucht diese Arbeit gebräuchliche dynamische Technologien auf
Ausfallsicherheit bzw. das Verhalten bei verschiedenen Generationen von Puerüberläu-
fen.
2
2.2. SPEICHERMANAGEMENT
2.2. Speichermanagement
hohe Speicheradressen
Stack
Heap
BSS Segment
Data Segment
Text Segment
niedere Speicheradressen
Hierzu muss allerdings angemerkt werden, dass verschiedene Compiler das Speicher-
modell in erweiterter oder abgeänderter Form implementieren und daher dieses Modell
nur zur prinzipiellen Veranschaulichung dient.
Die gezeigten Segmente haben folgende Aufgaben:
• Text Segment
Das Text-Segment beinhaltet den ausführbaren Programmcode und kann standard-
mäÿig zur Laufzeit nicht verändert werden (read-only).
• Data Segment
Im Data-Segment benden sich initialisierte globale Daten.
3
2.2. SPEICHERMANAGEMENT
• Heap-Segment
Das Heap-Segment (kurz Heap) wird dazu verwendet, um dynamisch allozierte
Variablen abzulegen. Dieses Segment wächst in seiner Gröÿe hin zu den höheren
Speicheradressen. (siehe Abbildung 2.1)
• Stack-Segment
Werden lokale Variablen deniert, einer Funktion Parameter übergeben oder Re-
gister während eines Funktionsaufrufs gesichert, so werden diese Daten auf dem
Stack-Segment (kurz Stack) gespeichert. Der Stack ist mit den typischen Befeh-
len push und pop ein FILO-Puer (First-In Last-Out) und wächst hin zu den
niederen Speicheradressen.
([Viking2006], S.5)
Die Korrelation zwischen Variablen in einem Programm und den einzelnen Segmenten
ist in Abbildung 2.2 illustriert.
0 #include <stdlib.h>
1
2 int g_counter = 0; // g_counter liegt im Data Segment
3 int g_current_char; // g_current_char liegt im BSS Segment
4
5 void main(void)
6 {
7 static char idx; // idx liegt im BSS Segment
7 int tmp = 1; // tmp liegt im Stack Segment
8 void* buffer = malloc(24); // buffer zeigt auf das Heap Segment
9
10 g_counter ++; // Befehle liegen im Text Segment
11 }
4
2.2. SPEICHERMANAGEMENT
0 #include <stdio.h>
1
2 int mult(int a, int b)
3 {
4 int x;
5 char buffer[2];
6 x = a * b;
7 return x;
8 }
9
10 void main(int argc, char ** argv)
11 {
12 int res;
13 res = mult(3, 7);
14 }
Bevor die Ausführung des Unterprogramms mult() erfolgt, werden folgende Daten auf
den Stack geschrieben:
• Argumente
(im Beispiel die Werte 3 und 7)
Die Argumente werden dem Unterprogramm übergeben, indem sie auf den Stack
geschrieben werden.
In Abbildung 2.4 wird der Stack vor und während der Ausführung des Unterprogramms
mult() illustriert. Dabei ist die Reihenfolge ersichtlich, in der Daten auf dem Stack liegen.
Der Stack wird dabei hin zu den niedrigen Speicheradressen gefüllt, die Variablen wachsen
im Gegensatz dazu hin zu den hohen Speicheradressen.
5
2.3. BUFFER-OVERFLOW-ANGRIFFE
a) b) hohe Speicheradressen
Argument: *argv Argument: *argv
Argument: argc Argument: argc
"RIP" "RIP"
Stackframe
von main()
vorhergehender "SFP" vorhergehender "SFP"
Stackframe
lokale Variable: res lokale Variable: res
von main()
Argument: "7"
Argument: "3"
"RIP"
"SFP" von main()
Stackframe
von mult()
lokale Variable: b[1]
lokale Variable: b[0]
lokale Variable: x
niedere Speicheradressen
Abbildung 2.4.: Stack-Segment eines UNIX-Prozesses bei einem Funktionsaufruf (a) vor
dem Funktionsaufruf von mult() (b) bei der Ausführung von mult()
([BryantO'Hallaron2003], S.170f )
2.3. Buer-Overow-Angrie
• Denial-of-Service (DoS)
Das Ziel eines DoS-Angris ist, dass das Zielservice nicht mehr zur Verfügung
steht. Die Durchführung von DoS-Attacken bei Buer-Overows ist meist trivial.
([Klein2003a], S.49)
6
2.3. BUFFER-OVERFLOW-ANGRIFFE
0 #include <string.h>
1 #include <stdio.h>
2
3 void argcpy(char * ar)
4 {
5 char buffer[11];
6 strcpy(buffer, ar);
7 printf("%s\n", buffer);
8 }
9
10 int main(int argc, char ** argv)
11 {
12 argcpy(argv[1]);
13 printf ("finish\n");
14 return 0;
15 }
Wie in Zeile 5 ersichtlich ist, hat die lokale Variable buer eine Gröÿe von 11 Bytes
- daher ist bei einem String Platz für 10 Zeichen plus das terminierende NULL-Byte.
Die Funktion strcpy() (Zeile 6) kopiert nun den Inhalt des Arguments ar in die lokale
Variable buer, ohne dessen maximale Gröÿe zu berücksichtigen.
Abbildung 2.6.: Aufruf des Programms aus Abb. 2.5 (a) Normale Ausführung (b)
Puerüberlauf
7
2.3. BUFFER-OVERFLOW-ANGRIFFE
In Abbildung 2.6b ist das Ergebnis eines Puerüberlaufs zu sehen, der zur Kategorie
der DoS-Angrie gehört, denn das Programm wird durch den Buer-Overow auÿerplan-
mäÿig beendet ([Klein2003a], S.49). Die entsprechenden Ausschnitte des Stacks werden
in Abbildung 2.7b gezeigt. Hierbei ist ebenso ersichtlich, weshalb man bei derartigen
Attacken von Stack-Zertrümmerung (engl. stack-smashing) spricht: Es werden Bereiche
des Stack-Segments mit beliebigen Werten überschrieben - dadurch benden sich anstelle
von sorgfältig gewählten Zeigern (RIP und vorhergehender SFP), ASCII-Zeichen auf dem
Stack ([AlephOne1996]).
a) b) ’6’ c)
’5’
Argument: *ar ’4’ NULL
"RIP" ’3’ "RIP"
manipulierter "RIP"
vorhergehender "SFP" ’2’ manipulierter "SFP"
NULL ’1’ ’1’
’0’ ’0’ ’0’
’9’ ’9’ ’9’
Stackframe
Stackframe
Stackframe
’8’ ’8’ ’8’
’7’ ’7’ ’7’
’6’ ’6’ ’6’
’5’ ’5’ ’5’
’4’ ’4’ ’4’
’3’ ’3’ ’3’
’2’ ’2’ ’2’
’1’ ’1’ ’1’
Abbildung 2.7.: Stack-Segment bei den Aufrufen aus Abb. 2.6 - (a) ohne Überlauf (b)
mit willkürlichem Überlauf (c) mit gezieltem Überlauf
Durch den Umstand, dass man mit einem stackbasierten Buer-Overow den RIP
und den gesicherten SFP überschreiben kann, ist es möglich, diese zwei Pointer mit
präparierten Werten zu versehen
1 (siehe Abbildung 2.7c). Beispielsweise kann man den
RIP mit der Adresse einer bestimmten Funktion überschreiben, was zu der Ausführung
dieser Funktion nach dem Abschluss der aktuellen Prozedur führt. Die Manipulation
des SFP wird hier nicht behandelt, da dieser das Kapitel 2.3.3 gewidmet ist.
Hierbei gibt es grundsätzlich drei Möglichkeiten, das Programm zu manipulieren, wie
Abbildung 2.8 zeigt. Der RIP kann so manipuliert werden, dass eine bereits vorhandene
Prozedur nach Beendigung der Aktuellen ausgeführt wird (Abbildung 2.8a). Unter
diversen UNIX-Betriebssystemen kann man hiermit beispielsweise Prozeduren der
libc-Programmbibliothek ausführen. Diese Angristechnik wird in der Literatur oft als
return-to-libc oder kurz ret2libc bezeichnet ([Klein2003a], S.405).
1
Eine gleichzeitige und direkte Manipulation von SFP und RIP bei der strcpy() -Funktion ist auf man-
chen Systemen schwierig, da keine (führenden) NULL-Bytes bei Adressen verwendet werden können.
NULL-Bytes terminieren Strings - daher kopiert die strcpy() -Funktion den übergebenen String nur
bis zum ersten NULL-Byte.
8
2.3. BUFFER-OVERFLOW-ANGRIFFE
a) b) c)
STACK-SEGMENT
STACK-SEGMENT
STACK-SEGMENT
Programm-Code
Programm-Code
Programm-Code
manipulierter "RIP"
"RIP" Programm-Code NOP
’2’ Programm-Code NOP
’1’ Programm-Code NOP
Programm-Code
Programm-Code
TEXT-SEGMENT
Programm-Code NOP
Programm-Code NOP
Programm-Code NOP
"RIP"
manipulierter "RIP" manipulierter "RIP"
"RIP"
’2’ ’2’
Prozedur
’1’ ’1’
Abbildung 2.8.: Angris-Typen bei gezielter Manipulation des RIP (a) Ausführung von
bereits vorhandenem Programmcode (b) Einschleusen und Ausführung
von beliebigem Programmcode (c) Angri mit NOP-Sliding
Es ist oft schwierig, den RIP bei letzteren Attacken auf die exakte Start-Adresse des
eingeschleusten Codes zu setzen. Daher gibt es eine hilfreiche Technik für derartige
Attacken - das NOP Sliding. Eine NOP2 -Anweisung selbst hat keine Auswirkungen auf
ein Programm, auÿer dass im nächsten CPU-Takt sofort die nachfolgende Anweisung
ausgeführt wird ([Leidecker2006], S.41). Diese Eigenschaft wird nun dazu verwendet,
um einen Bereich auf dem Stack, ein NOP Slide, zu erstellen, der unmittelbar an den
eingeschleusten Code anschlieÿt und in den der RIP höchstwahrscheinlich zeigt. Wird
nun ein beliebige NOP-Anweisung innerhalb dieses Bereichs angesprungen, so werden die
NOPs ausgeführt, bis der Programmcode erreicht ist.
2
Die Abkürzung NOP steht hier für den Assembler-Befehl No OPeration
9
2.3. BUFFER-OVERFLOW-ANGRIFFE
0 #include <string.h>
1 #include <stdio.h>
2
3 void argcpy(char * arg)
4 {
5 char buffer[10];
6 int i = 0;
7
8 for(i=0;i<=10;i++)
9 buffer[i] = arg[i];
10 }
11
12 void do_copy(char ** argv)
13 {
14 argcpy(argv[1]);
15 }
16
17 int main(int argc, char ** argv)
18 {
19 do_copy(argv);
20 printf("finish\n");
21 return 0;
22 }
Auf einer 32 Bit Plattform ist sowohl der RIP als auch der SFP 4 Byte groÿ - bei 64
Bit sind es entsprechend 8 Byte ([Leidecker2006], S.13f; [Klein2003a], S.21). Mit einer
typischen o-by-one-Schwachstelle (wie in Abb. 2.9) lässt sich nun das letzte Byte des
gesicherten SFP überschreiben, wodurch sich dieser Zeiger um maximal 255 Bytes ver-
schieben lässt.
Dies kann ausreichen, um den SFP so zu verschieben, dass dieser auf ein gefälschtes
Stack-Frame mit zugehörigem RIP zeigt (Abbildung 2.10b). Beim Verlassen der aktuel-
len Prozedur argcpy() wird das manipulierte Stack-Frame als ursprüngliches Stack-Frame
10
2.3. BUFFER-OVERFLOW-ANGRIFFE
betrachtet und äquivalent der manipulierte RIP als ursprünglicher RIP. Beim Verlassen
der vorhergehenden Prozedur do_copy() wird die Ausführung unmittelbar bei der Posi-
tion des manipulierten RIP fortgesetzt.
Es ist jedoch unbedingt zu beachten, dass für das Einschleusen von beliebigem Pro-
grammcode auch hier ein ausführbarer Stack benötigt wird.
a) b)
Abbildung 2.10.: Stack während der Ausführung von dem Programm aus Abb. 2.9 (a)
während der normalen Ausführung von argcpy() (b) SFP-Overwrite
während argcpy()
Trivialerweise kann eine gezielte Manipulation des SFP auch dazu benutzt werden, den
Programmuss maÿgeblich zu beeinussen, ohne einen Programmcode einzuschleusen.
Dies wird etwa durch die gezielte Konstruktion eines Stackframes erreicht, das lokale
Variablen auf gezielte Werte setzt ([Klein2003a], S.126).
11
2.3. BUFFER-OVERFLOW-ANGRIFFE
0 #include <string.h>
1 #include <stdio.h>
2
3 int (*fct)(int);
4 char buffer[11];
5
6 int add10(int j)
7 {
8 return (j + 10);
9 }
10 void argcpy(char * ar)
11 {
12 strcpy(buffer, ar);
13 printf("%s\n", buffer);
14 }
15 int main(int argc, char ** argv)
16 {
17 int k = 0;
18 fct = &add10; // setze function-pointer
19 argcpy(argv[1]);
20 k = fct(k); // führe function-pointer aus
21 }
12
2.3. BUFFER-OVERFLOW-ANGRIFFE
*fct
buffer[10]
BSS-SEGMENT
buffer[9]
buffer[2]
"RIP"
buffer[1]
buffer[0]
TEXT-SEGMENT
Prozedur
Abbildung 2.12.: Stack während der Ausführung von dem Programm aus Abb. 2.11
cherstellen dar. Diese indirekte Veränderung wird aufgrund des Speicher-Layouts in ei-
nigen Heap-Implementierungen ermöglicht. Bei der Implementierung des Heap-Speichers
von Lea, die auch in einer alten GNU C-Bibliothek verwendet wird, werden beispiels-
weise mit free() freigegebene Blöcke als doppelt verkettete Liste im Speicher markiert.
([Lea2005], Zeile 1482 )
Gelingt es nun diese Management-Information durch einen gezielten Puerüberlauf zu
überschreiben, so ist es unter bestimmten Umständen möglich, beim Aufruf der Funktion
free() Schreiboperationen auf eine beliebige Speicherstelle (etwa einen RIP) auszuführen.
In Abbildung 2.13 ist zu sehen, wie der Heap unter Verwendung der malloc-
Implementation von Lea ([Lea2005]) strukturiert ist. Der Header besteht dabei aus drei
wichtigen Komponenten:
Diese Angristechnik ist seit einigen Jahren bei der GNU C Library nur mehr teilweise
möglich, da heutzutage nur mehr sehr wenig Information in den Header-Bereichen liegt
und diese auch auf Konsistenz überwacht wird. Für die in dieser Arbeit verwendeten
DoS-Attacken ist ein Programm mit einer Heap-Overow-Schwachstelle allerdings noch
immer anfällig, weshalb diese Technik hier ausführlicher beschrieben wird.
Mit dem Aufruf der Funktion malloc() wird im nächsten freien Block, der eine entspre-
chende Gröÿe hat, ein neuer Chunk angelegt und der Pointer auf den Daten-Bereich des
13
2.3. BUFFER-OVERFLOW-ANGRIFFE
Daten−Bereich
vorherige Chunk−Größe
Header
Zeiger auf den Chunk−Größe 1
allokierten Speicher
Chunk
Daten−Bereich
vorherige Chunk−Größe
Chunk−Größe 1
Daten−Bereich
Abbildung 2.13.: Struktur des Heap bei Verwendung der Implementation der GNU C-
Library von Lea ([Lea2005], Zeile 1484 - 1530)
Chunk−Größe 1 Chunk−Größe 1
vorheriger freier Chunk vorheriger freier Chunk
feigegebener Chunk
Daten−Bereich Daten−Bereich
Abbildung 2.14.: (a) Freigabe eines einzelnen Chunks (b) Konsolidierung mit nachfolgen-
dem freien Chunk (c) Konsolidierung mit vorhergehendem freien Chunk
([Lea2005], Zeile 1530 )
14
2.3. BUFFER-OVERFLOW-ANGRIFFE
Chunks zurückgeliefert (Abbildung 2.13). Wird nun ein Chunk mit der Funktion free()
freigegeben, so werden im Daten-Bereich zwei Zeiger (in der Literatur in Anlehnung an
[Lea2005] oft fd und bk genannt) abgelegt, die für die doppelt verkettete Liste von freien
Chunks verwendet werden (Abbildung 2.14a).
Bendet sich vor oder nach dem Chunk ein freier Chunk, so wird dieser mit dem
aktuellen in einen gröÿeren freien Chunk konsolidiert (Abbildung 2.14b,c), der sich
wiederum richtig in die doppelt verkettete Liste integrieren muss (Abbildung 2.15a).
(a) (b)
überschreibt RIP
vorherige Chunk−Größe vorherige Chunk−Größe
Chunk−Größe 1 Chunk−Größe 0
vorheriger freier Chunk
Daten−Bereich
nächster freier Chunk
Abbildung 2.15.: (a) doppelte Verkettung von zwei freien Speicherblöcken (b) Beispiel
für einen gezielten Buer-Overow im Heap-Segment mit Einschleusen
von deniertem Programmcode
Die Eigenschaft, dass bei dieser Heap-Implementation sich der Header im gleichen
Speicherbereich mit den Daten bendet, kann dazu verwendet werden, über einen Puer-
überlauf gezielt manipulierte Header-Informationen einzuschleusen. Ein Angrisvektor
ist hier die Anpassung der doppelt-verketteten Liste bei der Konsolidierung mit einem
anderen freien Chunk. Hierbei müssen zwei Schreiboperationen ausgeführt werden,
um den neu entstandenen, gröÿeren freien Speicherblock in die doppelt verkettete
Liste einzufügen. Kann man nun den fd - und bk -Zeiger durch den Buer-Overow
manipulieren, so überschreibt die Konsolidierung die angegebenen Speicherstellen.
Abbildung 2.15b zeigt ein Beispiel eines Heap-Buer-Overows: Der Puerüberlauf
des ersten Chunks wird einerseits dazu verwendet, den Header des zweiten Chunks zu
manipulieren, andererseits wird auf diese Weise ein Programmcode eingeschleust, der
später zur Ausführung gebracht werden kann.
15
3. Dynamische Methoden zur
Verhinderung von
Buer-Overow-Angrien
Im Gegensatz zu statischen Methoden (etwa Source-Code-Analyse) wirken dynamische
Methoden direkt während der Laufzeit eines Programms. Dabei kann es erforderlich
sein, den Programmcode inklusive der dynamischen Schutz-Algorithmen neu zu kompi-
lieren oder ohne Neuübersetzung dem Betriebssystem die Kontroll- bzw. Schutzaufgabe
zu überlassen.
In den folgenden Punkten sollen dynamische Methoden vorgestellt werden, die häug in
der Praxis Verwendung nden. Beispielsweise sind unter anderem folgende Werkzeuge
derzeit verfügbar:
• PaX
http://pax.grsecurity.net/
• grsecurity
http://www.grsecurity.net/
• ProPolice / Stack-Smashing-Protector SSP
http://www.trl.ibm.com/projects/security/ssp/
• OpenWall Owl
http://www.openwall.com/Owl/
• R)
Betriebssystems-Funktionen (Linux, OpenBSD, Microsoft Windows
Eine einfache Möglichkeit, die Ausführung von fremdem Code zu verhindern ist,
bestimmte Speicherbereiche, die im normalen Programmuss nur zur Speicherung von
Daten dienen, als nicht-ausführbar zu kennzeichnen. Dies zielt im Besonderen auf das
Einschleusen von fremdem Programmcode ab (vgl. 2.3.1), wo Befehle beispielsweise in
den Stack- oder Heap-Bereich geschrieben und dort ausgeführt werden.
Sowohl der Stack- als auch der Heap-Bereich werden für die Speicherung von Daten
verwendet. Entzieht man diesen Bereichen die Berechtigung, Code auszuführen, so sollte
es - der Theorie nach - zu keiner Einschränkung kommen. In der Praxis sieht man
allerdings, dass bei einigen Programmen eine dynamische Code-Generierung in einem
der beiden Segmente vorausgesetzt wird. So verwendet etwa der Compiler GCC eine
Technik Trampolines, die einen ausführbaren Stack benötigt ([Leidecker2006]).
In der Literatur werden nicht-ausführbare Speicherbereiche oft mit dem Namen der
OpenBSD-Technik W xor X (Write eXclusive-OR eXecute) bezeichnet, was die Trennung
16
3.1. NICHT-AUSFÜHRBARE SPEICHERBEREICHE
Abbildung 3.1.: Ein Programm, das die Ausführungssperre überschreitet wird von der
Data-Execution-Prevention beendet (Windows)
In Abbbildung 3.2 ist das beschriebene Verhalten des Programms aus Anhang B beim
Versuch, eingeschleusten Programmcode auszuführen, zu sehen. Das System entspricht
der Spezikation im Anhang A.3.
Abbildung 3.2.: Nach dem Puerüberlauf wird zum Schutz vor dem eingeschleusten Pro-
grammcode das Programm abgebrochen
17
3.2. CANARY-VALUES
3.2. Canary-Values1
Wie bereits diskutiert, zielen viele Buer-Overow-Angrie auf das Überschreiben des
Return-Instruction-Pointers (RIP) ab, um den Algorithmus maÿgeblich zu verändern.
Daher wurde eine Technik entwickelt, um eine Manipulation dieses Werts erkennen zu
können, indem zwischen dem Stackframe einer Prozedur und dem RIP ein überwachter
Wert gespeichert wird. Dieser Wert wird meist als Canary-Value bezeichnet.
Will man durch einen klassischen, stack-basierten Puerüberlauf den RIP überschreiben,
so muss auch der Canary-Value überschrieben werden. ([Cowan1999], S.2f )
Wie in Abbildung 3.3 zu sehen ist, bildet der Canary-Value eine Grenze zwischen den
lokal verfügbaren Daten (lokale Variablen) und dem RIP sowie den Argumenten. Über-
schreibt nun der Puerüberlauf den Canary-Value mit einem beliebigen Wert, so kann
diese Wertänderung vor dem Rücksprung detektiert und entsprechend behandelt werden.
([Klein2003a], S.306)
Argument
"RIP"
Canary-Value
vorhergehender "SFP"
lokale Variable
Stackframe
lokale Variable
lokale Variable
lokale Variable
lokale Variable
Sollte es möglich sein, den Canary-Value mit dem korrekten Wert während des Buer-
Overow Angris zu überschreiben, so würde ein erfolgreicher Angri nicht erkannt wer-
den ([Klein2003a], S.311). Deshalb wird typischerweise für eine Canary-Value einer der
folgenden Werte gewählt:
1
Die Bezeichnung Canary-Value (Kanarienvogel-Wert) geht laut [Cowan1999] auf die Kanarienvögel
von Bergarbeitern zurück. Die Vögel starben bei gefährlichen Methankonzentrationen in den Stollen,
was wiederum den Arbeitern unmittelbare Gefahr signalisierte.
18
3.3. EXPLIZITE ERKENNUNG VON SPEICHERGRENZEN
Auch bei der Verwendung von Canary-Values werden die Ziel-Programme abgebro-
chen, sobald eine Manipulation detektiert wird. Dies geschieht, wie bereits erwähnt, um
das System vor Kompromittierung zu schützen. Die Verfügbarkeit eines angegrienen
Dienstes wird durch diese Maÿnahme nicht erhöht.
Wird bei jedem Schreibvorgang in den Speicher überprüft, ob die Speichergrenzen der
entsprechenden Variablen eingehalten werden, so kann es zu keinem Puerüberlauf kom-
men ([Klein2003a], S.305).
buffer[3] pointer
buffer[4]
limit
buffer[3]
pointer
buffer[2] base
buffer[1]
buffer[0]
2
Entspricht prinzipiell einem boolschen Wert {Canary-Value intakt, Canary-Value nicht-intakt}.
3
In der Literatur oft auch mit dem englischen Begri strict bounds checking bezeichnet.
19
3.4. VERBESSERTE HEAP-IMPLEMENTATION
Eine allgemeine Variante, die Erkennung der Speichergrenzen zu ermöglichen, ist das
Verwenden des in Abbildung 3.4 illustrierten Tripletts für Zeiger auf Speicherelemente.
Dabei wird der Zeiger auf den Anfang (base ), der Zeiger auf das aktuelle Speicher-
Element (pointer ) und der Zeiger auf das Ende (limit ) der Variable mitgeführt. Bei
Schreibvorgängen muss nun überwacht werden, dass der pointer nicht gröÿer als das
limit und nicht kleiner als die base wird ([JonesKelly1997], S.2f ).
Ein weiterer Vorteil, der sich implizit aus der Überprüfung bei jedem Schreibzugri
ergibt, liegt darin, dass ein Programm nicht abgebrochen werden muss, wenn ein nicht
erlaubter Zugri detektiert wird. Der Schreibvorgang kann verhindert werden und das
Programm fortgesetzt werden. Der Zugris-Fehler kann dem Benutzer etwa durch das
Konzept der Exceptions signalisiert werden ([Klein2003a], S.305).
Der Performance-Verlust durch Bounds Checking ist allerdings groÿ und wird, etwa
in [Frykholm2000], mit einer Verlangsamung der Programmausführung von 200% und
mehr angegeben. Weiters kann das gleichzeitige Verwenden von nicht-objektorientierte
und objektorientierter Logik (z.B. in C++) manchmal nicht korrekt erkannt werden,
was wiederum zu false positives und eine Beeinträchtigung des Programm-Algorithmus
führt. Dadurch wird diese Technologie in nicht-objektorientierten Sprachen praktisch
nicht eingesetzt und wird auch in dieser Arbeit nicht weiter behandelt.
In objektorientierten Hochsprachen wird eine ähnliche Technik verwendet, die beispiels-
4
weise eine Zeichenkette bereits als Objekt implementiert . Das Schreiben in die Zei-
chenkette kann somit mit Prüfungen versehen werden und Puerüberläufe erfolgreich
verhindern. Die Angrismöglichkeit liegt nun nicht mehr bei den Programmen selbst,
sondern in den Compilern bzw. den Virtual-Machines der Hochsprachen (siehe etwa die
Beschreibung der Java-Virtual-Machine-Schwachstelle in [SunSolve2007]).
Der in Abbildung 2.15b (Kapitel 2.3.5) gezeigte Puerüberlauf basiert auf der Möglich-
keit, die internen Zeiger einer doppelt verketteten Liste zu manipulieren, um etwa einen
RIP zu überschreiben. Diese Technik kann allerdings durch eine triviale Überprüfung der
Zeiger verhindert werden. In [Lea2005] ist beispielsweise eine Adressbereichsüberprüfung
vorhanden, die Zeiger auf Speicherstellen auÿerhalb des Heap-Segments nicht zulässt.
Weiters können durch diverse Laufzeitüberprüfungen beim Behandeln von Chunks
und Header-Informationen mögliche Attacken (z.B. Umlenken von Zeigern), aber auch
Programmierfehler, erkannt werden ([Lea2005], Zeile 236f ).
4
Diese Implementierung geht natürlich auf Kosten der Laufzeit-Performance der Applikation, da kein
direkter Speicherzugri erfolgen kann.
20
3.5. ADDRESS SPACE LAYOUT RANDOMIZATION
Somit kann eine verbesserte Implementierung der Heap-Verwaltung durchaus zur Ver-
fügbarkeit von Diensten während Heap-basierten Puerüberläufen beitragen, allerdings
ergeben sich zwei Einschränkungen: Die Fehlersuche wird durch ignorierte Fehler er-
schwert und die Laufzeit-Performance drastisch gesenkt ([Lea2005], Zeile 263-264).
Manche Techniken, wie etwa die nicht-ausführbaren Speicherbereiche (siehe 3.1), zielen
primär darauf ab, das Einschleusen von Programmcode zu verhindern. Es kann jedoch
genügen, eine bestimmte vorhandene Funktion auszuführen, um Zugri auf ein geschütz-
tes System zu erlangen (Vgl. return-to-libc, [Klein2003a], S.405).
Deshalb wurde eine Technik entwickelt, die das beabsichtigte Aufrufen von vorhandenen
Prozeduren erschweren soll, indem die Speicheradressen, an denen sich die Prozeduren
benden, variiert werden. Dies wird erreicht, indem sowohl die Reihenfolge als auch die
Position der Segmente auf zufällige Weise geändert wird (siehe Abbildung 3.5b).
a) b)
BSS Segment
Stack
Text Segment
Data Segment
Heap
Text Segment
0x0 0x0
Abbildung 3.5.: Festlegung der Position der Segmente (a) ohne ASLR (b) mit ASLR
(nach [Shacham2004], S.1)
Weiÿ ein Angreifer nicht, welche Speicherstelle welcher Prozedur zugeordnet ist,
so kann dieser keinen gezielten Prozeduraufruf durchführen. Hierbei wird weder der
Puerüberlauf selbst, noch die Veränderung von Rücksprungadressen verhindert oder
behandelt. Vielmehr baut diese Technologie auf der These auf, dass ein willkürlicher
Sprung an eine Speicherstelle meist zu einer Zugrisverletzung oder einem anderen
Programmabbruch führt ([Shacham2004], S.1f ).
21
3.5. ADDRESS SPACE LAYOUT RANDOMIZATION
Um festzustellen ob ein System ASLR einsetzt, kann beispielsweise das Programm aus
Abbildung 3.6 verwendet werden. Bei der mehrmaligen Ausführung zeigt sich, dass sich
bei aktiviertem ASLR die ausgegebene Adresse bei jeder Ausführung ändert (Abbildung
3.7b), bei einem statischen Memory-Layout jedoch unverändert bleibt.
0 #include <stdio.h>
1
2 int main(void)
3 {
4 static int x = 0;
5 printf ("address of x: 0x%0X\n", (unsigned int)&x);
6 return x;
7 }
(a) (b)
Abbildung 3.7.: Ausführung des Programms aus Abbildung 3.6 (a) ohne ASLR (b) mit
ASLR
ASLR ist eine eektive Möglichkeit um Attacken zu verhindern, die versuchen, mit
Sprüngen an bestimmte Speicherstellen die Kontrolle über Programme zu übernehmen.
Trotzdem gibt es auch hier Schwachstellen, die etwa auf der vorherigen Suche der
richtigen Speicheradresse oder auf nicht zufällig verteilten Speicherbereichen beruhen
([Shacham2004], S. 2 ).
Die ASLR-Technik kann aufgrund der Unvorhersagbarkeit des Sprungziels einen Pro-
grammabbruch (z.B. bei einer Speicherzugrisverletzung) natürlich nicht verhindern.
22
4. Diskussion und Ergebnisse
Es hat sich gezeigt, dass eine einzige dynamischen Methode - die explizite Erkennung
von Speichergrenzen - im Stande ist, Puerüberläufe vollständig zu unterbinden und da-
her auch zu keinem Ausfall des entsprechenden Dienstes bei einem Angri führt. Die in
dieser Methode verwendete Kapselung entspricht, wie bereits erwähnt, praktisch der Be-
trachtung eines Puers als Objekt nach Grundlage der Objektorientierung. Diese Technik
erfordert natürlich einen groÿen Overhead aufgrund der Verwaltungsmechanismen für die
Objekte, was wiederum zu einer verminderten Performance führt. Wenn eine gute Lauf-
zeitgeschwindigkeit des Algorithmus nicht das primäre Ziel ist und ausreichend Resourcen
vorhanden sind, kann daher durch Kapselung der Puer oder durch Verwendung einer
objektorientierten Hochsprache das Risiko eines Buer-Overows im Programm ausge-
schlossen werden.
Alle verbleibenden Methoden führen in der Standard-Konguration bei einem geziel-
ten Puerüberlauf zu einer Beendigung des entsprechenden Programms, und daher zu
einem Ausfall des Dienstes. Aus diesem Grund sollten diese Techniken nur der Schadens-
Begrenzung dienen, d.h. kommt es zu einem Buer-Overow, so darf es dem Angreifer
nicht möglich sein, das Programm unter seine Kontrolle zu bringen. Diese Methoden kön-
nen allerdings Angrie über Puerüberläufe nicht verhindern oder ausschlieÿen, da durch
einen Buer-Overow-Angri das Programm meist beendet wird und eine DoS-Attacke
trotz Einsatz der entsprechenden Techniken durchaus noch erfolgreich sein kann.
23
Literaturverzeichnis
[AlephOne1996] Aleph One. Smashing the stack for fun and prot. Phrack Magazine, 7,
11 1996.
http://www.phrack.org/issues.html?issue=49&id=14&mode=txt
Zugri 2008-10-28.
[Cristey2007] M. Cristey and R.A. Martin. Vulnerability type distributions in cve. Tech-
nical report, The MITRE Corporation, 2007.
http://cwe.mitre.org/documents/vuln-trends/index.html
Zugri 2008-10-28.
24
Literaturverzeichnis
[Nergal2001] Nergal. The advanced return-into-lib(c) exploits: Pax case study. Phrack
Magazine, 58, 12 2001.
http://www.phrack.com/issues.html?issue=58&id=4&mode=txt
Zugri 2008-10-18.
25
Literaturverzeichnis
26
Abbildungsverzeichnis
2.1. Speichermodell eines UNIX-Prozesses ([Viking2006], S.5) . . . . . . . . . . 3
2.2. Prozess-Segmente in einem C-Programm ([Viking2006], S.6) . . . . . . . . 4
2.3. Unterprogramm-Aufruf in einem C-Programm . . . . . . . . . . . . . . . . 5
2.4. Stack-Segment eines UNIX-Prozesses bei einem Funktionsaufruf (a) vor
dem Funktionsaufruf von mult() (b) bei der Ausführung von mult()
([BryantO'Hallaron2003], S.170f ) . . . . . . . . . . . . . . . . . . . . . . . 6
2.5. C-Programm mit einer Puerüberlauf-Schwachstelle . . . . . . . . . . . . 7
2.6. Aufruf des Programms aus Abb. 2.5 (a) Normale Ausführung (b) Puer-
überlauf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.7. Stack-Segment bei den Aufrufen aus Abb. 2.6 - (a) ohne Überlauf (b) mit
willkürlichem Überlauf (c) mit gezieltem Überlauf . . . . . . . . . . . . . . 8
2.8. Angris-Typen bei gezielter Manipulation des RIP (a) Ausführung von be-
reits vorhandenem Programmcode (b) Einschleusen und Ausführung von
beliebigem Programmcode (c) Angri mit NOP-Sliding . . . . . . . . . . . 9
2.9. C-Programm mit einer SFP-Overwrite -Schwachstelle . . . . . . . . . . . . 10
2.10. Stack während der Ausführung von dem Programm aus Abb. 2.9 (a) wäh-
rend der normalen Ausführung von argcpy() (b) SFP-Overwrite während
argcpy() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.11. C-Programm mit einer Puerüberlauf-Schwachstelle im BSS-Segment . . . 12
2.12. Stack während der Ausführung von dem Programm aus Abb. 2.11 . . . . . 12
2.13. Struktur des Heap bei Verwendung der Implementation der GNU C-
Library von Lea ([Lea2005], Zeile 1484 - 1530) . . . . . . . . . . . . . . . . 13
2.14. (a) Freigabe eines einzelnen Chunks (b) Konsolidierung mit nachfolgen-
dem freien Chunk (c) Konsolidierung mit vorhergehendem freien Chunk
([Lea2005], Zeile 1530 ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.15. (a) doppelte Verkettung von zwei freien Speicherblöcken (b) Beispiel für
einen gezielten Buer-Overow im Heap-Segment mit Einschleusen von
deniertem Programmcode . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.1. Ein Programm, das die Ausführungssperre überschreitet wird von der
Data-Execution-Prevention beendet (Windows) . . . . . . . . . . . . . . . 17
3.2. Nach dem Puerüberlauf wird zum Schutz vor dem eingeschleusten Pro-
grammcode das Programm abgebrochen . . . . . . . . . . . . . . . . . . . 17
3.3. Canary-Value auf dem Stack ([Klein2003a], S.307) . . . . . . . . . . . . . 18
3.4. Pointer als Triplett ([JonesKelly1997], S.2) . . . . . . . . . . . . . . . . . . 19
3.5. Festlegung der Position der Segmente (a) ohne ASLR (b) mit ASLR (nach
[Shacham2004], S.1) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.6. Programm zur Ausgabe der Speicher-Adresse der Variable x . . . . . . . . 22
3.7. Ausführung des Programms aus Abbildung 3.6 (a) ohne ASLR (b) mit
ASLR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
27
Abkürzungsverzeichnis
BSS Block Started by Symbol
CPU Central Processing Unit
DEP Data Execution Prevention
DoS Denial of Service
NOP No-Operation (Assembler Befehl)
NX No-eXecution (Bit einer Prozessor-Erweiterung)
RAM Random Access Memory
RIP Return Instruction Pointer
SFP Stack Frame Pointer
XOR eXclusive-OR (exklusiv-oder Verknüpfung)
28
A. Zielsystem-Spezikationen
R 2008
A.1. Microsoft Windows
• R WebServer 2008
Microsoft Windows
• Service Pack 1
• 2 GB RAM
• Full-Installation
• 1 GB RAM
A.3. OpenBSD
• OpenBSD 4.4
• Full-Installation
• NX-Bit verfügbar
• 3 GB RAM
29
B. Programm mit
Buer-Overow-Schwachstelle
/* buffer_overflow_stack.c
*/
#include <stdio.h>
30