GraR - Fachhochschule Salzburg

Transcription

GraR - Fachhochschule Salzburg
DIPLOMARBEIT
Lokalisierung und Vermeidung potentieller
Sicherheitsschwachstellen in komplexen
Softwaresystemen
durchgeführt am
Studiengang Informationstechnik und System–Management
an der
Fachhochschule Salzburg
vorgelegt von:
Roland J. Graf
Studiengangsleiter:
Betreuer:
FH-Prof. DI Dr. Thomas Heistracher
DI(FH) Thomas Kurz
Salzburg, September 2007
Eidesstattliche Erklärung
Hiermit versichere ich, Roland J. Graf, geboren am 6. Mai 1964, dass die vorliegende
Diplomarbeit von mir selbständig verfasst wurde. Zur Erstellung wurden von mir keine
anderen als die angegebenen Hilfsmittel verwendet.
0310032093
Roland Graf
Matrikelnummer
ii
Danksagung
Alles Wissen und alle Vermehrung unseres Wissens endet
nicht mit einem Schlusspunkt, sondern mit Fragezeichen.
Hermann Hesse (1877-1962)
Meinen Eltern gebührt an dieser Stelle mein besonderer Dank. Gerade die ersten
Jahre meiner schulischen Laufbahn waren alles andere als Erfolg versprechend und
trotzdem 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 fachlichen auch menschliche Werte vermitteln. Ich hatte das Glück, einige dieser ganz wenigen Lehrer zu treffen. Stellvertretend möchte ich hier besonders Hr. Prof. Dr. Gerold
Kerer hervorheben, der nicht nur bereits vor über 20 Jahren als mein HTL-Lehrer mit
seinen fachlichen und pädagogischen Fähigkeiten glänzte, sondern mich auch in der
FH-Salzburg wieder durch seine außergewöhnliche Menschlichkeit und Qualifikation
beeindruckt hat.
Dank gilt auch meinen Kollegen am Studiengang ITS, die mich in so manchen Gesprächen und Diskussionen mit ihren Eingaben, Ideen, Fragen und Hinweisen geleitet
haben. Besonders hervorheben möchte ich meinen Diplomarbeitsbetreuer DI(FH) Thomas Kurz und allen voran FH-Prof. DI Dr. Thomas Heistracher, welche mich auch als
Reviewer mit konstruktiver Kritik sehr unterstützt haben.
Von meinen Kommilitonen verdient Dietmar eine Erwähnung. Durch seine kritischen Verbesserungsvorschläge hat er mich oft zu einer Mehrleistung getrieben.
Zuletzt möchte ich noch Sabine danken. Ohne sie wäre mein Studium neben dem
Beruf so gar nicht möglich gewesen. Sie hat mich über all die Jahre tatkräftig unterstützt und mir auch in schweren Zeiten den notwendigen Halt, die Kraft und die
Stabilität gegeben, die ich gebraucht habe. Niemand kann so positiv formulieren, wie
sie es tut und so war ich bevorteilt, indem sie als Germanistin all meine Arbeiten
sprachlich redigiert hat. Ihr gebührt jedenfalls mein größter Dank!
Und wenn Hesse folgend nun all meine Anstrengung zur Vermehrung des Wissens
mit einem Fragezeichen endet, dann bleibt noch eine Frage zu stellen: Was kommt
jetzt?
iii
Informationen
Vor- und Zuname:
Institution:
Studiengang:
Titel der Diplomarbeit:
Betreuer an der FH:
Roland J. Graf
Fachhochschule Salzburg GmbH
Informationstechnik & System-Management
Lokalisierung und Vermeidung potentieller
Sicherheitsschwachstellen in komplexen Softwaresystemen
DI(FH) Thomas Kurz
Schlagwörter
1. Schlagwort:
2. Schlagwort:
3. Schlagwort:
Software Security
Software Vulnerability
Code Injection
Abstract
This diploma thesis documents the usability of tools to localize potential security vulnerabilities and evaluates the effectiveness of development methods to avoid them. Mostly,
vulnerabilities are based on software bugs and design flaws. This paper provides the
basics of memory segmentation, processor registers and stack frames, before it explains
software bugs as the cause of potential software vulnerabilities and their risk potential. A variety of software tools are available to implement Static White Box Tests and
Dynamic Black Box Tests. Source Code Analysis Tools support the developers to parse
for potential bugs in the source code, Debugging, Tracing and Monitoring Tools help
the software and security testers to spy on data flows, function calls and flaws in executable binaries. This document reports the strengths and weaknesses of tested tools
and methods and discusses their expected effectiveness in production environments.
Adapted development methods can increase the resistance of software to attacks and
unauthorized data manipulations. Finally, an introduction to Defensive Programming
with helpful programming hints, additional tables and references, code examples, and
Best Practices for programmers will be given, which aims at helping developers to write
secure software.
iv
Inhaltsverzeichnis
Eidesstattliche Erklärung
ii
Danksagung
iii
Informationen
iv
Schlagwörter
iv
Abstract
iv
Abbildungsverzeichnis
x
Tabellenverzeichnis
xi
Listingverzeichnis
xii
1 Einführung
1
1.1
Global vernetzte Sicherheitsschwächen . . . . . . . . . . . . . . . . . .
2
1.2
Stabile Software(un-)sicherheit . . . . . . . . . . . . . . . . . . . . . . .
3
1.3
Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
1.4
Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
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
Format-String Fehler . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
3.3
4 Lokalisierung potentieller Schwachstellen
4.1
4.2
Allgemeines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
4.1.1
Informationsgewinnung . . . . . . . . . . . . . . . . . . . . . . .
33
4.1.2
Vollständige Sicherheitsanalyse . . . . . . . . . . . . . . . . . .
34
4.1.3
Statische und dynamische Analyseverfahren . . . . . . . . . . .
35
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
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
Bewertung der Methoden und Werkzeuge . . . . . . . . . . . . .
47
Binärcodebasierte Analyse . . . . . . . . . . . . . . . . . . . . . . . . .
48
4.2.2
4.2.3
4.3
32
vi
4.4
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
Integrierte Analyse und Überwachung . . . . . . . . . . . . . . . . . . .
63
4.4.1
Bounds Checking . . . . . . . . . . . . . . . . . . . . . . . . . .
63
4.4.2
Überwachung des Stacks . . . . . . . . . . . . . . . . . . . . . .
64
4.4.3
Überwachung von Funktionen . . . . . . . . . . . . . . . . . . .
65
4.4.4
Überwachung 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
Defensive Programmierung . . . . . . . . . . . . . . . . . . . . . . . . .
73
5.3.1
Überprüfung 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
Sicherere Programmiersprachen . . . . . . . . . . . . . . . . . . . . . .
82
5.4.1
Sichereres C und C++ . . . . . . . . . . . . . . . . . . . . . . .
82
5.4.2
Managed Code und Managed Memory . . . . . . . . . . . . . .
83
5.5
Zusätzliche Techniken . . . . . . . . . . . . . . . . . . . . . . . . . . . .
85
5.6
Bewertung der Methoden . . . . . . . . . . . . . . . . . . . . . . . . . .
86
5.3
5.4
vii
6 Zusammenfassung und Ausblick
88
6.1
Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
88
6.2
Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
90
6.3
Trends . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
90
Literaturverzeichnis
93
Abkürzungsverzeichnis
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 über Security Cookies . . . . . . . . . . . . . . 120
C.2 Einfache Speicherüberwachung in C++ . . . . . . . . . . . . . . . . . . 122
C.3 Defensive Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . 123
C.3.1 Überprüfung 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 über Return Codes . . . . . . . . . . 127
C.5.2 Sicherung von Funktionen über 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 für 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 Weiterführende 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 Aufspüren 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
Ausgewählte 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 für 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 Überladen 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 über Exceptions in einem korrekten C# Code . . . . 128
xii
1
Einführung
An application should be considered unsafe until demonstrated to be otherwise.
(Swiderski, 2004)
Die Häufung von Veröffentlichungen kritischer Sicherheitslücken in Applikationen, Netzwerkdiensten und Betriebssystemen macht deutlich, dass ein Großteil der Applikationen
und Computersysteme noch immer nicht sicher genug und ausreichend geschützt ist.
Technische Detailinformationen über bestimmte Einbruchsmöglichkeiten in Computersysteme und die ausgenutzten Schwachstellen werden größtenteils online publiziert1 .
Umfangreiche Beschreibungen der oftmals kreativen Methoden der Angreifer und der
möglichen Abwehrmethoden sind jedem Interessierten frei zugänglich. Diese machen
auch immer wieder deutlich, dass ein Großteil der Sicherheitsschwachstellen die Folge von Design- und Codierungsfehlern in einzelnen Teilen der Software ist. Ebenso
fällt dabei auf, dass bestimmte Fehler besonders häufig als Grund für eine Sicherheitsschwachstelle genannt werden.
Die Softwareindustrie wird vermehrt angehalten, fehlerfreie und sichere Software zu
entwickeln. Der Druck auf die Softwareentwickler steigt. Trotz der bekannten Mängel
1
Im Internet werden fast täglich Nachrichten von kritischen Softwarefehlern und Sicherheitslücken
veröffentlicht. Security Online Archive (z.B. SANS, CERT), einschlägige Mailinglisten, namhafte Unternehmen im Bereich Computer- und Netzwerksicherheit und News-Dienste (z.B. Heise Security,
SecurityFocus, Computer Crime & Intellectual Property Section), selbst Hacker und Cracker liefern
Informationen dazu größtenteils frei Haus. Eine Reihe weiterer Quellen vervollständigen diese Berichte und stellen Auswertungen über Trends, Statistiken und Top-10 Listen über mehr oder weniger
erfolgreiche Attacken auf verwundbare Computersysteme zur Verfügung. Die Gesamtheit dieser Informationen ergibt einen aktuellen Lagebericht über IT-Sicherheit, sicherheitskritische Softwaremängel
und deren Ursachen. Eine Liste ausgewählter Quellen findet sich im Anhang F am Ende dieses Dokuments.
1
1. Einführung
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 Sicherheitsschwächen
Die Firma Sun Microsystems2 hat vor einigen Jahren schon in der Vision The Net”
work is the Computer“ die hochgradige Vernetzung der Informationstechnologie und
die damit einhergehenden technologischen Veränderungen vorhergesehen. Mittlerweile
sind fast alle Computer über ein Netzwerk oder das weltumspannende Internet miteinander verbunden. Desktop-Computer, Server und Router, Pocket-Computer, mobile
Telefone, Embedded Systems, Fernseher, Multimedia Systeme, jede Menge mikroprozessorgesteuerte Geräte und eine Unzahl von Peripheriegeräten sind Teile eines oder
des globalen Netzwerks geworden. Dass sich durch diese Vernetzung auch das Bedrohungspotential durch mögliche 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 Geräte und
Systeme untereinander wächst auch der Druck, die Sicherheitsvorkehrungen bei Computersystemen und allen Systemteilen entsprechend anzupassen.
Konnte früher noch von lokalen Bedrohungen und Angriffen (Local Exploits), von lokalen 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 Systeme stellen dabei ein besonders großes Risiko und manchmal auch einen besonderen
Reiz für Angreifer dar. Das Internet ist eine feindselige Umgebung, deshalb muss der
Programmcode so entworfen sein, dass er einem Angriff widerstehen kann. Vernetzte
Systeme bedürfen also einer expliziten Sicherung gegen mögliche Angriffe. Jede potentielle Schwachstelle eines Systemglieds mindert die Sicherheit des gesamten Systems
oder stellt sie gar in Frage. Ein einzelner Sicherheitsmangel einer Applikation kann
2
http://www.sun.com
1. Einführung
3
schon für den Angriff des Gesamtsystems missbraucht werden. Ziel und Voraussetzung
für ein sicheres Computersystem ist demnach die Sicherheit jeder einzelnen Komponente. Nur so kann die Sicherheit des Gesamtsystems gewährleistet werden und ein
System den Attacken und Gefahren des Wild Wild Web“[26, S. 5] standhalten. Die
”
Entwicklung fehlerfreien Quellcodes ist längst nicht mehr genug, wenngleich eine der
unabdingbaren Voraussetzung für sichere Software.
1.2
Stabile Software(un-)sicherheit
Wenn von Sicherheitslücken 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 darüber seltener
berichtet wird, sind ebenfalls möglich und nicht minder gefährlich. Viele dieser oftmals lange unentdeckten Fehler stellen massive Sicherheitslücken dar. Sie bieten eine
Angriffsfläche für mögliche Attacken gegen einzelne Applikationen, Computersysteme
oder gesamte Netzwerke.
Vor einigen Jahren galt es noch als ausreichend stabile Software zu entwickeln. Es
genügte, wenn eine Applikation die Anforderungen der Endbenutzer erfüllte. Zusätzliche
sicherheitsrelevante Forderungen wurden kaum erhoben. Softwareentwickler wurden angehalten soliden, stabilen, wartbaren und erweiterbaren Code zu schreiben. In dieser
Zeit spielte Codesicherheit selbst in klassischen Standardwerken der Softwareentwicklung wie [35] und [36] kaum eine Rolle. Gute Software stellte sich dem Benutzer ausschließlich als stabil laufende Software dar. Dem Schutz vor mutwilligen Manipulationen
wurde kaum Beachtung geschenkt.
Mittlerweile wird vermehrt und ausdrücklich die Entwicklung sicherer Software gefordert. Langsam beginnen auch Softwareproduzenten und Benutzer ein allgemeines
Sicherheitsbewusstsein zu entwickeln. Für 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 Hinblick auf die Abwehr potentieller Gefahren und Attacken entworfen wurden. Auch wenn
fehlerfreier Code nicht automatisch eine sichere Software garantiert, so gilt ein Gutteil
1. Einführung
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 spezielle Methoden schon während der Entwicklung und während des gesamten Lebenszyklus einer Applikation erreicht werden. Die Bedrohungsmodellierung (Threat Modeling) hilft Gefährdungspotentiale frühzeitig zu identifizieren, zu evaluieren, sie zu
dokumentieren und Gegenmaßnahmen 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 möglichen gezielten Angriffen
standzuhalten. Die verwendeten Modelle, die Methoden und die Implementierungen
werden stetig angepasst und nötigenfalls erweitert, um im veränderten Risikoumfeld zu
bestehen und den ständig neuen Anforderungen zu entsprechen. Begleitende Maßnahmen während aller Entwicklungsphasen einer Applikation oder eines Softwaremoduls
schaffen erst die Voraussetzungen für 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 Überblick beschreibt die einzelnen Kapitel der
vorliegenden Arbeit.
1. Einführung
1.4
5
Überblick
Das folgende Kapitel 2 führt in ausgewählte technische Grundlagen der Softwareentwicklung ein und erklärt die zum Verständnis notwendigen Begriffe sowie das Speichermanagement einer Applikation. Nachdem ein Großteil der Systemsoftware nach
wie vor in C/C++ programmiert ist, werden diese beiden Programmiersprachen auch
bevorzugt in die Erklärungen einfließen.
In Kapitel 3 werden potentielle Sicherheitsschwachstellen einer Software und darauf basierende Angriffsmethoden vorgestellt. Einige einfache Beispiele zeigen die praktische Umsetzung und Einfachheit eines Buffer Overflow Angriffs auf ungesicherte Software.
Das Kapitel 4 untersucht ausgewählte Methoden der systematischen Lokalisierung
potentieller Code- und Sicherheitsschwachstellen. Zur Anwendung kommen dabei quellcodebasierte und binärcodebasierte Analysemethoden. Nachdem die manuelle Prüfung
von Code oft nicht effizient genug, sehr aufwändig und teuer ist, werden ebenso Werkzeuge zur automatischen Softwareanalyse geprüft.
In Kapitel 5 steht die Vermeidung potentieller Schwachstellen im Vordergrund. Schwerpunkt ist dabei die Diskussion von Methoden zur Erstellung von sicheren Designs und
zur Entwicklung sicherer Implementierungen. Das schon für die Designphase empfohlene Threat Modeling bleibt hier ebenso wenig unbehandelt wie die Anwendung der
Prinzipien des defensiven Programmierens.
In Kapitel 6 schließt ein kurzer Ausblick in die Zukunft die Arbeit ab. Darin wird
erläutert, wie aus der Sicht von Experten die Sicherheit komplexer Softwaresysteme in
Zukunft gewährleistet werden könnte. Die kurze Zusammenfassung schließt mit der
Beantwortung der Frage ab, ob die aktuellen Methoden der Softwareentwicklung schon
heute ausreichen würden, um die Sicherheit komplexer Softwaresysteme sicherzustellen
oder ob erst in Zukunft eine echte Softwaresicherheit möglich sein wird.
2
Grundlagen
Fast alle Sicherheitslücken basieren, wie aus den im Kapitel 1 angeführten Quellen hervorgeht, auf Programmfehlern und ein Großteil aller Angriffe basiert auf dem Prinzip
der Speichermanipulation. Selbst wenn ein Programm im Normalbetrieb über längere
Zeit stabil läuft, so bedeutet dies nicht zwingend, dass sich keine Fehler im zugrundeliegenden Programmcode befinden. Erst die Konfrontation eines Programms bzw. einer
Funktion mit für 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 Komplexität eines Quelltextes oder einer Softwarearchitektur. Bevor die
technischen Grundlagen des Speichermanagements einer Applikation erklärt werden,
wird der Begriff komplexe Softwaresysteme eingeführt.
2.1
Komplexe Softwaresysteme
Die Definition eines komplexen Softwaresystems kann gerade aus der Sicht der Softwareentwicklung eindeutig festgelegt werden. Unter komplex kann jede Software bezeichnet 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. Zeitgemäße und fortgeschrittene Entwicklungsmethoden versuchen der Komplexität durch
Modularisierung und Aufteilung in überschaubare Funktionseinheiten entgegenzuwirken. Dass dieses Vorhaben nicht zwingend zum Erfolg führen muss, zeigt die Zahl der
Veröffentlichungen (z.B. Bug Reports) und Fehlerkorrekturen (Bug Fixes) komplexer
Software der letzten Jahre1 .
2.2
Speicherorganisation
Dieses Kapitel führt einige Begriffe der Speicherverwaltung (Memory Management) ein.
Es erklärt 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 für fast alle anderen gängigen Prozessorarchitekturen.
Im folgenden Listing 2.1 wird ein kurzes C-Programm in Auszügen gezeigt. Es verwendet 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 Erläuterungen wird
wiederholt auf diesen Quellcode oder Teile davon Bezug genommen.
1
2
int _iGlobalValue;
static char _szGlobalMsg[] = "text";
// Var im BSS Segment
// Var im Data Segment
3
4
5
6
7
8
9
10
11
char* foo(const char *str1, const char* str2)
{
static int iLocal = 100;
// Var im Data Segment
char szBuffer[20];
// Puffer auf Stack
strcpy( szBuffer, str1 );
// => Stack Overflow Schwachstelle
...
return szBuffer;
// Pointer auf Stackpuffer
}
1
Häufig wird z.B. bei größeren Service Packs von Betriebssystemen und Office Paketen die Anzahl der behobenen Fehler mit einigen Hundert angegeben. Microsoft veröffentlicht in einem monatlichen Updatezyklus Service Packs, Updates und Patches und beziffert die Anzahl der kritischen und
zusätzlich geschlossenen Sicherheitslücken im Schnitt mit etwa 20 Fehlern pro Monat. Siehe dazu auch
http://www.microsoft.com/germany/technet/sicherheit/bulletins/aktuell/default.mspx
2. Grundlagen
8
12
13
14
15
16
int main(int argc, char *argv[])
{
static short iLen;
char* pBuff1, pBuff2;
// Var im BSS Segment
// Vars auf Stack
17
iLen = strlen( argv[1] );
pBuff1 = (char*)malloc( iLen );
for( int i=0; i <= iLen; i++ )
pBuff1[i] = argv[1][i];
strcpy( pBuff2, argv[2]);
18
19
20
21
22
23
printf( pBuff1 );
24
25
free( pBuff1 );
pBuff2 = foo(argv[1]);
free( pBuff1 );
return 0;
26
27
28
29
30
// => Integer Overflow Schwachst.
// Allokiert Puffer auf Heap
//
//
//
//
//
//
//
//
Heap und Off-By-One Overflow
=> unsichere Funktion strcpy()
=> uninitial. Zeiger pBuff2
Format String Schwachstelle
=> unsichere Funktion printf()
Freigabe des Pufferspeichers
=> Illegaler Zeiger in pBuff2
=> Double Free Fehler
}
Listing 2.1: Programm mit Variablen verschiedener Speicherklassen
2.2.1
Prozessspeicher
Ein Computerprogramm ist im klassischen Fall eine ausführbare Datei2 (Executable),
welche auf einem Datenträger gespeichert ist. Dabei kann es sich zum Beispiel, wie unter 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 ausführbaren Programmcode und dessen statische Daten, sondern beschreiben die Objektdatei. Sie speichern ebenso zusätzliche 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 ,
2
Ausführbare Scriptdateien, wie sie z.B. unter Unix-basierten Systemen häufig vorkommen, sind
von diesen Betrachtungen ausgenommen. Diese können nicht direkt ausgeführt werden, sondern
benötigen ein zusätzliches Programm (Interpreter ), welches die einzelnen Script Statements interpretiert und zur Ausführung bringt.
3
Diese Bezeichnung ist eigentlich irreführend, da dieser Header nicht optional ist und unbedingt
notwendige Informationen zur Größe des beim Start benötigten Speichers beinhaltet.
2. Grundlagen
9
Teile dieser Objektdatei vom Program Loader in den Hauptspeicher geladen, der Speicher entsprechend konfiguriert und der Programmcode zur Ausführung 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 Einsprungpunkt (Entry Point) die Funktion main. Ein laufendes Programm wird als Prozess
bezeichnet [23].
In modernen Betriebssystemen wird jedem laufenden Prozess ein virtueller Adressraum
zur Verfügung gestellt, welcher von der Memory Management Unit (MMU) in physische
Speicheradressen umgesetzt wird. Einem Prozess stehen nun separat organisierte Speicherregionen bzw. Speichersegmente (Memory Segments) innerhalb seines Adressbereichs zur Verfügung, in denen sich sein Programmcode und statische Daten befinden
und auch temporäre Daten abgelegt werden können. 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) ausführt, abgelegt.
Dieses Segment ist als read-only markiert, das heißt, es kann nur lesend darauf zugegriffen werden. Damit kann der ausführbare Code des Prozesses weder versehentlich
noch mutwillig modifiziert werden. Jeder Versuch, den Speicher in diesem Segment zu
manipulieren, würde sofort zu einer entsprechenden Ausnahmebehandlung (Exception)
und zu einem Programmabbruch führen.
2.2.3
Data-Segment
Im Data-Segment werden nur bestimmte Daten des Prozesses, jedoch kein ausführbarer
Code abgelegt. Das Data-Segment hat eine feste, beim Programmstart zugewiesene
Größe und nimmt alle vor dem eigentlichen Programmstart initialisierten globalen Variablen (z.B. primitive Variablen, Arrays, Puffer, Strukturen, Zeiger, Objektdaten) auf.
2. Grundlagen
10
Das Data-Segment kann während der Programmausführung gelesen und beschrieben
werden, um den Inhalt der dort gespeicherten Variablen während der Laufzeit ändern
zu können.
0xC0000000
hohe
Adresswerte
Stack
vorhergehende
Stack Frames
dynamisches
Wachstum
dynamisches
Wachstum
Heap
BSS
Function Stack Frame
Verfügbarer Speicher
Funktionsparameter
Funktion Return Address.
gesicherter Frame Pointer
Lokal deklarierte
Variablen und Puffer
optionale Prozessdaten
Data
niedrige
Adresswerte
0x08000000
Text
Abbildung 2.1: Typischer Prozessspeicher- und Stackaufbau einer C/C++ Applikation
Der BSS-Bereich 4 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), wohingegen alle initialisierten globalen und statischen Variablen außerhalb 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 Betriebssystem mit Nullen gefüllt. 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 Länge
4
BSS steht als Abkürzung für Block Started by Symbol
Ein Großteil 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-Bits
enthält) abgeschlossen sein muss. A string is a contiguous sequence of characters terminated by and
”
including 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 value
of a string is the sequence of the values of the contained characters, in order.“ [11, S. 164] Explizite
Längenangaben werden also nicht gespeichert. Würde das 0-Byte als Ende-Zeichen fehlen, würden
String-verarbeitende Funktionen diese Zeichenkette als so lange interpretieren, bis zufällig ein 0-Byte
im Speicher vorkommt.
5
2. Grundlagen
11
0 [11]. So wird sichergestellt, dass sich keine unerwünschten Werte in den uninitialisierten 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 Möglichkeit, erst während der Programmausführung Speicher
vom Betriebssystem anzufordern. Dafür werden eigene Bibliotheksfunktionen (Memory Management Functions) zur Verfügung gestellt, die den verfügbaren und belegten
Speicher verwalten. Ein Prozess kann einen Speicher anfordern und erhält dabei einen
Zeiger auf diesen Speicher (z.B. über malloc(), siehe Listing 2.1, Zeile 19). Benötigt
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 Speicherallokation noch die Größe des Speichers vorgegeben ist, wird von einer dynamischen
Speicherverwaltung bzw. einer Dynamic Memory Allocation gesprochen.
Alle dynamisch angeforderten Speicherblöcke befinden sich innerhalb eines speziell
dafür vorgesehenen, dynamisch wachsenden Speicherbereichs, dem sogenannten Heap
des Programms. Die Größe des Heaps wird durch den verfügbaren Speicher abzüglich
der Größe des Stacks limitiert (siehe Abbildung 2.1). Ein Prozess kann maximal soviel
Speicher benutzen, wie diesem von der Speicherverwaltung auf Abruf zur Verfügung
gestellt wird. Die Lage der Speicherblöcke ist vom laufenden Prozess nicht beeinflussbar
und wird von der Speicherverwaltung des Betriebssystems bestimmt. Mehrmalige Speicheranforderungen und Freigaben führen aufgrund der internen Organisation der belegten und freien Speicherbereiche zu einer Fragmentierung (Memory Fragmentation)
des Speichers. Auf dem Heap allokierter Speicher ist solange gültig, bis er wieder freigegeben 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 Verfügung stellen zu können.
2. Grundlagen
2.2.5
12
Stack
Der Stack wächst dynamisch und teilt sich gemeinsam mit dem Heap den einer Applikation zur Verfügung stehenden freien Speicher. Der Stack wächst, 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 höchsten Adresse beginnend nach
und nach den Stack befüllt. Auf dem Stack eines Prozessors einer Intel x86 Architektur
können folgende Daten innerhalb eines einzigen Stackframes abgelegt werden [8]:
• Funktionsparameter - Alle beim Aufruf einer Funktion übergebenen Parameter liegen auf dem Stack. Innerhalb der Funktion entspricht ein übergebener
Parameter einer lokalen Variablen.
• Funktionsrücksprungadresse - Unmittelbar vor Beendigung einer Funktion
wird der Rückgabewert der Funktion in einem Register des Prozessors oder auf
dem Stack abgelegt und zur aufrufenden Funktion bzw. zur Funktionsrücksprungadresse (Function Return Address) zurückgekehrt. 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 Effizienzgründen auf dem Stack gesicherte Frame Pointer enthält 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 müssen, entweder im allgemeinen
Datensegment oder im BSS-Bereich abgelegt.
• Optionale Prozessdaten und Zeiger - Je nach Architektur und Compiler
können noch weitere Daten auf dem Stack abgelegt werden, z.B. eine Adresse zur Ausnahmebehandlung (Exception Handler Frame), zwischengespeicherte
Register des Prozessors (Callee Save Registers).
2. Grundlagen
13
Im Gegensatz zu Speicherblöcken auf dem Heap hat ein Stack Frame immer eine begrenzte Lebensdauer und limitierte Größe. Im Beispiel aus Listing 2.1 werden alle lokalen auto-Variablen (siehe Listing 2.1, Zeilen 16 und 7) und Verwaltungsdaten (Funktionsrücksprungadresse) auf dem Stack gespeichert. Wird die Funktion verlassen, werden
auch die aktuellen Stack Frames wieder vom Stack entfernt. Die Zugriffsorganisation
erfolgt ähnlich 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
Größe des Stacks. Eine Fragmentierung des Stacks aufgrund der LIFO-Organisation ist
nicht zu befürchten.
2.3
Register
Ein Prozessor besitzt nur einen sehr kleinen, jedoch sehr schnellen internen Speicher
zur Abarbeitung eines Programms. Ein Teil dieses Speichers wird für interne Zwecke
verwendet und ist von Außen bzw. für Programme nicht zugänglich. Ein anderer kleiner
Teil dieser Speicherplätze wird für Ein- und Ausgabeoperationen, das Verschieben und
Manipulieren von Speicher, zur Übergabe bestimmter Parameter, zur Adressierung und
Indizierung von Speicher, zur Rückgabe von Ergebnissen und für Zähler verwendet.
Dieser Teil der Speicherplätze wird im Allgemeinen als Register bezeichnet. Die für
eine Applikation verfügbaren Register werden als General Purpose Registers (GPR)
bezeichnet, welche in allen Architekturen in ähnlicher Form zur Verfügung 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 für 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 angeführten 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 Prozessoren haben in der Regel einen größeren und schnelleren internen Speicher und können
2. Grundlagen
14
oft mehrere Speicherplätze in kürzester Zeit oder parallel bearbeiten. Sie bieten eine
größere Registerbreite (Bits pro Register) und können dadurch mehr Speicher direkt
adressieren. Die Anzahl und Art der Register ist höchst unterschiedlich, alle modernen 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, für Angreifer vielleicht beabsichtigte, Auswirkung auf das Programm oder den Programmfluss haben
kann. Die für 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 für einen Zeiger auf eine Speicheradresse. Man unterscheidet dabei zwischen Zeigern auf Daten (Data Pointer ) und Zeigern auf Funktionen
(Function Pointer ). Ein Zeiger zeigt immer an den Beginn eines Speicherbereichs. Über
den Zeiger selbst, welcher ausschließlich nur die Adresse darstellt, kann keinerlei Aussage über die Größe des Speicherblocks, die an dieser Adresse abgelegten Daten und deren
Datentypen getroffen werden. Über die Lage des Speicherblocks kann maximal auf die
grundsätzliche Verwendung - Daten oder Programmcode - geschlossen werden.[47]
Datenzeiger (Data Pointer)
Funktionszeiger (Function Pointer)
int iValue;
Normale Variable
int (*pfFoo)();
Wert
Pointer Variable
Adresse
int* pValue;
Pointer Variable
Adresse
*pValue = iValue;
Wert
pfFoo();
int foo()
{
...
}
Abbildung 2.2: Daten- und Funktionszeiger
Innerhalb eines Programms wird häufig über Zeigervariablen auf Daten oder Funktionen zugegriffen (siehe Abbildung 2.2). Programmiersprachen wie C und C++ stellen
2. Grundlagen
15
dafür eigene Pointertypen zur Verfügung, die einen einfachen Zugriff auf Speicher und
eine einfache Zeigermanipulation (Zeigerarithmetik ) ermöglichen.
In den Programmiersprachen C und C++ repräsentiert schon ein Funktionsname den
Zeiger auf die Funktion, also jene Speicheradresse, an der der Funktionscode beginnt.
Erst die Klammerung nach dem Funktionsnamen lässt den Compiler erkennen, dass es
sich um einen Aufruf der Funktion an dieser Adresse mit den angegebenen Parametern
handelt. Zeigervariablen können sich prinzipiell in jedem Speicherbereich befinden, je
nachdem welcher Speicherklasse sie zugeordnet werden. Typische auf dem Stack abgelegte Pointer sind beispielsweise die als Argumente einer Funktion übergebenen Zeiger,
lokale Zeigervariablen der Speicherklasse auto innerhalb einer Funktion, Rücksprungadressen, Adressen auf Exception Handler und gesicherte Frame Pointer. In C++ geschriebene Programme speichern für deren Objektinstanzen während der Laufzeit auch
Tabellen mit Zeigern auf Funktionen (Vector Tables oder VTables), um virtuelle Methoden abzubilden.
Zeiger innerhalb des Prozesspeichers stellen bei allen Attacken das Hauptangriffsziel
dar. Können Zeiger oder ganze Zeigertabellen von Außen manipuliert werden, können
damit andere Daten, als ursprünglich vorgesehen, gelesen oder gespeichert werden. Bei
manipulierten Funktionszeigern werden nicht vorgesehene Funktionen aufgerufen und
damit der Programmfluss gezielt verändert. Ebenso ist es denkbar, Zeiger auf Dateien
zu manipulieren und damit externe Dateien in einen laufenden Prozess einzuschleusen.
Auch Handles 6 auf Dateien sind letztendlich nur Zeiger.
6
Als Handle wird in der Softwareentwicklung ein Identifikator, Nickname oder Alias auf digitale
Objekte wie z.B. Dateien, Prozesse, Ressourcen, angeschlossene Geräte, usw. bezeichnet. Das Betriebssystem vergibt ein systemweit eindeutiges Handle beim Erzeugen eines Objekts oder dem Aufbau einer
Verbindung mit einem Objekt. Im Programm werden diese Objekte dann nur noch über dieses Handle
angesprochen.
3
Potentielle
Sicherheitsschwachstellen
Unter einer potentiellen Sicherheitsschwachstelle (Security Vulnerability) versteht man
eine Systemschwäche, welche einen Einbruch in das System zumindest theoretisch
möglich macht. Eine Schwachstelle ist immer die Folge eines Fehlers im Code (Coding 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, welche für einen Großteil der Sicherheitsschwachstellen verantwortlich sind. Allen voran
Designfehler und die sogenannten Pufferüberläufe.
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 also im Quelltext einer Software zu finden sein. Oft aber sind designbasierte Sicherheitsschwachstellen die Folge von fehlenden Codeteilen oder einer unzureichenden Implementierung notwendiger Sicherungs- und Verteidigungsmechanismen. Die folgende
16
3. Potentielle Schwachstellen
17
Auflistung sicherheitsrelevanter Designfehler lässt schnell erkennen, welche fehlerhaften
oder fehlenden Funktionen die typischen Designfehler heutiger Software sein können:
• Eingabe- und Parameterprüfung: Buffer Overflows, Parametermanipulation,
SQL Injection und Cross-Site Scripting (XSS) basieren auf ungeprüften Benutzereingaben oder Funktionsparametern. Nur die strikte Überprüfung aller Eingangsdaten kann Manipulationen verhindern.
• Authentisierung und Authentifizierung: Das erste zweier Subjekte (z.B. Benutzer, Prozesse, Services, Clients) muss einen Nachweis seiner Identität erbringen, sich authentisieren. Das zweite Subjekt als sein Gegenüber muss die Identität
seines Gegenübers überprüfen, die Identität seines Partners authentifizieren.
• Verschlüsselung: Unverschlüsselte Daten sind für jedermann lesbar. Eine Verschlüsselung der Daten lässt nur jenen die Informationen zukommen, für die sie
auch gedacht sind.
• Sicherung: Ungesicherte Daten sind manipulierbar. Codierungsverfahren können
Daten vor Manipulationen sichern oder jede Manipulation aufdecken (z.B. Checksumme, Signatur).
• Zugriffs- und Ausführungsberechtigungen: 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 interne Systeminformationen. Logdateien, Speicherauszüge und Stacktraces sollten
nicht sichtbar sein oder mit einer entsprechenden Zugriffsberechtigung versehen
werden.
Designfehler lassen sich im Allgemeinen nicht durch die Prüfung einzelner Codezeilen erkennen. Was für eine einfache Applikation an Daten- und Codesicherung noch
ausreichend sein mag, ist für eine sicherheitskritische Anwendung bei weitem nicht
genug. Erst eine Klassifizierung der Sicherheitsanforderungen, das Erkennen der Bedrohungsszenarien, das Zusammenwirken einzelner Module und die Identifikation der
3. Potentielle Schwachstellen
18
Datenströme lässt mögliche Designfehler und darauf basierende Sicherheitsschwachstellen sichtbar werden.
3.2
Overflow Fehler
Die in den letzten Jahrzehnten weitaus am häufigsten zum Einbruch in ein Computersystem genutzten Programmfehler stellen so genannte Pufferüberläufe (Buffer Overflows oder Buffer Overruns) dar. Sie treten immer dann auf, wenn ein Programm
bzw. eine Funktion Daten in einem Speicher bestimmter Länge verarbeitet und dabei
die Grenzen eines Puffers (Memory Buffer ) schreibend über- oder unterschreitet. Dabei
werden angrenzende, sich ebenfalls im Speicher befindliche Daten oder Zeiger auf Daten
und Funktionen überschrieben, welche funktions- und ablaufrelevant sind. Besonders
häufig treten diese Fehler bei C- und C++-Programmen auf, da hier seitens der Sprachkonzepte und Compiler keine Überprüfungen der Speicher- und Arraygrenzen erfolgen.
Für die Vermeidung eines Buffer Overflows ist letztendlich immer der Programmierer zuständig. Moderne Programmiersprachen unterstützen die Entwickler, indem sie
Array- und Speichergrenzen verwalten und Zugriffe auf illegale Speicherbereiche unterbinden. Die Überprüfung aller Speicherzugriffe und Speichergrenzen hat natürlich
Performanceeinbußen zur Folge.
Overflows können durch einen Fehler scheinbar zufällig auftreten oder - bei Attacken absichtlich provoziert werden. In allen Fällen ist ein Programmfehler die Voraussetzung
für einen Überlauf. Das Problem kann über längere Zeit unentdeckt bleiben, wenn das
Programm keine sichtbaren Veränderungen im weiteren Ablauf zeigt. Ein Angreifer,
der derartige Sicherheitsschwachstellen (Security Vulnerabilities) ausnutzen möchte,
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 außerhalb der Spezifikationen liegen. Werden diese Eingangsdaten keiner expliziten
Prüfung unterzogen, kann es zu einem Pufferüberlauf kommen und im Speicher befinden 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 ausgewählte Überlauffehler 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 lokale Variablen, Sprungadressen und Funktionsparameter kurzfristig abgelegt werden.
In IA-32 Architekturen wächst, wie bei vielen anderen Prozessorarchitekturen auch,
der Stack von höheren zu niedrigeren Speicheradressen. Wird nun ein Puffer auf dem
Stack angelegt und kommt es bei einem Schreibvorgang zu einem Überschreiten der
Puffergrenzen, so werden an den Puffer angrenzende Speicherstellen auf dem Stack
überschrieben.
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 bezeichnet [21], weil dieser Overflow wohl zu den am längsten bekannten Schwachstellen gehört.
Bei einem klassischen Stack Overflow Exploit ist das Ziel meist die Manipulation der
Function Return Address, also der Rücksprungadresse zur aufrufenden Funktion. Kann
dieser Zeiger gezielt manipuliert werden, kann eine eigene eingeschleuste Funktion oder
eine Bibliotheksfunktion aufgerufen werden.
Ein kurzes Beispiel soll die Vorgänge auf dem Stack bei einem Funktionsaufruf und einem Stack Overflow verdeutlichen. In dem im Listing 3.1 gezeigten Beispielprogramm
wird aus der Funktion main() die Funktion foo() aufgerufen. Die Applikation ist syntaktisch korrekt und ausführbar, obwohl die Funktion foo einige Fehler bzw. Schwachstellen aufweist, welche für einen Angriff missbraucht werden könnten. Die unsichere
1
Halvar teilt in [21] die verschiedenen Exploit-Techniken erstmals in Generationen ein. Er klassifiziert damit die Arten der Buffer Overflow Schwachstellen anhand der zeitlichen Abfolge, in der
diese veröffentlicht wurden. Darauf basierend erweitert Klein in [31] diese Einteilung und weist die
Overflows jeweils einer bestimmten Generation zu.
3. Potentielle Schwachstellen
20
Funktion strcpy (siehe Anhang A.1) prüft nicht, ob die Länge des zu kopierenden
Strings die des Puffers auf dem Stack überschreitet.
1
2
#include <stdio.h>
#include <string.h>
3
4
5
6
7
8
9
void foo(const char* pStr)
{
char szBuffer[20];
strcpy(szBuffer, pStr);
printf(szBuffer);
}
// Stack Overflow Schwachstelle
// Format-String Schwachstelle
10
11
12
13
14
15
int main(int argc, char* argv[])
{
foo(argv[1]);
// Illegal Pointer Schwachstelle
return 0;
}
Listing 3.1: Beispielprogramm StackOverflow.c
Ebenso bleibt das Argument der Funktion printf ungeprüft, was eine im nächsten
Abschnitt besprochene Format-String Schwachstelle zur Folge hat (siehe Kapitel 3.3).
0x0012FF5C
0x0012FF60
0x0012FF64
0x0012FF68
0x0012FF6C
0x0012FF70
0x0012FF74
0x0012FF78
0x0012FF7C
0x0012FF80
0x0012FF84
0x0012FF88
4f
c0
3c
18
20
7c
1f
1f
c0
60
02
e8
1d
ff
10
30
30
ff
18
28
ff
11
00
27
13
12
40
40
40
12
40
35
12
40
00
35
78
00
00
00
00
00
00
00
00
00
00
00
O..x
Àÿ..
<.@.
.0@.
0@.
|ÿ..
..@.
.(5.
Àÿ..
‘.@.
....
è’5.
// Beginn des Puffers szBuffer
//
//
//
//
//
//
//
//
//
5*4 = 20 Bytes
Ende des Puffers szBuffer
Gesicherter Frame Pointer
Rücksprungadresse aus der Funktion foo
Funktionsparameter pStr (Adr. auf argv[1])
Gesicherter Frame Pointer
Rücksprungadresse aus der Funktion main()
Funktionsparameter argc
Funktionsparameter argv
Listing 3.2: Stackdump innerhalb des Programms StackOverflow.c
Beide Funktionen legen nacheinander einen Stackframe auf dem Stack an. Der im Listing 3.2 dargestellte Speicherauszug (Memory Dump), erstellt mit einem Debugger
unmittelbar vor dem Aufruf der Funktion strcpy, zeigt deutlich sowohl die auf dem
Stack abgelegten lokalen Variablen und jeweiligen Funktionsparameter als auch die auf
3. Potentielle Schwachstellen
21
dem Stack gesicherten Register EIP (die Rücksprungadresse) und EPB (den Frame
Pointer). Der lokale Puffer szBuffer der Funktion foo und auch die lokalen Variablen
von main() liegen ebenfalls auf dem Stack.
hohe
Adresswerte
Function Return Address
gesicherter Frame Pointer
Funktionsparameter
pStr
Function Return Address
gesicherter Frame Pointer
Lokal deklarierter
Puffer szBuffer[20]
(a) Stack vor dem Buffer Overflow
Overflow
Funktionsparameter
argc
argv[ ]
Stack Frame foo()
Stack Frame main()
vorhergehende
Stack Frames
Puffer
niedrige
Adresswerte
Stack Frame foo()
Stackwachstum
Stack Frame main()
vorhergehende
Stack Frames
Funktionsparameter
argc
argv[ ]
Function Return Address
gesicherter Frame Pointer
Funktionsparameter
pStr
Function Return Address
Lokal deklarierter
Puffer szBuffer[20] mit
eingeschleustem Code
(b) Stack nach dem Buffer Overflow
Abbildung 3.1: Einschleusen und Aufrufen von Code über einen Stack Overflow
Im folgenden Beispiel wird von der Annahme ausgegangen, dass der Angreifer das Programm 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 präparierten String2 in die Funktion foo ein, welcher länger ist als
der lokal angelegte Puffer szBuffer, kommt es wegen der fehlende Längenüberprüfung
in foo() und auch in der Funktion strcpy zu einem Overflow des Puffers szBuffer. Der
Angreifer überschreibt 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 Rücksprungadresse
(Function Return Address) und den gesicherten Frame Pointer, sondern schleust damit
mitunter gleichzeitig Code bzw. Adressen in den Puffer szBuffer bzw. auf den Stack
2
Um Code oder Adressen über einen String in ein Programm einzuschleusen, muss eine Zeichenkette
mit einer Reihe nicht-druckbarer Zeichen erzeugt werden. Perl erlaubt die Zusammenstellung von
Strings über Escape-Sequenzen (siehe dazu [56]), ähnlich der Programmiersprache C, und die Übergabe
der Parameter und den Aufruf des Programms über ein Script von der Console heraus [26]. Linux/Unix
bieten mit der Bash-Shell ähnliche Möglichkeiten [30].
3. Potentielle Schwachstellen
22
ein (siehe Abbildung 3.1.b). Die Rücksprungadresse lässt er auf den eingeschleusten
Code zeigen. Beim Beenden der Funktion wird der EIP vom Stack geholt und auf diese
Adresse gesprungen. Anstatt in main() fährt der Programmfluss (Program Flow ) im
eingeschleusten Code fort.
Stack Overflows gehören zu den einfachsten Overflows, weil sie am leichtesten auszunutzen sind. Ebenso gibt es mittlerweile mehrere Methoden, wie Stack Overflows verhindert oder zumindest erschwert werden können. Welche das sind und wie wirkungsvoll
derartige Methoden sind, wird in späteren Kapiteln noch geprüft 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 Rücksprungadresse manipuliert werden, kommt es bei einem Frame Pointer
Overwrite zu einem Überschreiben 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 geändert werden. Mit dieser Exploit-Technik kann ebenfalls
zuvor eingeschleuster Programmcode zur Ausführung gebracht werden. Der Artikel [32]
zeigt mit einem primitiven Beispiel in beeindruckender Einfachheit die Funktionsweise
eines Frame Pointer Overwrites und dessen Ausnutzung für einen Exploit.
3.2.2
Heap Overflow
Heap Overflows gehören zu den Overflows der 4. Generation [21]. Sie funktionieren nach
einem ähnlichen Prinzip wie Stack Overflows, sind deutlich aufwändiger auszunutzen
und in Quellen wie [15] detailliert beschrieben. Nach [15] und WSEC stellen sie aus
folgenden Gründen eine zusätzliche Gefahr dar,
• weil viele Compilererweiterungen und softwarebasierte Schutzmethoden (spezielle
Bibliotheken) nur den Stack nicht aber den Heap schützen können,
• weil viele hardwarebasierte Schutzmethoden moderner Prozessoren nur das Ausführen von Code auf dem Stack verhindern können
3. Potentielle Schwachstellen
23
• und vor allem, weil vielfach noch von der falschen Annahme ausgegangen wird,
dass Heap Overflows nicht für Angriffe nutzbar seien und damit eine Codeänderung von local buffer auf static buffer ausreichend für einen wirksamen Schutz
sei.
Auf dem Heap werden, wie im Kapitel 2.2.4 beschrieben, jene Puffer abgelegt, die erst
während der Laufzeit mit speziellen API-Funktionen alloziert (z.B. mit malloc()) und
später wieder freigegeben (z.B. mit free()) werden. Dieser dynamische Speicherbereich (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 Speicherblöcke in ihrer Reihenfolge
schwer vorhersehbar im Speicher abgelegt. Belegte und freie Speicherbereiche können
sich beliebig abwechseln. Bei einem Heap Overflow läuft keineswegs der Heap selbst
über, sondern, ähnlich wie beim Stack Overflow, ein auf dem Heap abgelegter Puffer.
Kommt es im Heap zu einem Pufferüberlauf, dann können davon freie Speicherbereiche, angrenzende belegte Speicherbereiche und auch interne zur Verwaltung des Heaps
notwendige Verwaltungsdaten betroffen sein, welche ebenfalls im dynamischen Bereich
des Heap Segments abgelegt werden. Über Heap Overflows können wie bei Stack Overflows auch Zeiger manipuliert werden, über die auf Daten zugegriffen wird oder über
die der Programmfluss kontrolliert wird.
1
2
#include <stdio.h>
#include <stdlib.h>
3
4
5
6
7
8
struct PufferPtr
{
char* pPufferA;
char* pPufferB;
};
9
10
11
12
13
14
int main(int argc, char* argv[])
{
PufferPtr* pPufferP = (PufferPtr*)malloc(sizeof(PufferPtr));
pPufferP->pPufferA = (char*)malloc(10);
pPufferP->pPufferB = (char*)malloc(10);
15
16
strcpy(pPufferP->pPufferA, argv[1]);
3. Potentielle Schwachstellen
24
strcpy(pPufferP->pPufferB, argv[2]);
17
18
free(pPufferP->pPufferB);
free(pPufferP->pPufferA);
free(pPufferP);
19
20
21
22
return 0;
23
24
}
Listing 3.3: Beispielprogramm HeapOverflow.c
Ein kurzes Beispiel zeigt einen möglichen Angriff über einen Heap Overflow. Dabei wird,
um die Komplexität derartiger Angriffe zu verdeutlichen, wiederum das Einschleusen
einer eigenen Funktion angenommen. Um den Code auszuführen, 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 Speicherblöcke ebenfalls auf
dem Heap (Puffer P) ablegt. Das Speicherabbild könnte 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 Datenblöcke und deren
Adressen auf dem Heap, so könnte er versuchen, mit der ersten Eingabe einen Überlauf
zu provozieren und gleichzeitig damit seinen Code einzuschleusen (siehe Abbildung
3.2.b.(1)). Durch den Überlauf des Puffers A kommt es gleichzeitig zum Überschreiben
des Zeigers pPufferB. Der Angreifer lässt diesen Zeiger nun durch seine Manipulation 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
gelöscht, springt das Programm automatisch auf die zuvor im Stack manipulierte Function Return Address (siehe Abbildung 3.2.b.(3)). Damit ist es dem Angreifer gelungen,
in zwei Schritten Code auf dem Heap einzuschleusen und später auch auszuführen. Die
gezeigte Methode übergeht auch etwaige Schutzmechanismen der Compiler, die später
noch diskutiert werden.
3. Potentielle Schwachstellen
hohe
Adresswerte
25
vorhergehende
Stack Frames
Funktionsparameter
Stack Frame foo()
Stack Frame foo()
vorhergehende
Stack Frames
Function Return Address
gesicherter Frame Pointer
Lokale Variablen
Funktionsparameter
Function Return Address
gesicherter Frame Pointer
Lokale Variablen
2
P
Puffer B
1
Eingeschleuster Pointer
Eingeschleuster
Code
3
char* pPufferA
char* pPufferB
A
A
Puffer A
HEAP
Puffer A
B
P
char* pPufferA
char* pPufferB
B
HEAP
Puffer B
niedrige
Adresswerte
(a) Heap und Stack vor dem Buffer Overflow
(b) Heap und Stack nach dem Buffer Overflow
Abbildung 3.2: Einschleusen und Aufrufen von Code über einen Heap Overflow
Dieses Szenario mag konstruiert erscheinen, unzählige Einträge in einschlägigen Listen
beweisen aber, dass Heap Overflows oft für Angriffe genutzt werden. Viele der Heap
Overflow basierten Attacken stützen sich auch auf die Manipulation der Verwaltungsdaten des Memory Managers im Heap. Diese Attacken bedürfen neben der Lage der
Speicherblöcke und der detaillierten Kenntnisse über das Programm noch weiterer detaillierter Kenntnisse über die interne Organisation zur Verwaltung des dynamischen
Speichers [61]. Interne Strukturen der Heap- bzw. Memory Manager sind aber gut dokumentiert (vgl. [21, 61]) oder der Sourcecode der Manager ist ohnehin frei zugänglich
(z.B. Linux-Quellcode und Open Source3 ).
3
Für die Bibliothek Glibc und somit viele Linux/Unix Systeme wird seit Jahren eine freie Implementierung von Wolfram Gloger mit dem Namen ptmalloc eingesetzt, deren Sourcecode frei zugänglich
ist. Nähere Informationen dazu sind unter http://www.malloc.de (Stand: 2007.05.02) verfügbar.
3. Potentielle Schwachstellen
3.2.3
26
Array Indexing Overflows
Array Indexing Overflows passieren, wenn beim Zugriff auf ein Array der Index falsch
ist und somit ein Zugriff außerhalb des Speicherbereichs eines Arrays erfolgt. Typischerweise 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.
hohe
Adresswerte
Off-By-One
Overrun
Lokal deklarierter
Puffer szBuffer[20]
(a) Stack vor einem Off-By-One Overrun
Stack Frame foo()
Funktionsparameter
pStr
Function Return Address
gesicherter Frame Pointer
vorhergehende
Stack Frames
Puffer
niedrige
Adresswerte
Stack Frame foo()
Stackwachstum
vorhergehende
Stack Frames
Funktionsparameter
pStr
Function Return Address
gesicherter Frame Pointer
Lokal deklarierter
Puffer szBuffer[20] mit
Off-By-One-Overrun
(b) Manipulierter Frame Pointer nach
einem Off-By-One Overrun
Abbildung 3.3: Manipulation des gesicherten Frame Pointers über einen Off-By-One
Overrun
Häufige Vertreter dieser Array Indexing Overflows sind die sogenannten Off-By-One
Overflows. Wie der Name andeutet, handelt es sich dabei um einen Überlauf um 1
Byte. Eine typische Ausprägung dieses Fehlers ist eine ein Array bearbeitende Schleife,
wie es das kurze Beispiel aus Listing 3.4 zeigt:
1
2
3
4
5
6
void foo(const char* str)
{
char szBuffer[20];
for( int i=0; i <= sizeof(szBuffer); i++ )
szBuffer[i] = str[i];
}
Listing 3.4: Beispiel eines Off-By-One Fehlers im Programm StackOverflow.c
3. Potentielle Schwachstellen
27
Durch einen Durchlauf mehr als erlaubt (die Laufbedingung verwendet fälschlicherweise
<= anstatt <) wird die Größe des Arrays um exakt ein Element überschritten und dadurch Speicher über die Grenzen des Arrays hinaus beschrieben. Liegt ein char -Array
zum Beispiel im Speicher direkt vor einem Zeiger (z.B. der gesicherten Rücksprungadresse auf dem Stack), so kann durch einen Überlauf um nur ein Zeichen bei einer
schreibenden Funktion ein Pointer schon um ein Byte manipuliert werden. (siehe Abbildung 3.3). Der aus dem Beispiel generierte Stackauszug zeigt das manipulierte Byte
(unterstrichener Wert) des gesicherten EBP.
0x0012FF5C
0x0012FF60
0x0012FF64
0x0012FF68
0x0012FF6C
0x0012FF70
41
45
49
4d
51
55
42
46
4a
4e
52
ff
43
47
4b
4f
53
12
44
48
4c
50
54
00
ABCD
EFGH
IJKL
MNOP
QRST
Uÿ..
// Beginn des Puffers szBuffer
//
5*4 = 20 Bytes
// Ende des Puffers szBuffer
// Manipulierter Frame Pointer
Listing 3.5: Frame Pointer Manipulation durch Off-By-One Fehler
Auf einer Little Endian First Architektur (Intel-Format einer Intel x86-Architektur )
führt dies zu einer Manipulation des Least Significant Byte 4 (siehe Listing 3.5), auf
einer Big Endian First Architektur (Motorola-Format einer 68000-Architektur ) zum
Überschreiben des Most Significant Bytes 5 [7]. Die Manipulation des niederwertigsten
Bytes eines Zeigers lässt das Verschieben eines Zeigers im Bereich von 256 Byte zu.
Die Änderung des höchstwertigsten Bytes macht hingegen wenig Sinn, weil der legale
Speicherbereich durch einen Verweis auf ein anderes Segment sofort verlassen würde.
Little Endian First Architekturen sind demnach anfälliger für Off-By-One-Angriffe.
3.2.4
BSS Overflow
BSS-Overflows gehören zu den Overflows der 3. Generation [21]. Im BSS-Bereich, einem Teilbereich des Datensegments (siehe Kapitel 2.2.3), werden uninitialisierte globale
4
Die Bezeichnung Little Endian First bezeichnet die Reihenfolge der Bytes im Speicher eines Computers, mit der Zahlenwerte dargestellt werden. Bei einer Little Endian First Architektur liegt das
niederwertigste Byte vor den höherwertigen Bytes im Speicher. Das erste Byte eines Zahlenwertes im
Speicher ist also das Least Significant Byte.
5
Die Bezeichnung Big Endian First bezeichnet die Reihenfolge der Bytes im Speicher eines Computers, mit der Zahlenwerte dargestellt werden. Bei einer Big Endian First Architektur liegt das
höchstwertigste Byte vor den niederwertigen Bytes im Speicher. Das erste Byte eines Zahlenwertes im
Speicher ist demnach das Most Significant Byte.
3. Potentielle Schwachstellen
28
und statische lokale Variablen abgelegt (siehe Listing 2.1, Zeile 1 und 15). BSS Overflows erlauben, ebenso wie auch bei Heap Overflows (Kapitel 3.2.2) gezeigt wurde, die
Manipulation von Zeigern. Attacken lassen sich besonders dann einfach durchführen,
wenn nicht-initialisierte Zeiger und Puffer in der Applikation verwendet wurden, welche
dann im BSS Segment liegen und deren Überlauf nicht verhindert wird. Ein wesentlicher Vorteil und eine deutliche Erleichterung für einen Angreifer ist die Tatsache, dass
Puffer im BSS-Segment eine feste Größe haben und in ihrer Lage unverändert bleiben.
Die Lage der Puffer wird schon vom Compiler und Linker während der Übersetzung des
Quelltextes und dem Erstellen einer ausführbaren Datei bestimmt. Kann der Angreifer
die Reihenfolge der Puffer und die Abstände 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
Sicherheitslücken und Exploits aufgrund von Format-String-Schwachstellen sind erst
in den späten 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 für
Funktionen mit einer beliebigen Anzahl von Parametern. Typische Vertreter sind die
ANSI-C-Funktionen der printf()-Familie zur formatierten Ausgabe und Stringformatierung. Funktionen mit einer variablen Anzahl von Parametern ist es nicht möglich,
den Datentyp der über den Stack übergebenen Variablen und Konstanten während der
Laufzeit festzustellen.
Bei printf()-Funktionen wird der Datentyp mit einem String, welcher der Formatierung der Ausgabe dient (Format-String), beschrieben. Im Prototyp der Funktion
int printf(const char* format,...) beschreibt der Stringparameter format jene
6
Oft reicht ein einfaches Probierverfahren aus, um die Lage der Puffer untereinander zu bestimmen.
Dabei werden durch manipulierte Eingaben Pufferüberläufe provoziert und durch die Beobachtung
der Ausgaben und anderer Puffer die Lage zueinander bestimmt. Man beobachtet dabei also, wo die
eingeschleusten Daten wieder sichtbar werden.
3. Potentielle Schwachstellen
29
Zeichenkette, welche den Ausgabetext und die durch das Zeichen % eingeleitete Formatanweisungen enthält. Die Formatanweisung beschreibt dabei nicht nur den Datentyp, 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 abgelegt. Innerhalb der Funktion printf werden nun der format-String geparst, die dem
%-Zeichen entsprechenden Daten dem Stack entnommen und ein Ausgabestring generiert. 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 lesen If there are insufficient arguments for the for”
mat, the behavior is undefined.“[11, S. 273]. Sowohl die Anzahl der Parameter als auch
der tatsächlich an die Funktion übergebene Datentyp muss mit den Platzhaltern des
Format-Strings in jedem Fall übereinstimmen. Das heißt, die Formatbeschreibung muss
den auf dem Stack abgelegten Daten entsprechen. [47].
Das folgende kurze Beispiel aus dem Listing 3.6 zeigt eine typische Format-StringSicherheitslücke, bei der ein String zur Ausgabe mit printf von einem Benutzer
übergeben werden kann.
1
2
3
4
5
int main(int argc, char* argv[])
{
printf( argv[1] );
return 0;
}
Listing 3.6: Beispielprogramm PrintDemo.c
Die Eingabe bleibt in diesem Beispiel ungeprüft und der Benutzer könnte das Programm mit PrintDemo "%p %p %p %p" aufrufen. Der String mit den Formatanweisungen 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 unabhängig davon, wie viele Parameter zuvor auf dem Stack abgelegt wurden. Die Funktion printf,
wie im Listing 3.6 ersichtlich, legt keine zusätzlichen vier Zeiger bzw. 16 Byte auf dem
3. Potentielle Schwachstellen
30
Stack ab, sondern ausschließlich nur den Zeiger auf das Arrayelement argv[1] bzw. 4
Byte. Dieser Missstand wird auch nicht erkannt, daher gibt die Funktion die nächsten
zufällig 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 Funktion main() erstellt wurde, dann ist leicht zu erkennen, dass printf Teile des Stacks
ausgegeben hat7 .
0x0012FF7C
0x0012FF80
0x0012FF84
0x0012FF88
c0
5a
02
b0
ff
11
00
4f
12
40
00
dd
00
00
00
02
Àÿ..
Z.@.
....
◦ O Ý.
//
//
//
//
Gesicherter Frame Pointer
Rücksprungadresse aus der Funktion main()
Funktionsparameter argc
Funktionsparameter argv
Listing 3.7: Stackdump innerhalb des Programms PrintDemo.c
Damit sind für den Benutzer oder, wenn Absicht dahinter steht, für den Angreifer die
Daten des Stacks sichtbar. Die wichtigsten für einen Angriff verwendeten Format-String
Platzhalter sind in der folgenden Tabelle 3.1 aufgeführt.
Platzhalter
%d
%u
%x
%c
%s
%p
%n
Datentyp, Formatierung
int, Dezimaldarstellung mit Vorzeichen
unsigned int, Dezimaldarstellung
unsigned int, Hexadezimaldarstellung
char, einzelnes Zeichen (Character)
char*, Zeichenkette (String)
void-Zeiger (Pointer in Hexadezimaldarstellung)
int*, schreibt Anzahl der ausgegebenen Zeichen an Adresse
Tabelle 3.1: Ausgewählte Format-String Platzhalter der printf-Familie
Eine vollständige Beschreibung aller Formatanweisungen und Steuerzeichen ist der
IEEE Spezifikation [27]8 zu entnehmen.
7
Die umgekehrte Reihenfolge der Bytes entsteht aufgrund der Little Endian Architektur der Intel
Prozessoren.
8
http://www.opengroup.org/onlinepubs/009695399/functions/printf.html
(Stand:2007.04.29)
3. Potentielle Schwachstellen
31
Die Möglichkeiten 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 gehören, ist dem Anhang zu entnehmen (siehe Tabelle A.1).
4
Lokalisierung potentieller
Schwachstellen
Bevor Sicherheitsschwachstellen entfernt oder umgangen werden können, müssen diese als solche identifiziert werden. Einfache Sicherheitsschwachstellen weisen oft klare Merkmale auf und können systematisch lokalisiert und vermieden werden. Meist
handelt es sich um lokale Fehler in der Implementierung, also einfache Codierungsfehler. Komplexe Schwachstellen sind oft schwer zu lokalisieren, so dass sie bei einer
isolierten Betrachtung im Code keine auffälligen Merkmale aufweisen. Oft entsteht erst
durch das Zusammenwirken verschiedenster Funktionen, Module oder Applikationen
eine Schwachstelle, die für 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 gängigsten Methoden zur Lokalisierung von Sicherheitsschwachstellen. Dabei kommen Werkzeuge zur Untersuchung des Quellcodes und
zur Untersuchung des kompilierten Programms zur Anwendung. In Programme integrierte spezielle Zusatzfunktionen dienen einerseits der Überwachung des Programms,
andererseits melden diese Programme dadurch Auffälligkeiten, die ebenfalls einer Sicherheitsanalyse dienlich sind.
32
4. Lokalisierung potentieller Schwachstellen
4.1
33
Allgemeines
Die Lokalisierung potentieller Sicherheitsschwachstellen erfolgt über sogenannte Sicherheitsanalysen. Dies sind spezielle Verfahren zur gezielten Prüfung von Software
auf mögliche Sicherheitslöcher im Gesamtsystem. Ein Sicherheitsloch ist jeder Fehler
”
in Hardware, Software oder Richtlinien, der es einem Angreifer ermöglicht, unautorisierten Zugang zu Ihrem System zu bekommen.“ [2, S. 334] Eine Sicherheitsanalyse
ist demnach immer die Suche möglicher Hardware-, Software- und Designfehler oder
-schwächen. Ebenso entspricht eine Analyse immer einem systematischen Sammeln von
Daten zur Informationsgewinnung.
4.1.1
Informationsgewinnung
Für die Analyse eines Systems benötigen Entwickler, Sicherheitsbeauftragte, Tester
und auch potentielle Angreifer technische Details, wie z.B. Informationen über die
laufenden Programme und die Umgebung (z.B. Betriebssystem, Bibliotheken), unter
denen eine Software läuft. Für eine umfangreiche Analyse einer Software stehen mehrere
Informationsquellen zur Verfügung (basierend auf [33]):
• Quellcode (Source Code) der Applikation und/oder der Bibliotheken
• Dokumentation (z.B. Handbücher, technische Dokumentation, Hilfesysteme)
• Datendateien und Speicherabbilder (Core Dumps)
• Log- und Trace-Dateien (z.B. Status- und Fehlerinformationen)
• Informationen aus dem laufenden Prozess durch Debugging
Grundsätzlich muss davon ausgegangen werden, dass sowohl dem Entwickler als auch
dem potentiellen Angreifer die gleichen Daten zur Verfügung stehen, auch wenn sich die
Datengewinnung für den Angreifer als aufwändiger erweisen kann. Dementsprechend
sind einerseits die benötigten Kenntnise und andererseits die Mittel und Methoden zur
Datengewinnung während eines Sicherheitstests denen eines Angreifers nicht unähnlich.
Treten während der Analyse ungewollt sensible Daten zutage, so stehen diese auch
jedem Angreifer zur Verfügung.
4. Lokalisierung potentieller Schwachstellen
4.1.2
34
Vollständige Sicherheitsanalyse
Während einer vollständigen Sicherheitsanalyse werden alle aus den verschiedensten
Quellen gewonnenen Daten zusammengefasst und mit Fokus auf Sicherheit und Fehlerfreiheit ausgewertet. Die Analyse einer Software auf Sicherheit ist auch immer die
Analyse einer Software auf Fehlerfreiheit, weil Sicherheitsschwachstellen praktisch immer als Folge von Design- oder Implementierungsfehlern auftreten. Die vollständige Sicherheitsanalyse einer Software besteht aus folgenden Teilen (basierend auf [22, 26]):
• Lokalisierung sicherheitsrelevanter Designfehler: Die Lokalisierung der Design Flaws umfasst die Identifikation der Datenflüsse, der Programm- und Dateneintrittspunkte (Entry Points) und der potentiellen Gefahren, welche von Außen
auf ein Programm einwirken.
• Lokalisierung sicherheitsrelevanter Implementierungsfehler: Die Lokalisierung der Coding Bugs umfasst die Analyse der Quelltexte und der binären und
ausführbaren Dateien.
• Stabilitätstests: Diese Tests umfassen Methoden zum absichtlichen Herbeiführen
von Ausnahme- und Grenzsituationen. Die Software wird unter Stress getestet
und ihr Verhalten in Extremsituationen beobachtet (Stress Test).
• Sicherheitstests: Diese Tests umfassen eine Prüfung der Daten und Prozesssicherheit auf Basis der Designvorgaben, wie z.B. die Prüfung der Zugriffs- und
Rechteverwaltung, die Verschlüsselung, uvm. (vlg. [26]).
Das naheliegendste, wenn auch nicht immer einfachste Verfahren, um Fehler und Sicherheitsschwächen einer Software zu finden, ist das manuelle Audit. Liegt der Quelltext vor, können durch eine umfassende Quelltextinspektion (Source Code Audit) die
Quelldateien zeilenweise überprüft und gleichzeitig die gefundenen Schwachstellen behoben werden. Fehler in einfachen Programmen lassen sich manuell durchaus noch
lokalisieren. Bei größeren Projekten ist dies aufgrund der hohen Komplexität und des
Umfangs des Quelltextes äußerst zeit-, ressourcen- und kostenintensiv [31]. Ein weiteres
und nicht unerhebliches Problem bei der Durchführung manueller Source Code- bzw.
Sicherheits-Audits ist die dafür notwendige Fachkenntnis im Bereich der defensiven
4. Lokalisierung potentieller Schwachstellen
35
Programmierung. Die Zuhilfenahme spezieller Werkzeuge für automatisierte SoftwareAudits 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 statische Analyse verzichtet auf das Ausführen der Software. Die Software wird auf Basis
des Quelltextes und zusätzlicher Dateien (wie z.B. Konfigurationsdateien) analysiert.
Dabei werden Funktionen und Konstrukte im Quelltext gesucht, welche eine Sicherheitslücke darstellen könnten. Die erweiterte statische Analyse ist eine auf dem Quelltext basierende Vorwegnahme der Aktionen und Reaktionen der Software. Aus der
Sicht der Werkzeuge ist sie aufwändig, weil auf Basis des Quelltextes die Abläufe, die
möglichen Aktionen und Programmfäden (Threads) simuliert werden müssen. Bei der
dynamischen Analyse wird das laufende Programm überwacht bzw. die Datenströme
eines Programms analysiert. Die Tests können in der realen oder einer simulierten
und kontrollierten Umgebung stattfinden. Während der Beobachtung der Abläufe und
der Re-/Aktionen der Software können 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 möglich. [31, 60]
Am Beginn jeder Softwareanalyse steht die Untersuchung des Quelltextes. Diese kann
und sollte auch schon begleitend während der Entwicklung der Software stattfinden.
Erst wenn der Code vermeintlich korrekt ausgeführt ist, werden komplexere Analysemethoden das System während der Laufzeit analysieren. Der nun folgende Abschnitt
beschreibt einige Methoden und Werkzeuge der quelltextbasierten Sicherheitsanalyse.
4.2
Quelltextbasierte Analyse
Die quelltextbasierte Softwareanalyse gehört zu den statischen Analysemethoden, welche auf die Ausführung der zu prüfenden Software verzichten. Der Quellcode wird einer
4. Lokalisierung potentieller Schwachstellen
36
Reihe von formalen Prüfungen, den sogenannten White Box Tests, unterzogen, wobei
damit hauptsächlich mögliche Schwachstellen in der Implementierung entdeckt werden.
Mit der Quelltextanalyse können folgende Mängel entweder mit (semi-)automatischen
Werkzeugen oder durch ein manuelles Code Audit lokalisiert werden:
• fehlerhaften Typenkonvertierungen (Illegal Casts)
• mögliche Überläufe (Overflows)
• illegale Zeiger und Nullzeiger (Null Pointer )
• Über- und Unterschreitung von Speicherbereichsgrenzen (Bounds Checks)
• Speichermanagementfehler (Memory Leaks)
• Compiler- und Betriebssystemabhängigkeiten
• uninitialisierte Variablen und Speicherbereiche
• unsichere Funktionen
• das Nichteinhalten von Codierungsvorschriften (Coding Standards)
• das Nichteinhalten von Formvorschriften (Design Guides)
Designfehler sind mit Code Audits nur sehr eingeschränkt erkennbar, da sie meist
nicht Teil der Implementierung, sondern der Planung und der Architektur sind. Fehler
im Design sind oft die Folge von fehlendem Code, nicht von fehlerhaftem Code, und
somit schwer zu entdecken. Das Nichtauffinden bestimmter Fehler ist auch eine der
Schwierigkeiten bei der Bewertung von Analysewerkzeugen.
Bewertung der Quelltextsanalyse
Diese Analyseform des Quellcodes zählt zu den falsifizierenden Analyseverfahren, da
nach dem Vorhandensein von Fehlern gesucht wird. Eine Analyse sollte im Idealfall alle
kritischen und fehlerhaften Module, Funktionsaufrufe, Konfigurationen und Optionen
liefern. Bei der Beurteilung der Analysemethode unterscheidet man, entsprechend einer
Wahrheitsmatrix (Confusion Matrix ), zwischen den vier Ergebnistypen (basierend auf
[31, 54]):
4. Lokalisierung potentieller Schwachstellen
37
• True Positives (TP) sind jene Ergebnisse eines Tests, die richtig ausgegeben
wurden, bei denen also ein Fehler angezeigt wird, der tatsächlich sicherheitskritisch ist.
• False Positives (FP) sind sämtliche Hinweise, die fälschlicherweise auf Fehler
und kritische Stellen hinweisen, obwohl keinerlei Fehler vorliegen (Falschmeldungen). Besser eine Warnung zu viel als zu wenig mag im Allgemeinen als sicherer
gelten. Zu viele False Positives führen jedoch dazu, dass der Entwickler nicht
mehr in der Lage (oder willig) ist, alle kritischen Stellen einzeln zu prüfen, um
später vielleicht festzustellen, dass es sich um eine Falschmeldung handelt. Jede
zusätzliche, jedoch unnötige Prüfung erzeugt zusätzlichen Aufwand und dadurch
zusätzliche Kosten. Ebenso mindern zu viele False Positives das Vertrauen in die
eingesetzte Prüfmethode.
• True Negatives (TN) sind jene nicht angezeigten Fälle, welche auch keinen
Fehler darstellen (kann nur in Testszenarien bewertet werden).
• False Negatives (FN) sind sämtliche in einer Analyse nicht gemeldeten Funktionen und Problembereiche, welche eine Sicherheitsschwachstelle darstellen. Dieses
Nichterkennen kritischer Programmteile wiegt umso schwerer, da diese Fehler im
Allgemeinen weiter unentdeckt bleiben und damit ein falsches Sicherheitsgefühl
suggeriert wird. Die Methode wird so selbst zum Sicherheitsproblem.[31]
Die ideale Analysemethode sollte weder zu False Positives noch zu False Negatives
führen. In den folgenden Abschnitten werden einige quellcodebasierte Werkzeuge zur
lexikalischen und semantischen Analyse geprüft und deren Tauglichkeit festgestellt.
4.2.1
Lexikalische Analyse
Bei der einfachen lexikalischen Analyse des Quellcodes wird nur in Bezug auf das
Vorkommen einzelner Wörter oder Wortgruppen innerhalb eines Programms geprüft,
ohne den Kontext bzw. semantische Zusammenhänge zu berücksichtigen. Die Methoden
und Tools verwenden keinerlei Wissen“ über die Programmierung im Allgemeinen,
”
funktionale Zusammenhänge oder sicherheitsrelevante Fragen.
4. Lokalisierung potentieller Schwachstellen
4.2.1.1
38
Grep
Die einfachste Prüfung zum Auffinden von Sicherheitsschwachstellen ist das Durchsuchen des Quellcodes auf das Vorkommen bestimmter Zeichenketten oder -kombinationen
(Codemuster ). Wie in den Kapiteln Speicherorganisation (2.2) und Format-String Fehler (3.3) schon diskutiert wurde, gelten bestimmte Funktionen als potentiell unsicher
oder sind aufgrund von schweren Sicherheitsmängeln zu vermeiden. Ein einfaches Tool
zur lexikalischen Überprüfung sämtlicher Quelldateien ist das Hilfsprogramm Grep
(Global Regular Expression Print), welches für sämtliche Plattformen oder als Quellcode zur Verfügung steht. Mit Grep und seinen leistungsfähigen regulären Ausdrücken
(Regular Expressions) zum Parsen von Dateien können schnell und einfach bestimmte
Zeichenfolgen gefunden werden. Grep zeigt den Dateinamen, die Zeilennummer und
die Quelltextzeile an, in der eine der gesuchten Zeichenketten gefunden wurde (ein
Protokollauszug der Grep-Suche befindet sich im Anhang B.1.1).
Grep-basierte Suchwerkzeuge sind einfach in der Anwendung. Die reine Textsuche weist
jedoch gravierende Nachteile auf (auf Basis von [54] und [10]):
• Sie erfordert zuviel Wissen seitens des Anwenders.
• Sie ist sowohl bei der Suche als auch bei der Aufbereitung der Ergebnisse zu
inflexibel.
• Bestimmte Schwachstellen sind nicht detektierbar, da für deren Analyse eine einfache Textsuche nicht ausreicht (z.B. Race Conditions 1 , bei Precompiler Makros).
• Sie liefert tendenziell zu viele False Positives, da jedes Vorkommen einer Zeichenkette gefunden wird, egal in welchem Zusammenhang sie im Quelltext vorkommt
(z.B. auch in Kommentaren, Wortteilen)
Viele integrierte Entwicklungsumgebungen (IDE) und Editoren unterstützen ebenfalls
eine grep-ähnliche Funktion zum Suchen von Text mittels regulärer Ausdrücke, mit
denen ganze Verzeichnisse schnell und einfach durchsucht werden können. Über Scriptdateien lassen sich derartige Überprüfungen gut automatisieren und zum Beispiel auf
1
Als Race Conditions werden in diesem Zusammenhang jene Angriffsszenarien bezeichnet, welche
eine zeitliche Abfolge eines Angriffs voraussetzen.
4. Lokalisierung potentieller Schwachstellen
39
die Suche bestimmter Funktionen (z.B. unsichere POSIX-Funktionen, siehe Anhang
A.1.1) anpassen.
4.2.1.2
RATS
RATS 2 (Rough Auditing Tool for Security), ein unter der GNU Public License 3 (GPL)
frei verfügbarer Parser der Firma Fortify Software Inc.4 , scannt wie Grep den Quellcode auf der Suche nach sicherheitsrelevanten Fehlern. Dabei legt RATS das Hauptaugenmerk auf Schwachstellen auf Basis von Buffer Overflows und TOCTOU (Time
Of Check - Time Of Use) Race Conditions. Häufigstes Muster dieser Angriffe ist die
Überprüfung oder Erlangung von Rechten zu einem bestimmten Zeitpunkt (Time of
Check ), um zu einem späteren Zeitpunkt die Rechte für sicherheitskritische Aktionen
zu nutzen (Time of Use). Typische Vertreter dieser Fehler durch Abhängigkeiten sind
Dateioperationen mit den Funktionen zum Öffnen der Datei und zeitlich verzögert zur
Verarbeitung der Datei [5, 6]. RATS scannt nicht nur Programme der Programmiersprachen C und C++, sondern auch Perl, PHP und Phyton.
RATS weist explizit gefundene TOCTOU Vulnerabilities mit den beteiligten Funktionen in seinen Reports wie folgt aus:
badsource.c:669: Medium: stat
A potential TOCTOU (Time Of Check, Time Of Use) vulnerability exists. This is
the first line where a check has occured.
The following line(s) contain uses that may match up with this check:
147 (open)
Wie der ausführlichere Auszug des Protokolls im Anhang B.1.3 zeigt, findet RATS nicht
nur kritische Quelltextzeilen, sondern gibt auch Auskunft über mögliche Methoden zur
Behebung der Schwachstelle.
2
http://www.fortifysoftware.com/security-resources/rats.jsp
Englisches Original unter http://www.gnu.org/licenses/gpl.html;
die deutsche Übersetzung unter http://www.gnu.de/documents/gpl.de.html verfügbar
4
http://www.fortifysoftware.com
3
4. Lokalisierung potentieller Schwachstellen
4.2.1.3
40
Flawfinder
Flawfinder 5 ist ein in Python entwickelter quelloffener Source Code Analyzer, der Sicherheitsschwachstellen innerhalb eines C++ Programms lokalisiert. Flawfinder unterliegt ebenfalls der GPL2 und steht als Open Source zur Verfügung. Dieses Programm
arbeitet mit einer Datenbank, in der Funktionen mit bekannten Sicherheitslücken beschrieben sind, die im Quelltext lokalisiert werden. Das Analysewerkzeug erzeugt ein
umfangreiches Protokoll mit näheren Informationen zu den gefundenen Sicherheitsschwachstellen, kategorisiert die Fehler (z.B. Misc, Buffer, Access) und bewertet diese zusätzlich mit einem numerischen Risikofaktor (Risk Level ) zwischen 1 (minimales) und 5 (maximales Risiko). Dieser Level wird in erster Linie aus der Datenbank
entnommen, jedoch hat die Anwendung der Funktion und auch die Parametrisierung
ebenso Einfluss auf diesen Faktor. Die Einflussnahme der Parameter auf den Risikofaktor erscheint in der Praxis ausgesprochen sinnvoll, weil mit den Parametern einer
Funktion viele Sicherheitslücken erst entstehen (z.B. bei Format-String Fehlern). Flawfinder unterstützt eine Reihe von Optionen, welche die Auswertung und Darstellung
der gefundenen Schwachstellen betreffen oder eine komfortable Weiterverarbeitung in
Linux-Editoren (vi, emacs) unterstützen. [57]
Am Ende eines Flawfinder -Protokolls findet sich, wie auch im Protokollauszug im Anhang B.1.4 ersichtlich ist, eine kurze Statistik zu den gefundenen kritischen Codestellen,
was sich bei automatischen und scriptgestützten Auswertungen als sinnvoll erweist:
[dau]\$ flawfinder *.c
[...]
Hits = 84
Lines analyzed = 1260 in 0.64 seconds (9082 lines/second)
Physical Source Lines of Code (SLOC) = 955
Hits@level = [0]
0 [1] 48 [2] 28 [3]
0 [4]
8 [5]
0
Hits@level+ = [0+] 84 [1+] 84 [2+] 36 [3+]
8 [4+]
8 [5+]
Hits/KSLOC@level+ = [0+] 87.9581 [1+] 87.9581 [2+] 37.6963
[3+] 8.37696 [4+] 8.37696 [5+]
0
Minimum risk level = 1
Not every hit is necessarily a security vulnerability.
There may be other security vulnerabilities; review your code!
5
http://www.dwheeler.com/flawfinder/
0
4. Lokalisierung potentieller Schwachstellen
41
Der Programmierer von Flawfinder David. A. Wheeler weist ausdrücklich darauf hin,
dass sein Programm zwar eine Erweiterung und deutliche Verbesserung zu Grep ist,
jedoch auch Flawfinder nicht alle Sicherheitslücken finden und auch Falschmeldungen
generieren kann. In fact note that flawfinder doesn’t really understand the semantics
”
of the code at all - it primarily does simple text pattern matching.“[57, S. 4]. Trotzdem
bestätigten Tests, dass Flawfinder ohne Vorkenntnisse deutlich bessere Ergebnisse als
Grep liefert.
4.2.1.4
ITS4
ITS4 6 ist ein für Unix und Windows Plattformen erhältlicher statischer Source Code
Scanner für C und C++ Programme. Dabei scannt es wie die zuvor eingeführten Tools
ebenfalls den Quelltext speziell nach potentiell gefährlichen Funktionen und versucht
wie Flawfinder eine Bewertung des Risikos. Die Daten entstammen dabei einer Datenbank mit einer Beschreibung potentiell gefährlicher Funktionen, deren Risikobewertung
(von No, Low und Moderate Risk zu Normal, Very und Most Risky) und Hinweisen
zur Behebung der jeweiligen Schwachstelle. [54]
Auch ITS4 unterstützt die Detektion von Race Conditions und meldet diese mit speziellen Hinweisen.
badsource.c:160:(Risky) fdopen
Can be involved in a race condition if you open
For example, don’t check to see if something is
opening it. Open it, then check bt querying the
run tests on symbolic file names...
Perform all checks AFTER the open, and based on
symbolic name.
things after a poor check.
not a symbolic link before
resulting object. Don’t
the returned object, not a
Aber ebenso wie RATS erkennt ITS4 die sogenannten Race Conditions nur unter bestimmten Voraussetzungen und Codekonstellationen. Nach der Analyse erzeugt ITS4
einen ausführlichen Report der analysierten C-Dateien. Ein Beispiel hierfür findet sich
im Anhang B.1.2.
6
http://www.cigital.com/its4/
4. Lokalisierung potentieller Schwachstellen
4.2.2
42
Semantische Analyse
Eine Erweiterung zur reinen lexikalischen Analyse stellt die semantische Quelltextanalyse dar. Sie bezieht den jeweiligen Zusammenhang der lexikalischen Bedeutungseinheiten (z.B. Funktionen, Daten, Funktionsgruppen) in die Analyse mit ein. Dafür ist
eine Programmfluss- und eine Datenflussanalyse notwendig, um Zusammenhänge und
Rückschlüsse der im Programm ablaufenden Vorgänge zu erkennen. Der Vorteil der
semantischen Analyse ist, dass das Programm dabei ebenso statisch auf Quelltextbasis
analysiert werden kann. Die während der Analyse gewonnenen Erkenntnisse jedoch sind
wesentlich detaillierter und sicherer. Der Nachteil ist der deutlich höhere Aufwand einer derartigen Analyse, da die semantische Interpretation einzelner Funktionen, Daten
und deren Zusammenhänge technisch wesentlich schwieriger ist. Werkzeuge, welche eine
derartige Funktionalität anbieten, bedienen sich compilerähnlicher Analysemethoden.
Entsprechend hoch ist der technische Aufwand der Implementierungen. [10]
4.2.2.1
C++ Compiler
Nachdem jeder Compiler auch eine semantische Analyse durchführen muss, um einen
Quelltext in einen Maschinencode zu übersetzen, liegt es nahe, dass Compiler die bei der
Programmanalyse gewonnenen Erkenntnisse auch für Überprüfungen nach bestimmten
Gesichtspunkten nutzen können. Mittlerweile verfügen fast alle Compiler der großen
Hersteller und auch der freie GNU C/C++ Compiler über die Möglichkeit, den Quelltext auf mögliche Fehler und sicherheitsrelevante Schwachstellen zu prüfen. Vorrangiges
Ziel ist dabei die Erhöhung der Stabilität. Daraus ergibt sich zusätzlich eine Verbesserung im Bereich der Softwaresicherheit (z.B. durch die Verhinderung von Overflows).
Mit dem folgenden kurzen Beispielprogramm wurde die Fehlererkennungsrate und die
Sinnhaftigkeit und Richtigkeit der Warnungen der Compiler überprüft:
1
2
3
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
4
5
6
int main(int argc, char* argv[])
{
4. Lokalisierung potentieller Schwachstellen
43
int i;
long j = 10;
char* pBuffer;
char szBuffer[10];
7
8
9
10
11
pBuffer = (char*)malloc(10);
strcpy( pBuffer, argv[1]);
free(pBuffer);
gets_s(pBuffer, 10);
gets(szBuffer);
printf("i=%d j=%d", i);
printf(argv[1]);
return 0;
12
13
14
15
16
17
18
19
20
}
Listing 4.1: Testprogramm zur Codeanalyse mit einem C++ Compiler
Der Microsoft C++ Compiler7 aus dem Visual Studio 2005 erzeugt mit dem höchsten
Warning Level (Level 4) und allen Optionen zur Codeprüfung nach der Übersetzung
des Programms einen umfangreichen Report, wie er im Anhang B.2.1 dargestellt wird.
Der Compiler bemängelt im Quellcode des Listings 4.1 vierzehn Schwachstellen oder
mögliche Fehler in Form von Warnungen. Die angezeigten Mängel decken dabei nicht
unerhebliche Sicherheitsrisiken auf. Einige Mängel sind schlichtweg Implementierungsfehler. Nicht erkannt hat der Compiler die Race Condition aus der Zeile 15, bei der
zwar die sichere Funktion gets_s verwendet wurde, stattdessen aber ein ungültiger
Zeiger auf einen zuvor freigegebenen Speicher auf dem Heap zur Anwendung kam.
Vergleichsweise schlecht schnitt bei diesen Tests der zweite getestete Compiler, der
GNU C++ Compiler
8
ab. Er lieferte trotz aller eingeschalteten Warning Options
(Compiler Option -Wall) nur einige Warnungen (siehe Report im Anhang B.2.2). Eine
einzige Warnung verweist auf die Schwachstelle in Zeile 17 des Listings 4.1. Alle anderen
Schwachstellen blieben vom GCC unerkannt und unkommentiert.
7
8
Microsoft C++ Compiler, Version 8.00.50727.762 aus dem Visual Studio 2005 SP1
GCC Compiler, Version 4.1.2
4. Lokalisierung potentieller Schwachstellen
4.2.2.2
44
Splint
Splint 9 (die Abkürzung für Secure Programming Lint) wird vom Entwickler David
Evans als Annotation-Assisted Lightweight Static Checking Tool zur statischen Quelltextanalyse von in ANSI C geschriebenen Programmen beschrieben. Das Programm
unterliegt der GPL und ist frei verfügbar. Es ist eine Weiterentwicklung einer ganzen
Reihe von Werkzeugen zur statischen Sourcecodeanalyse aus den Siebzigerjahren, die
unter dem Namen Lint 10 und später unter LCLint veröffentlicht wurden [20]. Lint
unterstützte in ersten Versionen die Entwicklung von stabilem Code mit einer einheitlichen Formatierung und systemunabhängiger Syntax. Der Schwerpunkt des nun
zu einem Programm zusammengefassten Werkzeugs Splint ist die Untersuchung des
Quelltextes auf mögliche Sicherheitsschwachstellen. Splint liefert bei der Untersuchung
des Programms aus dem Listing 4.1 zehn Warnungen, welche dem kompletten Protokoll
im Anhang B.2.3 zu entnehmen sind.
Splint verfügt über spezielle Erweiterungen, welche das Auffinden von Buffer Overflows
vereinfachen sollen. Es unterstützt sogenannte Kennzeichner (Annotations), welche die
Zustände der Parameter einer Funktion jeweils vor und nach ihrem Aufruf beschreiben. In [19] zeigt Evans ein einfaches Beispiel, wie der Prototyp der Funktion strcpy
mit zusätzlichen Informationen für Splint in Form von Annotations erweitert werden
kann. Die sogenannten Kennzeichner sind dabei als Kommentare zwischen /*@...@*/
eingebettet:
void /*@alt char * @*/ strcpy
(/*@unique@*/ /*@out@*/ /*@returned@*/ char *s1, char *s2)
/*@modifies *s1@*/
/*@requires maxSet(s1) >= maxRead(s2) @*/
/*@ensures maxRead(s1) == maxRead(s2) @*/;
The requires clause indicates that the buffer passed as s1 must be large enough to
”
hold the string passed as s2. The ensures clause specifies that maxRead of s1 after the
call is equal to maxRead of s2.“[19, S. 49] Mit den Bezeichnern requires und ensures
können Vorbedingungen und Beschränkungen festgelegt werden. Für eine Überprüfung
9
http://www.splint.org/
lint (engl.) = Fussel, Nähstaub,. . . ; auch unerwünschte Anteile von Fasern und Flaum in der
Schafwolle
10
4. Lokalisierung potentieller Schwachstellen
45
von Puffergrenzen müssen Bedingungen für deren Index beschrieben werden. Um in
einen Puffer schreiben (minSet und maxSet) und aus einem Puffer lesen zu dürfen
(minRead und maxRead) müssen diese Bedingungen stets erfüllt sein.
Die Kennzeichnung /*@notnull@*/ zum Beispiel kann wie ein Type Qualifier verwendet werden. In einer Parameterdeklaration gibt diese Kennzeichnung an, dass der Wert
dieses Parameters beim Aufruf der Funktion nicht NULL sein darf. [20]
Mit den Kennzeichnern lassen sich ohne Änderungen im eigentlichen Programmcode
eine Reihe von Überprüfungen einführen, welche auf Basis des Quelltextes durchgeführt
werden können. Dafür müssen aber die include-Dateien mit den Prototypen der eigenen Funktionen und jene der Bibliotheksfunktionen mit Annotations erweitert werden.
Dieser Aufwand ist bei großen Projekten und für große Bibliotheken beträchtlich. Dynamische, während der Laufzeit allokierte Puffer und deren Größen können damit nicht
überprüft werden. Splint bleibt nach wie vor eine statische Analyse vorbehalten. Die
Möglichkeiten von Splint sind beträchtlich. Eine Beschreibung aller Features würde den
Umfang dieser Arbeit bei weitem sprengen und ist der offiziellen Dokumentation [19]
zu entnehmen.
4.2.2.3
CQUAL
CQUAL11 ist ein weiteres Werkzeug zur statischen Analyse, welches sich besonders zum
Auffinden von Format-String Schwachstellen gut eignet. Es erweitert die vorhandenen
ANSI C Type-Qualifier (daraus wurde auch der Name des Programms abgeleitet),
mittlerweile steht auch eine Weiterentwicklung für C++ (Cqual++) zur Verfügung.
Ein oft verwendeter C/C++ Type Qualifier ist const, der dem C/C++ Compiler mitteilt, dass das in einer Variable gespeicherte Objekt nach der Zuweisung nicht mehr
geändert werden kann. Die für CQUAL neu eingeführten Type Qualifier $tainted und
$untainted unterscheiden zwischen nicht vertrauenswürdigen und vertrauenswürdigen
Daten. Alle jene Typen, die von außen manipuliert werden können, werden mit $tainted
markiert, alle anderen mit $untainted. Das Array argv[] der Funktion main ist diesbezüglich ein typischer Kandidat. Dieses Konzept der Taint-Checks ist der Programmiersprache Perl entliehen, welche ebenfalls tainted Variablen kennt (vgl. [49, 51]).
11
http://www.cs.umd.edu/~jfoster/cqual/
4. Lokalisierung potentieller Schwachstellen
46
Ist eine Variable tainted, kann sie einer untainted Variablen nicht zugewiesen werden.
Weiters kann diese Variable nicht als Parameter beim Aufruf einer Funktion verwendet
werden, welche einen untainted Parameter erwartet. Diese Methode eignet sich besonders für das Auffinden von Format-String Fehlern, weil die Parameter der gefährdeten
Funktionen über die entsprechende Typenerweiterung explizit vertrauenswürdige Daten erwarten können und keine $untainted Daten entgegennehmen. [49]
Eine detaillierte Auseinandersetzung mit diesem Werkzeug und der tainting-Methode
ist bei Shankar et al. in [49] zu finden. Wie Klein in [31] auf den Seiten 553ff zeigt, kann
QQUAL mitunter gute Ergebnisse beim Auffinden von Format-String Fehlern vorweisen. Er lässt dabei aber auch mögliche Probleme und einige Nachteile von CQUAL
nicht unerwähnt, die bei Quelltextanalysen vor allem zu vielen False Positives führen
können.
4.2.2.4
PREfast und PREfix
PREfix ist ein Tool, welches Microsoft intern einsetzt, um Applikationen auf Quelltextebene auf Fehler zu analysieren. PREfix überprüft alle Applikationstypen mit
aufwändigen Analyse- und Simulationsverfahren und ist deshalb sehr langsam. PREfix
wurde von Microsoft nie freigegeben und der Öffentlichkeit zur Verfügung gestellt. Ein
Teil der Technologie ist in das wesentlich performantere und frei verfügbare Analysewerkzeug PREfast 12 eingeflossen, welches Teil der Windows Device Driver Kits (WDK
oder DDK ) ist. PREfast for Drivers (Prefast.exe) is a static source code analysis tool
”
that detects certain classes of errors not easily found by the typical compiler.“[42] Es
wurde speziell zur Entwicklung stabiler Windows Kernel-Treiber adaptiert.
Speziell auf Treiberebene haben Programmfehler und Buffer Overflows eine fatale Wirkung auf das Gesamtsystem. In [41] schreibt Microsoft: “Code annotations significantly
enhance the ability of PREfast to detect potential bugs while lowering the rate of false
positives.“[41] Nachdem PREfast im Windows Driver Kit (WDK) for Windows Vista
als Standalone Applikation verfügbar ist, können auch herkömmliche Applikationen
einer eingehenden Prüfung mit diesem Werkzeug unterzogen werden.
12
http://www.microsoft.com/whdc/devtools/tools/PREfast.mspx
4. Lokalisierung potentieller Schwachstellen
47
PREfast arbeitet ähnlich wie Splint und CQUAL mit zusätzlichen Erweiterungen der
Type Qualifier. Diese beschreiben die Richtung des Datenflusses bei Funktionsaufrufen
mit __in und __out und können ebenso die statische Größe von Array z.B. mit einem __out_bcount beschreiben. Ein Beispiel zeigt die Beschreibung der Funktion für
gets_s, wie sie in der stdio.h des Visual Studio 2005 zu finden ist:
_CRTIMP __checkReturn_opt char * __cdecl fgets(
__out_ecount_z(_MaxCount) char * _Buf,
__in int _MaxCount, __inout FILE * _File );
Eine Warnung von PREfast aufgrund des Prototyps könnte wie folgt aussehen:
pftest.cpp (45): warning 6029: Possible buffer overrun in call to ’fgets’:
use of unchecked value ’line_length’
FUNCTION: testread (40)
Der große Vorteil von PREfast auf Windows Plattformen gegenüber CQUAL ist, dass
Microsoft mittlerweile einen Großteil der include-Dateien um entsprechende PREfastkompatible Qualifier erweitert hat, so dass hier die Beschreibung der Standardfunktionen schon vollständig ist.
Viele Analysemöglichkeiten von PREfast sind mittlerweile schon in den C++ Compiler
integriert worden. Auch dieser kann aufgrund zusätzlicher Kennzeichner bzw. Qualifier
eine genaue Analyse durchführen, wie die Tests aus Abschnitt 4.2.2.1 zeigten. Es ist
anzunehmen, dass Microsoft die Entwicklung in Richtung Integration in den Compiler
weiter vorantreibt und PREfast dadurch über kurz oder lang obsolet werden wird.
Eine Zusammenstellung dieser und weiterer Werkzeuge zur Quelltextanalyse findet sich
im Anhang D.1 und unter [44].
4.2.3
Bewertung der Methoden und Werkzeuge
Bei den Tests der quelltextbasierten Analysewerkzeuge hat sich gezeigt, dass der Umfang der verwendeten Testroutinen zu gering ist, um klare Erkenntnisse über die Qualität der Analysemethoden und der getesteten Werkzeuge gewinnen zu können. Die
4. Lokalisierung potentieller Schwachstellen
48
semantische und syntaktische Komplexität einer Applikation machen eine Quelltextanalyse schwierig und aufwändig. Eine Quelltextanalyse kann keinem strikten Regelwerk folgen, weil sich gerade durch das Zusammenwirken einzelner Funktionen und
Module untereinander Seiteneffekte und Abhängigkeiten ergeben. Diese können sicherheitsrelevante Auswirkungen haben.
Für eine objektive Bewertung der Werkzeuge fehlt ein vorab geprüfter und standardisierter Pool von Quelltextdateien, welche Analysen erst vergleichbar machen würden.
Von diesen Funktionen müssten die Analyseergebnisse schon vorweg bekannt sein, um
die Werkzeuge prüfen zu können. Führt die Analyse einer Software zur Aufdeckung
von Sicherheitsschwachstellen, so sagt dies nichts über die Qualität der verwendeten
Werkzeuge und Methoden aus. Ungeklärt bleibt dabei nämlich, wieviele Schwachstellen
übersehen und somit nicht aufgedeckt werden konnten (False Negatives).
Viele Werkzeuge sind auf die Detektion bestimmter Sicherheitslücken spezialisiert.
Kaum ein Programm kann mit mehreren Problemfeldern gleich gut umgehen. Klein
versucht in [31] einen Vergleich einiger Werkzeuge. Am Umfang der beschriebenen
Tests und der Zusammenfassung der Testergebnisse wird aber schnell ersichtlich, dass
dies nur ein Anhaltspunkt sein kann. Wielander et al. weisen bei ihren Vergleichen mit
We do not claim that this test suite is perfectly fair, nor complete.“ in [58, S. 11] aus”
drücklich darauf hin, dass diese Vergleiche nur einen groben Überblick und einen Trend
wiedergeben können13 . Younan et al. vergleichen in [60] Features, eingeteilt in Kategorien der wichtigsten statischen Analysewerkzeuge. Auch sie geben keine Bewertungen
ab.
4.3
Binärcodebasierte Analyse
Im Unterschied zu quelltextbasierten Analysen greifen binärcodebasierte Softwareanalysemethoden bzw. die sogenannten Black Box Tests auf die kompilierten und
ausführbaren Binärdateien (Executable Binaries) und deren verwendete Bibliotheken
13
Wielander arbeitet nach eigenen Angaben zum Zeitpunkt des Schreibens dieser Arbeit an einer
umfangreichen Quelltextsammlung. Diese sollte nach Abschluss seiner Arbeiten als Open Source zur
Verfügung stehen und somit standardisierte und vergleichbare Tests zulassen.
4. Lokalisierung potentieller Schwachstellen
49
zurück. Steht kein Sourcecode zur Verfügung, wie zum Beispiel bei proprietärer Software, so ist die Prüfung und Beobachtung der kompilierten Applikation oft die einzige
Möglichkeit, um Imformationen über deren Verhalten zu gewinnen. Die Rückgewinnung
der Informationen aus den Binaries wird als Reverse Engineering bezeichnet.
Bei einer binärcodebasierten Softwareanalyse wird zwischen einer statischen und einer dynamischen Analyse unterschieden. Die statische Softwareanalyse verwertet
nur die Programm- und Datendateien. Die Software an sich wird dabei nicht zur
Ausführung gebracht. Die statische Analyse ist eine Analyse des Programmcodes, also weitgehend eine Implementierungsanalyse. Öfter zur Anwendung kommt die dynamische Softwareanalyse, bei der das Softwaresystem in einer kontrollierten und
möglichst realen Umgebung auf sein Laufverhalten und seine Sicherheit überprüft wird.
Die dynamische Analyse ist eine Designanalyse, da Designschwächen besonders bei der
Beobachtung einer Applikation zur Laufzeit sichtbar werden (z.B. fehlende Authentisierung, unverschlüsselte Daten, mangelnde Rechteverwaltung).
Klein unterscheidet in [31] nur zwei grundsätzliche Ansätze zur Überprüfung eines Binary, die Analyse der Programmdateien und den Test des Programms mit fehlerhaften
Daten. Vervollständigt man Kleins Ansätze noch um weitere, bei denen die konzeptionellen Abhängigkeiten der Programmdateien, die Datenflüsse einer Software und die
grundsätzliche Architektur einer Software berücksichtigt werden, dann ergeben sich
daraus folgende Analysemethoden:
• Überprüfung der Programm-Disassemblies: Bei diesem statischen Analyseverfahren wird der aus den Binaries generierte Maschinencode auf etwaige
Schwachstellen geprüft. Nachdem ein lesbarer Maschinencode (Assembler Code)
generiert wurde, entspricht diese Methode weitgehend der quelltextbasierten Analyse.
• Prozess- und Datenflussanalyse: Dabei handelt es sich fast immer um ein
dynamisches Analyseverfahren, bei dem die Programmabläufe, die dynamischen
Abhängigkeiten der einzelnen Module, die aufgerufenen Funktionen und alle Datenflüsse einer Applikation analysiert werden.
4. Lokalisierung potentieller Schwachstellen
50
• Fault Injection: Bei diesem dynamischen Verfahren wird die Umgebung einer
Applikation absichtlich und gezielt manipuliert und die Reaktion des Softwaresystems beobachtet. Einige Manipulationen gehen gezielt an oder über die erlaubten Spezifikationen des Systems hinaus, um das Verhalten der Software unter
Stress zu beobachten. Diese Verfahren werden oft auch Stress Tests genannt.
Im folgenden Abschnitt werden verschiedene Werkzeuge und Methoden des Reverse
Engineerings zur binärbasierten Sicherheitsprüfung einer Applikation beschrieben. Die
angewandten Methoden und Werkzeuge sind im Prinzip auch jene, deren sich potentielle Angreifer bedienen, um mehr über ein Programm und dessen Schwächen zu erfahren.
4.3.1
Disassembling
Mit sogenannten Disassemblern oder Decompilern können die in einer Maschinensprache binär codierten Programme und Bibliotheken in eine von Menschen leichter lesbare Form rück-übersetzt werden. Die generierte Beschreibung der Dateien erfolgt in
Assembler, der Sprache der jeweiligen Prozessorsarchitektur. Diese Sprache erlaubt,
wie mit dem Quellcode der ursprünglich verwendeten Hochsprache, eine Analyse des
Programms.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// int main(int argc,
// {
004017E0 push
004017E1 mov
//
printf( argv[1]
004017E3 mov
004017E6 mov
004017E9 push
004017EA call
004017F0 add
//
return 0;
004017F3 xor
// }
004017F5 pop
004017F6 ret
char* argv[])
ebp
ebp,esp
);
eax,dword ptr [argv]
ecx,dword ptr [eax+4]
ecx
dword ptr [__imp__printf (4020A0h)]
esp,4
eax,eax
ebp
Listing 4.2: Disassembliertes Programm PrintfDemo.c
4. Lokalisierung potentieller Schwachstellen
51
Das Beispiel aus dem Listing 4.2 zeigt die disassemblierte Funktion main(...) des
Programms PrintfDemo.c aus dem Kapitel 3.3. Zur Veranschaulichung wurde der ursprüngliche C-Code in Form von Kommentaren in den Assemblercode integriert. Von
besonderem Interesse sind die (Rück-)gewinnung einer Beschreibung des Programmflusses, der Verhaltensweisen des Programms, der verwendeten Datenstrukturen und
Dateien und die Prüfung der Entry Points. Wie aus dem einfachen Assemblercode
des Listings 4.2 bei einer Sicherheitsprüfung schnell ersichtlich würde, wird der importierten Funktion printf der Parameter argv[1] über den Stack übergeben, ohne die
Zeiger argv und Daten in argv[1] der Funktion main(...) überhaupt auf Gültigkeit
zu prüfen. Ein typischer Fehler, durch den eine Sicherheitsschwachstelle entsteht.
Bestimmte Hochsprachen und deren binäre Formate erlauben sogar eine Rückübersetzung in eine gut lesbare Hochsprache (z.B. das Werkzeug .NET Reflector 14 übersetzt
das im Binärformat vorliegende .NET-Programm wahlweise in C# oder VB.NET). Bei
mit hochoptimierenden C und C++ Compilern erstellten Programmen ist dies kaum
möglich. Liegt jedoch ein Quellcode vor, kann mit quellcodebasierten Werkzeugen wie
unter Kapitel 4.2 beschrieben auf etwaige Sicherheitslücken geprüft werden.
4.3.2
Debugging
Debugging bedeutet soviel wie Fehler (Bugs) aus einem Programm entfernen. Zur
Aufdeckung von Fehlern werden die zu prüfenden Programme unter der Kontrolle
eines sog. Debuggers gestartet. Ein Debugger ist ein Hilfsprogramm zum Diagnostizieren eines Bugs in einer Software oder einer Hardware. Er ermöglicht die Verfolgung
des Programmablaufs, das Setzen von Haltepunkten (Break Points), eine schrittweise
Ausführung des Programms und die Manipulation der Programmdaten. Der Entwickler
hat jederzeit die Möglichkeit, Variablen, Speicherbereiche und Register des Prozessors
einzusehen und zu ändern. Die in der Abbildung 4.1 gezeigte Debugging Session deckt
auf, dass die Applikation mit illegalen Parametern "%p-%p-%p-%p-%p-%p-%p..." aufgerufen wird, welche in Kombination mit der Format-String Schwachstelle zu einem
massiven Sicherheitsproblem führen.
14
http://www.aisto.com/roeder/dotnet/
4. Lokalisierung potentieller Schwachstellen
52
Abbildung 4.1: Debugging Session innerhalb einer Applikation
Moderne Debugger erlauben eine Änderung im Programmcode während einer Debugging Session, eine sofortige Übersetzung und die Fortführung des Programms. Diese
Methode wird als Just In Time Debugging bezeichnet. Steht der Quelltext der Applikation nicht zur Verfügung, wird der Programmcode in der Maschinensprache (sog.
Assemblersprache) der jeweiligen Prozessorarchitektur angezeigt.
First-chance exception at 0x252d7025 in PrintfDemo.exe:
0xC0000005: Access violation reading location 0x252d7025.
First-chance exception at 0x7c9378ae (ntdll.dll) in PrintfDemo.exe:
0xC0000005: Access violation writing location 0x00030fd4.
Eine durch einen Fehler ausgelöste Ausnahmebehandlung (Exception) stoppt das Programm an der Fehlerstelle und der Debugger wird automatisch aktiviert. Dabei gibt er
den Grund der Ausnahmebehandlung und dessen Codeadresse wie im oben gezeigten
Beispiel aus.
4. Lokalisierung potentieller Schwachstellen
4.3.3
53
Tracing und Monitoring
Die dynamische Analyse einer Software ist eine Analyse während der Laufzeit. Dabei
arbeitet das Programm in einer möglichst realen Umgebung. Während der Sicherheitsanalyse werden die Daten (z.B. Dateien, Netzwerkverkehr) der Software ebenso verfolgt
wie das Verhalten des Programms (z.B. Logfiles, Trace Files, Dumps). Das Ziel einer
dynamischen Analyse ist, mit speziellen Werkzeugen Bedingungen und Fehler ausfindig zu machen, welche zu Sicherheitsschwachstellen führen könnten. Die verwendeten
Werkzeuge der Prüfer sind dabei oft die gleichen wie jene der späteren Angreifer.
Um während der Laufzeit Daten über das System oder eine zu überwachende Applikation zu erhalten, werden sog. Tracer verwendet. Ein Tracer ist ein Überwachungswerkzeug,
welches in der Lage ist den Ablauf eines Programms und seine Daten zu überwachen,
den Ablauf in Dateien zu protokollieren und diese Protokolldateien in Echtzeit oder
zu einem späteren Zeitpunkt zu analysieren. Der Tracer im klassischen Sinne ist ein
Debugger (vgl. [31]). Ein Softwareentwickler verwendet diese Werkzeuge für Tests und
zur Fehlersuche in Programmen. Angreifern liefern diese Werkzeuge Daten über das
Programmverhalten, ohne einen Zugang zu den Quelldateien zu benötigen. Die folgenden Beispiele geben eine kurze Zusammenfassung der Möglichkeiten unter Einsatz
verschiedenster Werkzeuge und einiger Tracer.
4.3.3.1
API-Schnittstellenanalyse
Das Betriebssystem und Bibliotheken stellen Programmierschnittstellen (Application
Programming Interface - API) zur Anbindung der Programme an das System zur
Verfügung. Bei der Prüfung einer im Binärformat vorliegenden Applikation ist von
Interesse, wann, wo und wie bestimmte System- oder Bibliotheksfunktionen (APIFunktionen) aufgerufen werden. Nun kann mit einem Debugger der Programmablauf
Schritt für Schritt verfolgt werden. Das schrittweise Ausführen komplexer Programme
ist jedoch aufgrund des Aufwands nicht zielführend. Beim API-Monitoring oder aus
der Sicht des Angreifers API-Spying überwacht eine Applikation eine andere gerade
laufende Applikation und listet dabei die aufgerufenen Funktionen. Alle aufgerufenen
Funktionen gelistet ergeben den kompletten Ablaufplan der überwachten Applikation.
4. Lokalisierung potentieller Schwachstellen
54
Bei einer Sicherheitsprüfung sind nur die sicherheitsrelevanten Funktionen von Interesse. Konfigurierbare Filter gewährleisten nun, dass nur diese Funktionsaufrufe protokolliert und überwacht werden. [24, 22]
Abbildung 4.2: APISPY beim Aufspüren sicherheitskritischer Funktionen
Um API-Aufrufe auf binärer Basis zu überwachen, werden sogenannte API Hooking
Techniken angewandt, um die Funktionsaufrufe gezielt abzufangen (API Interception). Ein unter Windows verfügbares Monitoring Tool, welches IAT Patching zur
Überwachung und Protokollierung der API-Funktionsaufrufe nutzt, ist APISPY3215 .
Beim IAT Patching werden einzelne Sprungadressen der sogenannten Import Address
Table (IAT), eine in den Binaries gehaltene Tabelle mit den Adressen der dynamisch
nachgeladenen Funktionen, manipuliert. APISPY32 ändert die Zeiger in den IATs der
laufenden Prozesse automatisch und ermöglicht so ohne jede Programmierung und
Änderung des Binaries eine Überwachung aller Systemaufrufe.
Protokolliert wird dabei der Processname, die ProcessID im System, die Namen der
aufgerufenen API-Funktionen samt Parameter und die Speicheradressen der aufgerufenen Funktionen. Mit der Filterung der Protokolldaten kann schnell festgestellt werden,
ob und wie eine Applikation oder deren Bibliotheken sicherheitskritische Funktionen
aufrufen. Die Abbildung 4.2 zeigt die Protokollierung mehrerer Aufrufe der unsicheren
Funktion lstrcpy samt ihren Parametern, nachdem in der APISPY32-Konfigurationsdatei die Filter, wie im folgenden Beispiel gezeigt wird, konfiguriert wurden:
15
http://www.internals.com/
4. Lokalisierung potentieller Schwachstellen
55
KERNEL32.DLL:lstrcpy(PSTR,PSTR)
KERNEL32.DLL:lstrcpyA(PSTR,PSTR)
KERNEL32.DLL:lstrcat(PSTR,PSTR)
KERNEL32.DLL:lstrcatA(PSTR,PSTR)
Weitere Methoden zur Überwachung von API-Schnittstellen werden in [29] ausführlich
behandelt. Unter Linux stehen ebenfalls Tools wie das gezeigte zur Überwachung der
Systemaufrufe zur Verfügung. Eines davon ist z.B. STrace16 .
4.3.3.2
Datenflussanalyse
Neben der eigentlichen Applikation sind die Datenströme der Applikation bei Sicherheitsprüfungen von Bedeutung. Die offensichtlichsten Daten sind die Eingabedaten des
Benutzers und die Datendateien mit den Dokumentdaten. Nur diese Daten zu prüfen,
ist aber bei weitem nicht ausreichend. Neben den eigentlichen Programmdaten gibt es
eine Reihe anderer Daten und Datenströme, welche als sicherheitsrelevant eingestuft
werden müssen und schon in der Planung einer Applikation Beachtung finden sollten.
Die wichtigsten Daten eines Softwaresystems lassen sich wie folgt zusammenfassen (vgl.
[26, 53]):
• Dokumentdaten: im Speicher oder in Datendateien
• Temporäre Daten der Applikation, der Bibliotheken und des Betriebssystems:
im Speicher oder in ausgelagerten, temporären Dateien
• Infrastrukturdaten: z.B. in Konfigurationsdateien (unter Windows werden diese oft in einer zentralen Konfigurationsdatenbank, der Registry abgelegt)
• Systemdaten: z.B. interne Daten zur Aufrechterhaltung des laufenden Systems
(Messages, Pipes, Shared Memory)
• Ausgelagerte Daten: z.B. Speicherbereiche in Auslagerungsdateien (Swap Files),
Spooldateien
• Externe Daten: Daten anderer Applikationen und Systeme, z.B. am seriellen
und parallelen Port, USB, Netzwerk, CD-ROM.
16
http://sourceforge.net/projects/strace/
4. Lokalisierung potentieller Schwachstellen
56
Diese Aufzählung, die je nach Systemumgebung nicht vollständig sein muss, soll klarmachen, dass unentwegt Datenströme auf das Softwaresystem einwirken. Jeder fehlerhafte Programmcode in den datenverarbeitenden Funktionen führt unweigerlich zu
einem erheblichen Sicherheitsrisiko.
Bei Softwaresicherheitstests muss klargestellt werden, welche dieser Datenströme Einfluss auf die zu prüfende Applikation haben. Angreifer versuchen dies ebenfalls herauszufinden, weil jeder Dateneingang auch ein Entry Point für Schadcode sein könnte.
Für viele dieser Datenströme gibt es einfache Monitoringmöglichkeiten, ohne die Binaries ändern oder anpassen zu müssen. Wie beim API-Monitoring (Kapitel 4.3.3.1)
werden dabei bestimmte Systemaufrufe überwacht bzw. die Aufrufe und die über diese
Funktionen transferierten Daten protokolliert. Verschiedene Anbieter stellen eine Reihe
von Monitoring Tools für die Datenflüsse in das System, aus dem System und innerhalb des Systems zur Verfügung. Einige während dieser Arbeit verwendete Monitoring
Werkzeuge zur Überwachung der Entry und Exit Points sind:
• File Monitor: protokolliert alle Dateioperationen in Echtzeit; dazu zählen auch
Named Pipes und Mail Slots; z.B. FileMon 17
• Registry Monitor: protokolliert alle Zugriffe auf die Windows Registry; z.B.
RegMon 18
• Disk Monitor: protokolliert alle Festplattenaktivitäten; z.B. DiskMon 19
• Port Monitor: protokolliert alle Zugriffe auf seriellen und parallelen Ports; z.B.
PortMon 20
• Message Monitor: protokolliert alle Process-, Thread- und Windows Messages;
z.B. Spy++ 21
• Network Monitor: protokolliert alle Aktivitäten an den Netzwerkschnittstellen;
z.B. Wireshark 22
17
http://www.microsoft.com/technet/sysinternals/FileAndDisk/Filemon.mspx
http://www.microsoft.com/technet/sysinternals/utilities/regmon.mspx
19
http://www.microsoft.com/technet/sysinternals/FileAndDisk/Diskmon.mspx
20
http://www.microsoft.com/technet/sysinternals/utilities/portmon.mspx
21
ist Teil von Visual Studio aller 32/64-Bit Editionen
22
http://www.wireshark.org/
18
4. Lokalisierung potentieller Schwachstellen
57
Es gibt noch eine Reihe weiterer Monitoring Tools, die an jeweilige spezielle Bedürfnisse
angepasst sind, wie etwa zur Überwachung von COM, COM++, DCOM, OLE, .NET,
ODBC, spezielle Netzwerkprotokolle. Letztendlich handelt es sich dabei nur um eine
andere Darstellung und Filterung der Daten. Die Datenflüsse könnten auch mit den
oben genannten Basiswerkzeugen protokolliert und sichtbar gemacht werden.
Abbildung 4.3: RegMon beim Protokollieren von Zugriffen auf die Windows Registry
Die Registry bietet unter Windows ebenso wie das Dateisystem eine gute Möglichkeit,
Daten in eine Applikation einzuschleusen [24]. Ein typischer Designfehler dabei ist,
dass viele Applikationen zwar mittlerweile den Zugriff auf die Dateien kontrollieren,
die Registry unter Windows bei Sicherheitsprüfungen jedoch oft vernachlässigt wird.
Eine der getesteten Applikationen zur Überwachung und Protokollierung der Zugriffe
auf die Windows Registry ist RegMon 23 . Wie die Abbildung 4.3 zeigt, können mit Hilfe
von RegMon eine oder mehrere Applikationen beim Zugriff auf die Windows Registry
überwacht werden. Im Protokoll werden der Zeitstempel, der Name des Prozesses, die
Zugriffsfunktion, der Registrypfad, der Status und die Daten vermerkt. Registry-Daten
können mit speziellen Editoren, wie dem bei jedem Windows mitgelieferten Editor
RegEdit, einfach manipuliert werden. Grundsätzlich ist also bei Registry Einträgen die
selbe Vorsicht gefragt wie bei der Verwaltung von Dateien [26].
4.3.3.3
Speichermanagementanalyse
Bei der Speichermanagementanalyse wird das Speichermanagement einer Applikation
oder des gesamten Systems beobachtet. Viele Sicherheitslücken entstehen erst als Folge
23
http://www.microsoft.com/technet/sysinternals/utilities/regmon.mspx
4. Lokalisierung potentieller Schwachstellen
58
eines Fehlers in der Speicherverwaltung. Pufferüberläufe und die Verwendung uninitialisierter Zeiger oder Zeiger auf zuvor freigegebene Speicherbereiche (wilde Zeiger) sind
typische Softwarebugs. Viele Angriffe auf Computersysteme haben nicht nur das Ziel,
eine einzelne Applikation zu kompromittieren, sondern sind oft nur Mittel zum Zweck,
um das Gesamtsystem zu überlasten und dadurch Schwachstellen zu generieren oder
zu finden. Denial of Service Attacken werden oft verwendet, um Computersysteme
durch Überlastung zum Absturz zu bringen oder um dadurch automatisch erstellte
Logdateien oder Memory Dumps mit sensiblen Daten zu erhalten [22]. Fehler in der
Speicherverwaltung sind deshalb besonders kritisch, weil sie das Gesamtsystem destabilisieren können (z.B. durch Ressourcenknappheit).
Während Pufferüberläufe über eine zusätzliche Überprüfung der Puffergrenzen noch
einfach gefunden werden (einige Bibliotheken, Sprachen und Compiler bieten automatische Überprüfungen an - Array Bound Checking Options, vgl. dazu [12, 43]), sind
wilde Zeiger oft schwer zu lokalisieren. Uninitialisierte Zeiger nehmen Zufallswerte an.
Zeiger auf freigegebene Speicherbereiche und damit nicht mehr gültige Puffer auf dem
Heap führen nicht immer zu einer Ausnahmebehandlung. Mitunter sind solche Fehler
nur unter bestimmten Konstellationen und Konfigurationen zu finden.
Spezielle Debugging Tools erlauben die Verfolgung der Speicherauslastung, erkennen
Fehler in der Heapverwaltung und in der Nutzung der Speicherblöcke auf dem Heap.
Andere generieren Fehlerbedingungen (Fault Conditions) oder simulieren einen Speichermangel. Pageheap, ein Werkzeug aus den Debugging Tools for Windows 24 , erkennt
illegale Heap Pointer, einen unsynchronisierten Zugriff auf den Heap, doppelte Speicherfreigaben, den Zugriff auf einen Puffer nach dessen Freigabe uvm. Tritt einer der
detektierten Fehler auf, wird eine Exception ausgelöst, welche weitere Untersuchungen
der gestoppten Applikation mit einem Debugger zulässt. Ebenso lassen sich damit Speicherfehler und -mängel (mit einer bestimmten Wahrscheinlichkeit auftretend) simulieren, um die Fehlerbehandlung (Error Handling) des Softwaresystems zu überprüfen.
24
http://www.microsoft.com/whdc/devtools/debugging/default.mspx
4. Lokalisierung potentieller Schwachstellen
4.3.3.4
59
Speicherabbilder
Eine weitere Möglichkeit einer binärbasierten Prüfung einer Applikation ist die Erstellung eines Speicherabbilds (Memory Dump) zu einem bestimmten Zeitpunkt. Dabei
wird der gesamte Speicher in eine Datei geschrieben, um diese später mit entsprechenden Werkzeugen auszuwerten. Durch diese Methode können Passwörter und sensible
Daten aus dem Heap einer Applikation einfach ausspioniert werden, ohne in die laufende Applikation eingreifen zu müssen. Daten im Speicher oder auf dem Heap sind also
keinesfalls sicher vor Zugriffen, selbst wenn der Zugriff auf den Heap eines Prozesses zur
Laufzeit durch einen anderen Prozess vom Betriebssystem verhindert wird. Microsofts
User Mode Process Dumper 25 erstellt bei bestimmten Ereignissen wahlweise automatisch, Speicherauszüge in Dateien. Viele Betriebssysteme erstellen beim Absturz des
Systems einen Memory Dump, um eine spätere Fehlersuche zu erleichtern. Sind diese
Speicherauszüge allen Benutzern des Systems zugänglich, können damit sensible Daten
in falsche Hände fallen. Die Prüfung eines Speicherauszugs ist eine einfache Methode
um festzustellen, ob sensible Daten im Speicher unverschlüsselt sichtbar werden. Derartige Daten sollten aus diesem Grund nur verschlüsselt im Speicher abgelegt werden.
Auch Windows erzeugt bei einem Systemabsturz einen Memory Dump. Die Applikation Dr.Watson (DRWTSN32.EXE), auf jedem Windows System installiert, erzeugt
Dumps und Logdateien, wenn Applikationen durch Ausnahmebehandlungen beendet
werden. Ein Codeauszug der Logdatei protokolliert jene Codezeilen, durch die der Fehler ausgelöst wurde. Das ist viel Information für einen Softwareentwickler, aber auch
für einen Angreifer, der eine Applikation vielleicht absichtlich zum Absturz gebracht
hat.[26]
4.3.3.5
Status- und Fehlerinformationen
Unabhängig von allen bisher beschriebenen Tools zur Überwachung von Programmen
verraten viele Applikationen von sich aus viel über ihr Innenleben oder ihre Verarbeitung von Daten. Viele Programme schreiben Logdateien (Linux) oder Event Messages
25
http://www.microsoft.com/downloads/details.aspx?familyid=
E089CA41-6A87-40C8-BF69-28AC08570B7E&displaylang=en
4. Lokalisierung potentieller Schwachstellen
60
(Windows) mit ihren Statusinformationen. Im Fehlerfall geben sie oft freizügig Auskunft über den Grund und den Ort des Fehlers und über interne und sensible Daten.
Moderne Software, allen voran Programme mit Managed Code (.NET Applikationen)
oder auf virtuellen Maschinen (Java Programme innerhalb einer Java Virtual Machine),
liefert dem Normalbenutzer bei Programmfehlern oft einen Dump des gesamten Programmspeichers, den CallStack mit allen Funktionsnamen und Parametern der aufgerufenen Funktionen oder einen Stack Trace mit allen Stackdaten. Diese Informationen
stellen für den Softwareentwickler zwar eine große Hilfe bei der Suche des Fehlers dar,
liefern dem potentiellen Angreifer aber oft auch viele Informationen über die Applikation und vor allem ihre Schwächen. Somit stellen gut gemeinte Fehlermeldungen und
Zusatzinformationen oft eine beträchtliche Sicherheitslücke dar.[37]
4.3.4
Fault Injection
Eine Fault Injection ist ein Testverfahren, bei dem der Umgang mit fehlerhaften Daten,
mit mangelnden Ressourcen oder partiellen Systemausfällen und die sog. Fehlertoleranz (Fault Tolerance) getestet wird. Dieses Verfahren wird auch als Stress Testing
bezeichnet. Die Tests dienen hauptsächlich dazu, die Stabilität einer Applikation trotz
wechselnder, widriger oder fehlerbehafteter Bedingungen zu prüfen und durch Anpassungen sicherzustellen. Von einer sicheren Applikation wird gefordert, dass sie auch mit
stark eingeschränkten Umgebungen umgehen kann und auch in Ausnahmesituationen
keine Sicherheitslücken aufweist. Typische Stresstests sind die Reduktion des freien
RAM- und Plattenspeichers auf ein Minimum, die Erhöhung der Prozessorauslastung
oder die Manipulation von Daten. Sie können sowohl manuell als auch mit speziellen Werkzeugen durchgeführt werden. Für Softwaretests wird heute meist auf eine als
Software implementierte Fault Injection (SWIFI) zurückgegriffen.[55]
In [24] wird zwischen hostbasierten und netzwerkbasierten Fault Injectors unterschieden. Die lokal auf dem Host laufenden Generatoren arbeiten wie Debugger und manipulieren die Daten und Programmstatus im laufenden Betrieb. Die netzwerkbasierten
Injektoren manipulieren den Netzverkehr, um die Auswirkungen auf die Empfänger zu
4. Lokalisierung potentieller Schwachstellen
61
beobachten. Eine spezielle Art der sogenannten Fault Injection Tools generiert automatisch illegale Eingabedaten. Damit werden absichtlich fehlerhafte Daten und Datenformate in zu lesende Dateien, in über ein Netzwerk übertragene Datenpakete oder manuelle Eingabedaten integriert, um Fehler bei der Verarbeitung der Daten zu provozieren
oder die Behandlung dieser Falscheingaben zu beobachten. Aus sicherheitstechnischer
Sicht sind jene Fehler von Interesse, welche dem Angreifer als Folge der Fehler das
gezielte Einschleusen von Code oder schadhaften Daten erlauben würden.[31]
Fuzzing-Tools oder kurz Fuzzer sind Werkzeuge, welche automatisch zufällige Zeichenfolgen und Daten generieren, welche dann mit zusätzlichen Werkzeugen in eine Applikation als Eingabedaten eingebracht werden können. Fuzzer gibt es mittlerweile für
eine Reihe von Tests, spezialisiert auf Web-Applikationen, Server, ActiveX Objekte,
verschiedenste Dateiformate und Netzprotokolle [3]. Klein beschreibt in [31] die Anwendung zweier einfacher Fuzzer (Sharefuzz 26 und BFBTester - Brute Force Binary
Tester 27 ) zur Manipulation der Umgebungsvariablen und der Aufrufargumente von
Shell-Applikationen. Mittlerweile stehen eine Reihe frei verfügbarer Fuzzing-Tools zur
Verfügung.
Fuzzer
AxMan
BFBTester
FTPStress
Peach
Sharefuzz
SPIKE
Bezugsquelle
http://metasploit.com/users/hdm/tools/axman
http://bfbtester.sourceforge.net
http://www.infigo.hr/en/in_focus/tools
http://peachfuzz.sourceforge.net
http://sharefuzz.sourceforge.net
http://www.immunitysec.com/resources-freesoftware.shtml
Tabelle 4.1: Zusammenstellung einiger Fuzzing Werkzeuge (Stand: 2007.06.13)
Die hier besprochenen Fault Injection Methoden beschreiben die Prüfung ausführbarer
Binärdateien. Auch auf Quellcodebasis werden im Rahmen von automatischen Tests
einzelner Funktionen und Funktionsgruppen schon während der Entwicklung Fault
Injection Methoden angewandt (Unit Tests).
26
27
http://sharefuzz.sourceforge.net
http://bfbtester.sourceforge.net
4. Lokalisierung potentieller Schwachstellen
4.3.5
62
Bewertung der Methoden und Werkzeuge
Die Analyse eines Programms auf Fehler und Sicherheitslücken ist in der Regel eine kombinierte Anwendung verschiedenster Methoden und Werkzeuge, wie sie hier
aufgrund der Komplexität und Mannigfaltigkeit nur auszugsweise beschrieben werden
konnten. Microsoft empfiehlt für die Entwicklung sicherer Windows-Programme noch
eine Reihe weiterer Werkzeuge und Methoden [43]. Der Einsatz der Mittel ist vielfach
abhängig vom Typ der zu entwickelnden Applikation (z.B. Treiber, Dienste, Controls)
oder der Zielumgebung (z.B. Serverapplikationen). Die Auswahl der in dieser Arbeit
verwendeten Werkzeuge orientiert sich an deren Verfügbarkeit, deren einfachen Anwendung und an Methoden zur Entwicklung einfacher Benutzerapplikationen.
Die binärcodebasierten Analysemethoden haben gezeigt, dass schon mit herkömmlichen
System- und Entwicklungswerkzeugen eine Unmenge von Daten über die laufenden
Applikationen und das System gesammelt werden kann, selbst wenn keine Quelltexte
verfügbar sind. Mit den richtigen Werkzeugen und den entsprechenden Kenntnissen
über Systemumgebung, Architekturen und Softwareentwicklungsmethoden ausgestattet, lassen sich beinahe beliebige Informationen sammeln, ohne spezielle Änderungen
an den Applikationen vornehmen zu müssen. Eine Reihe spezieller Werkzeuge lässt in
die verborgensten Teile der Speicher, der Software und Hardware blicken.
Die binärcodebasierte Analyse ist bei weitem nicht so einfach automatisierbar wie die
Analyse eines Quelltextes. Tests kleiner Funktionseinheiten lassen sich hingegen gut
in den Entwicklungsprozess integrieren (Unit Tests). Die Auswertung der gesammelten Laufzeitdaten erfolgt weitestgehend manuell, einfache Auswerteskripts können die
Arbeit jedoch erleichtern. Die Anzahl der Analysemethoden und der Werkzeuge lässt
erkennen, wie vielseitig Schwachstellen auftreten können. Fehler im Code sind über
Fault Injections noch einfacher lokalisierbar als Designfehler, welche einen Memory
Dump oder aufwändige Datenflussanalysen notwendig machen.
4. Lokalisierung potentieller Schwachstellen
4.4
63
Integrierte Analyse und Überwachung
In die Binaries integrierte Analysen und Überwachungsfunktionen sind eine weitere
Möglichkeit Fehler während der Laufzeit (Run Time) zu entdecken. Diese Funktionen
übernehmen spezielle Überwachungsaufgaben (z.B. Überwachung des Heaps, des Stack,
der Array-Grenzen, Zeiger, Dateien) und können entweder während der Testphase integriert werden oder als Schutz gegen Angriffe in den fertigen Applikationen verbleiben.
Wird nun ein Fehler, eine Speichermanipulation bzw. ein Angriff erkannt, so lösen
die integrierten Überwachungsfunktionen eine Exception aus, wodurch die Applikation
abgebrochen oder der Fehler über einen Debugger weiterverfolgt wird.
4.4.1
Bounds Checking
Das sogenannte Bounds Checking ist jene Methode, bei der überprüft wird, ob beim
Zugriff auf einen Speicherbereich die erlaubten Grenzen des Puffers überschritten werden. C und C++ sind auf Geschwindigkeit optimierte Programmiersprachen, die Compiler sehen deshalb keine in die Sprache integrierte Überwachung der Arraygrenzen
vor. Bei anderen, neueren Programmiersprachen (z.B. C#, Java, Visual Basic) waren
diese Überprüfungen schon immer Teil der Sprach- und Speicherkonzepte bzw. des
vom Compiler erstellten Codes. Viele Sprachen haben deshalb auch auf Zeiger und die
Möglichkeit der direkten Speichermanipulation verzichtet. Dies ist aber noch immer
einer der Gründe für den Einsatz und die hohe Performance von C und C++.
Mittlerweile gibt es einige Compilererweiterungen, die dieses Feature auch für C und
C++ Programme nachrüsten. Kombiniert mit einer Überwachung der Speicherverwaltung werden dabei alle Zugriffe auf einen Speicher einer expliziten Überprüfung der
Adressen zugeführt. Ein Beispiel für eine derartige Erweiterung des GCC Compilers,
eingeschränkt auf die Programmiersprache C, ist Bounds Checking 28 (die neuere Version wurde auf CRED - C Range Error Checking umbenannt).
28
http://sourceforge.net/projects/BoundsChecking
4. Lokalisierung potentieller Schwachstellen
4.4.2
64
Überwachung des Stacks
Zur Überwachung und zum Schutz des Stacks vor Manipulation kommen verschiedene
Methoden zum Einsatz, welche in der Regel durch den Compiler unterstützt werden.
Dazu fügt der Compiler die benötigten Codeteile automatisch in die Applikation bzw.
an den kritischen Stellen ein. Die am häufigsten angewandten Methoden sind:
• Canary Words: Der Compiler generiert für jede Funktion eine sogenannte
Prolog- und Epilog-Routine. Beim Eintritt in die überwachte Funktion legt die
Prolog-Routine auf dem Stack ein zusätzliches Canary Word ab (siehe Abbildung
4.4.b). Bevor die Funktion verlassen wird, überprüft die Epilog-Routine, ob das
Canary Word auf dem Stack manipuliert wurde. Ist dies der Fall, so erfolgte die
Manipulation als Folge eines Stack Overflow oder als Folge einer gezielten Manipulation des Stacks.
Zur Anwendung kommt diese Technik z.B. bei der für den GCC verfügbaren
Compiler-Erweiterung StackGuard [31, 18] oder beim Microsoft C++ Compiler29
durch die Compiler Option /GS. Microsoft bezeichnet das Canary Word als Security Cookie, manchmal auch als Speed Bump und fügt es an einer anderen Stelle
ein (siehe Abbildung 4.4.c). Das Listing C.1 im Anhang C.1 zeigt die vom Microsoft Compiler eingebundenen Prolog- und Epilog-Routinen zur Überwachung
des Stacks. [9, 38].
• Global-Ret-Stack Method: Der Compiler generiert ebenfalls Prolog- und Epilog-Routinen. Die Prolog-Routine sichert beim Eintritt in die Funktion die Rücksprungadresse in einem eigenen Speicherbereich auf dem Heap. Vor dem Verlassen
der Funktion vergleicht die Epilog-Routine die Rücksprungadresse auf dem Stack
mit der zuvor gesicherten Adresse. Zur Anwendung kommt diese Technik z.B.
bei der für den GCC verfügbaren Compiler-Erweiterung StackShield 30 oder bei
LibSafe 31 , einer Bibliothekserweiterung für Unix-basierte Systeme.[4, 31]
• Ret-Range-Check Method: Bei diesem Verfahren wird in einer Epilog-Routine
geprüft, ob die Rücksprungadresse auf dem Stack in das nicht manipulierbare
29
Für diese Arbeit wurde der Microsoft C/C++ Compiler, Version 8.00.50727.762 verwendet.
http://www.angelfire.com/sk/stackshield/
31
http://http://directory.fsf.org/libsafe.html
30
4. Lokalisierung potentieller Schwachstellen
65
Text- bzw. Code-Segment der Applikation zeigt. Auch diese Technik kommt bei
StackShield zur Anwendung.[31]
Die auf Cookies basierenden Methoden wie StackShields Canary Words oder Microsofts Security Cookies können eine zweistufige Attacke, wie sie in der Einführung der
Heap Overflows gezeigt wurde, weder erkennen noch verhindern. Dabei wird zwar auch
die auf dem Stack gespeicherte Rücksprungadresse manipuliert, jedoch nicht als Folge
eines Stack Overflows (vlg. auch [46]). Einzig zusätzliche Sicherungsmethoden, wie in
StackShield verwendet, detektieren auch derartige Fehler und Manipulationen.
Function Return Address
Saved Frame Pointer
Local declared
Variables and Buffers
Optional Process Data
(a)
Function Parameters
Function Return Address
Canary Word
Saved Frame Pointer
Local declared
Variables and Buffers
Optional Process Data
(b)
Previous
Stack Frames
Function Stack Frame
Function Parameters
Previous
Stack Frames
Function Stack Frame
Function Stack Frame
Previous
Stack Frames
Function Parameters
Function Return Address
Saved Frame Pointer
Security Cookie
Local declared
Variables and Buffers
Optional Process Data
(c)
Abbildung 4.4: Stackschutzmechanismen: (a) Unmodifizierter Stack Frame; (b) StackGuard Modifikation mit einem Canary Word; (c) Microsoft Compiler Option /GS Modifikation mit einem Security Cookie
Für den Test einer Applikation auf Stabilität erscheint dies weniger relevant, da es sehr
unwahrscheinlich ist, dass zufällig Fehler zu einer derartigen Problemstellung führen.
Für Sicherheitstests ist diese Einschränkung wichtig, da es sich trotzdem um eine potentielle Schwachstelle handelt und über andere Methoden behoben werden muss.
4.4.3
Überwachung von Funktionen
Zur Überwachung der von einer Applikation verwendeten Funktionen kommen verschiedene Methoden zur Anwendung, welche in der Regel durch zusätzliche Bibliotheken
4. Lokalisierung potentieller Schwachstellen
66
oder Bibliothekserweiterungen unterstützt werden. Der Vorteil dieser Bibliotheken ist
hauptsächlich der, dass die überwachte Applikation nicht neu kompiliert werden muss
und somit auch der Sourcecode nicht zur Verfügung stehen muss. Spezielle Bibliotheken
wie z.B. LibFormat oder LibSafe auf Unix-basierten Betriebssystemen tauschen beim
dynamischen Nachladen von API-Funktionen unsichere Funktionen gegen sichere aus
und binden zusätzliche Parameter- und Zeigerüberprüfungen in Funktionsaufrufe ein.
LibSafe 32 erweitert bestimmte als unsicher ausgewiesene Funktionen um spezielle Prologund Epilog-Funktionen, welche Prüf- und Überwachungsaufgaben übernehmen. LibFormat 33 verwendet die gleichen Methoden und überwacht vor allem Funktionen mit
möglichen Format-String Schwachstellen. Im Prolog dieser Funktionen wird der als
Parameter übergebene Format-String einer genauen Überprüfung unterzogen und nur
dann die Bibliotheksfunktion aufgerufen, wenn die Überprüfung die Unbedenklichkeit
der Parameter ergeben hat. Im Verdachts- oder Fehlerfall wird die Applikation mit
einer entsprechenden Fehlerbehandlung abgebrochen. Das Einschleusen von Code über
Format-Strings wird dadurch deutlich erschwert. FormatGuard 34 erweitert die Gnu C
Library (GLibc) und kapselt alle Format-String Funktionen ab. In diesem Fall muss
die Applikation jedoch neu übersetzt werden. [4, 17, 31]
4.4.4
Überwachung des Heaps
Viele Bibliotheken und Compiler bieten eine Überwachung des Speichers auf dem Heap
und Stack während der Laufzeit an. Manchmal sind diese zusätzlich in das Programm
eingebundenen Überwachungsfunktionen auf die Debug-Version 35 beschränkt, manchmal verbleiben diese Überwachungsfunktionen auch in der endgültigen und für den
Kunden bestimmten Release-Version. Typische Erweiterungen sind Funktionen zur
32
http://directory.fsf.org/libsafe.html
http://www.securityfocus.com/tools/1818
34
http://www.securityfocus.com/tools/1844
35
Eine Debug-Version ist eine während der Entwicklung speziell für Testzwecke kompilierte Version.
Sie wird oft mit zusätzlichen Funktionen und Informationen versehen, die es dem Entwickler einfacher
machen, Fehler aufzudecken bzw. die fehlerhaften Stellen im Code zu lokalisieren. Debug-Versionen
werden in C/C++ meist mit einem gesetzten DEBUG-Flag kompiliert, welches den Precompiler anweist, speziellen Code einzubinden. Sie sind aufgrund der erweiterten Funktionen oft umfangreicher
und deutlich langsamer in ihrer Ausführungsgeschwindigkeit als die für den Endkunden bestimmten
Release-Versionen.
33
4. Lokalisierung potentieller Schwachstellen
67
Überwachung des Heaps (Memory Allocation Tracing) und der auf dem Heap allokierten Speicherblöcke (Memory Overflow Detection). Weiters bieten viele Bibliotheken
auch die Möglichkeit, nicht freigegebene Speicherblöcke am Ende einer Funktion oder
der Applikation (Memory Leak Detection) aufzufinden.
Eine einfache und wirkungsvolle Methode für C++ Programme, die mit jedem C++konformen Compiler funktioniert, ist das Überschreiben der globalen new- und deleteOperatoren. Wird ein Objekt mit dem Operator new erzeugt, wird dafür die globale
::operator new()-Funktion aufgerufen. Wird ein Objekt vom Typ T erzeugt, wird
dafür die T::operator new()-Funktion aufgerufen. Gleiches gilt beim Löschen eines
Objekts über delete, bei dem die globale operator delete()- oder T::operator
delete()-Funktion aufgerufen wird.[52]
Wie im Anhang vereinfacht dargestellt, werden in der include-Datei (siehe Listing C.2)
neue new- und delete-Operatoren deklariert. Ist das DEBUG-Flag gesetzt (siehe Listing
C.3), werden beim nächsten Kompilierdurchgang die new- und delete-Operatoren
überschrieben. Im gezeigten Beispiel aus dem Listing C.3 wird die Operatorfunktion void* operator new[](size_t nSize, char* pFileName, int nLine) aufgerufen, welche eine erweiterte Speicherverwaltung implementiert. Wird der Speicher freigegeben, wird dabei die Funktion void operator delete(void* p) aufgerufen und
die implementierte Speicherverwaltung gibt den Speicher frei. Durch das Überladen der
beiden Operatoren new und delete können neben der Speicherallokation zusätzliche
Aufgaben zur Sicherung des Speichers ausgeführt werden.[48]
Abbildung 4.5: Fehlermeldung nach der Detektion eines Heap Overflows
4. Lokalisierung potentieller Schwachstellen
68
Microsoft verwendet in den Bibliotheken der Microsoft Foundation Classes (MFC)
ebenfalls eine einfache Erweiterung zur Überwachung des Speichers, welche das Prinzip des Operator Overloadings nutzt. Die Funktionen der MFC allokieren einige Bytes
mehr als angefordert und versehen die Speicherblöcke mit zusätzlichen Security Cookies.
Wird ein Speicherblock freigegeben (siehe Anhang Listing C.3, Zeile 13), werden die Security Cookies auf eine mögliche Manipulation überprüft. Je nachdem, welches Cookie
verändert wurde, kann damit ein Buffer Overflow oder Buffer Underflow festgestellt
werden. Wird eine Manipulation festgestellt, so wird eine Exception ausgelöst und eine
Fehlermeldung, wie in der Abbildung 4.5 dargestellt, gezeigt. Zusätzlich überprüfen die
für die Speicherverwaltung zuständigen Afx-Funktionen der MFC am Ende der Applikation, ob alle angeforderten Speicherblöcke von der Applikation freigegeben wurden.
Ist dies nicht der Fall, so weist dies auf vergessene Speicherfreigaben (sogenannte Memory Leaks) hin. Memory Leaks werden von der MFC beim Terminieren einer Applikation
wie folgt protokolliert:
Detected memory leaks!
Dumping objects ->
{127} normal block at 0x003598E0, 10 bytes long.
Data: <ABCDEFGHI > 41 42 43 44 45 46 47 48 49 00
Object dump complete.
Neben derartigen in den Code integrierten Prüfungen können auch Funktionen eingefügt werden, die mit zusätzlichen Tools kooperieren und kommunizieren. Dabei kann
es sich um einfache Tracer, aber auch um Funktionen handeln, die in Kombination mit
speziellen Debuggern und Überwachungsprogrammen Verwendung finden.
4.4.5
Bewertung der integrierten Methoden
Nachdem sowohl die Analyse des Quellcodes als auch die Analyse der Binaries und
der Runtimedaten einen zusätzlichen Aufwand darstellt, geht der Trend eindeutig zu
automatisch in eine Sprache, Laufzeitumgebung oder die fertige Applikation integrierte Sicherheitsroutinen. Diese erfordern vom Softwareentwickler weder spezielles Knowhow, noch einen zusätzlichen Aufwand. Es wird dabei versucht, einserseits Fehler schon
in frühen Entwicklungsstadien zu entdecken, andererseits die möglichen Auswirkungen
4. Lokalisierung potentieller Schwachstellen
69
etwaiger Fehler zu minimieren. Eine ständige Überwachung der Software auf ein Fehlverhalten ist naheliegend.
Fest in die Software integrierte Analyse- und Überwachungsfunktionen haben den Vorteil, im fertigen Programm verbleiben zu können und zur Laufzeit keine speziellen
Werkzeuge und Konfigurationen zu benötigen. Diese Routinen als Teil der fertigen Applikation überwachen eine Software fortwährend und auf jedem System. Sie reagieren
beim Auftreten von kritischen oder fehlerhaften Zuständen mit der Auslösung einer
Ausnahmebehandlung. Fest in die Software integrierte Funktionen zur Überwachung
von Stack- oder Heapmanipulationen sind schon sehr ausgereift. Deren Anwendung ist
mittlerweile üblich, wie die Default-Einstellungen des Microsoft C++ Compilers zeigen. Berichte von Attacken und Artikel über etwaige Schwächen diverser Sicherungsmethoden beweisen aber immer wieder, dass auch diese zusätzlichen Hürden mitunter
überwindbar sind (vgl. [46]).
5
Vermeidung potentieller
Schwachstellen
Die beste Absicherung eines Computersystems ist die Entwicklung fehlerfreier Software
ohne sicherheitskritische Schwachstellen. Aus diesem Grund beschäftigt sich dieses Kapitel damit, mit welchen Methoden im Design und der Implementierung sicherheitskritische Fehler verhindert werden können. Dabei werden Methoden diskutiert, die
kritische Mängel schon während der Testphase aufzeigen und fatale Auswirkungen etwaiger Fehler verhindern. Beachtung finden dabei auch typische sicherheitskritische
Designfehler (Security Flaws), wenngleich der Fokus der Arbeit auf sicherheitskritischen Implementierungsfehlern (Security Bugs) liegt.
5.1
Allgemeines
Die sicherheitsorientierte Softwareentwicklung favorisiert mittlerweile eine mehrschichtige Verteidigungsstrategie durch das Errichten mehrerer Verteidigungslinien. Dies bedeutet, dass Sicherheit auf allen Ebenen integraler Bestandteil ist. Sowohl die Architektur als auch alle Teile der Implementierung haben den Prinzipien der sicherheitsorientierten Softwareentwicklung zu folgen. Jedes Modul, jede Schicht der Architektur,
jede Funktion ist für sich für ihre eigene Sicherheit verantwortlich.[25, 26]
70
5. Vermeidung potentieller Schwachstellen
71
Sichere Software zu entwickeln bedeutet in erster Linie, die Sicherheit durch sicheres
Design (Security by Design) und sichere Implementierung (Security by Implementation) zu gewährleisten. Keinesfalls kann Sicherheit durch Geheimhaltung (Security by
Obscurity) erreicht werden. Dies kann einen Angriff vielleicht verzögern, wirkungsvoll
verhindern jedoch kaum.
5.2
Sicheres Design
Softwaresicherheit ist ohne entsprechende Planung und ohne ein darauf ausgerichtetes Design nicht möglich [26]. Oft sind Einzelteile eines Programms technisch korrekt
ausgeführt und fehlerfrei. Im Zusammenwirken und in der geplanten Umgebung (Environment) können sie trotzdem potentiell sicherheitskritische Mängel aufweisen. Oft
sind eine fehlende Planung oder ein falsches Sicherheitsdesign dafür verantwortlich [22].
Typische Designfehler, welche zu einer potentiellen Schwachstelle führen, sind (siehe
auch Kapitel 3.1):
• fehlende oder schwache Eingabe- und Parameterprüfung
• fehlende oder schwache Authentisierung und Authentifizierung
• fehlende oder schwache Verschlüsselung und Datensicherung
• fehlende Verwaltung der Zugriffs- und Ausführungsberechtigungen
• unsichere Anwendungs- und Systemkonfiguration
• fehlende oder fehlerhafte Fehlerbehandlung
Selbst heute wird noch ein Großteil der Software ohne vorherige detaillierte Beschreibung des Softwaredesigns, der Datenflüsse, einer konkreten Zielsetzung bezüglich Sicherheit und der Modellierung eines Bedrohungsmodells entwickelt. Dabei ist die Entwicklung einer sicheren Software ohne Berücksichtigung im Design nicht möglich. Wie
Michael Howard et al. in [26] wiederholt betonen, ist Softwaresicherheit kein Softwarefeature und keinesfalls nachträglich zu implementieren, sondern Teil des Gesamtkonzepts und eine begleitende Anforderung während aller Phasen eines Softwareprojekts.
Mit der Modellierung einer Softwarearchitektur beginnt auch die Modellierung des Umgangs mit den Gefahren, denen eine zukünftige Applikation ausgesetzt sein wird.
5. Vermeidung potentieller Schwachstellen
5.2.1
72
Threat Modeling
Die Entwicklung sicherer Software muss schon bei der Modellierung eines Softwaresystems beginnen. Ein Teil der Planung einer sicheren Software ist die Modellierung
eines Bedrohungsmodells mit dem sog. Thread Modeling. Die Idee hinter dieser Modellierung ist die Sammlung und Dokumentation aller Bedrohungen, denen eine Software
ausgesetzt sein könnte. Mit den gewonnenen Erkenntnissen werden sicherheitskritische
Fehler im Design der Software schon vor der Implementierung erkannt. Dadurch kann
das Gesamtrisiko verringert werden. Am Ende dieses Prozesses steht die Entwicklung
eines modul- und systemübergreifenden Sicherheitskonzepts.
Bedrohungen lassen sich nach Howard und LeBlanc im sogenannten STRIDE-Modell
klassifizieren. STRIDE ist ein Akronym der ersten Buchstaben der folgenden sechs
Bedrohungskategorien (vgl. [25] und [53]):
• Spoofing Identity: Vortäuschen einer falschen Identität, z.B. die Fälschung von
Authentifizierungsdaten
• Tampering with Data: Fälschen von Daten, z.B. Änderung von Dateien
• Repudiation: Verweigerung, z.B. Unterzeichnung einer Quittung
• Information Disclosure: Preisgeben von Daten, z.B. ungesicherte Dateien
• Denial of Service: Verweigern eines Dienstes, z.B. Dienste (Webserver, usw.)
sind aufgrund von Angriffen nicht erreichbar
• Elevatation of Privilege: Höherstufen der Rechte, z.B. ein Benutzer kann die
Rechte eines priviligierten Benutzers erlangen
In der Praxis sollte das Thread Modeling erst nach der Definition aller Funktionen
(Features) stattfinden. Nachdem alle Features definiert wurden und die funktionalen
Komponenten feststehen, werden alle Entry Points und Datenflüsse der Applikation
identifiziert und die zu erwartenden Bedrohungen auf das System ermittelt. Die einzelnen Bedrohungen werden bewertet und für jeden Entry Point die beste technologische
Lösung zur Abwehr der Bedrohung diskutiert. Neben einer vollständigen Dokumentation ist die Erstellung von speziellen Testplänen, welche die identifizierten Bedrohungen
berücksichtigen, ebenfalls Teil des Threat Modeling Prozesses (vgl.[53] und [26]).
5. Vermeidung potentieller Schwachstellen
5.3
73
Defensive Programmierung
Die Erstellung von robustem Code war das vorrangige Ziel der 90er Jahre. Standardwerke (vgl. [35] und [36]) haben sich ausführlich damit auseinandergesetzt, ohne explizit
sicherheitstechnische Aspekte zu diskutieren. Erst neuere Auflagen werden entsprechend ergänzt. Bei der Erstellung von solidem Code sucht man im Allgemeinen die
Balance zwischen [36]:
• Robustheit: die Software kann auch mit Fehlern umgehen; oberste Priorität ist
die Lauffähigkeit der Software
• Korrektheit: die Software bricht bei Fehlern kontrolliert ab; oberste Priorität
ist die Richtigkeit der Ergebnisse
Sicherheitskritische Applikationen legen im Allgemeinen mehr Wert auf Korrektheit
”
als auf Robustheit“, schreibt Steve McConnell in [36, S. 206]. Dies mag z.B. für Geräte
im medizinischen Bereich gelten, im Consumerbereich wird der Robustheit im Allgemeinen der Vorrang gegeben. Solider Code, der beide Ziele verfolgt, ist die Basis einer
sicheren und stabilen Software. Einige der für die Sicherheit von Softwaresystemen wesentlichsten Regeln wurden schon in den 90er Jahren unter der Bezeichnung Defensive
Programmierung eingeführt. Die im Folgenden diskutierten defensiven Programmierregeln von damals gelten heute mehr denn je.
Viele Programmierer gehen davon aus, dass der Programmablauf ungestört ist, die
Daten einer Applikation ohne Fehler sind, die Infrastruktur bzw. Umgebung einer Applikation den Vorgaben entspricht und ein Fehler nur einen Ausnahmefall darstellt.
Die Defensive Programmierung geht umgekehrt von der Annahme aus, dass die Daten
jederzeit ungültig sein können und widrige Ereignisse und Fehler immer möglich sind.
Mit einer derartigen Annahme ausgestattet, ist das Ergebnis einer defensiven Programmierung ein robusterer Code, weil die Berücksichtigung möglicher Fehlerszenarien Teil
der Planung und der Implementierung ist. Können Fehler immer auftreten, so müssen
auch die Daten immer auf Richtigkeit überprüft werden und zu jeder Zeit Verfahren
für das Auftreten eines Fehlers definiert sein.
5. Vermeidung potentieller Schwachstellen
5.3.1
74
Überprüfung der Ein- und Ausgabedaten
Alle Funktionen, welche Daten von Außen entgegennehmen und verarbeiten, sind
als potentielle Sicherheitsschwachstellen einzustufen und einer genauen Codeprüfung
(Code Review ) zu unterziehen. Wird eine Funktion aufgerufen, so sollte eine Vorbedingung (Pre-Condition) und eine Nachbedingung (Post-Condition) zwischen dem Aufrufer und der Funktion formuliert worden sein. Dies kann auch Teil der Dokumentation
sein. Über die Vorbedingungen werden die Parameter, also die Eingangsdaten in die
Funktion, festgelegt und nur wenn diese Bedingungen exakt eingehalten werden, arbeitet die Funktion korrekt. Die Nachbedingung definiert, wie das Ergebnis einer Funktion
bei einer korrekten Ausführung aussehen muss. Defensive Programmierung verlangt die
Überprüfung der Datenflüsse in beide Richtungen. In der Praxis bedeutet dies, dass jede Funktion am Beginn ihrer Ausführung die Eingangsdaten überprüft und auf Fehler
entsprechend reagiert (siehe Beispiel im Anhang, Listing C.4). Ebenso hat der Aufrufer
einer Funktion die Ergebnisdaten zu prüfen, bevor er sie weiterverwendet.
Kompromittierende Daten können sowohl über einfache Benutzereingaben als auch
über Dateien oder über ein Netzwerk empfangene Datenpakete in das System gelangen.
In [26] bringen Michael Howard und David LeBlanc den notwendigen Umgang mit Dateneingaben mit der einfachen Annahme All input is evil until proven otherwise.”[26,
”
S. 341] zum Ausdruck. Nicht nur den eigentlichen Programmdaten ist dabei Beachtung
zu schenken, sondern ebenfalls den Infrastrukturdaten (z.B. Konfigurationsdateien, angeschlossene Geräte) sollte immer misstraut werden.
Die Behandlung von Ein- und Ausgabedaten über Interfaces und Funktionen kann
auf einige wichtige Good Practices zusammengefasst werden, welche im Anhang E.1
aufgelistet sind.
5.3.2
Sichere Zeiger- und Speicherverwaltung
Bei der Defensiven Programmierung wird davon ausgegangen, dass jeder Zeiger auf
einen ungültigen Speicherbereich zeigen kann, die Daten im Speicher ungültig sein
können und der belegte Speicher von anderen Applikationen später wiederverwendet
5. Vermeidung potentieller Schwachstellen
75
werden könnte. Nur solange sensible Daten im System gespeichert werden, sind sie auch
angreifbar. Das Speichern dieser Daten ist, wenn irgendwie möglich, grundsätzlich zu
vermeiden [26]. Lässt es sich nicht vermeiden, so sollten nicht mehr benötigte Daten
sofort gelöscht werden. Die Freigabe nicht mehr benötigten Speichers entlastet zudem
das System und verhindert in der Folge Memory Leaks. Ein kurzes Beispiel im Listing
C.5 zeigt eine mögliche Umsetzung defensiver Programmierung bei der Behandlung
von Zeigern und Speicher.[59]
Eine weitere Fehlerquelle bei der Verwendung von Zeigern und Speicherblöcken variabler Größe ist oftmals in einer fehlerhaften Zeigerarithmetik zu suchen. Grundsätzlich
sollte jede Funktion, welche mit einem Puffer arbeitet, auch die Längeninformation
dazu erhalten und verarbeiten. Viele der unsicheren POSIX-Funktionen (siehe Kapitel
5.3.5.2 und Anhang A) wurden um zusätzliche Parameter erweitert, welche die Länge
von Datenpuffern beschreiben. Eigene Funktionen sollten als Parameter neben einem
Zeiger ebenso immer die Länge des Puffers erhalten (siehe Listing C.5, Zeile 10).
Die Behandlung der Zeiger und Speicherblöcke kann auf einige wichtige Good Practices
zusammengefasst werden, welche im Anhang E.2 aufgelistet sind.
5.3.3
Fehlerbehandlung
Wie in der Einführung dieses Kapitels schon erwähnt, ist die Korrektheit und die Robustheit einer Software die Voraussetzung für deren Sicherheit. Viele Angriffe zielen
in erster Linie darauf ab, die Software in einen Fehlerzustand zu treiben, um dann
das geschwächte System zu kompromittieren. Dabei gibt es Methoden, die sich neben
der Manipulation der Daten auch der Manipulation der Systemressourcen bedienen.
Stresstests sind künstlich erzeugte Testszenarien, bei denen eine Software einer fehlerbehafteten Umgebung ausgesetzt wird, um die Behandlung der Fehler zu überprüfen.
Defensive Programmierung hat zum Ziel, das System zu jedem Zeitpunkt vor Fehlern
zu schützen, welche im Regelbetrieb nicht erwartet werden. In einer schichtbasierten
Software-Architektur zum Beispiel kann die Fehlerbehandlung auf bestimmte Ebenen
(Levels) eingeschränkt werden. Die Funktionen der darunter liegenden Ebenen (LowLevel-Functions) müssen in diesem Fall die Fehler weitermelden und dürfen keinesfalls
5. Vermeidung potentieller Schwachstellen
76
Falschergebnisse oder Systemabstürze erzeugen. Demnach ist in jeder Schicht für sich
ein Fehlermanagement festzulegen, auch wenn nur der Code der obersten Schicht (HighLevel-Code) die Fehler dem Benutzer meldet oder in Logdateien schreibt.
Die meisten Programmiersprachen erlauben es, Fehlercodes in Form von Rückgabewerten einfach zu ignorieren. In der Tat ist es sehr aufwändig, jeden Rückgabewert
einer Funktion explizit zu prüfen. Defensives Programmieren verlangt diese Prüfung
jedoch. Ein weiterer Mechanismus zur Meldung von Fehlern und Ausnahmezuständen
sind sogenannte Ausnahmen (Exceptions). Tritt innerhalb einer Funktion eine Ausnahmesituation ein, welche an dieser Stelle nicht behandelt werden kann, so wirft der
betroffene Code eine Ausnahme (to throw an exception). Beim Werfen einer Exception
wird die betreffende Funktion sofort abgebrochen. Darüberliegende Softwareschichten
sollten immer Codeteile zur Verfügung stellen, welche Ausnahmen der darunterliegenden Funktionen abfangen (to catch an exception). Ausnahmen können in fast allen
Hochsprachen mit geringem Programmieraufwand abgefangen werden. In C++1 stehen
zum Werfen der Ausnahmen das Schlüsselwort throw und zum Fangen der Ausnahmen
spezielle Schlüsselwortkombinationen wie try-catch-finally zur Verfügung.
Viele Systeme bieten auch Möglichkeiten an, nicht-abgefangene Ausnahmen zu verarbeiten (z.B. in C++ die Funktion std::unexpected()). Diese Funktionen bieten oft
die letzte Möglichkeit, Dateien und Speicher zu sichern und zu bereinigen, bevor die
Applikation terminiert wird. Jede Applikation sollte einen Mechanismus anbieten, der
unbehandelte Ausnahmen bearbeitet und ein kontrolliertes Terminieren der Applikation veranlasst.[36]
Die Behandlung der Fehler und Ausnahmen kann auf einige wichtige Good Practices
zusammengefasst werden, welche im Anhang E.3 aufgelistet sind.
5.3.4
Hilfen zur Fehlersuche
Nachdem eine defensive Programmierung vom Auftreten beliebiger Fehler ausgeht,
sollten auch Mechanismen vorgesehen werden, welche die Fehlersuche und Tests der
Software unterstützen. Rätzmann schreibt in [45] sogar von einem testorientierten
1
C, C# und Java bieten ähnliche Möglichkeiten.
5. Vermeidung potentieller Schwachstellen
77
Anwendungsdesign, welches Entwickler und Tester während der Entwicklungszyklen
unterstützen. Gerade während der Entwicklung hilft zusätzlicher Code (Debug Code)
alle außergewöhnlichen Zustände aufzudecken und dem Entwickler oder Tester weitere
Informationen über Probleme zu melden. Die Überprüfung der Integrität der Daten
und die ständige Überwachung des Systems erfordern Systemressourcen und können
sich dadurch auch während der Laufzeit deutlich bemerkbar machen. Dies ist auch
der Grund dafür, dass diese zusätzlichen Routinen oft nicht in der Endversion einer
Software verbleiben.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define DEBUG
[...]
switch( iValue )
{
case INCASE_A:
[...] // do something here
break;
case INCASE_B:
[...] // do something here
break;
default: // this should not happen
#ifdef DEBUG
// additional debug code should be here
[...]
// and assert to exit the application
assert("Variable iValue=%d. Variable is out-of-range.", iValue);
#endif
// if it happens log the error
LogBadValue("Illegal value in switch-case.")
}
Listing 5.1: Erweiterung zur Fehlersuche in einer DEBUG-Version
Realisiert wird die Einbindung von Debugcode mit Präprozessoren, wie sie auch in
C und C++ zur Anwendung kommen. Wird eine Applikation zum Beispiel mit dem
DEBUG-Flag kompiliert, werden zusätzliche Prüfroutinen, Code zur Fehlerbehandlung,
assert-Meldungen und Logdateieinträge erzeugt (siehe Beispiel im Listing 5.1).
5. Vermeidung potentieller Schwachstellen
78
McConnell beschreibt Methoden der sogenannten offensiven Programmierung“[36,
”
S.215ff.], welche auch von Howard et al. in [26] diskutiert werden. Offensives Programmieren ist nicht als das Gegenstück zur Defensiven Programmierung zu sehen,
sondern vielmehr eine Ergänzung dazu. Eine offensive Programmierung während der
Entwicklung fordert in Applikationen im Debugmode zusätzliche Funktionen, um Fehler schneller zu finden, zu provozieren oder einzugrenzen. Eingebunden werden diese
Codeteile auch über den Präprozessor, wie es im Listing 5.1, Zeile 12 gezeigt wird.[45]
Die Methoden der Defensiven und der Offensiven Programmierung erlauben nicht nur
die Entwicklung fehlerfreierer Applikationen, sondern eignen sich im selben Maße auch
zur Entwicklung sicherer Applikationen. Je früher diese Methoden in die Entwicklung
eingeführt werden, desto hilfreicher sind sie und desto fehlerfreier wird der Code letztendlich werden. Die Methoden zur Erleichterung der Fehlersuche können auf einige
wichtige Good Practices zusammengefasst werden, welche im Anhang E.4 aufgelistet
sind.
5.3.5
Sichere Bibliotheksfunktionen
Defensive Programmierung bedeutet die Erstellung von sicherem Code und sicheren
Funktionen. Werden Bibliotheken benutzt, so besteht nicht immer die Möglichkeit,
die Bibliotheksfunktionen den defensiven Programmierrichtlinien anzupassen oder den
Code auf Sicherheit zu überprüfen. Von der Änderung fremden oder zugekauften Quellcodes ist auch deshalb abzuraten, weil dadurch unter Umständen später erhältliche
Bugfixes2 und Erweiterungen der Hersteller nicht ohne weiteres übernommen werden
können. In diesem Fall empfiehlt sich vielmehr die Vermeidung aller als unsicher geltenden Funktionen, die Einführung von Bibliothekserweiterungen oder die Erstellung
von Ersatzfunktionen.
2
to fix (engl.) = reparieren, ausbessern. Mit Bugfix wird die Behebung eines Programmfehlers
bezeichnet, indem eine korrigierte Version verfügbar gemacht wird.
5. Vermeidung potentieller Schwachstellen
5.3.5.1
79
Fehlerfreie Bibliotheksfunktionen
Bibliotheksfunktionen benötigen besondere Sicherheitsvorkehrungen, da bei der Programmierung dieser Funktionen kaum klar ist, in welchem Kontext die Funktionen
später eingesetzt werden. In einer komplexen Softwarearchitektur ist jede Ebene für die
Sicherung der eigenen Daten und Schnittstellen verantwortlich. Der Programmierer von
Bibliotheksfunktionen kann nicht annehmen, dass in anderen Ebenen der Architektur
die Daten gesichert oder überprüft wurden. Hier gilt also umso mehr die Annahme,
alle Daten und Parameter als potentiell unsicher anzusehen. Die Daten sind eigenen
Sicherungsmethoden zuzuführen.
Eine Fehlerbehandlung ist so vorzunehmen, wie es für die Bibliotheksfunktion selbst
am besten ist. Es kann nicht davon ausgegangen werden, dass die aufrufende Funktion die Fehlerbehandlung übernimmt. Zur Behandlung der Fehler bieten sich zwei
Möglichkeiten an:
• Wird eine stabile Implementierung bevorzugt, so reicht es, die Bibliotheksfunktionen kontrolliert zu verlassen. Ein entsprechender Status- oder Fehlercode signalisiert dabei die Gültigkeit der Funktionsergebnisse oder die Art des aufgetretenen Fehlers (siehe Beispiel im Listing C.8). C und C++ Funktionen der
Standardbibliotheken und Betriebssysteme sind auf Stabilität ausgelegt.
• Wird eine stabile und korrekte Implementierung bevorzugt, so wird eine Exception ausgelöst und die Funktion an der Stelle des Fehlers abgebrochen (siehe Beispiel im Listing C.9). Die Bibliotheken moderner Programmiersprachen,
z.B. für Java und C#, melden Fehler meist über das Auslösen von Exceptions.
Fehler werden somit mit einem Funktionsabbruch quittiert und Ergebnisse nur
dann zurückgegeben, wenn die Funktionen zur Gänze fehlerfrei ausgeführt werden
konnten.
Grundsätzlich gilt die Signalisierung von Fehlern über Exceptions als sicherer. Erstens,
weil diese vom Entwickler nicht einfach ignoriert werden können, sondern in jedem Fall
5. Vermeidung potentieller Schwachstellen
80
Fehler behandelnden Code verlangen. Zweitens, weil Exceptions nicht so einfach manipuliert werden können wie etwa die Signalisierung von Fehlern mit globalen Variablen
und Error Codes 3 .
5.3.5.2
Bibliothekserweiterungen
In den letzten Jahren wurden eine Reihe der C-Standard- und POSIX-Funktionen
als unsicher eingestuft. Vor allem String- und Format-String Funktionen erlangten
durch Buffer Overflow Attacken traurige Bekanntheit. C/C++ Zeichenketten werden
mit keiner Längeninformation versehen, sondern ein 0-Byte schließt diese Zeichenketten ab. Aus diesem Grund verwenden die C-Standardfunktionen keine expliziten
Längenangaben, sondern verlassen sich darauf, dass die Zeichenketten richtig terminiert sind. Stringfunktionen kopieren Zeichenketten in einen Puffer, ohne dessen Größe
zu überprüfen. Einige typische Vertreter dieser unsicheren Funktionen sind:
char * strcpy( char *dest, const char *source);
char * strcat( char *dest, const char *source);
char * gets(char* buffer);
int
sprintf(char* buffer, const char*format,...);
Insgesamt wurden weit mehr als hundert Funktionen als unsicher eingestuft. Verschiedene Funktionen der Betriebssystem-API gelten mittlerweile ebenso als unsicher. Das
ISO/IEC Committee4 schlägt die Entwicklung und Verwendung alternativer Funktionen und Methoden vor, um diese Sicherheitslücken zu schließen. Für alle als unsicher geltenden Funktionen wurden Ersatzfunktionen definiert, welche die Schwächen
der alten Funktionsprototypen beheben. Eigene als unsicher geltende Funktionen sollten ebenfalls, dem Modell der ISO/IEC Empfehlungen folgend, gegen sichere Funktionen ausgetauscht werden. Berücksichtigt wurden in den Empfehlungen des Dokuments ISO/IEC TR 24731 aus dem Jahr 2005 [12] vor allem String- und StringformatFunktionen, Dateioperationen, Sortier- und Suchfunktionen, Zeitfunktionen und die
Verarbeitung von Multibyte-Characters.
3
Die Win32 Funktion GetLastError() gibt nur den Wert einer globalen Win32-Variablen zurück,
die mit SetLastError() gesetzt werden kann. Gelingt es einem Angreifer z.B. in einer Multi-Threading
Umgebung in einem anderen Thread den Wert mit SetLastError() beliebig zu manipulieren, so kann
er Fehlersignalisierungen beliebig unterdrücken oder auslösen.
4
http://www.open-std.org
5. Vermeidung potentieller Schwachstellen
81
Die neuen nach ISO definierten Funktionen verarbeiten zusätzliche Längenangaben bei
Strings und Puffern, geben einen Fehlercode zurück und brechen bei falschen Parametern die Funktion ab. Die Prototypen der Ersatzfunktionen für die oben als Beispiel
gezeigten Funktionen lauten wie folgt (vgl. [13, 12]):
errno_t strcpy_s( char *dest, size_t bufferSize, const char *source);
errno_t strcat_s( char *dest, sizte_t bufferSize, const char *source);
char * gets_s(char* buffer, size_t bufferSize);
int
sprintf_s(char* buffer, size_t bufferSize, const char*format,...);
Eine Liste aller unsicheren POSIX-Funktionen der Standardbibliotheken, für die Ersatzfunktionen definiert wurden, ist im Anhang A.1, die Empfehlungsdokumente der
ISO/IEC sind unter [12] und [13] zu finden. Unsichere Funktionen sind in jedem Fall
zu meiden bzw. gegen sichere Ersatzfunktionen zu tauschen, außer sie werden niemals
direkt aufgerufen, sondern ausschließlich über Wrapper.
5.3.5.3
Wrapper
Ein Wrapper bezeichnet in der Informatik eine Hülle über eine Klasse, eine Schnittstelle oder Daten. Wrapper bilden eine Barrikade (Barricade) um ein zu schützendes
oder zu kontrollierendes Objekt. Ein Wrapper ist eine Art Zwischenschicht, welche zwischen einen Aufrufer (Sender) und einen Aufgerufenen (Empfänger) geschoben wird.
Werden z.B. die unsicheren Methoden einer Klasse über eine öffentliche Schnittstelle
einer Wrapperklasse aufgerufen, so übernehmen die Methoden der Wrapperklasse die
Sicherung der Daten und den Aufruf der unsicheren Methoden. Sie können bei Bedarf
die Daten prüfen und korrigieren, bevor sie die unsicheren Methoden aufrufen. Nach
Erhalt der Ergebnisse können sie diese wiederum validieren, bei Bedarf konvertieren
und dann erst an die aufrufende Funktion weitergeben. Ein wesentlicher Vorteil dieser
Methode ist, dass damit der Fluss von fehlerhaften Daten (unsauberen Daten) bereinigt werden kann und die Auswirkungen von Fehlern weitgehend eingedämmt werden
können.[36]
Ein Grund für die höhere Stabilität von .NET-Applikationen verglichen mit nativen
Windows-Applikationen ist die konsequente Verwendung der Wrapper-Technologie. In
5. Vermeidung potentieller Schwachstellen
82
den .NET-Bibliotheken der Common Language Runtime (CLR) werden alle Parameter und Datenflüsse zwischen den Applikationen und der Windows-API einer strikten
Überprüfung zugeführt. Funktionsaufrufe und Daten, welche zu Fehlern und Buffer
Overflow Schwachstellen führen, sind damit kaum möglich, da sie die Barrikaden der
.NET Bibliotheken nur noch schwer durchbrechen können.
5.4
Sicherere Programmiersprachen
Die Defensive Programmierung verlangt vom Softwareentwickler eine aktive Teilnahme am Sicherungsprozess einer Software. Dies wiederum setzt entsprechende Kenntnisse und ein entsprechendes Sicherheitsbewusstsein der Entwickler voraus. Viele der
in dieser Arbeit besprochenen Sicherheitsmängel basieren auf C und C++ spezifischen
Möglichkeiten der Speicherverwaltung und -manipulation. Weitere Möglichkeiten potentielle Schwachstellen in Programmen zu vermeiden, sind der bewusste Verzicht der
direkten Speichermanipulation innerhalb von C/C++ Programmen, die Verwendung
alternativer und als sicherer geltender Programmiersprachen oder die Kapselung der
Programme in geschützten Umgebungen. Als sicherere Programmiersprachen gelten
die Sprachen der .NET Technologie und Java, welche ohne Zeiger auskommen und
lediglich mit verwaltetem Speicher und Code arbeiten.
5.4.1
Sichereres C und C++
Wie in der technischen Einführung und im Kapitel 3 angeführt, sind Zeigerfehler
und eine fehlerhafte Stringverwaltung oft die typischen Gründe für eine Softwaresicherheitsschwachstelle. Ein falscher Datenzeiger erlaubt mitunter die Manipulation
von Speicher auf dem Stack und Heap und die Änderung einer Sprungadresse. Ein Pufferüberlauf überschreibt andere Speicherbereiche und ein fehlender 0-Character zum
Abschluss eines Strings kann Bibliotheksfunktionen zum Absturz bringen.
Die Fehleranfälligkeit der C-Funktionen und der Aufwand, der notwendig ist, um
C-Programme fehlertolerant zu machen, wurden schon früh als Schwächen der Programmiersprache C erkannt. Mit dem Verzicht auf die direkte Speichermanipulation
5. Vermeidung potentieller Schwachstellen
83
und auf Zeiger scheint eine mögliche Lösung gefunden zu sein. Einige Programmiersprachen (Java, C#) verzichten ganz auf Zeiger, andere setzen auf entsprechende Erweiterungen und Ersatzlösungen.
Als objektorientierte Weiterentwicklung von C bieten C++ und seine C++ Standardbibliotheken zusätzliche Typen, Klassen und Methoden an. Die Standard Template
Library 5 (STL) bietet generische Container, Iteratoren, Algorithmen und Funktionen
zur Verwaltung von Daten in Listen, Arrays, Sets, Maps, usw.. In der später aus der
STL hervorgegangenen und standardisierten C++ Standardbibliothek 6 wurde auch der
Typ string eingeführt, um die Speicherung und Manipulation von Zeichenketten zu
erleichtern. Die Klasse string verwaltet den Speicher und die Länge des Strings. Stringfehler aufgrund fehlender 0-Character sind damit also nicht mehr möglich (siehe Listing
C.6, Zeilen 6ff). Zur Verwaltung von Zeigern wurden sogenannte Smart-Pointer, in der
STL mit dem Template auto_ptr implementiert (siehe Listing C.6, Zeile 22), eingeführt. Smart-Pointer übernehmen die intelligente Verwaltung der Zeiger und die
Freigabe der Ressourcen (z.B. Speicher, Handles). Verlässt ein Smart-Pointer den
Gültigkeitsbereich, so wird automatisch der Destruktor des Pointers aufgerufen und
darin der allokierte Speicher freigegeben.
All diese Sprach- und Bibliothekserweiterungen haben das gemeinsame Ziel, den Softwareentwicklern effiziente und getestete Funktionen zur Verwaltung von Daten anzubieten und fehleranfällige Routinen durch Standardroutinen zu ersetzen. Mit den
Klassen und Templates der C++ Standardbibliothek kann auf Zeiger und die direkte
Bearbeitung von Speicher weitgehend verzichtet werden.
5.4.2
Managed Code und Managed Memory
Die Verwendung von string, auto_ptr und einer Reihe weiterer Features der STL
und von C++ erlauben die Entwicklung von Applikationen, die deutlich stabiler und
5
http://www.sgi.com/tech/stl/
Die heute gültige C++ Standardbibliothek hat die ursprünglich von Hewlett-Packard seit 1971
entwickelte STL nicht zur Gänze übernommen, sondern nur Teile davon. In einigen Details unterscheidet sich die C++ Standardbibliothek von der ursprünglichen STL, die im Jahr 1993 dem C++
Standardisierungskomitee zur Verfügung gestellt wurde. Weiters hat erst die C++ Standardbibliothek Klassen zur Verwaltung von Zeichenketten (Strings) und Ein- und Ausgabeströme (Streams)
aufgenommen.
6
5. Vermeidung potentieller Schwachstellen
84
sicherer sind. Die Entwickler können diese Features wahlweise nutzen, eine defensive
Programmierung, die Verwendung einer STL oder sicherer Typen sind in C++ aber
letztendlich freiwillig.
Anders ist dies bei höheren Programmiersprachen, wie z.B. Pascal, Modula-2 und
Oberon. Sie unterstützen ein Bounds Checking während der Compile- oder Run-Time.
In Java und C# fehlen Zeiger gänzlich und Daten können nur in Collections 7 verwaltet
werden. Zusätzlich laufen Programme dieser Sprachen in einem streng kontrollierten
Prozessraum ab. Jeder Zugriff auf Daten außerhalb legaler Grenzen führt ausnahmslos zu einer Ausnahme. Den Code dazu erzeugen die Compiler automatisch. Speicherüberläufe oder Array Indexing Overflows, wie einer im Listing C.7 gezeigt wird,
quittiert die .NET CLR mit folgender Exception:
’OverrunApp.vshost.exe’ (Managed): Loaded ’[...]\OverrunApp.exe’,
Symbols loaded.
A first chance exception of type ’System.IndexOutOfRangeException’ occurred
in OverrunApp.exe
In der Java Virtual Machine (JVM) und unter .NET sind alle Speicherzugriffe und
Manipulationen streng kontrolliert. JVM als auch .NET mit seiner Language Runtime
Infrastructure (standardisiert nach ISO/IEC 23271:20068 oder Standard ECMA-3359 )
verwalten den Speicher selbständig und bieten eine mehrstufige Garbage Collection,
bei der unbenutzter Speicher selbständig freigegeben wird und somit Memory Leaks
wirkungsvoll verhindert werden.
Um diese Code- und Speicherkontrolle zu gewährleisten, arbeiten sowohl Java als auch
das .NET Framework nach dem sogenannten Write Once Run Anywhere (WORA)Prinzip. Dabei wird vom Compiler ein plattformunabhängiger Zwischencode (Common
Intermediate Language - CIL) erzeugt, der erst unmittelbar vor der Ausführung von
einem Just-in-Time (JIT) Compiler in plattformabhängigen Code übersetzt wird. Diese
Vorgehensweise erlaubt die maximale Kontrolle sowohl über den verwalteten Code
(Managed Code) als auch über den Speicher der Applikation.
7
Collections entsprechen den Containern der STL. Bei Java und C# haben sich aber etwas andere Terminologien durchgesetzt. In beiden wird z.B. nicht von Templates gesprochen, sondern von
sogenannten Generics. In der Anwendung sind diese Konzepte durchaus ähnlich, auch wenn C++
Templates leistungsfähiger sind.
8
http://www.iso.org/iso/en/CatalogueDetailPage.CatalogueDetail?CSNUMBER=42927
9
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf
5. Vermeidung potentieller Schwachstellen
5.5
85
Zusätzliche Techniken
Neben der Absicherung der Software auf Codebasis und einem sicheren Design gibt
es noch eine Reihe weiterer Methoden und Einflussgrößen, welche die Sicherheit einer Software fördern oder beschränken. Diese werden im Rahmen dieser Arbeit nicht
ausführlicher behandelt, sollten aber dennoch nicht gänzlich unerwähnt bleiben. Dazu
gehören [25, 26, 45]:
• Code Reviews: z.B. Prüfungen, ob die Regeln des defensiven Programmierens
eingehalten wurden, nur sichere API-Funktionen verwendet wurden, die Sicherheitsvorgaben entsprechend umgesetzt wurden.
• Softwaretests: z.B. spezielle Unit-, Modul- und Integrationstests, Stresstests,
Tests der Rechte- und Zugriffsverwaltung, spezielle Sicherheitstest.
• Dokumentation: z.B. Dokumentation der Security Requirements, der Threat
Models, Nachrichten, Privilegien, Dateiformate und -rechte; Dokumentation von
Best Practices für Entwickler, Administratoren und Benutzer.
• Softwareinstallation und -konfiguration: z.B: Einhaltung des Prinzips der
minimalen Rechte, vollständige Installation und Deinstallation, Installationsvoraussetzungen.
• Patch-Management und Bug-Fixing: z.B. in welchen Zyklen und wie Sicherheitslücken gehandhabt werden.
Die Entwicklung und der Betrieb sicherer Software steht oder fällt nicht allein mit
der Sicherheit einer Applikation. Sicherheit muss im Kontext des Gesamtsystems und
des gesamten Entwicklungszyklus gesehen und gewährleistet werden und kann nicht
isoliert betrachtet werden. Demnach umfasst sie immer den gesamten Entwicklungsund Lebenszyklus einer Software (vgl. [26]).
Einige weitere Techniken befassen sich mit der Einführung einer Softwaresicherung,
bei der die Software mit in das Betriebs- oder Laufzeitsystem integrierte Methoden
nachträglich abgesichert wird. Aus der Sicht des Softwareentwicklers handelt es sich
dabei um eine passive Sicherung, da diese nicht Teil des Softwarekonzepts oder einer
5. Vermeidung potentieller Schwachstellen
86
aktiven Implementierung sind. Einige dieser Methoden (z.B. der nachträgliche Austausch von Bibliotheken, zusätzliche Wrapper) wurden auch im Kapitel 4.4 schon besprochen. Hierbei handelt es sich oft nur um die Minimierung eines etwaigen Schadens.
Nachdem dabei nicht von einer wirklichen Vermeidung von Schwachstellen gesprochen
werden kann, wurde diese Form der Sicherung in dieser Arbeit nicht weiter verfolgt
(siehe dazu auch Kapitel 6.3).
5.6
Bewertung der Methoden
Die gewonnene Sicherheit aufgrund defensiver Programmierung, Wrapper und der Verwendung sicherer Klassen und Funktionen ist groß. Automatische, durch den Compiler
eingefügte Sicherungsroutinen, spezielle Bibliotheken und kontrollierte Umgebungen
erhöhen die Sicherheit weiter, ohne dass die Entwickler dafür einen zusätzlichen Aufwand treiben müssten. Sichere Konzepte und Programmiersprachen (z.B. Java, C#)
verzichten auf besonders unsichere und fehleranfällige Konstruktionen wie Zeiger und
eine direkte Speichermanipulation. Bei der Einführung einer konsequenten defensiven
Implementierung zeigt sich schnell, dass die Wirkung einer aktiven Sicherung von Software als Teil des Entwicklungsprozesses deutlich größer ist, als durch spätes Hinzufügen
externer Sicherungsmethoden. Dies bedarf natürlich eines entsprechenden Aufwands
sowohl in der Designphase als auch in der Implementierung, sowie der Ausbildung der
Entwickler.
Zusätzliche Überprüfungen und weitere Softwareschichten verlangen zusätzliche Ressourcen. Performanceverluste und ein erhöhter Speicherbedarf wegen zusätzlichem Code
treten aber aufgrund steigender Leistung der Systeme immer mehr in den Hintergrund.
Nichtsdestotrotz ist gerade bei Systemen mit geringen Ressourcen der Einsatz komplexer Bibliotheken, von Managed Code und moderner Programmiersprachen nicht
möglich. Im Bereich der Entwicklung von Embedded Systems sind C und C++ derzeit noch immer die Programmiersprachen der ersten Wahl. In diesen Fällen können
sich Entwickler nicht auf zusätzliche Sprachfeatures und virtuelle Umgebungen verlassen. Hier ist die Defensive Programmierung, ein sicheres Design und ein entsprechendes
Maß an Misstrauen noch immer die einzige Möglichkeit, sichere Software zu entwickeln.
5. Vermeidung potentieller Schwachstellen
87
Dass sich dies bald ändern könnte, zeigen die letzten Entwicklungen des .NET Micro
Frameworks 10 , welches die .NET CLR speziell für Embedded Systems adaptiert hat
und sogar als Betriebssystemersatz verwendet werden kann.
10
http://msdn2.microsoft.com/en-us/embedded/bb267253.aspx
6
Zusammenfassung und Ausblick
Im Rahmen dieser Arbeit wurde untersucht, mit welchen Methoden und Werkzeugen
Sicherheitsschwachstellen lokalisiert und vermieden werden können. Die folgende Zusammenfassung gibt einen Überblick über die Erkenntnisse der Untersuchungen. Der
darauf folgende Ausblick schließt mit Vorschlägen ab, wie eine weiterführende Arbeit
zu diesem Thema aussehen könnte.
6.1
Zusammenfassung
Die Lokalisierung potentieller Sicherheitsschwachstellen (Kapitel 4) mittels manueller Audits erfordert eine hohe fachliche Qualifikation der Entwickler und Auditoren. Manuelle Audits sind fehleranfällig und aufwändig. Quelltextbasierte und statische
Analysen (Kapitel 4.2) mit Werkzeugen haben gezeigt, dass es zumindest einer semantischen Prüfung bedarf, um gute Ergebnisse zu erhalten. Mit ausschließlich lexikalischen Analysen des Quelltextes sind komplexe Mängel nicht aufzuspüren. Die meisten
Schwachstellen konnte Splint und der Microsoft C/C++ Compiler aufdecken, der Rest
der getesteten Werkzeuge konnte nicht wirklich überzeugen. Keines der getesteten Analysewerkzeuge erkannte alle Schwachstellen und die meisten Race Conditions blieben
von den getesteten Source Code Analyzern weitgehend unentdeckt. Binärcodebasierte
und dynamische Analysen (Kapitel 4.3) einer Software führten dann zum Erfolg, wenn
88
6. Zusammenfassung und Ausblick
89
gezielt nach bestimmten Schwachstellen geforscht wurde. Dies erfordert von den Auditoren sehr gute Fachkenntnisse über Systemarchitekturen und Softwaredesign. Werkzeuge gibt es für beliebige Anwendungsfälle. Erschwerend kommt hinzu, dass sich nur
bestimmte Analysen automatisieren lassen. Untersuchungen mit einem Debugger bleiben weitgehend Handarbeit auf hohem Niveau. Sicherheitstests mittels Fault Injection
lassen sich hingegen gut automatisieren und Unit Tests sind schon früh und einfach in
den Entwicklungsprozess integrierbar.
Die Vermeidung von Sicherheitsschwachstellen (Kapitel 5) erfordert ein sicheres Design (Kapitel 5.2) und die Entwicklung einer fehlerfreien Implementierung. Die
Defensive Programmierung (Kapitel 5.3), welche Fehler während des Programmablaufs bewusst annimmt, erweist sich gerade in Bezug auf die Entwicklung sicherer
Software als unverzichtbar, wenngleich auch als aufwendig. Die Überprüfung aller Eingaben ist genauso wichtig, wie ein kontrollierter Ablauf im Ausnahmefall. Sicherere
Programmiersprachen (Kapitel 5.4) und abgesicherte Bibliotheken verlagern Teile der
defensiven Strategien in von Compilern generierten Code und in Laufzeitumgebungen.
Dies inkludiert meist auch den Verzicht von Zeigern. Das Ergebnis ist eine wesentlich
stabilere und sicherere Software, wie gerade Java und .NET Programme zeigen.
Die durch ein sicheres Design und durch eine konsequente defensive Programmierung
gewonnene Sicherheit kann durch nachträglich installierte Sicherheitspakete, Stack- und
Speicherüberwachungen und Compilererweiterungen (Kapitel 4.4) kaum erreicht werden. Diese Methoden basieren fast immer auf der Detektion von Fehlern erst während
der Laufzeit und der nachträglichen Einschränkung negativer Auswirkungen. Wirkungsvolle Sicherheit ist aber vorweg in die Software zu integrieren und im Quellcode
abgebildet. Dass die Entwicklung sicherer Software ein ganzheitliches Konzept voraussetzt, welches alle Entwicklungszyklen umfasst, wurde im Kapitel 5.5 einmal mehr
betont.
Die Frage, ob mit den heutigen Methoden fehlerfreie und sichere Software entwickelt
werden kann, ist mit einem Ja-Aber“ zu beantworten. Ja, es ließe sich auch heute
”
schon fehlerfreie und sichere Software entwickeln, aber nur mit großem technischen
und personellen Einsatz. Dies lässt viele Softwareproduzenten untätig zuwarten.
6. Zusammenfassung und Ausblick
6.2
90
Ausblick
Die Untersuchungen haben gezeigt, dass die Ergebnisse quelltextbasierter Analysewerkzeuge mehrheitlich unbefriedigend sind. Die einfachen Quelltexte der fiktiven Testszenarien entsprechen in ihrer Komplexität kaum denen realer Software. Dementsprechend
gering sind die Treffer bei der Analyse einer realen Anwendung. Es ist zu befürchten,
dass ein großer Teil sicherheitskritischer Fehler somit unentdeckt bleibt. Ein vergleichender Test der Werkzeuge ist derzeit nicht möglich. Wie unter Kapitel 4.2.3 schon
begründet, muss vorher ein umfangreicher und standardisierter Pool realistischer Testszenarien geschaffen werden, um die Analysewerkzeuge objektiv bewerten zu können.
Der Schwerpunkt dieser Arbeit beschäftigt sich mit der Analyse von Quelltext und mit
der Vermeidung von Fehlern im Quelltext einer Applikation. Geht man davon aus, dass
zukünftige Applikationen vermehrt mit sichereren Programmiersprachen für gemanagte Laufzeitumgebungen entwickelt werden, so werden die typischen Implementierungsfehler deutlich abnehmen und Designfehler dementsprechend an Bedeutung gewinnen.
Zukünftige Testszenarien müssten sich mehr noch mit Thread Modeling, Security Flaws
und Black Box Tests beschäftigen, denn diese sind vermutlich die verbleibenden Sicherheitsschwachstellen der näheren Zukunft.
6.3
Trends
Zur Erstellung sicherer Software ist technologisch bereits viel möglich. In alle Systeme
haben diese Technologien aber noch nicht Einzug gefunden. Einige der wesentlichen
Entwicklungen zeigen die Trends bei der Sicherung der Softwaresysteme:
• Managed Code - Mit der Einführung von Java und der Java Virtual Machine
(JVM) und später mit den .NET Sprachen und der .NET Common Language
Infrastructure (ISO/IEC 23271:2006151 oder Standard ECMA-335162 ) wurden
sicherere Speicherverwaltungen mit einer Garbage Collection etabliert.
1
2
http://www.iso.org/iso/en/CatalogueDetailPage.CatalogueDetail?CSNUMBER=42927
http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-335.pdf
6. Zusammenfassung und Ausblick
91
• Virtuelle Umgebungen und Sandboxes - Sie kapseln und schützen ganze Betriebssystemumgebungen (z.B. VMWare3 ), ausgewählte Applikationen und ihre
Daten (z.B. Altiris Software Virtualization Solution4 ) oder Applikationen, welche
in speziellen Programmiersprachen geschrieben wurden (z.B. Java mit der JVM,
C# mit der CLI).
• Nicht-ausführbarer Speicher - In speziell markierten Speichersegmenten wird
die Ausführung von Code verhindert. Diese Methode erschwert die Ausführung
von eingeschleustem Code (umgesetzt z.B. in Non-executable Stack in OpenWall5 , Non-executable Pages in PaX6 , ExecShield7 ).
• Schutz der Prozessumgebung - Der Zugriff auf Systemobjekte und -ressourcen
wird kontrolliert oder verhindert (z.B. Mandatory Access Control (MAC) implementiert in SELinux8 , AppArmor9 , TrustedBSD10 ).
• Hardware basierte Schutzmethoden - Markierung von Segmenten oder Teilen des Speichers als Non-executable (z.B. für moderne x86- Architekturen (32/64
bit) unterstützt AMD die No-Execute-Bits und Intel die Execute-Disable-Bits)[28].
Mittlerweile unterstützen alle modernen Betriebssysteme das hardwarebasierte
Address Space Layout Randomization (ASLR).
• Managed Betriebssysteme - Einige Hersteller und Forscher arbeiten heute
schon an möglichen Betriebssystemen von morgen. Nach deren Vorstellung sollten
diese nur noch aus Managed Code bestehen (z.B. Microsofts Singularity11 ) und
auf Basis virtualisierter Systeme arbeiten.
Die steigende Komplexität der Softwaresysteme legt nahe, dass die Entwicklung dieser immer mehr automatisiert und dem Computer überlassen werden wird. Automatisch generierter Code, Komponententechnologien, neue Programmiersprachen und
3
http://www.vmware.com
http://www.altiris.com
5
http://www.openwall.com
6
http://pax.grsecurity.net
7
http://www.redhat.com
8
http://www.nsa.gov/selinux
9
http://de.opensuse.org/Apparmor
10
http://www.trustedbsd.org
11
http://research.microsoft.com/os/singularity/
4
6. Zusammenfassung und Ausblick
92
-konzepte und virtualisierte Umgebungen sollen die Sicherheit erhöhen. Auffallend dabei ist, dass sich ein wesentlicher Teil der heutigen Lösungsansätze mehr mit der Minimierung der Auswirkungen von Fehlern beschäftigt als mit den eigentlichen Ursachen
der Probleme. Offensichtlich erscheint die Entwicklung fehlerfreier Software heute noch
vielen unmöglich.
Es ist anzunehmen, dass neben Buffer Overflows auch die unmanaged Sprachen, Umgebungen und manuellen Methoden mehr und mehr der Vergangenheit angehören werden.
Ob die Designer und Entwickler gänzlich von der Aufgabe entbunden werden können
sichere Software zu entwickeln, ist fraglich. Ausgenommen von allen Betrachtungen in
dieser Arbeit bleibt der Unsicherheitsfaktor Mensch. Ein Großteil der Angriffe auf Computersysteme basiert immer noch auf Social Engineering. Die notwendige Veränderung
in Richtung eines Sicherheitsbewusstseins betrifft also nicht nur Administratoren, Designer und Entwickler, sondern auch den einfachen Benutzer.
Literaturverzeichnis
[1] Aleph One (Pseudonym): Smashing The Stack For Fun And Profit. Phrack, 7(49),
November 1996.
URL: http://www.phrack.org/archives/49/P49-14 (Stand: 1. Marz 2007).
[2] Anonymous: hacker’s guide - Sicherheit im Internet und im lokalen Netz. Markt &
Technik, Buch- und Software-Verlag, Haar bei München, 1999, ISBN 3827254604.
[3] Bachfeld, D.: Die Axt im Walde - Einführung in Fuzzing Tools. Heise Security,
August 2006.
URL: http://www.heise.de/security/artikel/76512 (Stand: 13. Juni 2007).
[4] Baratloo, A., N. Singh, and T. Tsai: Libsafe: Protecting critical elements of
stacks, December 1999.
URL: http://pubs.research.avayalabs.com/pdfs/ALR-2001-019-whpaper.
pdf (Stand: 19. April 2007).
[5] Bishop, M.: Race Conditions, Files, and Security Flaws; or the Tortoise and the
Hare Redux, 1995.
URL: http://citeseer.ist.psu.edu/bishop95race.html (Stand: 18. Mai
2007).
[6] Bishop, M. and M. Dilger: Checking for Race Conditions in File Accesses.
Computing Systems, 9(2):131–152, 1996.
URL: http://citeseer.ist.psu.edu/article/bishop96checking.html
(Stand: 18. Mai 2007).
93
Literaturverzeichnis
94
[7] Blanc, B. and B. Maaraoui: Endianness or Where is Byte 0? - White Paper,
2005.
URL: http://3bc.bertrand-blanc.com/endianness05.pdf (Stand: 1. Mai
2007).
[8] Bray, B.: Compiler Security Checks In Depth - Anatomy of the x86 Stack. Visual
Studio Technical Article, 2002.
URL: http://msdn2.microsoft.com/en-us/library/aa290051(VS.71).aspx#
vctchcompilersecuritychecksindepthanchor3 (Stand: 7. April 2007).
[9] Bray, B.: Compiler Security Checks In Depth - Run-Time Checks & What /GS
Does. Visual Studio Technical Article, 2002.
URL: http://msdn2.microsoft.com/en-us/library/aa290051(VS.71).aspx#
vctchcompilersecuritychecksindepthanchor4 (Stand: 21. Februar 2007).
[10] Chess, B. and G. McGraw: Static analysis for security. IEEE Security and Privacy,
2(6):76–79, 2004, ISSN 1540-7993.
[11] Committee, ISO/IEC: ISO/IEC 9899:TC2 - Programming languages - C
(ISO/IEC Comittee Draft 2005). ISO/IEC Committee, May 2005.
URL: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf
(Stand: 12. Februar 2007).
[12] Committee, ISO/IEC: ISO/IEC TR 24731 -Extension to the C Library, Part I:
Bounds-checking interfaces (Document WG14 N1114).
ISO/IEC Committee,
April 2005.
URL: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1173.pdf
(Stand: 18. Mai 2007).
[13] Committee, ISO/IEC: Specification for Safer, More Secure C Library Functions
(ISO/IEC Draft Technical Report, Ref.: TR 24731).
ISO/IEC Committee,
September 2005.
URL: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1135.pdf
(Stand: 25. April 2007).
Literaturverzeichnis
95
[14] Committee, TIS: Tool Interface Standard (TIS), Executable and Linking Format
(ELF), Specification, Version 1.2. TIS Committee, May 1995.
URL: http://refspecs.freestandards.org/elf/elf.pdf (Stand: 29. April
2007).
[15] Conover, M. and w00w00 Security Team: w00w00 on heap overflows, 1999.
URL: http://www.w00w00.org/files/articles/heaptut.txt (Stand: 11.
April 2007).
[16] Corporation, Microsoft: Microsoft Portable Executable and Common Object File
Format Specification, Revision 8.0. Microsoft Corporation, May 2006.
URL: http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.
mspx (Stand: 29. April 2007).
[17] Cowan, C., M. Barringer, S. Beattie, G. Kroah-Hartman, M. Frantzen, and
J. Lokier: FormatGuard:
Automatic Protection From printf Format String
Vulnerabilities. In Usenix Security Symp., pages 191–200, August 2001.
URL: http://citeseer.ist.psu.edu/cowan01formatguard.html (Stand: 19.
April 2007).
[18] Cowan, C., C. Pu, D. Maier, J. Walpole, P. Bakke, S. Beattie, A. Grier, P.
Wagle, Q. Zhang, and H. Hinton: StackGuard: Automatic Adaptive Detection
and Prevention of Buffer-Overflow Attacks.
In Proc. 7th USENIX Security
Conference, pages 63–78, San Antonio, Texas, January 1998.
URL: http://citeseer.ist.psu.edu/cowan98stackguard.html (Stand: 3.
März 2007).
[19] Evans, D.: Splint manual (2002), 2002.
URL: http://citeseer.ist.psu.edu/evans02splint.html (Stand: 2. Juni
2007).
[20] Evans, D. and D. Larochelle: Improving security using extensible lightweight static
analysis. IEEE Software, 19(1):42–51, 2002.
URL: http://citeseer.ist.psu.edu/evans02improving.html (Stand: 2.
Juni 2007).
Literaturverzeichnis
96
[21] Flake, H.: Third Generation Exploitation - Smashing the Heap under Win2k,
2002.
URL: https://www.blackhat.com/presentations/win-usa-02/
halvarflake-winsec02.ppt (Stand: 12. März 2007).
[22] Gallagher, T., L. Landauer, and B. Jeffries: Hunting Security Bugs. Microsoft
Press, Redmond, Washington, USA, 2006, ISBN 073562187X.
[23] Herold, H. und J. Arndt: C-Programmierung unter Linux. Beispiele, Anwendung und Programmiertechniken.
SuSE Press, Nürnberg, Germany, 2001,
ISBN 3935922086.
[24] Hoglund, G. and G. McGraw: Exploiting Software: How to Break Code (AddisonWesley Software Security Series).
Addison-Wesley Professional, Boston, USA,
2004, ISBN 0201786958.
[25] Howard, M. und D. LeBlanc: Sichere Software programmieren. Microsoft Press
Deutschland, Unterschleissheim, Germany, 2002, ISBN 386063674X.
[26] Howard, M. and D.C. LeBlanc: Writing Secure Code, Second Edition. Microsoft
Press, Redmond, Washington, USA, 2nd edition, 2002, ISBN 0735617228.
[27] IEEE: The Open Group Base Specifications Issue 6 (IEEE Std 1003.1, 2004
Edition). Institute of Electrical and Electronics Engineers, Inc., 2004.
URL: http://www.opengroup.org/onlinepubs/009695399/mindex.html
(Stand: 29. April 2007).
[28] Intel Corporation: Intel 64 and IA-32 Architectures Software Developer’s Manuals,
2007.
URL: http://www.intel.com/products/processor/manuals/index.htm
(Stand: 16. März 2007).
[29] Kaplan, Y.: API Spying Techniques for Windows 9x, NT and 2000, 2000.
URL: http://www.internals.com/articles/apispy/apispy.htm (Stand: 13.
Juni 2007).
Literaturverzeichnis
97
[30] Karsten, G. und T. Zilm: Bash 3.0 GE-PACKT. Mitp-Verlag, Landsberg, 2. Auflage, 2005, ISBN 3826615549.
[31] Klein, T.: Buffer Overflows und Format-String-Schwachstellen. Funktionsweisen,
Exploits und Gegenmaßnahmen.
dpunkt Verlag, Heidelberg, Germany, 2003,
ISBN 3898641929.
[32] Klog (Pseudonym): The Frame Pointer Overwrite.
Phrack, 9(55), September
1999.
URL: http://www.phrack.org/archives/55/P55-08 (Stand: 3. Mai 2007).
[33] Lee, K. and S. Chapin: Buffer Overflow and Format String Overflow Vulnerabilities. Software Practice Experience, 33(5):423–460, 2003, ISSN 0038-0644.
URL: http://citeseer.ist.psu.edu/lhee02buffer.html (Stand: 12. März
2007).
[34] Litchfield, D.: Windows 2000 Format String Vulnerabilities, 2001.
URL: http://www.nextgenss.com/papers/win32format.doc (Stand: 7. Mai
2007).
[35] Maguire, S.: Writing Solid Code: Microsoft’s Techniques for Developing Bug-Free
C Programs (Microsoft Programming Series). Microsoft Press, Redmond, Washington, USA, 1993, ISBN 1556155514.
[36] McConnell, S.: Code Complete - Deutsche Ausgabe der Second Edition.
Mi-
crosoft Press Deutschland, Unterschleissheim, Germany, 2. Auflage, 2005,
ISBN 386063593X.
[37] McKay, E.: Writing Error Messages for Security Features.
MSDN - Security
Technical Articles, 2002.
URL: http://msdn.microsoft.com/library/default.asp?url=/library/
en-us/dnsecure/html/securityerrormessages.asp (Stand: 15. Juni 2007).
[38] Microsoft: .NET Framework and Language Features - C++ Runtime Security
Checks. Visual Studio Professional Guided Tour, 2006.
Literaturverzeichnis
98
URL: http://msdn.microsoft.com/vstudio/tour/vs2005_guided_tour/
VS2005pro/Framework/CPlusRuntimeSecurity.htm (Stand: 10. April 2007).
[39] Microsoft Corporation: MSDN Library - Run-Time Library Reference - Deprecated CRT Functions, 2005.
URL: http://msdn2.microsoft.com/en-us/library/ms235384(VS.80).aspx
(Stand: 11. April 2007).
[40] Microsoft Corporation: MSDN Library - Run-Time Library Reference - Security
Enhancements in the CRT, 2005.
URL: http://msdn2.microsoft.com/en-us/library/8ef0s5kh(VS.80).aspx
(Stand: 11. April 2007).
[41] Microsoft Corporation: WHDC Home - WDK and Developer Tools - Testing
Tools - PREfast Annotations, 2007.
URL: http://www.microsoft.com/whdc/DevTools/tools/annotations.mspx
(Stand: 29. Mai 2007).
[42] Microsoft Corporation: WHDC Home - WDK and Developer Tools - Testing
Tools - PREfast for Drivers, 2007.
URL: http://www.microsoft.com/whdc/devtools/tools/PREfast.mspx
(Stand: 29. Mai 2007).
[43] Microsoft Lab: Writing Secure Native Code with Visual C++.
In Microsoft
Tech*Ed 2006, page 16ff, May 2006.
URL: http://download.microsoft.com/download/7/0/9/
70964f31-bac7-4379-b8bf-17ef35301ace/TECHEDSEA2006_SLIDES/HOL/
DEV011_Manual.doc (Stand: 12. März 2007).
[44] Nick, J.: List of tools for static code analysis, May 2007.
URL: http://en.wikipedia.org/wiki/User:Nickj/List_of_tools_for_
static_code_analysis (Stand: 17. Mai 2007).
[45] Rätzmann, M.: Software-Testing - Rapid Application Testing, Softwaretest, Agiles
Qualitätsmanagement. Galileo Press, Bonn, 2004, ISBN 3898422712.
Literaturverzeichnis
99
[46] Richarte, G.: Four different tricks to bypass StackShield and StackGuard protection, 2002.
URL: http://downloads.securityfocus.com/library/StackGuard.pdf
(Stand: 16. Februar 2007).
[47] Schmaranz, K.: Softwareentwicklung in C. Springer, Berlin, Heidelberg, New York,
2001, ISBN 3540419586.
[48] Schmaranz, K.: Softwareentwicklung in C++. Springer, Berlin, Heidelberg, New
York, 2003, ISBN 3540443436.
[49] Shankar, U., K. Talwar, J.S. Foster, and D. Wagner: Detecting Format String
Vulnerabilities with Type Qualifiers. In Proceedings of the 10th USENIX Security
Symposium, 2001.
URL: http://www.usenix.org/publications/library/proceedings/sec01/
shankar.html (Stand: 3. Juni 2007).
[50] [email protected] (Pseudonym): Bughunter Security Papers - Format String
Exploitation Techniques, 2001.
URL: http://doc.bughunter.net/format-string/technique.html (Stand: 7.
Mai 2007).
[51] Srinivasan, S.: Advanced Perl Programming.
O’Reilly Media, Inc., Cambridge,
1st edition, 1997, ISBN 1565922204.
[52] Stroustrup, B.: Die C ++ Programmiersprache. 2.überarbeitete Auflage. Addison
Wesley Verlag, Bonn München Paris, 2. Auflage, 1992, ISBN 3893193863.
[53] Swiderski, F. and W. Snyder: Threat Modeling (Microsoft Professional). Microsoft
Press, Redmond, Washington, USA, 2004, ISBN 0735619913.
[54] Viega, J., J.T. Bloch, Y. Kohno, and G. McGraw: ITS4: A static vulnerability
scanner for C and C++ code. acsac, 00:257, 2000, ISSN 1063-9527.
[55] Voas, J.: Fault injection for the masses.
ISSN 0018-9162.
Computer, 30(12):129–130, 1997,
Literaturverzeichnis
100
URL: http://www.cigital.com/papers/download/fi-masses.ps (Stand:
12.März 2007).
[56] Vromans, J.: Perl - kurz & gut. O’Reilly, Köln, Paris, Cambridge, 4. Auflage, 2003,
ISBN 3897212471.
[57] Wheeler, D.: Flawfinder Documentation, 2004.
URL: http://www.dwheeler.com/flawfinder/flawfinder.pdf (Stand: 12.
März 2007).
[58] Wilander, J. and M. Kamkar: A Comparison of Publicly Available Tools for Static
Intrusion Prevention. In Proceedings of the 7th Nordic Workshop on Secure IT
Systems, pages 68–84, Karlstad, Sweden, November 2002.
URL: http://citeseer.ist.psu.edu/wilander02comparison.html (Stand: 5.
April 2007).
[59] Wolf, J.: C++ von A bis Z. Das umfassende Handbuch. Galileo Press, Bonn, 2006,
ISBN 3898428168.
[60] Younan, Y., W. Joosen, and F. Piessen: Code Injection in C and C++: A Survey
of Vulnerabilities and Countermeasures. Technical Report CW386, Departement
Computerwetenschappen, Katholieke Universiteit Leuven, 2004.
URL: http://www.cs.kuleuven.ac.be/publicaties/rapporten/cw/CW386.
abs.html (Stand: 9. Jänner 2007).
[61] Younan, Y., W. Joosen, F. Piessen, and H. van den Eynden: Security of Memory
Allocators for C and C++. Technical Report CW419, Departement Computerwetenschappen, Katholieke Universiteit Leuven, 2005.
URL: http://citeseer.ist.psu.edu/younan05security.html (Stand: 9.
Jänner 2007).
Abkürzungsverzeichnis
ANSI . . . . . . . . . . . . . . . . . .
American National Standards Institute
API . . . . . . . . . . . . . . . . . . .
Application Programming Interface
ASCII . . . . . . . . . . . . . . . . .
American Standard Code for Information Interchange
ASLR . . . . . . . . . . . . . . . . .
Address Space Layout Randomization
BSS . . . . . . . . . . . . . . . . . . .
Block Started by Symbol
CIL . . . . . . . . . . . . . . . . . . . .
Common Intermediate Language
CLI . . . . . . . . . . . . . . . . . . . .
Common Language Infrastructure
CLR . . . . . . . . . . . . . . . . . . .
Common Language Runtime
COFF . . . . . . . . . . . . . . . . .
Common Object File Format
CPU . . . . . . . . . . . . . . . . . . .
Central Processing Unit
CRT . . . . . . . . . . . . . . . . . . .
C Run-Time Library
DDK . . . . . . . . . . . . . . . . . .
Device Driver Kit
DIN . . . . . . . . . . . . . . . . . . .
DIN Deutsches Institut für Normung e. V.
DLL . . . . . . . . . . . . . . . . . . .
Dynamic Link Library
ELF . . . . . . . . . . . . . . . . . . .
Executable and Linking Format
EN . . . . . . . . . . . . . . . . . . . .
Europäische Norm
ETSI . . . . . . . . . . . . . . . . . .
European Telecommunications Standards Institute
GNU . . . . . . . . . . . . . . . . . .
GNU’s Not Unix
GPR . . . . . . . . . . . . . . . . . . .
General Purpose Registers
HTML . . . . . . . . . . . . . . . . .
Hyper Text Markup Language
IA . . . . . . . . . . . . . . . . . . . . .
Intel-Architektur
IAT . . . . . . . . . . . . . . . . . . . .
Import Address Table
IEC . . . . . . . . . . . . . . . . . . . .
International Electrotechnical Commission
IEEE . . . . . . . . . . . . . . . . . .
Institute of Electrical and Electronics Engineers
101
Abkürzungsverzeichnis
102
ISO . . . . . . . . . . . . . . . . . . . .
International Organization for Standardization
JVM . . . . . . . . . . . . . . . . . . .
Java Virtual Machine
LIFO . . . . . . . . . . . . . . . . . .
Last In First Out
MAC . . . . . . . . . . . . . . . . . .
Mandatory Access Control
MFC . . . . . . . . . . . . . . . . . .
Microsoft Foundation Classes
MMU . . . . . . . . . . . . . . . . . .
Memory Management Unit
PE . . . . . . . . . . . . . . . . . . . .
Portable Executeable Format
POSIX . . . . . . . . . . . . . . . .
Portable Operating System Interface
SDK . . . . . . . . . . . . . . . . . . .
Software Development Kit
STL . . . . . . . . . . . . . . . . . . .
Standard Template Library
SQL . . . . . . . . . . . . . . . . . . .
Structured Query Language
SWIFI . . . . . . . . . . . . . . . . .
Software Implemented Fault Injection
TC . . . . . . . . . . . . . . . . . . . .
Technical Committee
TIS . . . . . . . . . . . . . . . . . . . .
Tool Interface Standard
WORA . . . . . . . . . . . . . . . .
Write Once Run Anywhere
WWW . . . . . . . . . . . . . . . . .
World Wide Web
XML . . . . . . . . . . . . . . . . . .
Extensible Markup Language
XSS . . . . . . . . . . . . . . . . . . .
Cross Site Scripting
Anhang
103
A
APIs und Bibliothekserweiterungen
In den letzten Jahren wurden eine Reihe von Erweiterungen und Überarbeitungen
der POSIX1 - und CRT2 -Libraries veröffentlicht, welche Sicherheitslücken in den Bibliotheksfunktionen schließen sollen. Ein Großteil der Funktionen konnte aufgrund der
gegebenen Prototypen nicht sicherer gemacht werden, stattdessen wurden Ersatzfunktionen, Makros oder Templates vorgeschlagen bzw. implementiert. [13, 39]
A.1
Die Standardbibliotheken
Einige Funktionen aus der C-Standardbibliothek wurden als unsicher ausgewiesen und
sollten in neuen Applikationen vermieden bzw. aus bestehenden Applikationen entfernt
werden. Für diese Funktionen stehen in den meisten Fällen Ersatzfunktionen mit einem
an den ursprünglichen Funktionsnamen angehängten _s als Suffix zur Verfügung.
A.1.1
Unsichere POSIX C-Funktionen
Aus den Portable Operating System Interface (POSIX ) Bibliotheken sind in erster
Linie alle stringverarbeitenden Funktionen, eine Reihe von Eingabe- und Ausgabefunktionen und alle davon abgeleiteten Funktionen betroffen. Viele Funktionen werden
nicht explizit angeführt, gelten jedoch ebenfalls als kritisch, weil sie wiederum Funktionen der POSIX-Libraries verwenden (z.B. syslog, etc.). Die folgende Tabelle A.1
zeigt die als unsicher ausgewiesenen Basisfunktionen in alphabetischer Reihenfolge [39]:
1
Das Portable Operating System Interface (POSIX) ist eines von der IEEE und der Open Group
hpts. für Unix entwickeltes und standardisiertes Interface als Schnittstelle zwischen den Applikationen und dem Betriebssystem. Der Standard ist unter der Bezeichnung DIN/EN/ISO/IEC 9945
veröffentlicht, wobei im Abschnitt P1003.1 [27] der Betriebssystemkern und die C-Bibliotheken beschrieben werden.
2
Microsoft kürzt seine C Run-Time Libraries mit CRT ab und meint damit eine Reihe von statisch
oder dynamisch zu Applikationen gelinkte Bibliotheken mit Windows oder C-Standardfunktionen.
104
A. APIs und Bibliothekserweiterungen
access
cabs
cgets
chdir
chmod
chsize
close
cprintf
cputs
creat
cscanf
cwait
dup
dup2
ecvt
eof
execl
execle
execlp
execlpe
execv
execve
execvp
execvpe
fcloseall
fcvt
fdopen
fgetchar
filelength
fileno
flushall
fputchar
gcvt
getch
getche
getcwd
getpid
getw
hypot
inp
inpw
isascii
isatty
iscsym
iscsymf
itoa
j0
j1
jn
kbhit
lfind
locking
lsearch
lseek
105
ltoa
memccpy
memicmp
mkdir
mktemp
open
outp
outpw
putch
putenv
putw
read
rmdir
rmtmp
setmode
sopen
spawnl
spawnle
spawnlp
spawnlpe
spawnv
spawnve
spawnvp
spawnvpe
strcmpi
strdup
stricmp
strlwr
strnicmp
strnset
strrev
strset
strupr
swab
tell
tempnam
toascii
tzset
ultoa
umask
ungetch
unlink
wcsdup
wcsicmp
wcsicoll
wcslwr
wcsnicmp
wcsnset
wcsrev
wcsset
wcsupr
write
y0, y1
yn
Tabelle A.1: Unsichere POSIX-Funktionen
A.1.2
Unsichere Windows CRT-Funktionen
Auch Microsoft hat Teile der Windows C Run-Time Libraries (CRT ) überarbeitet oder
Ersatzfunktionen in ihre aktuellen Entwicklungsumgebungen eingepflegt. Microsoft dokumentiert für die mit Visual Studio 2005 ausgelieferten CRT Security Enhancements
folgende Änderungen und Erweiterungen [40]:
• Parameter Validation - Überprüfung der den Funktionen der CRT übergebenen
Funktionsparameter auf Null-Zeiger, deren allgemeine Gültigkeit (enumerated validity) und deren Gültigkeitsbereiche (range validity)
• Sized Buffers - Zwingende Übergabe und Überprüfung der Puffergrößen an die
CRT Funktionen
• Null Termination - Null-Terminierung von Strings
• Enhanced Error Reporting - Erweiterte Errorcodes zum leichteren Auffinden von
Fehlern
• Filesystem Security - Sichere Dateifunktionen mit sicheren Defaulteinstellungen
• Windows Security - Sicheres Prozessmanagement (Secure Process Policies)
A. APIs und Bibliothekserweiterungen
106
• Format-String Syntax Checking - Erkennung von Format-String Fehlern oder
fehlenden Parametern bei printf-Formatangaben
Folgende Teile der CRT-Libraries (in der Tabelle A.2 in alphabetischer Reihenfolge
gelistet) wurden von Microsoft als unsicher und veraltet markiert (der Compiler meldet
deprecated functions) und bieten entsprechende Ersatzfunktionen an [39]:
_alloca
asctime
_cgets,
_cgetws
_chsize
_controlfp
_creat
_cscanf
_cscanf_l
ctime
_ctime32
_ctime64
_cwscanf
_cwscanf_l
_ecvt
_fcvt
fopen
freopen
fscanf
_fscanf_l
fwscanf
_fwscanf_l
_gcvt
getenv
gets
_getws
gmtime
_gmtime32
_gmtime64
_i64toa
_i64tow
_itoa
_itow
localtime
_localtime32
_localtime64
_ltoa, _ltow
_mbccpy
_mbccpy_l
_mbscat
_mbscpy
_mbslwr
_mbslwr_l
_mbsnbcat
_mbsnbcat_l
_mbsnbcpy
_mbsnbcpy_l
_mbsnbset
_mbsnbset_l
_mbsncat
_mbsncat_l
_mbsncpy
_mbsncpy_l
_mbsnset
_mbsnset_l
mbsrtowcs
_mbsset
_mbsset_l
_mbstok
_mbstok_l
mbstowcs
_mbstowcs_l
_mbsupr
_mbsupr_l
memcpy
memmove
_mktemp
_open
scanf
_scanf_l
_searchenv
setbuf
_snprintf
_snprintf_l
_snscanf
_snscanf_l
_snwprintf
_snwprintf_l
_snwscanf
_snwscanf_l
_sopen
_splitpath
sprintf
_sprintf_l
sscanf
_sscanf_l
strcat
strcpy
_strdate
strerror
_strerror
_strlwr
_strlwr_l
strncat
_strncat_l
strncpy
_strncpy_l
_strnset
_strnset_l
_strset
_strset_l
_strtime
strtok
_strtok_l
_strupr
_strupr_l
swprintf
_swprintf_l
swscanf
_swscanf_l
tmpfile
_ui64toa
_ui64tow
_ultoa
_ultow
_umask
vsnprintf
_vsnprintf
_vsnprintf_l
_vsnwprintf
_vsnwprintf_l
vsprintf
_vsprintf_l
vswprintf
_vswprintf_l
__vswprintf_l
_wasctime
_wcreat
wcrtomb
wcscat
wcscpy
_wcserror
__wcserror
_wcslwr
_wcslwr_l
wcsncat
Tabelle A.2: Unsichere Windows CRT-Funktionen
wcsncat_l
_wcsncpy
_wcsncpy_l
_wcsnset
_wcsnset_l
wcsrtombs
_wcsset
_wcsset_l
wcstok
_wcstok_l
wcstombs
_wcstombs_l
_wcsupr_l
_wcsupr
_wctime
_wctime32
_wctime64
wctomb
_wctomb_l
_wfopen
_wfreopen
_wgetenv
wmemcpy
wmemmove
_wmktemp
_wopen
_wscanf
_wscanf_l
_wsearchenv
_wsopen
_wsplitpath
_wstrdate
_wstrtime
A. APIs und Bibliothekserweiterungen
107
Für eine Reihe weiterer unsicherer Funktionen (siehe Tabelle A.3) bietet die Microsoft
Entwicklungsumgebung sichere Template Overloads an [39]:
_cgets
_cgetws
gets
_getws
_itoa
_i64toa
_ui64toa
_itow
_i64tow
_ui64tow
_ltoa
_ltow
_mbsnbcat
_mbsnbcat_l
_mbsnbcpy
_mbsnbcpy_l
mbsrtowcs
mbstowcs
_mbstowcs_l
_mktemp
_wmktemp
_searchenv
_wsearchenv
_snprintf
_snprintf_l
_snwprintf
_snwprintf_l
sprintf
_sprintf_l
swprintf
_swprintf_l
__swprintf_l
strcat
wcscat
_mbscat
_mbsncat_l
strcpy
strncpy
wcscpy
_strncpy_l
_mbscpy
wcsncpy
_strdate
_wcsncpy_l
_wstrdate
_mbsncpy
_strlwr
_mbsncpy_l
_wcslwr
_strtime
_mbslwr
_wstrtime
_strlwr_l
_strupr
_wcslwr_l
_strupr_l
_mbslwr_l
_mbsupr
strncat
_mbsupr_l
_strncat_l _wcsupr_l
wcsncat
_wcsupr
wcsncat_l
_ultoa
_mbsncat
_ultow
vsnprintf
_vsnprintf
_vsnprintf_l
_vsnwprintf
_vsnwprintf_l
vsprintf
_vsprintf_l
vswprintf
_vswprintf_l
__vswprintf_l
wcrtomb
wcsrtombs
wcstombs
_wcstombs_l
Tabelle A.3: Template Overloads für unsichere Windows CRT-Funktionen
B
Protokolle
B.1
B.1.1
Lexikalische Quelltextanalysen
grep Protokoll
Der folgende Auszug1 aus einem Grep Protokoll zeigt die unter Anwendung der einfachen Suchmaske printf|scanf|gets|strcpy gefundenen Quelltextzeilen. Nachdem
es sich bei Grep um ein einfaches Programm zur Suche und Filterung definierter Zeichenketten in beliebigen Dateien handelt, gibt Grep keinerlei Zusatzinformationen zu
den gefunden Schwachstellen aus.
[dau]$ grep -nE printf|scanf|gets|strcpy"*.c
[...]
badsource.c:66:
notice that the argument to strcpy is a constant string
badsource.c:77: strcpy(buffer, foo);
[...]
badsource.c:157:
fprintf (stderr, "%s not a normal file\n", argv[1]);
[...]
badsource.c:533: printf("Please enter your user id :");
badsource.c:534: fgets(buffer, 1024, stdin);
badsource.c:540:
strcpy(errormsg, "that isn’t a valid ID");
[...]
badsource.c:712: fgets(buffer, 1024, stdin);
[...]
stattest.c:7:
printf("hello\n");
[...]
stattest.c:20: printf(bf, x);
[...]
[dau]$
1
Aus Platzgründen wurde nicht das komplette Protokoll, sondern jedes Vorkommen einer bestimmten Warnung nur einmal gelistet. Mehrfach protokollierte Warnungen wurden entfernt und stattdessen
durch [. . . ] ersetzt.
108
B. Protokolle
B.1.2
109
ITS4 Analyseprotokoll
Der folgende Auszug2 aus einem ITS4 Protokoll zeigt die von ITS4 gefundenen kritischen Quelltextzeilen. ITS4 beschreibt etwaige Methoden zur Vermeidung der gefundenen Schwachstellen. Es prüft nicht nur das Vorkommen bestimmter sicherheitskritischer
Funktionen, sondern warnt auch vor leichtfertiger Anwendung bestimmter Funktionen.
Ebenso gibt ITS4 zu jeder gefundenen möglichen Schwachstelle eine Bewertung in Form
einer errechneten Risikostufe aus.
[dau]$ its4 badsource.c
badsource.c:144:(Urgent) fprintf
badsource.c:148:(Urgent) fprintf
[...]
Non-constant format strings can often be attacked.
Use a constant format string.
---------------badsource.c:533:(Urgent) printf
[...]
Non-constant format strings can often be attacked.
Use a constant format string.
---------------badsource.c:669:(Very Risky) stat
Potential race condition on: argv["1"]
Points of concern are:
badsource.c:669: stat
badsource.c:147: open
[...]
badsource.c:777: fopen
Manipulate file descriptors, not symbolic names, when possible.
---------------badsource.c:77:(Very Risky) strcpy
[...]
This function is high risk for buffer overflows
Use strncpy instead.
---------------badsource.c:160:(Risky) fdopen
[...]
Can be involved in a race condition if you open things after a poor check. For
example, don’t check to see if something is not a symbolic link before opening
2
Aus Platzgründen wurde nicht das komplette Protokoll, sondern jedes Vorkommen einer bestimmten Warnung nur einmal gelistet. Mehrfach protokollierte Warnungen wurden entfernt und stattdessen
durch [. . . ] ersetzt.
B. Protokolle
110
it. Open it, then check bt querying the resulting object. Don’t run tests on
symbolic file names...
Perform all checks AFTER the open, and based on the returned object, not a
symbolic name.
---------------badsource.c:259:(Risky) fopen
[...]
Can be involved in a race condition if you open things after a poor check. For
example, don’t check to see if something is not a symbolic link before opening
it. Open it, then check bt querying the resulting object. Don’t run tests on
symbolic file names...
Perform all checks AFTER the open, and based on the returned object, not a
symbolic name.
---------------badsource.c:151:(Risky) fstat
[...]
Can lead to process/file interaction race conditions (TOCTOU category C)
Manipulate file descriptors, not symbolic names, when possible.
---------------badsource.c:139:(Risky) open
[...]
Can be involved in a race condition if you open things after a poor check. For
example, don’t check to see if something is not a symbolic link before opening
it. Open it, then check bt querying the resulting object. Don’t run tests on
symbolic file names...
Perform all checks AFTER the open, and based on the returned object, not a
symbolic name.
---------------badsource.c:255:(Risky) umask
[...]
Setting a liberal umask can be bad when you exec an untrusted process.
Reset the umask to something sane before execing.
---------------badsource.c:293:(Some risk) read
[...]
Be careful not to introduce a buffer overflow when using in a loop.
Make sure to check your buffer boundries.
----------------
B. Protokolle
B.1.3
111
RATS Analyseprotokoll
Der Auszug3 aus einem RATS Protokoll zeigt die von RATS gefundenen kritischen
Quelltextzeilen. RATS beschreibt etwaige Methoden zur Vermeidung der gefundenen
Schwachstellen. Es findet bei gleichem Quelltext zwar weniger kritische Zeilen als ITS4,
weist aber ebenso auf mögliche TOCTOU4 Schwachstellen hin. RATS bewertet die
gefundenen möglichen Schwachstellen auch mit einer Risikostufe (High, Medium, Low).
[dau]$ rats badsource.c
Entries in perl database: 33
Entries in python database: 62
Entries in c database: 336
Entries in php database: 55
Analyzing badsource.c
badsource.c:30: High: fixed size local buffer
[...]
Extra care should be taken to ensure that character arrays that are allocated
on the stack are used safely. They are prime targets for buffer overflow
attacks.
badsource.c:77: High: strcpy
[...]
Check to be sure that argument 2 passed to this function call will not copy
more data than can be handled, resulting in a buffer overflow.
badsource.c:255: High: umask
[...]
umask() can easily be used to create files with unsafe priviledges. It should
be set to restrictive values.
badsource.c:439: High: strncat
[...]
Consider using strlcat() instead.
badsource.c:439: High: strncat
[...]
Check to be sure that argument 1 passed to this function call will not copy
3
Aus Platzgründen wurde nicht das komplette Protokoll, sondern jedes Vorkommen einer bestimmten Warnung nur einmal gelistet. Mehrfach protokollierte Warnungen wurden entfernt und stattdessen
durch [. . . ] ersetzt.
4
TOCTOU sowie gelegentlich auch TOCTTOU steht für die engl. Bezeichnung Time of Check to
”
Time of Use“
B. Protokolle
112
more data than can be handled, resulting in a buffer overflow.
badsource.c:555: High: fprintf
[...]
Check to be sure that the non-constant format string passed as argument 2 to
this function call does not come from an untrusted source that could have added
formatting characters that the code is not prepared to handle.
badsource.c:293: Medium: read
[...]
Check buffer boundaries if calling this function in a loop and make sure you are
not in danger of writing past the allocated space.
badsource.c:669: Medium: stat
A potential TOCTOU (Time Of Check, Time Of Use) vulnerability exists.
the first line where a check has occured.
The following line(s) contain uses that may match up with this check:
147 (open)
Total lines analyzed: 1261
Total time 0.002426 seconds
519785 lines per second
This is
B. Protokolle
B.1.4
113
Flawfinder Analyseprotokoll
Der Auszug5 aus einem Flawfinder Protokoll zeigt die von Flawfinder gefundenen
möglichen kritischen Quelltextzeilen. Flawfinder gibt etwaige zusätzliche Hilfestellungen
zur Vermeidung der gefundenen Schwachstellen und am Ende des Protokolls eine Zusammenfassung der Analyse.
[dau]$ flawfinder badsource.c
Flawfinder version 1.26, (C) 2001-2004 David A. Wheeler.
Number of dangerous functions in C/C++ ruleset: 158
Examining badsource.c
badsource.c:77: [4] (buffer) strcpy:
Does not check for buffer overflows when copying to destination.
Consider using strncpy or strlcpy (warning, strncpy is easily misused).
[...]
badsource.c:555: [4] (format) fprintf:
If format strings can be influenced by an attacker, they can be
exploited. Use a constant for the format specification.
[...]
badsource.c:30: [2] (buffer) char:
Statically-sized arrays can be overflowed. Perform bounds checking,
use functions that limit length, or ensure that the size is larger than
the maximum possible length.
[...]
badsource.c:139: [2] (misc) open:
Check when opening files - can an attacker redirect it (via symlinks),
force the opening of special file type (e.g., device files), move
things around to create a race condition, control its ancestors, or change
its contents?.
[...]
badsource.c:259: [2] (misc) fopen:
Check when opening files - can an attacker redirect it (via symlinks),
force the opening of special file type (e.g., device files), move
things around to create a race condition, control its ancestors, or change
its contents?.
[...]
badsource.c:719: [2] (buffer) strcat:
Does not check for buffer overflows when concatenating to destination.
Consider using strncat or strlcat (warning, strncat is easily misused).
5
Aus Platzgründen wurde nicht das komplette Protokoll, sondern jedes Vorkommen einer bestimmten Warnung nur einmal gelistet. Mehrfach protokollierte Warnungen wurden entfernt und stattdessen
durch [. . . ] ersetzt.
B. Protokolle
Risk is low because the source is a constant string.
[...]
badsource.c:33: [1] (buffer) strncpy:
Easily used incorrectly; doesn’t always \0-terminate or check for
invalid pointers.
badsource.c:190: [1] (buffer) strlen:
Does not handle strings that are not \0-terminated (it could cause a
crash if unprotected).
[...]
badsource.c:255: [1] (access) umask:
Ensure that umask is given most restrictive possible setting (e.g.,
066 or 077).
badsource.c:293: [1] (buffer) read:
Check buffer boundaries if used in a loop.
[...]
badsource.c:857: [1] (buffer) read:
Check buffer boundaries if used in a loop.
[...]
badsource.c:1107: [1] (access) umask:
Ensure that umask is given most restrictive possible setting (e.g.,
066 or 077).
[...]
Hits = 84
Lines analyzed = 1260 in 0.64 seconds (9082 lines/second)
Physical Source Lines of Code (SLOC) = 955
Hits@level = [0]
0 [1] 48 [2] 28 [3]
0 [4]
8 [5]
0
Hits@level+ = [0+] 84 [1+] 84 [2+] 36 [3+]
8 [4+]
8 [5+]
0
Hits/KSLOC@level+ = [0+] 87.9581 [1+] 87.9581 [2+] 37.6963
[3+] 8.37696 [4+] 8.37696 [5+]
0
Minimum risk level = 1
Not every hit is necessarily a security vulnerability.
There may be other security vulnerabilities; review your code!
114
B. Protokolle
B.2
B.2.1
115
Semantische Quelltextanalysen
Microsoft C/C++ Compiler Analyseprotokoll
Der Auszug6 aus einem Compile Protokoll zeigt die vom Microsoft C++ Compiler
gefundenen möglichen kritischen Quelltextzeilen.
1>---- Build started: Project: CompilerTest, Configuration: Debug Win32 ---1>Compiling...
1>CompilerTest.cpp
1>compilertest.cpp(13) : warning C4996: ’strcpy’: This function or variable
may be unsafe. Consider using strcpy_s instead. To disable deprecation,
use _CRT_SECURE_NO_WARNINGS. See online help for details.
1> string.h(74) : see declaration of ’strcpy’
1>compilertest.cpp(16) : warning C4996: ’gets’: This function or variable
may be unsafe. Consider using gets_s instead. To disable deprecation,
use _CRT_SECURE_NO_WARNINGS. See online help for details.
1> stdio.h(270) : see declaration of ’gets’
1>compilertest.cpp(17) : warning C4313: ’printf’ : ’%d’ in format string
conflicts with argument 2 of type ’const char *’
1>compilertest.cpp(5) : warning C4100: ’argc’ : unreferenced formal parameter
1>compilertest.cpp(8) : warning C4189: ’j’ : local variable is initialized
but not referenced
1>compilertest.cpp(13) : warning C6204: Possible buffer overrun in call to
’strcpy’: use of unchecked parameter ’argv[...]’
1>compilertest.cpp(16) : warning C6202: Buffer overrun for ’szBuffer’, which
is possibly stack allocated, in call to ’gets’: length ’4294967295’
exceeds buffer size ’10’
1>compilertest.cpp(16) : warning C6031: Return value ignored: ’gets’
1>compilertest.cpp(17) : warning C6271: Extra argument passed to ’printf’:
parameter ’3’ is not used by the format string
1>compilertest.cpp(16) : warning C6386: Buffer overrun: accessing
’argument 1’, the writable size is ’10’ bytes, but ’4294967295’ bytes
might be written: Lines: 7, 8, 9, 10, 12, 13, 14, 15, 16
1>compilertest.cpp(17) : warning C6001: Using uninitialized memory ’i’:
Lines: 7, 8, 9, 10, 12, 13, 14, 15, 16, 17
1>compilertest.cpp(13) : warning C6387: ’argument 1’ might be ’0’: this does
not adhere to the specification for the function ’strcpy’:
Lines: 7, 8, 9, 10, 12, 13
6
Aus Platzgründen wurde nicht das komplette Protokoll, sondern jedes Vorkommen einer bestimmten Warnung nur einmal gelistet. Mehrfach protokollierte Warnungen wurden entfernt und stattdessen
durch [. . . ] ersetzt.
B. Protokolle
116
1>compilertest.cpp(15) : warning C6387: ’argument 1’ might be ’0’: this does
not adhere to the specification for the function ’gets_s’:
Lines: 7, 8, 9, 10, 12, 13, 14, 15
1>compilertest.cpp(17) : warning C4700: uninitialized local variable ’i’ used
[...]
1>CompilerTest - 0 error(s), 14 warning(s)
B. Protokolle
B.2.2
117
GCC Compiler Analyseprotokoll
Der Auszug7 aus einem Compile Protokoll zeigt die vom GCC Compiler gefundenen
möglichen kritischen Quelltextzeilen. Im Vergleich zum Microsoft C++ Compiler (siehe
Protokoll im Anhang B.2.1) ist bei gleichem Quelltext die Anzahl der bemängelten
Quelltextzeilen sehr gering.
[dau]$ gcc -Wall CompilerTest.cpp
CompilerTest.cpp: In function ’int main(int, char**)’:
CompilerTest.cpp:15: error: ’gets_s’ was not declared in this scope
CompilerTest.cpp:17: warning: too few arguments for format
CompilerTest.cpp:8: warning: unused variable ’j’
7
Aus Platzgründen wurde nicht das komplette Protokoll, sondern jedes Vorkommen einer bestimmten Warnung nur einmal gelistet. Mehrfach protokollierte Warnungen wurden entfernt und stattdessen
durch [. . . ] ersetzt.
B. Protokolle
B.2.3
118
Splint Analyseprotokoll
Der Auszug8 aus einem Splint Analyseprotokoll zeigt die von Splint gefundenen möglichen
kritischen Quelltextzeilen.
[dau]$ splint CompilerTest.c
Splint 3.1.1 --- 03 Nov 2006
CompilerTest.c: (in function main)
CompilerTest.c:13:12: Possibly null storage pBuffer passed as non-null param:
strcpy (pBuffer, ...)
A possibly null pointer is passed as a parameter corresponding to a formal
parameter with no /*@null@*/ annotation. If NULL may be used for this
parameter, add a /*@null@*/ annotation to the function parameter
declaration. (Use -nullpass to inhibit warning)
CompilerTest.c:12:14: Storage pBuffer may become null
CompilerTest.c:15:4: Unrecognized identifier: gets_s
Identifier used in code has not been declared. (Use -unrecog to inhibit
warning)
CompilerTest.c:15:4: Variable pBuffer used after being released
Memory is used after it has been released (either by passing as an only
param or assigning to an only global). (Use -usereleased to inhibit warning)
CompilerTest.c:14:9: Storage pBuffer released
CompilerTest.c:16:4: Use of gets leads to a buffer overflow vulnerability.
Use fgets instead: gets
Use of function that may lead to buffer overflow. (Use -bufferoverflowhigh
to inhibit warning)
CompilerTest.c:16:4: Return value (type char *) ignored: gets(szBuffer)
Result returned by function call is not used. If this is intended, can cast
result to (void) to eliminate message. (Use -retvalother to inhibit warning)
CompilerTest.c:17:24: Variable i used before definition
An rvalue is used that may not be initialized to a value on some execution
path. (Use -usedef to inhibit warning)
CompilerTest.c:17:4: No argument corresponding to printf format code 2 (%d):
"i=%d j=%d"
Types are incompatible. (Use -type to inhibit warning)
CompilerTest.c:17:20: Corresponding format code
CompilerTest.c:18:4: Format string parameter to printf is not a compile-time
constant: argv[2]
Format parameter is not known at compile-time. This can lead to security
8
Aus Platzgründen wurde nicht das komplette Protokoll, sondern jedes Vorkommen einer bestimmten Warnung nur einmal gelistet. Mehrfach protokollierte Warnungen wurden entfernt und stattdessen
durch [. . . ] ersetzt.
B. Protokolle
119
vulnerabilities because the arguments cannot be type checked. (Use
-formatconst to inhibit warning)
CompilerTest.c:8:9: Variable j declared but not used
A variable is declared but never used. Use /*@unused@*/ in front of
declaration to suppress message. (Use -varuse to inhibit warning)
CompilerTest.c:5:14: Parameter argc not used
A function parameter is not used in the body of the function. If the
argument is needed for type compatibility or future plans, use /*@unused@*/
in the argument declaration. (Use -paramuse to inhibit warning)
Finished checking --- 10 code warnings
C
Listings
C.1
Absicherung des Stacks über Security Cookies
Das folgende Beispiel im Listing C.1 zeigt die Absicherung des Stacks über Security
Cookies, wie sie der Microsoft C/C++ Compiler über die Option /GS einbindet. Im
Programm wird die Ausgabe in einen auf dem Stack liegenden lokalen Puffer kopiert.
Nachdem keine Sicherheitsüberprüfungen stattfinden, kann es dabei leicht zu einem
Stack Overflow oder zu einer gezielten Stackmanipulation kommen.
Der Compiler fügt am Beginn der Funktion main das Security Cookie auf dem Stack ein.
Vor dem Verlassen der Funktion main wird die Funktion __security_check_cookie
aufgerufen, welche das Cookie auf seine Richtigkeit überprüft. Wird dabei eine Manipulation bzw. ein Stack Overflow festgestellt, wird eine Ausnahmebehandlung ausgelöst,
ansonsten kehrt die Funktion zurück und von main aus wird mit ret auf die gesicherte
Rücksprungadresse zurückgesprungen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// int main(int argc, char* argv[])
// {
00401000 push
ebp
00401001 mov
ebp,esp
00401003 sub
esp,58h
00401006 mov
eax,dword ptr [___security_cookie (403000h)]
0040100B xor
eax,ebp
0040100D mov
dword ptr [ebp-4],eax
00401010 push
ebx
00401011 push
esi
00401012 push
edi
//
char szBuffer[20];
//
sprintf(szBuffer, argv[1]);
00401013 mov
eax,dword ptr [ebp+0Ch]
00401016 mov
ecx,dword ptr [eax+4]
00401019 push
ecx
120
C. Listings
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
0040101A lea
0040101D push
0040101E call
00401024 add
//
return 0;
00401027 xor
// }
00401029 pop
0040102A pop
0040102B pop
0040102C mov
0040102F xor
00401031 call
00401036 mov
00401038 pop
00401039 ret
121
edx,[ebp-18h]
edx
dword ptr [__imp__sprintf (4020A8h)]
esp,8
eax,eax
edi
esi
ebx
ecx,dword ptr [ebp-4]
ecx,ebp
__security_check_cookie (401040h)
esp,ebp
ebp
Listing C.1: Prolog- und Epilog-Erweiterungen zur Behandlung von Security Cookies
C. Listings
C.2
122
Einfache Speicherüberwachung in C++
Das folgende Beispiel zeigt, wie unter C++ eine eigene Speicherverwaltung aktiviert
werden kann. Im folgenden Listing C.2 werden die globalen new- und delete-Operatoren
überladen. Diese neuen Operatoren unterstützen zusätzliche Parameter zur einfachen
Fehlersuche beim Auftreten etwaiger Memory Leaks.
1
2
[...]
#if defined(DEBUG)
3
4
#define MY_NEW new(__FILE__, __LINE__)
5
6
7
8
9
10
11
// Memory tracking allocation
void* operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
void operator delete(void* p);
[...]
#endif
[...]
Listing C.2: Include-Datei zum Überladen des new-Operators
Wird nun wie im Listing C.3 ein Speicher mit dem Operator new allokiert und mit
delete freigegeben, so werden die eigenen Speicherverwaltungsroutinen aufgerufen.
1
2
3
#ifdef _DEBUG
#define new MY_NEW
#endif
4
5
6
7
8
9
10
11
12
13
[...]
m_pBuffer1 = new char[10];
strcpy(m_pBuffer1, "ABCDEFGHI");
[...]
m_pBuffer2 = new char[20]; // erzeugt aufgrund des #define den Code
// m_pBuffer = new(__FILE__, __LINE__)char[20];
strcpy(m_pBuffer2, "01234567890123456789012");
[...]
delete [] m_pBuffer2;
Listing C.3: Quelltext mit Buffer Overflow und Memory Leak Fehler
C. Listings
C.3
Defensive Programmierung
C.3.1
Überprüfung der Eingabedaten
123
Typisches Merkmal der Defensiven Programmierung ist die Überprüfung aller Eingangsdaten in eine Funktion. Das Beispiel zeigt eine Funktion aus der .NET Standard
Library, erstellt mit dem .NET Reflector1 .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public virtual int Read([In, Out] char[] buffer, int index, int count)
{
if (buffer == null)
{
throw new ArgumentNullException("buffer", Environment.GetResource...
}
if (index < 0)
{
throw new ArgumentOutOfRangeException("index", Environment.GetResource...
}
if (count < 0)
{
throw new ArgumentOutOfRangeException("count", Environment.GetResource...
}
if ((buffer.Length - index) < count)
{
throw new ArgumentException( Environment.GetResourceString...
}
int num = 0;
[...] // regulärer Funktionscode, der nur ausgeführt wird,
// wenn alle Eingangsdaten gültig sind
return num;
}
Listing C.4: Defensive Programmierung innerhalb der .NET Standard Library
1
http://www.aisto.com/roeder/dotnet/
C. Listings
C.3.2
124
Zeiger und Speicherbehandlung
Typisches Merkmal der Defensiven Programmierung ist die explizite Überprüfung aller
Zeiger, aller Rückgabewerte von Funktionen und empfangenen Daten. Werden sensible
Daten gespeichert, wird der Speicher vor dem Freigeben überschrieben und dann erst
freigegeben.
1
2
3
// Allocate a buffer
void* pSecData = malloc(SIZEOFSECDATA);
4
5
6
7
// 1. Check if pointer is valid (not null, valid segment,...)
if( !IsPointerValid(pSecData) )
throw "Invalid pointer";
8
9
10
11
// 2. Get data and check if the function failed
if( GetData(pSecData, SIZEOFSECDATA) != SIZEOFSECDATA )
throw "Retrieving data failed";
12
13
14
15
// 3. Check if data is valid (checksum, header,...)
if( !IsDataValid(pSecData) )
throw "Invalid data";
16
17
[...]
// do something with data
18
19
20
// 4. Overwrite data buffer
memset(pSecData, 0, SIZEOFSECDATA);
21
22
23
// 5. Free the buffer it is not further used
free(pSecData);
Listing C.5: Zeiger- und Speicherbehandlung bei defensiver Programmierung
C. Listings
C.4
125
Sichere Programmiersprachen
C.4.1
Sicheres C++
C++ gilt nicht grundsätzlich als sichere Programmiersprache, bietet aber Klassen und
Templates, welche den Verzicht auf einfache Zeiger unterstützen. Im folgenden Listing
C.6 werden als Beispiel die sichere string-Klasse und ein auto_ptr der Standard Template Library (STL) verwendet.
1
2
#include <memory>
#include <string>
3
4
using namespace std;
5
6
7
8
9
10
11
string foo1(const string url)
{
string dir;
[...] // do something here
return url + "/" + dir;
}
12
13
14
15
16
int foo2(int* pBuffer, size_t size)
{
[...] // do something here
}
17
18
19
20
21
22
int main()
{
string sUrl
= "http://www.esplines.com";
string sServer = foo1(sUrl);
auto_ptr<int> pBuffer( new int[SIZEOFDATA] );
23
foo2(pBuffer.get(), SIZEOFDATA);
[...]
24
25
26
}
Listing C.6: Auszug aus einem STL Programm mit Smart-Pointers und Strings
C. Listings
C.4.2
126
Automatisches Bounds Checking in C#
Die Ausführung des C# Programms aus dem Listing C.7 führt aufgrund der automatischen Überprüfung der Arraygrenzen beim Durchlauf i==10 in Zeile 15 zu einer
Ausnahme (Exception).
1
2
3
using System;
using System.Collections.Generic;
using System.Text;
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace OverrunApp
{
class Program
{
static void Main(string[] args)
{
Random rnd = new Random();
int[] buffer = new int[10];
for (int i = 0; i <= 10; i++)
{
buffer[i] = rnd.Next(int.MaxValue);
}
}
}
}
Listing C.7: C# Programm mit einem Array Indexing Fehler
Die von der Exception generierte Trace-Meldung sieht wie folgt aus:
’OverrunApp.vshost.exe’ (Managed): Loaded ’[...]\OverrunApp.exe’,
Symbols loaded.
A first chance exception of type ’System.IndexOutOfRangeException’ occurred
in OverrunApp.exe
C. Listings
C.5
C.5.1
127
Sichere Bibliotheksfunktionen
Sicherung der Funktionen über Return Codes
Mit dem Windows Platform SDK könnte die Abfrage des Status- und Fehlercodes einer
I/O Routine z.B. über den Rückgabewert und GetLastError() erfolgen. Diese kann
wie das folgende Beispiel im Listing C.8 aussehen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// I/O Funktion
BOOL bReturn = ReadFileEx( hFile, szBuffer, dwAmount,
&tOverlapped, FileIOCompletionRoutine);
// Überprüfung des Rückgabewertes ob Funktion erfolgreich
if( !bReturn )
{ // Abfrage des Fehlercodes
DWORD err = GetLastError();
// Auswertung des Fehlercodes
if(
err == ERROR_INVALID_USER_BUFFER
|| err == ERROR_NOT_ENOUGH_QUOTA
|| err == ERROR_NOT_ENOUGH_MEMORY )
{
// Fehlerbehandlung für schweren Fehler
return MYERR_FATAL_ERROR;
}
// Ein weiterer Versuch
continue;
}
// ok, Funktion erfolgreich
[...]
Listing C.8: Fehlerauswertung mittels GetLastError()
C. Listings
C.5.2
128
Sicherung von Funktionen über Exceptions
Die Bibliotheken moderner Programmiersprachen, z.B. für Java und C#, melden Fehler
meist über das Auslösen von Exceptions. Fehler werden mit einem Abbruch quittiert
und Ergebnisse nur dann zurückgegeben, wenn die Funktionen fehlerfrei ausgeführt
werden konnten.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
try
{
int nBytesRead = fs.Read(ByteBuffer, 0, Amount);
[...]
}
catch(IOException ex)
{
// Fehlerbehandlung für schweren Fehler
throw new MyFatalErrorException(MYERR_FATAL_ERROR, ex);
}
catch(Exception ex)
{
[...]
}
Listing C.9: Fehlerbehandlung über Exceptions in einem korrekten C# Code
D
Sicherheits-Tools und Bibliotheken
In diesem Teil des Anhangs werden einige hilfreiche Werkzeuge und Bibliotheken zur
Lokalisierung und Vermeidung von Sicherheitsschwachstellen aufgezählt. Ein Teil dieser
Werkzeuge ist kostenlos und oft mit dem Quelltext verfügbar, andere sind proprietäre,
kostenpflichtige Produkte oder Teil einer kommerziellen integrierten Entwicklungsumgebung (IDE). In den letzten Jahren ist der Druck des Marktes auf die Software produzierende Industrie enorm gestiegen, fehlerfreie und sichere Software auszuliefern.
Demzufolge ist auch das Angebot der Tools mittlerweile entsprechend groß geworden,
wenngleich kaum ein einzelnes Tool allen Anforderungen eines Entwicklers genügt. Die
meisten dieser Werkzeuge decken nur einen Teil und spezielle Aufgaben ab.
D.1
Statische Analysewerkzeuge
Analyzer
Beschreibung / Bezugsquelle
BOON
BOON ist ein frei verfügbarer C Source Code Anylyzer, welcher
auf Buffer Overflow Schwachstellen spezialisiert ist.
URL: http://www.cs.berkeley.edu/~daw/boon/ (Stand: 20.
Mai 2007)
C/C++ Code Statisches Source Code Analyse Tool zur Entwicklung sicherer
Analyzer
Windows Applikationen. Dieses Werkzeug ist in die Visual Studio
2005 IDE integriert und über das Compiler Flag /analyze (Enable Code Analysis for C/C++) aufrufbar.
URL: http://msdn2.microsoft.com/en-us/library/
d3bbz7tz(VS.80).aspx (Stand: 17. April 2007)
129
D. Sicherheits-Tools und Bibliotheken
Flawfinder
130
Ein in Python geschriebener, frei verfügbarer lexikalischer Source
Code Analyzer, der die gefundenen potentiellen Schwachstellen
nach Risiko sortiert ausgibt.
URL: http://www.dwheeler.com/flawfinder/ (Stand: 18. Mai
2007)
FxCop
Dieses Analyse Tool des Microsoft .NET Frameworks überprüft
den Managed Code auf die Einhaltung der .NET Framework Design Guidelines und sicherheitsrelevanter Vorschriften. FxCop ist
sowohl in die Visual Studio 2007 IDE integriert als auch als eigenständiges Tool frei verfügbar.
URL: www.gotdotnet.com/Team/FxCop/ (Stand: 4. März 2007)
MOPS
MOPS ist ein frei verfügbares Tool für sicherheitsrelevante
Prüfungen auf Basis von C Quellcode.
URL: http://www.cs.berkeley.edu/~daw/mops/ (Stand: 18.
Mai 2007)
.NET
Das frei verfügbare Tool .NET Reflector ist sowohl ein Decompi-
Reflector
ler für .NET Managed Code als auch ein Class Browser und Code
Analyzer. Es eignet sich besonders zur Überprüfung von .NET
Assemblies, deren Code nicht verfügbar ist.
URL: http://www.cs.berkeley.edu/~daw/mops/ (Stand: 4.
April 2007)
PREfast
PREfast ist Teil von Visual Studio 2007 oder als eigenständiges
Tool als Teil des Windows Device Driver Kits (DDK) verfügbar.
URL: http://www.microsoft.com/whdc/devtools/tools/
PREfast.mspx (Stand: 18. Mai 2007)
D. Sicherheits-Tools und Bibliotheken
RATS
131
RATS spürt die wichtigsten sicherheitsrelevanten Programmierfehler, wie Buffer Overflows und Race Conditions im Quelltext
von C, C++, PHP, Perl und Phthon Programmen auf. RATS ist
frei verfügbar.
URL: http://www.fortifysoftware.com/security-resources/
rats.jsp (Stand: 18. Mai 2007)
Splint
Frei verfügbare Weiterentwicklung eines der ältesten und ersten
statischen Code-Analyse Programme zum Aufspüren gefährlicher
Codestellen oder möglicher Fehler. In Kenntnis heutiger Bedrohungsszenarien werden nur noch einfache Überprüfungen des
Quelltextes vorgenommen.
URL: http://www.splint.org (Stand: 18. Mai 2007)
Tabelle D.1: Auswahl einiger Werkzeuge zur statischen
Quelltextanalyse
Eine wesentlich ausführlichere Liste statischer Quelltext-Analyse-Werkzeuge sortiert
nach Programmiersprachen und Plattformen findet sich in [44].
D. Sicherheits-Tools und Bibliotheken
D.2
132
Dynamische Analysewerkzeuge
Werkzeug
Beschreibung / Bezugsquelle
APISpy32
APISpy32 ist ein frei verfügbares Werkzeug, welches API-Aufrufe
aktiver Prozesse und ihrer DLLs protokolliert und überwacht.
URL: http://www.internals.com (Stand: 26. Juli 2007)
AppVerifier
Das AppVerifier ist ein freies Tool von Microsoft zur Überprüfung
von Applikationen auf Windows XP Kompatibilität. In diesem Zusammenhang überwacht es die zu testenden Applikationen auch auf Speichermanagementfehler, unsichere StackManipulationen und API-Aufrufe, Low Resource Handling, uvm..
URL: http://support.microsoft.com/?scid=kb%3Ben-us%
3B286568&x=14&y=13 (Stand: 26. Juli 2007)
Dependency
Microsofts freies Werkzeug Dependency Walker zeigt die
Walker
Abhängigkeiten von Applikationen und DLLs zu anderen DLLs.
Dieses Tool dient in erster Linie dazu, Konflikte beim dynamischen Laden von Bibliotheken aufzuspüren und aufzuzeigen, welche Bibliotheken benötigt werden.
URL: http://www.dependencywalker.com (Stand: 22. Juni
2007)
Detours
Detours ist eine frei verfügbare Bibliothek zur Manipulation von
API Aufrufen und Datensegmenten während der Laufzeit. Mit
Hilfe von Detours können eigene Überwachungsfunktionen in eine
Software integriert werden.
URL: http://research.microsoft.com/sn/detours/ (Stand:
26. Juli 2007)
D. Sicherheits-Tools und Bibliotheken
FileMon
133
Das Tool FileMon überwacht Dateisystemaktivitäten auf einem
Windows-System in Echtzeit. Es generiert Trace-Protokolle oder
Logdateien, welche zur Auswertung noch sortiert oder gefiltert
werden können.
URL: http://www.microsoft.com/germany/technet/
sysinternals/utilities/Filemon.mspx (Stand: 26. Juli 2007)
Gflags
Das Werkzeug Global Flags (GFlags) aus den Microsoft Driver
Development Tools ist ein Werkzeug zur Steuerung von systeminternen Debugging- und Überwachungsfunktionen unter Windows.
URL: http://technet2.microsoft.com/windowsserver/en/
library/b6af1963-3b75-42f2-860f-aff9354aefde1033.mspx?
mfr=true (Stand: 22. Juni 2007)
IDA Pro
IDA Pro ist ein kostenpflichtiger High End Disassembler und Debugger für verschiedenste Plattformen und Prozessoren. Er bietet weit mehr Möglichkeiten als die gängigen Debugger der freien
SDKs und arbeitet mit einer graphischen Benutzeroberfläche. Eine Besonderheit von IDA Pro ist die graphische Aufbereitung von
Ablaufplänen und Funktionsdiagrammen.
URL: http://www.datarescue.com/idabase/ (Stand: 26. Juli
2007)
Microsoft
Die Microsoft Debugging Tools sind eine Ansammlung verschie-
Debugging
denster Debugging Werkzeuge für Windows 32-Bit und Windows
Tools for
64-Bit Versionen. Sie sind Teil der Windows SDKs, DDKs und
Windows
Customer Support Diagnostics CD.
URL: http://www.microsoft.com/whdc/DevTools/Debugging/
default.mspx (Stand: 26. Juli 2007)
D. Sicherheits-Tools und Bibliotheken
134
Process
Der Process Explorer zeigt eine Übersicht der aktuellen Prozesse
Explorer
und ihrer verwendeten DLLs. Weiters zeigt der Process Explorer Informationen über die Speicherbelegung, die Prozessorauslastung, Dateizugriffe, verwendete Handles auf Systemressourcen,
Berechtigungen, Datenströme, uvm..
URL: http://www.microsoft.com/germany/technet/
sysinternals/utilities/ProcessExplorer.mspx (Stand: 26.
Juli 2007)
Registry
RegMon ist ein Programm zur Überwachung der Windows Re-
Monitor
gistry. Es zeigt an, welche Applikationen mit welchen APIFunktionen schreibend oder lesend auf die Registry zugreifen.
Neben den Datenflüssen kann damit auch die Handhabung der
Zugriffsberechtigungen auf bestimmte Regionen der Registry
überwacht werden.
URL: http://www.microsoft.com/germany/technet/
sysinternals/utilities/Regmon.mspx (Stand: 26. Juli 2007)
Spy++
Spy++ ist ein Programm zur Überwachung und Protokollierung
der System Messages bzw. GUI Messages unter Windows. Ferner können Informationen zu den grafischen Elementen (Controls, Windows, usw.) der laufenden Programme ermittelt werden.
Spy++ ist Teil der Visual Studio Installation. Für Managed Code
bietet Microsoft das frei verfügbare Diagnose Werkzeug Managed
Spy an.
URL: http://msdn.microsoft.com/msdnmag/issues/06/04/
ManagedSpy/ (Stand: 26. Juli 2007)
D. Sicherheits-Tools und Bibliotheken
WinDbg
135
WinDbg ist ein weiterer Debugger für Microsoft Windows. Es
ermöglicht im Vergleich zum in Visual Studio 2005 integrierten
Debugger komplexere Debuggingszenarien, welche vor allem in
der Treiber- und Serviceentwicklung auf systemnächster Softwareebene zur Anwendung kommen. WinDbg ist Teil der Debugging Tools for Windows.
URL: http://www.microsoft.com/whdc/devtools/debugging/
installx86.mspx (Stand: 26.Juli 2007)
WinSpector
Eine Erweiterung von Spy++ stellt das Werkzeug WinSpector
dar, welches neben den Funktionen von Spy++ noch eine Reihe weiterer und nützlicher Funktionen zur Überwachung eines
Windows-Systems zur Verfügung stellt.
URL: http://www.windows-spy.com/ (Stand: 26. Juli 2007)
Tabelle D.2: Auswahl einiger Werkzeuge zur dynamischen Softwareanalyse
D. Sicherheits-Tools und Bibliotheken
D.3
136
Sonstige Werkzeuge
Werkzeug
Beschreibung / Bezugsquelle
Threat
Die Threat Modeling Tools von Michael Howard (siehe Referenz
Modeling
[26]) stellen eine Sammlung einfachster Applikationen zur Erstel-
Tools
lung eines Threat Model in der Designphase einer Applikation
dar.
URL: http://www.microsoft.com/
downloads/details.aspx?FamilyID=
62830f95-0e61-4f87-88a6-e7c663444ac1&displaylang=en
(Stand: 13. Juli 2007)
Tabelle D.3: Werkzeug zum Thema Softwaresicherheit
E
Good Practices für sichere Software
E.1
Ein- und Ausgabedaten
• Allen einer Funktion übergebenen Daten sollte misstraut werden und sie sollten entsprechend behandelt werden. Dazu zählen auch Konfigurationsdaten, die
Daten angeschlossener Geräte, usw..
• Alle einer Funktion übergebenen Daten sollten typisiert werden. Untypisierte
Daten (void, object, usw.) sind zu vermeiden.
• Alle einer Funktion übergebenen Werte sollten auf ihre Gültigkeit geprüft werden
(Gültigkeitsbereich, usw.).
• Alle einer Funktion übergebenen Zeiger sollten innerhalb der Funktion auf ihre
Gültigkeit überprüft werden.
• Allen einer Funktion übergebenen Puffern sollte neben der Adresse noch eine
Längenangabe hinzugefügt werden.
• Alle einer Funktion übergebenen Daten sollten auf ihre Gültigkeit überprüft werden (Checksumme, Plausibilisierung, Headers, usw.)
• Alle datenverarbeitenden Funktionen sollten einen Status der Bearbeitung liefern
(z.B. als Rückgabewert der Funktion).
• Alle von einer Funktion erhaltenen Daten sollten auf ihre Gültigkeit überprüft
werden (Checksumme, Plausibilisierung, usw.)
• Alle über öffentliche Schnittstellen übertragenen Daten sollten gesichert werden.
• Alle über eine Schnittstelle übertragenen sensiblen Daten sollten verschlüsselt
werden.
137
E. Good Practices für sichere Software
E.2
138
Zeiger- und Speicherbehandlung
• Alle Zeiger sollten vor ihrer Verwendung auf Gültigkeit überprüft werden (Nullzeiger, Speicherbereich, usw.).
• Allen einer Funktion übergebenen Zeigern sollte eine Längeninformation über
den Puffer hinzugefügt werden.
• Alle Daten eines Puffers sollten nach deren Erhalt auf Gültigkeit überprüft werden.
• Alle sensiblen Daten sollten vor deren Speicherfreigabe überschrieben werden.
• Alle sensiblen Daten sollten wenn möglich nicht gespeichert werden. Für Passwörter reicht z.B. der Vergleich der CRC zur Prüfung der Richtigkeit.
• Alle sensiblen Daten sollten, wenn überhaupt notwendig, nur verschlüsselt im
Speicher gehalten werden.
• Alle sensiblen Daten sollten nur so lange im System gehalten werden wie notwendig.
E.3
Fehlerbehandlung
• Alle Codeteile einer Applikation sollten durch eine Fehlerbehandlung abgesichert
sein. Entweder wird diese direkt in der jeweiligen Funktion implementiert oder
ein darüberliegender Mechanismus behandelt die Fehler.
• Alle datenverarbeitenden Funktionen sollten ihren Status über Rückgabewerte
melden. Funktionen vom Typ void geben entweder keine Daten zurück oder
sollten jeden Fehler über Exceptions melden.
• Alle Funktionen sollten mit Hilfe von Ausnahmen andere Programmteile über
Fehler benachrichtigen, welche keinesfalls ignoriert werden dürfen.
• Alle Ausnahmen sollten nur wirkliche Fehler, also außergewöhnliche Bedingungen
melden, keinesfalls jedoch Status- oder andere Informationen.
• Alle Ausnahmen melden von der auslösenden Funktion nicht behandelbare Fehler. Exceptions sollten nicht dazu verwendet werden, eine Problembehandlung
E. Good Practices für sichere Software
139
zu delegieren. Bedingungen, die von der Funktion selbständig behandelt werden
können, werden nicht über Exceptions weitergegeben.
• Alle Funktionstypen, welche eine einfache Fehlermeldung mittels einer Exception nicht unterstützen (z.B. Konstruktoren und Destruktoren), sollten Exception
auslösenden Code vermeiden.
• Alle Ausnahmen sollten der jeweiligen Abstraktionsebene entsprechen.
• Alle gefangenen Ausnahmen sollten auch behandelt werden. Funktionslose catchBlöcke sind in jedem Fall zu vermeiden.
• Alle Ausnahmen sollten unter Berücksichtigung der Datensicherheit all jene Informationen aufnehmen, welche den Grund der Ausnahme erklären1 .
• Alle Ausnahmebehandlungen sollten innerhalb einer Software konsistent sein.
• Alle Codeteile zur Behandlung von Fehlern sollten zusammengefasst und minimiert werden.
• Für alle Entwürfe und Codeteile sollte festgelegt werden, ob Robustheit oder
Korrektheit ausschlaggebend ist.
• Für alle Teile der Architektur sollten schon im Entwurf die jeweiligen Fehlerbehandlungstechniken festgelegt werden (Rückgabewert, Fehlercode, Exceptions,
usw.).
• Für alle Level einer Architektur sollten die gleichen Fehlerbehandlungstechniken
zur Anwendung kommen.
• Jede Applikation sollte einen Mechanismus anbieten, der unbehandelte Ausnahmen bearbeitet und kontrolliertes Terminieren der Applikation veranlasst.
1
Moderne Programmiersprachen (z.B. C#, Java) unterstützen die Ausgabe von umfangreichen
Systeminformationen im Fehlerfall. Die Ausgabe von Stack Traces und Memory Dumps ist deshalb keine Seltenheit. Auch wenn diese Vorgangsweise für den Softwareentwickler hilfreich sein mag,
sollten Applikationen auf sicherheitskritischen Systemen diese Informationen keinem Standardbenutzer zugänglich machen. Erweiterte Fehler- und Systeminformationen sind vor unbefugtem Zugriff zu
schützen.
E. Good Practices für sichere Software
E.4
140
Hilfe zur Fehlersuche
Während der Entwicklung sollten Programme mit dem sogenannten DEBUG-Flag kompiliert und im Debug-Mode getestet werden. Spezieller Code erkennt Fehler und gibt
im Fehlerfall zusätzliche Informationen aus.
• Während der Entwicklung sollte über Präprozessoren spezieller Code eingebunden werden, welcher zusätzliche Daten- und Parameterprüfungen vornimmt und
gegebenenfalls Ausnahmen auslöst.
• Während der Entwicklung sollte der Debugcode über DEBUG-Flags ein- und
ausgeblendet werden können.
• Während der Entwicklung sind alle Fehler konsequent auszugeben.
• Während der Entwicklung sind die Programme im Fehlerfall abzubrechen. Der
Softwareentwickler und Tester sollte erst gar nicht die Möglichkeit bekommen,
den Fehler zu ignorieren, auch wenn in der Endversion dieser Fehlertyp übergangen
werden könnte.
• Während der Entwicklung sind alle case-default- und if-else-Fälle abzuhandeln und mit einer Fehlermeldung zu versehen.
• Während der Entwicklung sind alle Speicherblöcke vom Anfang bis zum Ende zu
befüllen, um Speicherfehler schneller aufzudecken.
• Während der Entwicklung sind der Speicher und die Objekte mit unsinnigen
Daten zu befüllen, bevor sie gelöscht werden.
• Während der Entwicklung sollten alle Fehlerinformationen (Dumps, Logfiles,
usw.) automatisch protokolliert und an den Entwickler weitergeleitet werden.
• Während der Entwicklung sollten Versionskontrolltools und Buildtools verwendet
werden, um konsistente Programmversionen erstellen zu können.
• Während der Entwicklung sollte nur soviel Code zusätzlich eingebaut werden,
wie notwendig ist, um alle Fehler zu entdecken.
• Die fertige Release-Version sollte im Fehlerfall nur noch Informationen liefern,
welche sicherheitstechnisch unbedenklich sind.
F
Weiterführende Online-Quellen
F.1
Dokumente und Links zu Codesicherheit
• Apple Inc., Secure Coding Guide, Mai 2006.
URL: http://developer.apple.com/documentation/Security%/Conceptual/
SecureCodingGuide/SecureCodingGuide.pdf (Stand: 20. April 2007)
• Build Security In.,
URL: https://buildsecurityin.us-cert.gov/daisy/bsi/home.html (Stand:
12. April 2007)
• CERT, Secure Coding, März 2007.
URL: http://www.cert.org/secure-coding/ (Stand: 12. April 2007)
• The Honeynet Project - To Learn the Tools, Tactics and Motivities involved in
Computer and Network Attacks
URL: http://project.honeynet.org (Stand: 16. Mai 2007)
• Microsoft Developer Network - Security (General)
URL: http://msdn2.microsoft.com/en-us/library/bb545118.aspx (Stand:
15. Juni 2007)
F.2
News, Newsletter und Mailing-Listen und RSSFeeds zu Softwaresicherheit
• CVE - Common Vulnerabilities and Exposures
URL: http://cve.mitre.org/ (Stand: 28. Mai 2007)
• Bugtraq
URL: http://www.securityfocus.com/archive/1 (Stand: 15. April 2007)
141
F. Weiterführende Online-Quellen
142
• United States Department of Justice, Computer Crime & Intellectual Property
Section
URL: http://www.cybercrime.gov (Stand: 3. April 2007)
• Full-Disclosure
URL: http://lists.openwall.net/full-disclosure (Stand: 15. April 2007)
• ISS - IBM Internet Security Systems
URL: http://xforce.iss.net/ (Stand: 14. Mai 2007)
• SANS, Computer Security Newsletters and Digests
URL: http://www.sans.org/newsletters (Stand: 15. April 2007)
• Heise Security - News Dienste und Hintergrundinformationen zum Thema Sicherheit
URL: http://www.heise.de/security (Stand: 14. April 2007)
• SecurityFocus
URL: http://www.securityfocus.com/bid (Stand: 14. April 2007)
• SecurityTracker
URL: http://www.securitytracker.com (Stand: 15. April 2007)
• US-CERT - United States Computer Emergency Readiness Team US-CERT
Channels
URL: http://www.us-cert.gov/channels (Stand: 15. Mai 2007)