Upload
doankhue
View
217
Download
0
Embed Size (px)
Citation preview
DIPLOMARBEIT
Lokalisierung und Vermeidung potentiellerSicherheitsschwachstellen in komplexen
Softwaresystemen
durchgefuhrt amStudiengang Informationstechnik und SystemManagement
an derFachhochschule Salzburg
vorgelegt von:Roland J. Graf
Studiengangsleiter: FH-Prof. DI Dr. Thomas HeistracherBetreuer: DI(FH) Thomas Kurz
Salzburg, September 2007
Eidesstattliche Erklarung
Hiermit versichere ich, Roland J. Graf, geboren am 6. Mai 1964, dass die vorliegendeDiplomarbeit von mir selbstandig verfasst wurde. Zur Erstellung wurden von mir keineanderen als die angegebenen Hilfsmittel verwendet.
0310032093Roland Graf Matrikelnummer
ii
Danksagung
Alles Wissen und alle Vermehrung unseres Wissens endetnicht mit einem Schlusspunkt, sondern mit Fragezeichen.
Hermann Hesse (1877-1962)
Meinen Eltern gebuhrt an dieser Stelle mein besonderer Dank. Gerade die erstenJahre meiner schulischen Laufbahn waren alles andere als Erfolg versprechend undtrotzdem haben sie mir bei der Wahl meiner Ausbildung stets alle Freiheiten gelassen,mir ihr Vertrauen entgegengebracht und an mich geglaubt.
Um die Lust am Lernen nicht zu verlieren, braucht es Lehrer, die neben den fachli-chen auch menschliche Werte vermitteln. Ich hatte das Gluck, einige dieser ganz weni-gen Lehrer zu treffen. Stellvertretend mochte ich hier besonders Hr. Prof. Dr. GeroldKerer hervorheben, der nicht nur bereits vor uber 20 Jahren als mein HTL-Lehrer mitseinen fachlichen und padagogischen Fahigkeiten glanzte, sondern mich auch in derFH-Salzburg wieder durch seine auergewohnliche Menschlichkeit und Qualifikationbeeindruckt hat.
Dank gilt auch meinen Kollegen am Studiengang ITS, die mich in so manchen Ge-sprachen und Diskussionen mit ihren Eingaben, Ideen, Fragen und Hinweisen geleitethaben. Besonders hervorheben mochte ich meinen Diplomarbeitsbetreuer DI(FH) Tho-mas Kurz und allen voran FH-Prof. DI Dr. Thomas Heistracher, welche mich auch alsReviewer mit konstruktiver Kritik sehr unterstutzt haben.
Von meinen Kommilitonen verdient Dietmar eine Erwahnung. Durch seine kriti-schen Verbesserungsvorschlage hat er mich oft zu einer Mehrleistung getrieben.
Zuletzt mochte ich noch Sabine danken. Ohne sie ware mein Studium neben demBeruf so gar nicht moglich gewesen. Sie hat mich uber all die Jahre tatkraftig un-terstutzt und mir auch in schweren Zeiten den notwendigen Halt, die Kraft und dieStabilitat gegeben, die ich gebraucht habe. Niemand kann so positiv formulieren, wiesie es tut und so war ich bevorteilt, indem sie als Germanistin all meine Arbeitensprachlich redigiert hat. Ihr gebuhrt jedenfalls mein groter Dank!
Und wenn Hesse folgend nun all meine Anstrengung zur Vermehrung des Wissensmit einem Fragezeichen endet, dann bleibt noch eine Frage zu stellen: Was kommtjetzt?
iii
Informationen
Vor- und Zuname: Roland J. GrafInstitution: Fachhochschule Salzburg GmbHStudiengang: Informationstechnik & System-ManagementTitel der Diplomarbeit: Lokalisierung und Vermeidung potentieller
Sicherheitsschwachstellen in komplexen Soft-waresystemen
Betreuer an der FH: DI(FH) Thomas Kurz
Schlagworter
1. Schlagwort: Software Security2. Schlagwort: Software Vulnerability3. Schlagwort: Code Injection
Abstract
This diploma thesis documents the usability of tools to localize potential security vulne-rabilities and evaluates the effectiveness of development methods to avoid them. Mostly,vulnerabilities are based on software bugs and design flaws. This paper provides thebasics of memory segmentation, processor registers and stack frames, before it explainssoftware bugs as the cause of potential software vulnerabilities and their risk potenti-al. A variety of software tools are available to implement Static White Box Tests andDynamic Black Box Tests. Source Code Analysis Tools support the developers to parsefor potential bugs in the source code, Debugging, Tracing and Monitoring Tools helpthe software and security testers to spy on data flows, function calls and flaws in exe-cutable binaries. This document reports the strengths and weaknesses of tested toolsand methods and discusses their expected effectiveness in production environments.Adapted development methods can increase the resistance of software to attacks andunauthorized data manipulations. Finally, an introduction to Defensive Programmingwith helpful programming hints, additional tables and references, code examples, andBest Practices for programmers will be given, which aims at helping developers to writesecure software.
iv
Inhaltsverzeichnis
Eidesstattliche Erklarung ii
Danksagung iii
Informationen iv
Schlagworter iv
Abstract iv
Abbildungsverzeichnis x
Tabellenverzeichnis xi
Listingverzeichnis xii
1 Einfuhrung 1
1.1 Global vernetzte Sicherheitsschwachen . . . . . . . . . . . . . . . . . . 2
1.2 Stabile Software(un-)sicherheit . . . . . . . . . . . . . . . . . . . . . . . 3
1.3 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4 Uberblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2 Grundlagen 6
2.1 Komplexe Softwaresysteme . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Speicherorganisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2.1 Prozessspeicher . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2.2 Text-Segment . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.3 Data-Segment . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.4 Heap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
v
2.2.5 Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.3 Register . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.4 Daten und Funktionszeiger . . . . . . . . . . . . . . . . . . . . . . . . . 14
3 Potentielle Schwachstellen 16
3.1 Designfehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.2 Overflow Fehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.2.1 Stack Overflow . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.2.1.1 Der klassische Stack Overflow . . . . . . . . . . . . . . 19
3.2.1.2 Frame Pointer Overwrite . . . . . . . . . . . . . . . . . 22
3.2.2 Heap Overflow . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.2.3 Array Indexing Overflows . . . . . . . . . . . . . . . . . . . . . 26
3.2.4 BSS Overflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.3 Format-String Fehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4 Lokalisierung potentieller Schwachstellen 32
4.1 Allgemeines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
4.1.1 Informationsgewinnung . . . . . . . . . . . . . . . . . . . . . . . 33
4.1.2 Vollstandige Sicherheitsanalyse . . . . . . . . . . . . . . . . . . 34
4.1.3 Statische und dynamische Analyseverfahren . . . . . . . . . . . 35
4.2 Quelltextbasierte Analyse . . . . . . . . . . . . . . . . . . . . . . . . . 35
4.2.1 Lexikalische Analyse . . . . . . . . . . . . . . . . . . . . . . . . 37
4.2.1.1 Grep . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.2.1.2 RATS . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2.1.3 Flawfinder . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.2.1.4 ITS4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.2.2 Semantische Analyse . . . . . . . . . . . . . . . . . . . . . . . . 42
4.2.2.1 C++ Compiler . . . . . . . . . . . . . . . . . . . . . . 42
4.2.2.2 Splint . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4.2.2.3 CQUAL . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.2.2.4 PREfast und PREfix . . . . . . . . . . . . . . . . . . . 46
4.2.3 Bewertung der Methoden und Werkzeuge . . . . . . . . . . . . . 47
4.3 Binarcodebasierte Analyse . . . . . . . . . . . . . . . . . . . . . . . . . 48
vi
4.3.1 Disassembling . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.3.2 Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.3.3 Tracing und Monitoring . . . . . . . . . . . . . . . . . . . . . . 53
4.3.3.1 API-Schnittstellenanalyse . . . . . . . . . . . . . . . . 53
4.3.3.2 Datenflussanalyse . . . . . . . . . . . . . . . . . . . . . 55
4.3.3.3 Speichermanagementanalyse . . . . . . . . . . . . . . . 57
4.3.3.4 Speicherabbilder . . . . . . . . . . . . . . . . . . . . . 59
4.3.3.5 Status- und Fehlerinformationen . . . . . . . . . . . . 59
4.3.4 Fault Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
4.3.5 Bewertung der Methoden und Werkzeuge . . . . . . . . . . . . . 62
4.4 Integrierte Analyse und Uberwachung . . . . . . . . . . . . . . . . . . . 63
4.4.1 Bounds Checking . . . . . . . . . . . . . . . . . . . . . . . . . . 63
4.4.2 Uberwachung des Stacks . . . . . . . . . . . . . . . . . . . . . . 64
4.4.3 Uberwachung von Funktionen . . . . . . . . . . . . . . . . . . . 65
4.4.4 Uberwachung des Heaps . . . . . . . . . . . . . . . . . . . . . . 66
4.4.5 Bewertung der integrierten Methoden . . . . . . . . . . . . . . . 68
5 Vermeidung potentieller Schwachstellen 70
5.1 Allgemeines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
5.2 Sicheres Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
5.2.1 Threat Modeling . . . . . . . . . . . . . . . . . . . . . . . . . . 72
5.3 Defensive Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . 73
5.3.1 Uberprufung der Ein- und Ausgabedaten . . . . . . . . . . . . . 74
5.3.2 Sichere Zeiger- und Speicherverwaltung . . . . . . . . . . . . . . 74
5.3.3 Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . 75
5.3.4 Hilfen zur Fehlersuche . . . . . . . . . . . . . . . . . . . . . . . 76
5.3.5 Sichere Bibliotheksfunktionen . . . . . . . . . . . . . . . . . . . 78
5.3.5.1 Fehlerfreie Bibliotheksfunktionen . . . . . . . . . . . . 79
5.3.5.2 Bibliothekserweiterungen . . . . . . . . . . . . . . . . 80
5.3.5.3 Wrapper . . . . . . . . . . . . . . . . . . . . . . . . . . 81
5.4 Sicherere Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . 82
5.4.1 Sichereres C und C++ . . . . . . . . . . . . . . . . . . . . . . . 82
5.4.2 Managed Code und Managed Memory . . . . . . . . . . . . . . 83
5.5 Zusatzliche Techniken . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
5.6 Bewertung der Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . 86
vii
6 Zusammenfassung und Ausblick 88
6.1 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
6.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
6.3 Trends . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Literaturverzeichnis 93
Abkurzungsverzeichnis 101
Anhang 103
A APIs und Bibliothekserweiterungen 104
A.1 Die Standardbibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . 104
A.1.1 Unsichere POSIX C-Funktionen . . . . . . . . . . . . . . . . . . 104
A.1.2 Unsichere Windows CRT-Funktionen . . . . . . . . . . . . . . . 105
B Protokolle 108
B.1 Lexikalische Quelltextanalysen . . . . . . . . . . . . . . . . . . . . . . . 108
B.1.1 grep Protokoll . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
B.1.2 ITS4 Analyseprotokoll . . . . . . . . . . . . . . . . . . . . . . . 109
B.1.3 RATS Analyseprotokoll . . . . . . . . . . . . . . . . . . . . . . 111
B.1.4 Flawfinder Analyseprotokoll . . . . . . . . . . . . . . . . . . . . 113
B.2 Semantische Quelltextanalysen . . . . . . . . . . . . . . . . . . . . . . . 115
B.2.1 Microsoft C/C++ Compiler Analyseprotokoll . . . . . . . . . . 115
B.2.2 GCC Compiler Analyseprotokoll . . . . . . . . . . . . . . . . . . 117
B.2.3 Splint Analyseprotokoll . . . . . . . . . . . . . . . . . . . . . . . 118
C Listings 120
C.1 Absicherung des Stacks uber Security Cookies . . . . . . . . . . . . . . 120
C.2 Einfache Speicheruberwachung in C++ . . . . . . . . . . . . . . . . . . 122
C.3 Defensive Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . 123
C.3.1 Uberprufung der Eingabedaten . . . . . . . . . . . . . . . . . . 123
C.3.2 Zeiger und Speicherbehandlung . . . . . . . . . . . . . . . . . . 124
C.4 Sichere Programmiersprachen . . . . . . . . . . . . . . . . . . . . . . . 125
C.4.1 Sicheres C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
viii
C.4.2 Automatisches Bounds Checking in C# . . . . . . . . . . . . . . 126
C.5 Sichere Bibliotheksfunktionen . . . . . . . . . . . . . . . . . . . . . . . 127
C.5.1 Sicherung der Funktionen uber Return Codes . . . . . . . . . . 127
C.5.2 Sicherung von Funktionen uber Exceptions . . . . . . . . . . . . 128
D Sicherheits-Tools und Bibliotheken 129
D.1 Statische Analysewerkzeuge . . . . . . . . . . . . . . . . . . . . . . . . 129
D.2 Dynamische Analysewerkzeuge . . . . . . . . . . . . . . . . . . . . . . . 132
D.3 Sonstige Werkzeuge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
E Good Practices fur sichere Software 137
E.1 Ein- und Ausgabedaten . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
E.2 Zeiger- und Speicherbehandlung . . . . . . . . . . . . . . . . . . . . . . 138
E.3 Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
E.4 Hilfe zur Fehlersuche . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
F Weiterfuhrende Online-Quellen 141
F.1 Dokumente und Links zu Codesicherheit . . . . . . . . . . . . . . . . . 141
F.2 News, Newsletter und Mailing-Listen . . . . . . . . . . . . . . . . . . . 141
ix
Abbildungsverzeichnis
2.1 Typisches Speicherabbild einer laufenden Applikation . . . . . . . . . . 10
2.2 Daten- und Funktionszeiger . . . . . . . . . . . . . . . . . . . . . . . . 14
3.1 Manipulationen durch einen Stack Overflow . . . . . . . . . . . . . . . 21
3.2 Manipulationen durch einen Heap Overflow . . . . . . . . . . . . . . . . 25
3.3 Manipulationen durch einen Off-By-One Overrun . . . . . . . . . . . . 26
4.1 Debugging Session innerhalb einer Applikation . . . . . . . . . . . . . . 52
4.2 APISPY beim Aufspuren sicherheitskritischer Funktionen . . . . . . . . 54
4.3 RegMon beim Protokollieren von Zugriffen auf die Windows Registry . 57
4.4 Stackschutzmechanismen mit Security Cookies . . . . . . . . . . . . . . 65
4.5 Fehlermeldung nach einem Heap Overflow . . . . . . . . . . . . . . . . 67
x
Tabellenverzeichnis
3.1 Ausgewahlte Format-String Platzhalter der printf-Familie . . . . . . . . 30
4.1 Zusammenstellung einiger Fuzzing Werkzeuge . . . . . . . . . . . . . . 61
A.1 Unsichere POSIX-Funktionen . . . . . . . . . . . . . . . . . . . . . . . 105
A.2 Unsichere Windows CRT-Funktionen . . . . . . . . . . . . . . . . . . . 106
A.3 Template Overloads fur unsichere Windows CRT-Funktionen . . . . . . 107
D.1 Auswahl einiger statischer Analysewerkzeuge . . . . . . . . . . . . . . . 131
D.2 Auswahl einiger dynamischer Analysewerkzeuge . . . . . . . . . . . . . 135
D.3 Auswahl einiger Sicherheitswerkzeuge . . . . . . . . . . . . . . . . . . . 136
xi
Listingverzeichnis
2.1 Programm mit Variablen verschiedener Speicherklassen . . . . . . . . . 7
3.1 Beispielprogramm StackOverflow.c . . . . . . . . . . . . . . . . . . . . 20
3.2 Stackdump innerhalb des Programms StackOverflow.c . . . . . . . . . . 20
3.3 Beispielprogramm HeapOverflow.c . . . . . . . . . . . . . . . . . . . . . 23
3.4 Beispiel eines Off-By-One Fehlers im Programm StackOverflow.c . . . . 26
3.5 Frame Pointer Manipulation durch Off-By-One Fehler . . . . . . . . . . 27
3.6 Beispielprogramm PrintDemo.c . . . . . . . . . . . . . . . . . . . . . . 29
3.7 Stackdump innerhalb des Programms PrintDemo.c . . . . . . . . . . . 30
4.1 Testprogramm zur Codeanalyse mit einem C++ Compiler . . . . . . . 42
4.2 Disassembliertes Programm PrintfDemo.c . . . . . . . . . . . . . . . . . 50
5.1 Erweiterung zur Fehlersuche in einer DEBUG-Version . . . . . . . . . . 77
C.1 Prolog- und Epilog-Erweiterungen zur Behandlung von Security Cookies 120
C.2 Include-Datei zum Uberladen des new-Operators . . . . . . . . . . . . . 122
C.3 Quelltext mit Buffer Overflow und Memory Leak Fehler . . . . . . . . . 122
C.4 Defensive Programmierung innerhalb der .NET Standard Library . . . 123
C.5 Zeiger- und Speicherbehandlung bei defensiver Programmierung . . . . 124
C.6 Auszug aus einem STL Programm mit Smart-Pointers und Strings . . . 125
C.7 C# Programm mit einem Array Indexing Fehler . . . . . . . . . . . . . 126
C.8 Fehlerauswertung mittels GetLastError() . . . . . . . . . . . . . . . . . 127
C.9 Fehlerbehandlung uber Exceptions in einem korrekten C# Code . . . . 128
xii
1
Einfuhrung
An application should be considered unsafe until demonstrated to be otherwise.
(Swiderski, 2004)
Die Haufung von Veroffentlichungen kritischer Sicherheitslucken in Applikationen, Netz-
werkdiensten und Betriebssystemen macht deutlich, dass ein Groteil der Applikationen
und Computersysteme noch immer nicht sicher genug und ausreichend geschutzt ist.
Technische Detailinformationen uber bestimmte Einbruchsmoglichkeiten in Computer-
systeme und die ausgenutzten Schwachstellen werden grotenteils online publiziert1.
Umfangreiche Beschreibungen der oftmals kreativen Methoden der Angreifer und der
moglichen Abwehrmethoden sind jedem Interessierten frei zuganglich. Diese machen
auch immer wieder deutlich, dass ein Groteil der Sicherheitsschwachstellen die Fol-
ge von Design- und Codierungsfehlern in einzelnen Teilen der Software ist. Ebenso
fallt dabei auf, dass bestimmte Fehler besonders haufig als Grund fur eine Sicherheits-
schwachstelle genannt werden.
Die Softwareindustrie wird vermehrt angehalten, fehlerfreie und sichere Software zu
entwickeln. Der Druck auf die Softwareentwickler steigt. Trotz der bekannten Mangel
1Im Internet werden fast taglich Nachrichten von kritischen Softwarefehlern und Sicherheitsluckenveroffentlicht. Security Online Archive (z.B. SANS, CERT), einschlagige Mailinglisten, namhafte Un-ternehmen im Bereich Computer- und Netzwerksicherheit und News-Dienste (z.B. Heise Security,SecurityFocus, Computer Crime & Intellectual Property Section), selbst Hacker und Cracker liefernInformationen dazu grotenteils frei Haus. Eine Reihe weiterer Quellen vervollstandigen diese Berich-te und stellen Auswertungen uber Trends, Statistiken und Top-10 Listen uber mehr oder wenigererfolgreiche Attacken auf verwundbare Computersysteme zur Verfugung. Die Gesamtheit dieser Infor-mationen ergibt einen aktuellen Lagebericht uber IT-Sicherheit, sicherheitskritische Softwaremangelund deren Ursachen. Eine Liste ausgewahlter Quellen findet sich im Anhang F am Ende dieses Doku-ments.
1
1. Einfuhrung 2
und deren technischer Ursachen scheinen sie derzeit aber kaum in der Lage, diesen
Forderungen nachzukommen und Software herzustellen, die im Umfeld der globalen
Vernetzung und Bedrohungen bestehen kann.
1.1 Global vernetzte Sicherheitsschwachen
Die Firma Sun Microsystems2 hat vor einigen Jahren schon in der VisionThe Net-
work is the Computer die hochgradige Vernetzung der Informationstechnologie und
die damit einhergehenden technologischen Veranderungen vorhergesehen. Mittlerweile
sind fast alle Computer uber ein Netzwerk oder das weltumspannende Internet mit-
einander verbunden. Desktop-Computer, Server und Router, Pocket-Computer, mobile
Telefone, Embedded Systems, Fernseher, Multimedia Systeme, jede Menge mikropro-
zessorgesteuerte Gerate und eine Unzahl von Peripheriegeraten sind Teile eines oder
des globalen Netzwerks geworden. Dass sich durch diese Vernetzung auch das Bedro-
hungspotential durch mogliche Angriffe aus dem Netz (Remote Exploits) vervielfacht
hat, wird dem Anwender und der Softwareindustrie aber erst heute immer mehr und
oftmals schmerzlich bewusst. Mit der globalen Vernetzung verschiedenster Gerate und
Systeme untereinander wachst auch der Druck, die Sicherheitsvorkehrungen bei Com-
putersystemen und allen Systemteilen entsprechend anzupassen.
Konnte fruher noch von lokalen Bedrohungen und Angriffen (Local Exploits), von lo-
kalen Sicherheitsschwachstellen und einem lokalen Risiko ausgegangen werden, so sind
sowohl die Gefahrenpotentiale als auch die Angriffsziele mittlerweile im globalen Netz
verteilt und somit auch die Auswirkungen globaler Natur. Einzelne Applikationen sind
Teile eines vernetzten und komplexen Systems. Mit dem Internet verbundene Syste-
me stellen dabei ein besonders groes Risiko und manchmal auch einen besonderen
Reiz fur Angreifer dar. Das Internet ist eine feindselige Umgebung, deshalb muss der
Programmcode so entworfen sein, dass er einem Angriff widerstehen kann. Vernetzte
Systeme bedurfen also einer expliziten Sicherung gegen mogliche Angriffe. Jede poten-
tielle Schwachstelle eines Systemglieds mindert die Sicherheit des gesamten Systems
oder stellt sie gar in Frage. Ein einzelner Sicherheitsmangel einer Applikation kann
2http://www.sun.com
http://www.sun.com
1. Einfuhrung 3
schon fur den Angriff des Gesamtsystems missbraucht werden. Ziel und Voraussetzung
fur ein sicheres Computersystem ist demnach die Sicherheit jeder einzelnen Kompo-
nente. Nur so kann die Sicherheit des Gesamtsystems gewahrleistet werden und ein
System den Attacken und Gefahren desWild Wild Web[26, S. 5] standhalten. Die
Entwicklung fehlerfreien Quellcodes ist langst nicht mehr genug, wenngleich eine der
unabdingbaren Voraussetzung fur sichere Software.
1.2 Stabile Software(un-)sicherheit
Wenn von Sicherheitslucken in Computersystemen berichtet wird, handelt es sich fast
immer um Fehler im Bereich der Softwareentwicklung, also Fehler im Programmcode.
Fehler in der Hardware oder den angewandten Richtlinien, auch wenn daruber seltener
berichtet wird, sind ebenfalls moglich und nicht minder gefahrlich. Viele dieser oft-
mals lange unentdeckten Fehler stellen massive Sicherheitslucken dar. Sie bieten eine
Angriffsflache fur mogliche Attacken gegen einzelne Applikationen, Computersysteme
oder gesamte Netzwerke.
Vor einigen Jahren galt es noch als ausreichend stabile Software zu entwickeln. Es
genugte, wenn eine Applikation die Anforderungen der Endbenutzer erfullte. Zusatzliche
sicherheitsrelevante Forderungen wurden kaum erhoben. Softwareentwickler wurden an-
gehalten soliden, stabilen, wartbaren und erweiterbaren Code zu schreiben. In dieser
Zeit spielte Codesicherheit selbst in klassischen Standardwerken der Softwareentwick-
lung wie [35] und [36] kaum eine Rolle. Gute Software stellte sich dem Benutzer aus-
schlielich als stabil laufende Software dar. Dem Schutz vor mutwilligen Manipulationen
wurde kaum Beachtung geschenkt.
Mittlerweile wird vermehrt und ausdrucklich die Entwicklung sicherer Software ge-
fordert. Langsam beginnen auch Softwareproduzenten und Benutzer ein allgemeines
Sicherheitsbewusstsein zu entwickeln. Fur eine sichere Software sind ein sicherer Code
und ein sicheres Design die unabdingbaren Voraussetzungen. Sichere Software meint in
diesem Zusammenhang, dass sowohl das Design als auch die Implementierung im Hin-
blick auf die Abwehr potentieller Gefahren und Attacken entworfen wurden. Auch wenn
fehlerfreier Code nicht automatisch eine sichere Software garantiert, so gilt ein Gutteil
1. Einfuhrung 4
der Aufmerksamkeit dem Ziel, fehlerfreien Code zu entwickeln. Mit welchen Methoden
dieses Ziel letztlich erreicht werden kann, ist eine der zentralen Fragestellungen dieser
Arbeit.
1.3 Motivation
Software- bzw. Codesicherheit kann nicht ohne entsprechenden Einsatz und ohne spe-
zielle Methoden schon wahrend der Entwicklung und wahrend des gesamten Lebens-
zyklus einer Applikation erreicht werden. Die Bedrohungsmodellierung (Threat Mo-
deling) hilft Gefahrdungspotentiale fruhzeitig zu identifizieren, zu evaluieren, sie zu
dokumentieren und Gegenmanahmen schon in der Designphase zu entwickeln. Der
Entwicklungszyklus (Development Life Cycle) und der Sicherheitszyklus (Security Life
Cycle) sind untrennbare Teile der Entwicklung einer Applikation. Sicherheitsprozesse
umfassen die Spezifikationen, den Quelltext, die Dokumentation und den Test und sind
Teil eines sicheren Software-Entwicklungszyklus. Software muss aktiv und explizit nach
bestimmten Sicherheitskriterien entwickelt werden, um moglichen gezielten Angriffen
standzuhalten. Die verwendeten Modelle, die Methoden und die Implementierungen
werden stetig angepasst und notigenfalls erweitert, um im veranderten Risikoumfeld zu
bestehen und den standig neuen Anforderungen zu entsprechen. Begleitende Manah-
men wahrend aller Entwicklungsphasen einer Applikation oder eines Softwaremoduls
schaffen erst die Voraussetzungen fur die Schaffung sicherer Softwaresysteme.
Basierend auf den oben genannten Forderungen sind das Ziel und die Motivation dieser
Arbeit die Untersuchung und die Diskussion potentieller Sicherheitsschwachstellen. Der
Fokus richtet sich vorwiegend auf die Implementierung, die Methoden zur Lokalisierung
von Codefehlern und die systematische Vermeidung von Schwachstellen in komplexen
Softwaresystemen. Der folgende kurze Uberblick beschreibt die einzelnen Kapitel der
vorliegenden Arbeit.
1. Einfuhrung 5
1.4 Uberblick
Das folgende Kapitel 2 fuhrt in ausgewahlte technische Grundlagen der Software-
entwicklung ein und erklart die zum Verstandnis notwendigen Begriffe sowie das Spei-
chermanagement einer Applikation. Nachdem ein Groteil der Systemsoftware nach
wie vor in C/C++ programmiert ist, werden diese beiden Programmiersprachen auch
bevorzugt in die Erklarungen einflieen.
In Kapitel 3 werden potentielle Sicherheitsschwachstellen einer Software und dar-
auf basierende Angriffsmethoden vorgestellt. Einige einfache Beispiele zeigen die prak-
tische Umsetzung und Einfachheit eines Buffer Overflow Angriffs auf ungesicherte Soft-
ware.
Das Kapitel 4 untersucht ausgewahlte Methoden der systematischen Lokalisierung
potentieller Code- und Sicherheitsschwachstellen. Zur Anwendung kommen dabei quell-
codebasierte und binarcodebasierte Analysemethoden. Nachdem die manuelle Prufung
von Code oft nicht effizient genug, sehr aufwandig und teuer ist, werden ebenso Werk-
zeuge zur automatischen Softwareanalyse gepruft.
In Kapitel 5 steht die Vermeidung potentieller Schwachstellen im Vordergrund. Schwer-
punkt ist dabei die Diskussion von Methoden zur Erstellung von sicheren Designs und
zur Entwicklung sicherer Implementierungen. Das schon fur die Designphase empfoh-
lene Threat Modeling bleibt hier ebenso wenig unbehandelt wie die Anwendung der
Prinzipien des defensiven Programmierens.
In Kapitel 6 schliet ein kurzer Ausblick in die Zukunft die Arbeit ab. Darin wird
erlautert, wie aus der Sicht von Experten die Sicherheit komplexer Softwaresysteme in
Zukunft gewahrleistet werden konnte. Die kurze Zusammenfassung schliet mit der
Beantwortung der Frage ab, ob die aktuellen Methoden der Softwareentwicklung schon
heute ausreichen wurden, um die Sicherheit komplexer Softwaresysteme sicherzustellen
oder ob erst in Zukunft eine echte Softwaresicherheit moglich sein wird.
2
Grundlagen
Fast alle Sicherheitslucken basieren, wie aus den im Kapitel 1 angefuhrten Quellen her-
vorgeht, auf Programmfehlern und ein Groteil aller Angriffe basiert auf dem Prinzip
der Speichermanipulation. Selbst wenn ein Programm im Normalbetrieb uber langere
Zeit stabil lauft, so bedeutet dies nicht zwingend, dass sich keine Fehler im zugrunde-
liegenden Programmcode befinden. Erst die Konfrontation eines Programms bzw. einer
Funktion mit fur den Regelbetrieb nicht vorhergesehenen Daten oder Situationen kann
Fehler hervorrufen. Jeder einzelne Fehler kann sowohl die Applikation selbst als auch
das Gesamtsystem in einen verwundbaren oder nicht geplanten Zustand versetzen.
Sowohl Design- als auch Implementierungsfehler entstehen nicht zwingend, aber oft
als Folge der Komplexitat eines Quelltextes oder einer Softwarearchitektur. Bevor die
technischen Grundlagen des Speichermanagements einer Applikation erklart werden,
wird der Begriff komplexe Softwaresysteme eingefuhrt.
2.1 Komplexe Softwaresysteme
Die Definition eines komplexen Softwaresystems kann gerade aus der Sicht der Soft-
wareentwicklung eindeutig festgelegt werden. Unter komplex kann jede Software be-
zeichnet werden, welche aufgrund ihres Umfangs nicht mehr ohne weiteres mit allen
Funktionen, deren Wechselwirkungen und deren Auswirkungen auf das Gesamtsystem
erfasst werden kann.
6
2. Grundlagen 7
Komplexe Applikationen neigen zu mehr und schwer zu entdeckenden Fehlern. Zeit-
gemae und fortgeschrittene Entwicklungsmethoden versuchen der Komplexitat durch
Modularisierung und Aufteilung in uberschaubare Funktionseinheiten entgegenzuwir-
ken. Dass dieses Vorhaben nicht zwingend zum Erfolg fuhren muss, zeigt die Zahl der
Veroffentlichungen (z.B. Bug Reports) und Fehlerkorrekturen (Bug Fixes) komplexer
Software der letzten Jahre1.
2.2 Speicherorganisation
Dieses Kapitel fuhrt einige Begriffe der Speicherverwaltung (Memory Management) ein.
Es erklart die Segmentierung des Speichers (Memory Segmentation) und deren Zweck.
Diese Beschreibung zieht als Beispiel die Segmentierung und Speicherverwaltung einer
32 Bit Intel x86-Architektur (IA-32)[28] heran. Das beschriebene Prinzip gilt jedoch
ebenfalls fur fast alle anderen gangigen Prozessorarchitekturen.
Im folgenden Listing 2.1 wird ein kurzes C-Programm in Auszugen gezeigt. Es ver-
wendet Variablen verschiedenster Typen und Speicherklassen. Dieses Programm weist
eine Reihe von Fehlern auf, welche - wie sich im Laufe dieses Dokuments noch zeigen
wird - ernste Sicherheitsschwachstellen darstellen. In den folgenden Erlauterungen wird
wiederholt auf diesen Quellcode oder Teile davon Bezug genommen.
1 int _iGlobalValue; // Var im BSS Segment
2 static char _szGlobalMsg[] = "text"; // Var im Data Segment
3
4 char* foo(const char *str1, const char* str2)
5 {
6 static int iLocal = 100; // Var im Data Segment
7 char szBuffer[20]; // Puffer auf Stack
8 strcpy( szBuffer, str1 ); // => Stack Overflow Schwachstelle
9 ...
10 return szBuffer; // Pointer auf Stackpuffer
11 }
1Haufig wird z.B. bei groeren Service Packs von Betriebssystemen und Office Paketen die An-zahl der behobenen Fehler mit einigen Hundert angegeben. Microsoft veroffentlicht in einem monat-lichen Updatezyklus Service Packs, Updates und Patches und beziffert die Anzahl der kritischen undzusatzlich geschlossenen Sicherheitslucken im Schnitt mit etwa 20 Fehlern pro Monat. Siehe dazu auchhttp://www.microsoft.com/germany/technet/sicherheit/bulletins/aktuell/default.mspx
http://www.microsoft.com/germany/technet/sicherheit/bulletins/aktuell/default.mspx
2. Grundlagen 8
12
13 int main(int argc, char *argv[])
14 {
15 static short iLen; // Var im BSS Segment
16 char* pBuff1, pBuff2; // Vars auf Stack
17
18 iLen = strlen( argv[1] ); // => Integer Overflow Schwachst.
19 pBuff1 = (char*)malloc( iLen ); // Allokiert Puffer auf Heap
20 for( int i=0; i unsichere Funktion strcpy()
23 // => uninitial. Zeiger pBuff2
24 printf( pBuff1 ); // Format String Schwachstelle
25 // => unsichere Funktion printf()
26 free( pBuff1 ); // Freigabe des Pufferspeichers
27 pBuff2 = foo(argv[1]); // => Illegaler Zeiger in pBuff2
28 free( pBuff1 ); // => Double Free Fehler
29 return 0;
30 }
Listing 2.1: Programm mit Variablen verschiedener Speicherklassen
2.2.1 Prozessspeicher
Ein Computerprogramm ist im klassischen Fall eine ausfuhrbare Datei2 (Executable),
welche auf einem Datentrager gespeichert ist. Dabei kann es sich zum Beispiel, wie un-
ter Linux und nahezu allen Unix-Derivaten verwendet, um Dateien im Executeable and
Linking Format (ELF) handeln [14]. Microsoft Windows Plattformen verwenden dazu
Dateien im sogenannten Portable Executable Format (PE Format) [16]. Diese Dateien
beinhalten nicht nur den ausfuhrbaren Programmcode und dessen statische Daten, son-
dern beschreiben die Objektdatei. Sie speichern ebenso zusatzliche Informationen zum
Starten der Applikation und zum Verwalten des Speichers. Wird nun ein Programm
gestartet, so werden, entsprechend der codierten Informationen im Optional Header 3,
2Ausfuhrbare Scriptdateien, wie sie z.B. unter Unix-basierten Systemen haufig vorkommen, sindvon diesen Betrachtungen ausgenommen. Diese konnen nicht direkt ausgefuhrt werden, sondernbenotigen ein zusatzliches Programm (Interpreter), welches die einzelnen Script Statements inter-pretiert und zur Ausfuhrung bringt.
3Diese Bezeichnung ist eigentlich irrefuhrend, da dieser Header nicht optional ist und unbedingtnotwendige Informationen zur Groe des beim Start benotigten Speichers beinhaltet.
2. Grundlagen 9
Teile dieser Objektdatei vom Program Loader in den Hauptspeicher geladen, der Spei-
cher entsprechend konfiguriert und der Programmcode zur Ausfuhrung gebracht. Der
Start im Speicher erfolgt durch den Aufruf einer speziellen Funktion (Startup-Routine)
an einer bestimmten Adresse (Einsprungadresse). Im Listing 2.1 ist dieser Einsprung-
punkt (Entry Point) die Funktion main. Ein laufendes Programm wird als Prozess
bezeichnet [23].
In modernen Betriebssystemen wird jedem laufenden Prozess ein virtueller Adressraum
zur Verfugung gestellt, welcher von der Memory Management Unit (MMU) in physische
Speicheradressen umgesetzt wird. Einem Prozess stehen nun separat organisierte Spei-
cherregionen bzw. Speichersegmente (Memory Segments) innerhalb seines Adressbe-
reichs zur Verfugung, in denen sich sein Programmcode und statische Daten befinden
und auch temporare Daten abgelegt werden konnen. Typische Segmente innerhalb des
Prozessspeichers sind das Text-, Data- und BSS-Segment sowie der Stack und der
Heap einer Applikation (siehe Abbildung 2.1). In den folgenden Abschnitten werden
diese Begriffe bzw. Speicherbereiche detailliert beschrieben [31].
2.2.2 Text-Segment
Im Text-Segment bzw. Code-Segment werden die maschinenlesbaren Instruktionen, also
jener Programmcode, welchen die Central Processing Unit (CPU) ausfuhrt, abgelegt.
Dieses Segment ist als read-only markiert, das heit, es kann nur lesend darauf zuge-
griffen werden. Damit kann der ausfuhrbare Code des Prozesses weder versehentlich
noch mutwillig modifiziert werden. Jeder Versuch, den Speicher in diesem Segment zu
manipulieren, wurde sofort zu einer entsprechenden Ausnahmebehandlung (Exception)
und zu einem Programmabbruch fuhren.
2.2.3 Data-Segment
Im Data-Segment werden nur bestimmte Daten des Prozesses, jedoch kein ausfuhrbarer
Code abgelegt. Das Data-Segment hat eine feste, beim Programmstart zugewiesene
Groe und nimmt alle vor dem eigentlichen Programmstart initialisierten globalen Va-
riablen (z.B. primitive Variablen, Arrays, Puffer, Strukturen, Zeiger, Objektdaten) auf.
2. Grundlagen 10
Das Data-Segment kann wahrend der Programmausfuhrung gelesen und beschrieben
werden, um den Inhalt der dort gespeicherten Variablen wahrend der Laufzeit andern
zu konnen.
DataBSS
Heap
Stack
Verfgbarer Speicher
Text0x08000000
0xC0000000hoheAdresswerte
niedrigeAdresswerte
dynamisches Wachstum
dynamischesWachstum
Funktionsparameter
vorhergehendeStack Frames
Funktion Return Address.gesicherter Frame Pointer
Lokal deklarierte Variablen und Puffer
Func
tion
Sta
ck F
ram
e
optionale Prozessdaten
Abbildung 2.1: Typischer Prozessspeicher- und Stackaufbau einer C/C++ Applikation
Der BSS-Bereich4 ist ein Unterbereich des Data-Segments. Er nimmt nicht-initialisierte
globale und nicht-initialisierte statische Variablen auf (siehe Listing 2.1 die Variable
_iGlobalValue in Zeile 1 und die lokale statische Variable iLen in Zeile 15), wohin-
gegen alle initialisierten globalen und statischen Variablen auerhalb des BSS-Bereichs
abgelegt werden (siehe Listing 2.1, Zeile 2 und 6 ). Wird das Programm gestartet, wird
der BSS-Bereich in der Regel noch vor dem eigentlichen Programmstart durch das Be-
triebssystem mit Nullen gefullt. Numerische Werte erhalten dadurch also alle den Wert
0, Strings sind den Konventionen der Programmiersprache C und C++5 entsprechend
immer mit dem Zeichen \0 (ASCII 0) abgeschlossen und haben damit auch die Lange
4BSS steht als Abkurzung fur Block Started by Symbol5Ein Groteil der Betriebssysteme und Systemprogramme ist in der Programmiersprache C/C++
implementiert. In diesen Sprachen ist eine Zeichenkette (String) per Definition eine Folge von Zeichen(ASCII-Zeichen) in einem char -Array, welche immer mit einem ASCII 0 (0 Byte, welches nur 0-Bitsenthalt) abgeschlossen sein muss. A string is a contiguous sequence of characters terminated by andincluding the first null character. [...] A pointer to a string is a pointer to its initial (lowest addressed)character. The length of a string is the number of bytes preceding the null character and the valueof a string is the sequence of the values of the contained characters, in order. [11, S. 164] ExpliziteLangenangaben werden also nicht gespeichert. Wurde das 0-Byte als Ende-Zeichen fehlen, wurdenString-verarbeitende Funktionen diese Zeichenkette als so lange interpretieren, bis zufallig ein 0-Byteim Speicher vorkommt.
2. Grundlagen 11
0 [11]. So wird sichergestellt, dass sich keine unerwunschten Werte in den uninitialisier-
ten Variablen befinden, vor allem aber auch keine Daten eines vorangegangenen und
wieder terminierten Prozesses, der diesen Speicherbereich zuvor verwendet bzw. mit
eigenen Daten beschrieben hat.
2.2.4 Heap
Jeder Prozess hat die Moglichkeit, erst wahrend der Programmausfuhrung Speicher
vom Betriebssystem anzufordern. Dafur werden eigene Bibliotheksfunktionen (Memo-
ry Management Functions) zur Verfugung gestellt, die den verfugbaren und belegten
Speicher verwalten. Ein Prozess kann einen Speicher anfordern und erhalt dabei einen
Zeiger auf diesen Speicher (z.B. uber malloc(), siehe Listing 2.1, Zeile 19). Benotigt
er den Speicher nicht mehr, kann er diesen Speicher jederzeit freigeben (zum Beispiel
mit free(), siehe Listing 2.1, Zeile 26). Nachdem weder der Zeitpunkt der Speicher-
allokation noch die Groe des Speichers vorgegeben ist, wird von einer dynamischen
Speicherverwaltung bzw. einer Dynamic Memory Allocation gesprochen.
Alle dynamisch angeforderten Speicherblocke befinden sich innerhalb eines speziell
dafur vorgesehenen, dynamisch wachsenden Speicherbereichs, dem sogenannten Heap
des Programms. Die Groe des Heaps wird durch den verfugbaren Speicher abzuglich
der Groe des Stacks limitiert (siehe Abbildung 2.1). Ein Prozess kann maximal soviel
Speicher benutzen, wie diesem von der Speicherverwaltung auf Abruf zur Verfugung
gestellt wird. Die Lage der Speicherblocke ist vom laufenden Prozess nicht beeinflussbar
und wird von der Speicherverwaltung des Betriebssystems bestimmt. Mehrmalige Spei-
cheranforderungen und Freigaben fuhren aufgrund der internen Organisation der be-
legten und freien Speicherbereiche zu einer Fragmentierung (Memory Fragmentation)
des Speichers. Auf dem Heap allokierter Speicher ist solange gultig, bis er wieder frei-
gegeben wird oder der Prozess beendet wird. Moderne Betriebssysteme geben den
gesamten Heap einer Applikation nach dessen Terminierung automatisch wieder frei,
um den Speicher anderen Applikationen zur Verfugung stellen zu konnen.
2. Grundlagen 12
2.2.5 Stack
Der Stack wachst dynamisch und teilt sich gemeinsam mit dem Heap den einer Ap-
plikation zur Verfugung stehenden freien Speicher. Der Stack wachst, im Gegensatz
zum Heap, von hohen Speicheradressen in Richtung niedrigere Speicheradressen (siehe
Abbildung 2.1). Jede aufgerufene Funktion erzeugt im Stack-Bereich einen eigenen
Speicherblock, genannt Stack Frame, welcher von der hochsten Adresse beginnend nach
und nach den Stack befullt. Auf dem Stack eines Prozessors einer Intel x86 Architektur
konnen folgende Daten innerhalb eines einzigen Stackframes abgelegt werden [8]:
Funktionsparameter - Alle beim Aufruf einer Funktion ubergebenen Para-
meter liegen auf dem Stack. Innerhalb der Funktion entspricht ein ubergebener
Parameter einer lokalen Variablen.
Funktionsrucksprungadresse - Unmittelbar vor Beendigung einer Funktion
wird der Ruckgabewert der Funktion in einem Register des Prozessors oder auf
dem Stack abgelegt und zur aufrufenden Funktion bzw. zur Funktionsrucksprung-
adresse (Function Return Address) zuruckgekehrt. Auf dieser Adresse liegt eines
der Hauptaugenmerke beim Versuch einer Attacke. Gelingt es einem Angreifer
diese Adresse zu manipulieren, kann er den Programmfluss gezielt beeinflussen.
Frame Pointer - Der aus Effizienzgrunden auf dem Stack gesicherte Frame Poin-
ter enthalt die Basisadresse des aktuellen Stack Frames. Er dient dem effizienten
Zugriff auf die auf dem Stack gesicherten Variablen, indem jede Variable mit
diesem Pointer und einem bestimmten Offset adressiert werden kann.
Lokale Variablen - Alle lokal deklarierten auto-Variablen werden auf dem Stack
abgelegt. Im Gegensatz dazu werden lokale statische Variablen, welche ihren Wert
auch nach dem Verlassen der Funktion behalten mussen, entweder im allgemeinen
Datensegment oder im BSS-Bereich abgelegt.
Optionale Prozessdaten und Zeiger - Je nach Architektur und Compiler
konnen noch weitere Daten auf dem Stack abgelegt werden, z.B. eine Adres-
se zur Ausnahmebehandlung (Exception Handler Frame), zwischengespeicherte
Register des Prozessors (Callee Save Registers).
2. Grundlagen 13
Im Gegensatz zu Speicherblocken auf dem Heap hat ein Stack Frame immer eine be-
grenzte Lebensdauer und limitierte Groe. Im Beispiel aus Listing 2.1 werden alle loka-
len auto-Variablen (siehe Listing 2.1, Zeilen 16 und 7) und Verwaltungsdaten (Funkti-
onsrucksprungadresse) auf dem Stack gespeichert. Wird die Funktion verlassen, werden
auch die aktuellen Stack Frames wieder vom Stack entfernt. Die Zugriffsorganisation
erfolgt ahnlich einem Stapel, denn die Daten werden immer in umgekehrter Reihenfolge
gelesen, als sie zuvor auf dem Stack geschrieben wurden (LIFO-Prinzip - Last In/First
Out). Der letzte auf dem Stack abgelegte Stack Frame bestimmt immer die aktuelle
Groe des Stacks. Eine Fragmentierung des Stacks aufgrund der LIFO-Organisation ist
nicht zu befurchten.
2.3 Register
Ein Prozessor besitzt nur einen sehr kleinen, jedoch sehr schnellen internen Speicher
zur Abarbeitung eines Programms. Ein Teil dieses Speichers wird fur interne Zwecke
verwendet und ist von Auen bzw. fur Programme nicht zuganglich. Ein anderer kleiner
Teil dieser Speicherplatze wird fur Ein- und Ausgabeoperationen, das Verschieben und
Manipulieren von Speicher, zur Ubergabe bestimmter Parameter, zur Adressierung und
Indizierung von Speicher, zur Ruckgabe von Ergebnissen und fur Zahler verwendet.
Dieser Teil der Speicherplatze wird im Allgemeinen als Register bezeichnet. Die fur
eine Applikation verfugbaren Register werden als General Purpose Registers (GPR)
bezeichnet, welche in allen Architekturen in ahnlicher Form zur Verfugung stehen.
Intel unterteilt in Intels 32-bit Architecture (IA-32) die Register je nach Verwendung
in die Gruppen General Data Registers, General Address Registers, Floating Point Stack
Registers, in Register fur spezielle Verwendungen (z.B. Multimedia) und Flags, Counter
und Pointer zur Kontrolle des Programmflusses (Instruction Pointer, Interrupt Control,
Paging, Mode Switching, uvm.). Weitere Details zur hier als Beispiel angefuhrten IA-32
Architektur sind [28] zu entnehmen.
Jeder Prozessor und jede Prozessorarchitektur hat spezielle Register, um mit dem
Programm zu kommunizieren oder den Programmfluss zu steuern. Moderne Prozes-
soren haben in der Regel einen groeren und schnelleren internen Speicher und konnen
2. Grundlagen 14
oft mehrere Speicherplatze in kurzester Zeit oder parallel bearbeiten. Sie bieten eine
groere Registerbreite (Bits pro Register) und konnen dadurch mehr Speicher direkt
adressieren. Die Anzahl und Art der Register ist hochst unterschiedlich, alle moder-
nen Prozessoren bieten mittlerweile aber Segment, Control, Debug und Test Register
zur Steuerung des Prozessors. Gemeinsam haben fast alle Prozessoren auch, dass jede
Manipulation eines Registers eine unerwartete und unbeabsichtigte, fur Angreifer viel-
leicht beabsichtigte, Auswirkung auf das Programm oder den Programmfluss haben
kann. Die fur einen Angriff wohl wichtigsten Register sind das ESP (Stack Pointer),
das EBP (Extended Base Pointer) und das EIP (Instruction Pointer) Register.
2.4 Daten und Funktionszeiger
Ein Pointer ist die Bezeichnung fur einen Zeiger auf eine Speicheradresse. Man unter-
scheidet dabei zwischen Zeigern auf Daten (Data Pointer) und Zeigern auf Funktionen
(Function Pointer). Ein Zeiger zeigt immer an den Beginn eines Speicherbereichs. Uber
den Zeiger selbst, welcher ausschlielich nur die Adresse darstellt, kann keinerlei Aussa-
ge uber die Groe des Speicherblocks, die an dieser Adresse abgelegten Daten und deren
Datentypen getroffen werden. Uber die Lage des Speicherblocks kann maximal auf die
grundsatzliche Verwendung - Daten oder Programmcode - geschlossen werden.[47]
Normale Variable Wert
Adresse
Wert
Pointer Variable
Pointer Variable Adresseint iValue;
int* pValue;
int (*pfFoo)();
int foo(){ ...}
pfFoo();
*pValue = iValue;
Datenzeiger (Data Pointer) Funktionszeiger (Function Pointer)
Abbildung 2.2: Daten- und Funktionszeiger
Innerhalb eines Programms wird haufig uber Zeigervariablen auf Daten oder Funktio-
nen zugegriffen (siehe Abbildung 2.2). Programmiersprachen wie C und C++ stellen
2. Grundlagen 15
dafur eigene Pointertypen zur Verfugung, die einen einfachen Zugriff auf Speicher und
eine einfache Zeigermanipulation (Zeigerarithmetik) ermoglichen.
In den Programmiersprachen C und C++ reprasentiert schon ein Funktionsname den
Zeiger auf die Funktion, also jene Speicheradresse, an der der Funktionscode beginnt.
Erst die Klammerung nach dem Funktionsnamen lasst den Compiler erkennen, dass es
sich um einen Aufruf der Funktion an dieser Adresse mit den angegebenen Parametern
handelt. Zeigervariablen konnen sich prinzipiell in jedem Speicherbereich befinden, je
nachdem welcher Speicherklasse sie zugeordnet werden. Typische auf dem Stack abge-
legte Pointer sind beispielsweise die als Argumente einer Funktion ubergebenen Zeiger,
lokale Zeigervariablen der Speicherklasse auto innerhalb einer Funktion, Rucksprung-
adressen, Adressen auf Exception Handler und gesicherte Frame Pointer. In C++ ge-
schriebene Programme speichern fur deren Objektinstanzen wahrend der Laufzeit auch
Tabellen mit Zeigern auf Funktionen (Vector Tables oder VTables), um virtuelle Me-
thoden abzubilden.
Zeiger innerhalb des Prozesspeichers stellen bei allen Attacken das Hauptangriffsziel
dar. Konnen Zeiger oder ganze Zeigertabellen von Auen manipuliert werden, konnen
damit andere Daten, als ursprunglich vorgesehen, gelesen oder gespeichert werden. Bei
manipulierten Funktionszeigern werden nicht vorgesehene Funktionen aufgerufen und
damit der Programmfluss gezielt verandert. Ebenso ist es denkbar, Zeiger auf Dateien
zu manipulieren und damit externe Dateien in einen laufenden Prozess einzuschleusen.
Auch Handles6 auf Dateien sind letztendlich nur Zeiger.
6Als Handle wird in der Softwareentwicklung ein Identifikator, Nickname oder Alias auf digitaleObjekte wie z.B. Dateien, Prozesse, Ressourcen, angeschlossene Gerate, usw. bezeichnet. Das Betriebs-system vergibt ein systemweit eindeutiges Handle beim Erzeugen eines Objekts oder dem Aufbau einerVerbindung mit einem Objekt. Im Programm werden diese Objekte dann nur noch uber dieses Handleangesprochen.
3
Potentielle
Sicherheitsschwachstellen
Unter einer potentiellen Sicherheitsschwachstelle (Security Vulnerability) versteht man
eine Systemschwache, welche einen Einbruch in das System zumindest theoretisch
moglich macht. Eine Schwachstelle ist immer die Folge eines Fehlers im Code (Co-
ding Bug) oder eines Fehlers im Design (Design Flaw). Ein Bug ist ein Fehler in der
Implementierung, z.B. eine fehlerhafte Stringbehandlung oder ein fehlerhafter Zeiger
innerhalb einer Funktion. Das Design einer Software kann keine Bugs haben, weil es
sich dabei nicht um Codierungsfehler handelt. Ein Flaw ist auf der Ebene des Designs,
der Planung und der Architektur einer Software zu suchen.
Die folgenden Abschnitte dieses Kapitels beschreiben einige der typischen Fehler, wel-
che fur einen Groteil der Sicherheitsschwachstellen verantwortlich sind. Allen voran
Designfehler und die sogenannten Pufferuberlaufe.
3.1 Designfehler
A flaw is instantiated in software code but is also present (or absent!) at the de-
sign level. schreiben Hoglund und McGraw in [24, S. 39]. Ein Designfehler kann al-
so im Quelltext einer Software zu finden sein. Oft aber sind designbasierte Sicher-
heitsschwachstellen die Folge von fehlenden Codeteilen oder einer unzureichenden Im-
plementierung notwendiger Sicherungs- und Verteidigungsmechanismen. Die folgende
16
3. Potentielle Schwachstellen 17
Auflistung sicherheitsrelevanter Designfehler lasst schnell erkennen, welche fehlerhaften
oder fehlenden Funktionen die typischen Designfehler heutiger Software sein konnen:
Eingabe- und Parameterprufung: Buffer Overflows, Parametermanipulation,
SQL Injection und Cross-Site Scripting (XSS) basieren auf ungepruften Benut-
zereingaben oder Funktionsparametern. Nur die strikte Uberprufung aller Ein-
gangsdaten kann Manipulationen verhindern.
Authentisierung und Authentifizierung: Das erste zweier Subjekte (z.B. Be-
nutzer, Prozesse, Services, Clients) muss einen Nachweis seiner Identitat erbrin-
gen, sich authentisieren. Das zweite Subjekt als sein Gegenuber muss die Identitat
seines Gegenubers uberprufen, die Identitat seines Partners authentifizieren.
Verschlusselung: Unverschlusselte Daten sind fur jedermann lesbar. Eine Ver-
schlusselung der Daten lasst nur jenen die Informationen zukommen, fur die sie
auch gedacht sind.
Sicherung: Ungesicherte Daten sind manipulierbar. Codierungsverfahren konnen
Daten vor Manipulationen sichern oder jede Manipulation aufdecken (z.B. Check-
summe, Signatur).
Zugriffs- und Ausfuhrungsberechtigungen: Berechtigungsstrategien legen
fest, was ein Benutzer oder Prozess darf oder nicht darf (z.B. Zugriff auf Dateien,
Ressourcen, Starten von Prozessen).
Anwendungs- und Systemkonfiguration: Konfigurationsdateien unterliegen
einer strengen Kontrolle (Zugriffs- und Manipulationsschutz).
Fehlerbehandlung und Logging: Falsche Fehlerbehandlungen verraten oft in-
terne Systeminformationen. Logdateien, Speicherauszuge und Stacktraces sollten
nicht sichtbar sein oder mit einer entsprechenden Zugriffsberechtigung versehen
werden.
Designfehler lassen sich im Allgemeinen nicht durch die Prufung einzelner Codezei-
len erkennen. Was fur eine einfache Applikation an Daten- und Codesicherung noch
ausreichend sein mag, ist fur eine sicherheitskritische Anwendung bei weitem nicht
genug. Erst eine Klassifizierung der Sicherheitsanforderungen, das Erkennen der Be-
drohungsszenarien, das Zusammenwirken einzelner Module und die Identifikation der
3. Potentielle Schwachstellen 18
Datenstrome lasst mogliche Designfehler und darauf basierende Sicherheitsschwachstel-
len sichtbar werden.
3.2 Overflow Fehler
Die in den letzten Jahrzehnten weitaus am haufigsten zum Einbruch in ein Computer-
system genutzten Programmfehler stellen so genannte Pufferuberlaufe (Buffer Over-
flows oder Buffer Overruns) dar. Sie treten immer dann auf, wenn ein Programm
bzw. eine Funktion Daten in einem Speicher bestimmter Lange verarbeitet und dabei
die Grenzen eines Puffers (Memory Buffer) schreibend uber- oder unterschreitet. Dabei
werden angrenzende, sich ebenfalls im Speicher befindliche Daten oder Zeiger auf Daten
und Funktionen uberschrieben, welche funktions- und ablaufrelevant sind. Besonders
haufig treten diese Fehler bei C- und C++-Programmen auf, da hier seitens der Sprach-
konzepte und Compiler keine Uberprufungen der Speicher- und Arraygrenzen erfolgen.
Fur die Vermeidung eines Buffer Overflows ist letztendlich immer der Programmie-
rer zustandig. Moderne Programmiersprachen unterstutzen die Entwickler, indem sie
Array- und Speichergrenzen verwalten und Zugriffe auf illegale Speicherbereiche un-
terbinden. Die Uberprufung aller Speicherzugriffe und Speichergrenzen hat naturlich
Performanceeinbuen zur Folge.
Overflows konnen durch einen Fehler scheinbar zufallig auftreten oder - bei Attacken -
absichtlich provoziert werden. In allen Fallen ist ein Programmfehler die Voraussetzung
fur einen Uberlauf. Das Problem kann uber langere Zeit unentdeckt bleiben, wenn das
Programm keine sichtbaren Veranderungen im weiteren Ablauf zeigt. Ein Angreifer,
der derartige Sicherheitsschwachstellen (Security Vulnerabilities) ausnutzen mochte,
provoziert einen Overflow, um Daten bzw. Code in das System zu injizieren (Data
Injection oder Code Injection). Dabei versorgt er das Programm gezielt mit Daten,
die auerhalb der Spezifikationen liegen. Werden diese Eingangsdaten keiner expliziten
Prufung unterzogen, kann es zu einem Pufferuberlauf kommen und im Speicher be-
finden sich gezielt injizierte Daten. Mitunter ist der weitere Prozessablauf durch diese
Daten gesteuert und im schlechtesten Fall durch einen Angreifer gezielt beeinflusst.
3. Potentielle Schwachstellen 19
Wie Hoglund et al. in [24] schreiben, erlauben unterschiedliche Programmfehler auch
unterschiedliche Methoden um ein System anzugreifen.Related programming errors
give rise to simular exploit techniques.[24, S. 38] Im Folgenden werden einige aus-
gewahlte Uberlauffehler und die darauf basierenden Angriffsmethoden vorgestellt.
3.2.1 Stack Overflow
Der Stack ist, wie im Abschnitt 2.2.5 beschrieben, ein Speicherbereich, in dem lo-
kale Variablen, Sprungadressen und Funktionsparameter kurzfristig abgelegt werden.
In IA-32 Architekturen wachst, wie bei vielen anderen Prozessorarchitekturen auch,
der Stack von hoheren zu niedrigeren Speicheradressen. Wird nun ein Puffer auf dem
Stack angelegt und kommt es bei einem Schreibvorgang zu einem Uberschreiten der
Puffergrenzen, so werden an den Puffer angrenzende Speicherstellen auf dem Stack
uberschrieben.
3.2.1.1 Der klassische Stack Overflow
Der klassische stack-basierte Buffer Overflow oder, in Form einer Attacke provoziert,
auch Stack Smashing Attack genannt [1], wird als Overflow der 1.Generation1 bezeich-
net [21], weil dieser Overflow wohl zu den am langsten bekannten Schwachstellen gehort.
Bei einem klassischen Stack Overflow Exploit ist das Ziel meist die Manipulation der
Function Return Address, also der Rucksprungadresse zur aufrufenden Funktion. Kann
dieser Zeiger gezielt manipuliert werden, kann eine eigene eingeschleuste Funktion oder
eine Bibliotheksfunktion aufgerufen werden.
Ein kurzes Beispiel soll die Vorgange auf dem Stack bei einem Funktionsaufruf und ei-
nem Stack Overflow verdeutlichen. In dem im Listing 3.1 gezeigten Beispielprogramm
wird aus der Funktion main() die Funktion foo() aufgerufen. Die Applikation ist syn-
taktisch korrekt und ausfuhrbar, obwohl die Funktion foo einige Fehler bzw. Schwach-
stellen aufweist, welche fur einen Angriff missbraucht werden konnten. Die unsichere
1Halvar teilt in [21] die verschiedenen Exploit-Techniken erstmals in Generationen ein. Er klas-sifiziert damit die Arten der Buffer Overflow Schwachstellen anhand der zeitlichen Abfolge, in derdiese veroffentlicht wurden. Darauf basierend erweitert Klein in [31] diese Einteilung und weist dieOverflows jeweils einer bestimmten Generation zu.
3. Potentielle Schwachstellen 20
Funktion strcpy (siehe Anhang A.1) pruft nicht, ob die Lange des zu kopierenden
Strings die des Puffers auf dem Stack uberschreitet.
1 #include
2 #include
3
4 void foo(const char* pStr)
5 {
6 char szBuffer[20];
7 strcpy(szBuffer, pStr); // Stack Overflow Schwachstelle
8 printf(szBuffer); // Format-String Schwachstelle
9 }
10
11 int main(int argc, char* argv[])
12 {
13 foo(argv[1]); // Illegal Pointer Schwachstelle
14 return 0;
15 }
Listing 3.1: Beispielprogramm StackOverflow.c
Ebenso bleibt das Argument der Funktion printf ungepruft, was eine im nachsten
Abschnitt besprochene Format-String Schwachstelle zur Folge hat (siehe Kapitel 3.3).
0x0012FF5C 4f 1d 13 78 O..x // Beginn des Puffers szBuffer
0x0012FF60 c0 ff 12 00 Ay..
0x0012FF64 3c 10 40 00
3. Potentielle Schwachstellen 21
dem Stack gesicherten Register EIP (die Rucksprungadresse) und EPB (den Frame
Pointer). Der lokale Puffer szBuffer der Funktion foo und auch die lokalen Variablen
von main() liegen ebenfalls auf dem Stack.
hoheAdresswerte
niedrigeAdresswerte
vorhergehendeStack Frames
Function Return Addressgesicherter Frame Pointer
Lokal deklarierter Puffer szBuffer[20]
Sta
ck F
ram
e fo
o()
Function Return Address
Sta
ck F
ram
e m
ain(
)
Funktionsparameterargc
argv[ ]
gesicherter Frame PointerFunktionsparameter
pStr
vorhergehendeStack Frames
Sta
ck F
ram
e fo
o()
Function Return Address
Sta
ck F
ram
e m
ain(
)
Funktionsparameterargc
argv[ ]
gesicherter Frame Pointer
Lokal deklarierter Puffer szBuffer[20] mit eingeschleustem Code
Function Return Address
Sta
ckw
achs
tum
(a) Stack vor dem Buffer Overflow (b) Stack nach dem Buffer Overflow
Ove
rflow
Puf
fer
FunktionsparameterpStr
Abbildung 3.1: Einschleusen und Aufrufen von Code uber einen Stack Overflow
Im folgenden Beispiel wird von der Annahme ausgegangen, dass der Angreifer das Pro-
gramm aus Listing 3.1 mit einem wahlfreien Parameter aufrufen kann. Die Abbildung
3.1.a zeigt den Stack vor dem Stack Overflow. Schleust der Angreifer nun einen mit
Code und Adressen praparierten String2 in die Funktion foo ein, welcher langer ist als
der lokal angelegte Puffer szBuffer, kommt es wegen der fehlende Langenuberprufung
in foo() und auch in der Funktion strcpy zu einem Overflow des Puffers szBuffer. Der
Angreifer uberschreibt durch den Overflow, wie aus dem Dump in Listing 3.2 und der
Abbildung 3.1 ersichtlich ist, nicht nur die auf dem Stack gesicherte Rucksprungadresse
(Function Return Address) und den gesicherten Frame Pointer, sondern schleust damit
mitunter gleichzeitig Code bzw. Adressen in den Puffer szBuffer bzw. auf den Stack
2Um Code oder Adressen uber einen String in ein Programm einzuschleusen, muss eine Zeichenkettemit einer Reihe nicht-druckbarer Zeichen erzeugt werden. Perl erlaubt die Zusammenstellung vonStrings uber Escape-Sequenzen (siehe dazu [56]), ahnlich der Programmiersprache C, und die Ubergabeder Parameter und den Aufruf des Programms uber ein Script von der Console heraus [26]. Linux/Unixbieten mit der Bash-Shell ahnliche Moglichkeiten [30].
3. Potentielle Schwachstellen 22
ein (siehe Abbildung 3.1.b). Die Rucksprungadresse lasst er auf den eingeschleusten
Code zeigen. Beim Beenden der Funktion wird der EIP vom Stack geholt und auf diese
Adresse gesprungen. Anstatt in main() fahrt der Programmfluss (Program Flow) im
eingeschleusten Code fort.
Stack Overflows gehoren zu den einfachsten Overflows, weil sie am leichtesten auszunut-
zen sind. Ebenso gibt es mittlerweile mehrere Methoden, wie Stack Overflows verhin-
dert oder zumindest erschwert werden konnen. Welche das sind und wie wirkungsvoll
derartige Methoden sind, wird in spateren Kapiteln noch gepruft und diskutiert.
3.2.1.2 Frame Pointer Overwrite
Im Unterschied zum klassischen Stack Overflow, bei dem Daten und Zeiger und in erster
Linie die Rucksprungadresse manipuliert werden, kommt es bei einem Frame Pointer
Overwrite zu einem Uberschreiben des auf dem Stack gesicherten Frame Pointers.
Kann dieser Pointer durch einen Angreifer gezielt manipuliert werden, zum Beispiel
wie in der Abbildung 3.3 gezeigt durch einen Off-By-One Overrun, so kann dadurch
der weitere Programmfluss geandert werden. Mit dieser Exploit-Technik kann ebenfalls
zuvor eingeschleuster Programmcode zur Ausfuhrung gebracht werden. Der Artikel [32]
zeigt mit einem primitiven Beispiel in beeindruckender Einfachheit die Funktionsweise
eines Frame Pointer Overwrites und dessen Ausnutzung fur einen Exploit.
3.2.2 Heap Overflow
Heap Overflows gehoren zu den Overflows der 4. Generation [21]. Sie funktionieren nach
einem ahnlichen Prinzip wie Stack Overflows, sind deutlich aufwandiger auszunutzen
und in Quellen wie [15] detailliert beschrieben. Nach [15] und WSEC stellen sie aus
folgenden Grunden eine zusatzliche Gefahr dar,
weil viele Compilererweiterungen und softwarebasierte Schutzmethoden (spezielle
Bibliotheken) nur den Stack nicht aber den Heap schutzen konnen,
weil viele hardwarebasierte Schutzmethoden moderner Prozessoren nur das Aus-
fuhren von Code auf dem Stack verhindern konnen
3. Potentielle Schwachstellen 23
und vor allem, weil vielfach noch von der falschen Annahme ausgegangen wird,
dass Heap Overflows nicht fur Angriffe nutzbar seien und damit eine Codeande-
rung von local buffer auf static buffer ausreichend fur einen wirksamen Schutz
sei.
Auf dem Heap werden, wie im Kapitel 2.2.4 beschrieben, jene Puffer abgelegt, die erst
wahrend der Laufzeit mit speziellen API-Funktionen alloziert (z.B. mit malloc()) und
spater wieder freigegeben (z.B. mit free()) werden. Dieser dynamische Speicherbe-
reich (Memory Pool) wird durch das Betriebssystem bzw. dessen Speicherverwaltung
(Heap Management oder Memory Management) organisiert. Durch die mit der Laufzeit
zunehmende Fragmentierung des Speichers sind die Speicherblocke in ihrer Reihenfolge
schwer vorhersehbar im Speicher abgelegt. Belegte und freie Speicherbereiche konnen
sich beliebig abwechseln. Bei einem Heap Overflow lauft keineswegs der Heap selbst
uber, sondern, ahnlich wie beim Stack Overflow, ein auf dem Heap abgelegter Puffer.
Kommt es im Heap zu einem Pufferuberlauf, dann konnen davon freie Speicherberei-
che, angrenzende belegte Speicherbereiche und auch interne zur Verwaltung des Heaps
notwendige Verwaltungsdaten betroffen sein, welche ebenfalls im dynamischen Bereich
des Heap Segments abgelegt werden. Uber Heap Overflows konnen wie bei Stack Over-
flows auch Zeiger manipuliert werden, uber die auf Daten zugegriffen wird oder uber
die der Programmfluss kontrolliert wird.
1 #include
2 #include
3
4 struct PufferPtr
5 {
6 char* pPufferA;
7 char* pPufferB;
8 };
9
10 int main(int argc, char* argv[])
11 {
12 PufferPtr* pPufferP = (PufferPtr*)malloc(sizeof(PufferPtr));
13 pPufferP->pPufferA = (char*)malloc(10);
14 pPufferP->pPufferB = (char*)malloc(10);
15
16 strcpy(pPufferP->pPufferA, argv[1]);
3. Potentielle Schwachstellen 24
17 strcpy(pPufferP->pPufferB, argv[2]);
18
19 free(pPufferP->pPufferB);
20 free(pPufferP->pPufferA);
21 free(pPufferP);
22
23 return 0;
24 }
Listing 3.3: Beispielprogramm HeapOverflow.c
Ein kurzes Beispiel zeigt einen moglichen Angriff uber einen Heap Overflow. Dabei wird,
um die Komplexitat derartiger Angriffe zu verdeutlichen, wiederum das Einschleusen
einer eigenen Funktion angenommen. Um den Code auszufuhren, muss gleichzeitig auch
der Stack manipuliert werden. Listing 3.3 zeigt eine Applikation, welche zwei Puffer A
und B auf dem Heap allokiert und die Zeiger der beiden Speicherblocke ebenfalls auf
dem Heap (Puffer P) ablegt. Das Speicherabbild konnte wie in der Abbildung 3.2.a
dargestellt aussehen.
Die Applikation liest nun zwei Benutzereingaben und speichert diese in den beiden
Puffern A und B. Wei der Angreifer um die Organisation der Datenblocke und deren
Adressen auf dem Heap, so konnte er versuchen, mit der ersten Eingabe einen Uberlauf
zu provozieren und gleichzeitig damit seinen Code einzuschleusen (siehe Abbildung
3.2.b.(1)). Durch den Uberlauf des Puffers A kommt es gleichzeitig zum Uberschreiben
des Zeigers pPufferB. Der Angreifer lasst diesen Zeiger nun durch seine Manipulati-
on gezielt auf die auf dem Stack liegende Function Return Address verweisen. Nun
kommt es zur zweiten Eingabe und der Angreifer gibt die Adresse von Puffer A ein.
Diese Adresse wird nun auf die Adresse pPufferB geschrieben, also in den Stack (siehe
Abbildung 3.2.b.(2)). Wird nun die Funktion beendet und der aktuelle Stack Frame
geloscht, springt das Programm automatisch auf die zuvor im Stack manipulierte Func-
tion Return Address (siehe Abbildung 3.2.b.(3)). Damit ist es dem Angreifer gelungen,
in zwei Schritten Code auf dem Heap einzuschleusen und spater auch auszufuhren. Die
gezeigte Methode ubergeht auch etwaige Schutzmechanismen der Compiler, die spater
noch diskutiert werden.
3. Potentielle Schwachstellen 25
hoheAdresswerte
niedrigeAdresswerte
vorhergehendeStack Frames
Function Return Addressgesicherter Frame Pointer
Lokale Variablen
Sta
ck F
ram
e fo
o()
(a) Heap und Stack vor dem Buffer Overflow (b) Heap und Stack nach dem Buffer Overflow
Funktionsparameter
Puffer B
Puffer A
char* pPufferAchar* pPufferBP
AB
HEAP
vorhergehendeStack Frames
Function Return Addressgesicherter Frame Pointer
Lokale Variablen
Sta
ck F
ram
e fo
o() Funktionsparameter
Puffer B
Puffer A
char* pPufferA
AB
HEAP
P char* pPufferB
Puffer AEingeschleusterCode
Eingeschleuster Pointer1
2
3
Abbildung 3.2: Einschleusen und Aufrufen von Code uber einen Heap Overflow
Dieses Szenario mag konstruiert erscheinen, unzahlige Eintrage in einschlagigen Listen
beweisen aber, dass Heap Overflows oft fur Angriffe genutzt werden. Viele der Heap
Overflow basierten Attacken stutzen sich auch auf die Manipulation der Verwaltungs-
daten des Memory Managers im Heap. Diese Attacken bedurfen neben der Lage der
Speicherblocke und der detaillierten Kenntnisse uber das Programm noch weiterer de-
taillierter Kenntnisse uber die interne Organisation zur Verwaltung des dynamischen
Speichers [61]. Interne Strukturen der Heap- bzw. Memory Manager sind aber gut do-
kumentiert (vgl. [21, 61]) oder der Sourcecode der Manager ist ohnehin frei zuganglich
(z.B. Linux-Quellcode und Open Source3).
3Fur die Bibliothek Glibc und somit viele Linux/Unix Systeme wird seit Jahren eine freie Imple-mentierung von Wolfram Gloger mit dem Namen ptmalloc eingesetzt, deren Sourcecode frei zuganglichist. Nahere Informationen dazu sind unter http://www.malloc.de (Stand: 2007.05.02) verfugbar.
http://www.malloc.de
3. Potentielle Schwachstellen 26
3.2.3 Array Indexing Overflows
Array Indexing Overflows passieren, wenn beim Zugriff auf ein Array der Index falsch
ist und somit ein Zugriff auerhalb des Speicherbereichs eines Arrays erfolgt. Typi-
scherweise kommt dies dann am ehesten vor, wenn es bei der Indexberechnung zu
einem Integer Overflow oder Integer Underflow gekommen ist, oder wenn bei einer
Iteration die erlaubten Indexbereiche verlassen werden.
hoheAdresswerte
niedrigeAdresswerte
Function Return Addressgesicherter Frame Pointer
Lokal deklarierter Puffer szBuffer[20]
Sta
ck F
ram
e fo
o() Funktionsparameter
pStr
vorhergehendeStack Frames
Sta
ck F
ram
e fo
o()
Lokal deklarierter Puffer szBuffer[20] mit
Off-By-One-Overrun
Sta
ckw
achs
tum
(a) Stack vor einem Off-By-One Overrun (b) Manipulierter Frame Pointer nach einem Off-By-One Overrun
Off-By-OneOverrun
Puf
fer
FunktionsparameterpStr
gesicherter Frame PointerFunction Return Address
vorhergehendeStack Frames
Abbildung 3.3: Manipulation des gesicherten Frame Pointers uber einen Off-By-OneOverrun
Haufige Vertreter dieser Array Indexing Overflows sind die sogenannten Off-By-One
Overflows. Wie der Name andeutet, handelt es sich dabei um einen Uberlauf um 1
Byte. Eine typische Auspragung dieses Fehlers ist eine ein Array bearbeitende Schleife,
wie es das kurze Beispiel aus Listing 3.4 zeigt:
1 void foo(const char* str)
2 {
3 char szBuffer[20];
4 for( int i=0; i
3. Potentielle Schwachstellen 27
Durch einen Durchlauf mehr als erlaubt (die Laufbedingung verwendet falschlicherweise
3. Potentielle Schwachstellen 28
und statische lokale Variablen abgelegt (siehe Listing 2.1, Zeile 1 und 15). BSS Over-
flows erlauben, ebenso wie auch bei Heap Overflows (Kapitel 3.2.2) gezeigt wurde, die
Manipulation von Zeigern. Attacken lassen sich besonders dann einfach durchfuhren,
wenn nicht-initialisierte Zeiger und Puffer in der Applikation verwendet wurden, welche
dann im BSS Segment liegen und deren Uberlauf nicht verhindert wird. Ein wesentli-
cher Vorteil und eine deutliche Erleichterung fur einen Angreifer ist die Tatsache, dass
Puffer im BSS-Segment eine feste Groe haben und in ihrer Lage unverandert bleiben.
Die Lage der Puffer wird schon vom Compiler und Linker wahrend der Ubersetzung des
Quelltextes und dem Erstellen einer ausfuhrbaren Datei bestimmt. Kann der Angreifer
die Reihenfolge der Puffer und die Abstande zueinander erst einmal bestimmen, kann
er sich auf deren fixe relative Lage zueinander verlassen6.[15]
Wie durch gezielte Zeiger- und Puffermanipulationen ein Angriff (Exploit) realisiert
werden kann, ist dem Kapitel 3.2.2 zu entnehmen.
3.3 Format-String Fehler
Sicherheitslucken und Exploits aufgrund von Format-String-Schwachstellen sind erst
in den spaten 1990er Jahren erstmals publiziert worden, obwohl sie eigentlich schon so
alt sind wie die Programmiersprache C und deren unsichere C-Library selbst (vgl. [17]
und [31]). Format-String Fehler beschreiben ein sehr C/C++-spezifisches Problem fur
Funktionen mit einer beliebigen Anzahl von Parametern. Typische Vertreter sind die
ANSI-C-Funktionen der printf()-Familie zur formatierten Ausgabe und Stringforma-
tierung. Funktionen mit einer variablen Anzahl von Parametern ist es nicht moglich,
den Datentyp der uber den Stack ubergebenen Variablen und Konstanten wahrend der
Laufzeit festzustellen.
Bei printf()-Funktionen wird der Datentyp mit einem String, welcher der Forma-
tierung der Ausgabe dient (Format-String), beschrieben. Im Prototyp der Funktion
int printf(const char* format,...) beschreibt der Stringparameter format jene
6Oft reicht ein einfaches Probierverfahren aus, um die Lage der Puffer untereinander zu bestimmen.Dabei werden durch manipulierte Eingaben Pufferuberlaufe provoziert und durch die Beobachtungder Ausgaben und anderer Puffer die Lage zueinander bestimmt. Man beobachtet dabei also, wo dieeingeschleusten Daten wieder sichtbar werden.
3. Potentielle Schwachstellen 29
Zeichenkette, welche den Ausgabetext und die durch das Zeichen % eingeleitete For-
matanweisungen enthalt. Die Formatanweisung beschreibt dabei nicht nur den Da-
tentyp, sondern optional das Ausgabeformat (Dezimal, Hexadezimal,. . . ), die Breite
der Ausgabe, die Anzahl der Vor- und Nachkommastellen, usw.. Wie im Kapitel 2.2.5
beschrieben, werden beim Aufruf einer Funktion die Parameter auf dem Stack abge-
legt. Innerhalb der Funktion printf werden nun der format-String geparst, die dem
%-Zeichen entsprechenden Daten dem Stack entnommen und ein Ausgabestring gene-
riert. Der erste Parameter wird dabei dem ersten auftretenden %-Zeichen zugeordnet,
der zweite Parameter dem zweiten %-Zeichen, usw.. Im ISO/IEC 9899:TC2 Standard
der Programmiersprache C ist zu lesenIf there are insufficient arguments for the for-
mat, the behavior is undefined.[11, S. 273]. Sowohl die Anzahl der Parameter als auch
der tatsachlich an die Funktion ubergebene Datentyp muss mit den Platzhaltern des
Format-Strings in jedem Fall ubereinstimmen. Das heit, die Formatbeschreibung muss
den auf dem Stack abgelegten Daten entsprechen. [47].
Das folgende kurze Beispiel aus dem Listing 3.6 zeigt eine typische Format-String-
Sicherheitslucke, bei der ein String zur Ausgabe mit printf von einem Benutzer
ubergeben werden kann.
1 int main(int argc, char* argv[])
2 {
3 printf( argv[1] );
4 return 0;
5 }
Listing 3.6: Beispielprogramm PrintDemo.c
Die Eingabe bleibt in diesem Beispiel ungepruft und der Benutzer konnte das Pro-
gramm mit PrintDemo "%p %p %p %p" aufrufen. Der String mit den Formatanwei-
sungen wird als Format-String interpretiert und die Funktion printf versucht vier
Zeiger aufgrund der vier %p auszugeben. Versuche zeigen nun, dass die Funktionen der
printf()-Gruppe die Daten entsprechend der Beschreibung im Format-String vom
Stack nehmen und daraus den Ausgabetext generieren. Dies geschieht unabhangig da-
von, wie viele Parameter zuvor auf dem Stack abgelegt wurden. Die Funktion printf,
wie im Listing 3.6 ersichtlich, legt keine zusatzlichen vier Zeiger bzw. 16 Byte auf dem
3. Potentielle Schwachstellen 30
Stack ab, sondern ausschlielich nur den Zeiger auf das Arrayelement argv[1] bzw. 4
Byte. Dieser Missstand wird auch nicht erkannt, daher gibt die Funktion die nachsten
zufallig auf dem Stack liegenden beliebigen 4 * 4 Byte aus:
[dau]\$ PrintDemo "%p %p %p %p"
0012FFC0 0040115A 00000002 02DD4FB0
Vergleicht man diese Ausgabe nun mit einem Stack Dump, der innerhalb der Funkti-
on main() erstellt wurde, dann ist leicht zu erkennen, dass printf Teile des Stacks
ausgegeben hat7.
0x0012FF7C c0 ff 12 00 Ay.. // Gesicherter Frame Pointer
0x0012FF80 5a 11 40 00 Z.@. // Rucksprungadresse aus der Funktion main()
0x0012FF84 02 00 00 00 .... // Funktionsparameter argc
0x0012FF88 b0 4f dd 02 OY. // Funktionsparameter argv
Listing 3.7: Stackdump innerhalb des Programms PrintDemo.c
Damit sind fur den Benutzer oder, wenn Absicht dahinter steht, fur den Angreifer die
Daten des Stacks sichtbar. Die wichtigsten fur einen Angriff verwendeten Format-String
Platzhalter sind in der folgenden Tabelle 3.1 aufgefuhrt.
Platzhalter Datentyp, Formatierung
%d int, Dezimaldarstellung mit Vorzeichen%u unsigned int, Dezimaldarstellung%x unsigned int, Hexadezimaldarstellung%c char, einzelnes Zeichen (Character)%s char*, Zeichenkette (String)%p void-Zeiger (Pointer in Hexadezimaldarstellung)%n int*, schreibt Anzahl der ausgegebenen Zeichen an Adresse
Tabelle 3.1: Ausgewahlte Format-String Platzhalter der printf-Familie
Eine vollstandige Beschreibung aller Formatanweisungen und Steuerzeichen ist der
IEEE Spezifikation [27]8 zu entnehmen.
7Die umgekehrte Reihenfolge der Bytes entsteht aufgrund der Little Endian Architektur der IntelProzessoren.
8http://www.opengroup.org/onlinepubs/009695399/functions/printf.html(Stand:2007.04.29)
http://www.opengroup.org/onlinepubs/009695399/functions/printf.html
3. Potentielle Schwachstellen 31
Die Moglichkeiten der Manipulation und vor allem die Einfachheit einer Code Injection
mittels Format-String Exploits zeigen die Beispiele in [50] und der Artikel [34]. Eine
Tabelle aller unsicheren POSIX-Funktionen der C-Standardbibliothek, zu denen auch
jene der printf-Gruppe gehoren, ist dem Anhang zu entnehmen (siehe Tabelle A.1).
4
Lokalisierung potentieller
Schwachstellen
Bevor Sicherheitsschwachstellen entfernt oder umgangen werden konnen, mussen die-
se als solche identifiziert werden. Einfache Sicherheitsschwachstellen weisen oft kla-
re Merkmale auf und konnen systematisch lokalisiert und vermieden werden. Meist
handelt es sich um lokale Fehler in der Implementierung, also einfache Codierungs-
fehler. Komplexe Schwachstellen sind oft schwer zu lokalisieren, so dass sie bei einer
isolierten Betrachtung im Code keine auffalligen Merkmale aufweisen. Oft entsteht erst
durch das Zusammenwirken verschiedenster Funktionen, Module oder Applikationen
eine Schwachstelle, die fur einen Angriff genutzt werden kann. Dabei handelt es sich
meist um Designfehler, welche sich nicht einfach als Codefehler lokalisieren lassen.
Dieses Kapitel befasst sich mit den gangigsten Methoden zur Lokalisierung von Sicher-
heitsschwachstellen. Dabei kommen Werkzeuge zur Untersuchung des Quellcodes und
zur Untersuchung des kompilierten Programms zur Anwendung. In Programme inte-
grierte spezielle Zusatzfunktionen dienen einerseits der Uberwachung des Programms,
andererseits melden diese Programme dadurch Auffalligkeiten, die ebenfalls einer Si-
cherheitsanalyse dienlich sind.
32
4. Lokalisierung potentieller Schwachstellen 33
4.1 Allgemeines
Die Lokalisierung potentieller Sicherheitsschwachstellen erfolgt uber sogenannte Si-
cherheitsanalysen. Dies sind spezielle Verfahren zur gezielten Prufung von Software
auf mogliche Sicherheitslocher im Gesamtsystem.Ein Sicherheitsloch ist jeder Fehler
in Hardware, Software oder Richtlinien, der es einem Angreifer ermoglicht, unautori-
sierten Zugang zu Ihrem System zu bekommen. [2, S. 334] Eine Sicherheitsanalyse
ist demnach immer die Suche moglicher Hardware-, Software- und Designfehler oder
-schwachen. Ebenso entspricht eine Analyse immer einem systematischen Sammeln von
Daten zur Informationsgewinnung.
4.1.1 Informationsgewinnung
Fur die Analyse eines Systems benotigen Entwickler, Sicherheitsbeauftragte, Tester
und auch potentielle Angreifer technische Details, wie z.B. Informationen uber die
laufenden Programme und die Umgebung (z.B. Betriebssystem, Bibliotheken), unter
denen eine Software lauft. Fur eine umfangreiche Analyse einer Software stehen mehrere
Informationsquellen zur Verfugung (basierend auf [33]):
Quellcode (Source Code) der Applikation und/oder der Bibliotheken
Dokumentation (z.B. Handbucher, technische Dokumentation, Hilfesysteme)
Datendateien und Speicherabbilder (Core Dumps)
Log- und Trace-Dateien (z.B. Status- und Fehlerinformationen)
Informationen aus dem laufenden Prozess durch Debugging
Grundsatzlich muss davon ausgegangen werden, dass sowohl dem Entwickler als auch
dem potentiellen Angreifer die gleichen Daten zur Verfugung stehen, auch wenn sich die
Datengewinnung fur den Angreifer als aufwandiger erweisen kann. Dementsprechend
sind einerseits die benotigten Kenntnise und andererseits die Mittel und Methoden zur
Datengewinnung wahrend eines Sicherheitstests denen eines Angreifers nicht unahnlich.
Treten wahrend der Analyse ungewollt sensible Daten zutage, so stehen diese auch
jedem Angreifer zur Verfugung.
4. Lokalisierung potentieller Schwachstellen 34
4.1.2 Vollstandige Sicherheitsanalyse
Wahrend einer vollstandigen Sicherheitsanalyse werden alle aus den verschiedensten
Quellen gewonnenen Daten zusammengefasst und mit Fokus auf Sicherheit und Feh-
lerfreiheit ausgewertet. Die Analyse einer Software auf Sicherheit ist auch immer die
Analyse einer Software auf Fehlerfreiheit, weil Sicherheitsschwachstellen praktisch im-
mer als Folge von Design- oder Implementierungsfehlern auftreten. Die vollstandige Si-
cherheitsanalyse einer Software besteht aus folgenden Teilen (basierend auf [22, 26]):
Lokalisierung sicherheitsrelevanter Designfehler: Die Lokalisierung der De-
sign Flaws umfasst die Identifikation der Datenflusse, der Programm- und Daten-
eintrittspunkte (Entry Points) und der potentiellen Gefahren, welche von Auen
auf ein Programm einwirken.
Lokalisierung sicherheitsrelevanter Implementierungsfehler: Die Lokali-
sierung der Coding Bugs umfasst die Analyse der Quelltexte und der binaren und
ausfuhrbaren Dateien.
Stabilitatstests: Diese Tests umfassen Methoden zum absichtlichen Herbeifuhren
von Ausnahme- und Grenzsituationen. Die Software wird unter Stress getestet
und ihr Verhalten in Extremsituationen beobachtet (Stress Test).
Sicherheitstests: Diese Tests umfassen eine Prufung der Daten und Prozesssi-
cherheit auf Basis der Designvorgaben, wie z.B. die Prufung der Zugriffs- und
Rechteverwaltung, die Verschlusselung, uvm. (vlg. [26]).
Das naheliegendste, wenn auch nicht immer einfachste Verfahren, um Fehler und Si-
cherheitsschwachen einer Software zu finden, ist das manuelle Audit. Liegt der Quell-
text vor, konnen durch eine umfassende Quelltextinspektion (Source Code Audit) die
Quelldateien zeilenweise uberpruft und gleichzeitig die gefundenen Schwachstellen be-
hoben werden. Fehler in einfachen Programmen lassen sich manuell durchaus noch
lokalisieren. Bei groeren Projekten ist dies aufgrund der hohen Komplexitat und des
Umfangs des Quelltextes auerst zeit-, ressourcen- und kostenintensiv [31]. Ein weiteres
und nicht unerhebliches Problem bei der Durchfuhrung manueller Source Code- bzw.
Sicherheits-Audits ist die dafur notwendige Fachkenntnis im Bereich der defensiven
4. Lokalisierung potentieller Schwachstellen 35
Programmierung. Die Zuhilfenahme spezieller Werkzeuge fur automatisierte Software-
Audits liegt ebenso nahe wie die Anwendung verschiedenster Verfahren.[26]
4.1.3 Statische und dynamische Analyseverfahren
Bei allgemeinen Softwareanalysen und der Lokalisierung von Sicherheitsschwachstellen
unterscheidet man zwischen statischen und dynamischen Analyseverfahren. Die stati-
sche Analyse verzichtet auf das Ausfuhren der Software. Die Software wird auf Basis
des Quelltextes und zusatzlicher Dateien (wie z.B. Konfigurationsdateien) analysiert.
Dabei werden Funktionen und Konstrukte im Quelltext gesucht, welche eine Sicher-
heitslucke darstellen konnten. Die erweiterte statische Analyse ist eine auf dem Quell-
text basierende Vorwegnahme der Aktionen und Reaktionen der Software. Aus der
Sicht der Werkzeuge ist sie aufwandig, weil auf Basis des Quelltextes die Ablaufe, die
moglichen Aktionen und Programmfaden (Threads) simuliert werden mussen. Bei der
dynamischen Analyse wird das laufende Programm uberwacht bzw. die Datenstrome
eines Programms analysiert. Die Tests konnen in der realen oder einer simulierten
und kontrollierten Umgebung stattfinden. Wahrend der Beobachtung der Ablaufe und
der Re-/Aktionen der Software konnen Logfiles mit Statusmeldungen erzeugt werden.
Tritt eine Fehler auf, so kann ein Speicherabbild (Memory Dump) oder ein Stackabbild
Stack Trace erzeugt werden. Auf Basis dieser Daten sind weitere Analysemethoden
zum Auffinden der Fehlerursache moglich. [31, 60]
Am Beginn jeder Softwareanalyse steht die Untersuchung des Quelltextes. Diese kann
und sollte auch schon begleitend wahrend der Entwicklung der Software stattfinden.
Erst wenn der Code vermeintlich korrekt ausgefuhrt ist, werden komplexere Analyse-
methoden das System wahrend der Laufzeit analysieren. Der nun folgende Abschnitt
beschreibt einige Methoden und Werkzeuge der quelltextbasierten Sicherheitsanalyse.
4.2 Quelltextbasierte Analyse
Die quelltextbasierte Softwareanalyse gehort zu den statischen Analysemethoden, wel-
che auf die Ausfuhrung der zu prufenden Software verzichten. Der Quellcode wird einer
4. Lokalisierung potentieller Schwachstellen 36
Reihe von formalen Prufungen, den sogenannten White Box Tests, unterzogen, wobei
damit hauptsachlich mogliche Schwachstellen in der Implementierung entdeckt werden.
Mit der Quelltextanalyse konnen folgende Mangel entweder mit (semi-)automatischen
Werkzeugen oder durch ein manuelles Code Audit lokalisiert werden:
fehlerhaften Typenkonvertierungen (Illegal Casts)
mogliche Uberlaufe (Overflows)
illegale Zeiger und Nullzeiger (Null Pointer)
Uber- und Unterschreitung von Speicherbereichsgrenzen (Bounds Checks)
Speichermanagementfehler (Memory Leaks)
Compiler- und Betriebssystemabhangigkeiten
uninitialisierte Variablen und Speicherbereiche
unsichere Funktionen
das Nichteinhalten von Codierungsvorschriften (Coding Standards)
das Nichteinh