-
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