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)