Security Briefs
Schützen Ihres Codes mit Visual C++-Verteidigungen
Michael Howard

Inhalt
Eine Menge Code wird in C und C++ geschrieben. Leider weist großer Teil davon Sicherheitsrisiken auf, denen sich viele Entwickler nicht bewusst sind. Programme können unabhängig von der Sprache, in der sie geschrieben werden, mit Sicherheitsrisiken behaftet sein, die dazu führen, dass ihre Benutzer Angriffen ausgesetzt sind. Den Sprachen C und C++ gebührt jedoch eine besondere Stellung in der Geschichte des Internets, da sich eine Vielzahl an Sicherheitsrisiken genau auf die Eigenschaft zurückführen lassen, die diese beiden Programmiersprachen so beliebt macht: den grenzenlosen Zugriff auf Computerhardware und die damit verbundene Leistung. Wenn im Zusammenhang mit Sicherheit von C oder C++ die Rede ist, tauchen in der Regel auch die Begriffe „Puffer“ und „Überlauf“ auf, da Puffer in der Regel ein Beispiel für direkten Speicherzugriff sind. Diese Art von Direktzugriff ist sehr leistungsfähig – und äußerst gefährlich.
Für die zahlreichen Pufferüberläufe in Produktionscode in C und C++ gibt es eine Reihe von Gründen. Der erste wurde hier bereits erwähnt. Die Sprachen ermöglichen den Direktzugriff auf anfälligen Speicher. Zweitens machen Entwickler Fehler. Und drittens bieten Compiler normalerweise keinen Schutz. Für das erste Problem lässt sich Abhilfe schaffen, aber dann werden aus C und C++ andere Sprachen.
Die Fehler der Entwickler lassen sich möglicherweise durch bessere Ausbildung verringern. Allerdings halte ich es für unwahrscheinlich, dass die Bildungseinrichtungen ihre Anstrengungen intensivieren. Natürlich gebührt der Sicherheitsausbildung auch ein Platz in der Branche. Allerdings ist jeder einzelne von uns Teil der Lösung oder Teil des Problems, und es wäre wirklich wünschenswert, wenn Hochschulen mehr in die Ausbildung ihrer Studenten zum Thema Softwaresicherheit investieren würden. Sie fragen sich wahrscheinlich: „Warum bieten Bildungseinrichtungen keine Lehrveranstaltungen zu diesem äußerst wichtigen Thema an?“ Mir ist das ein völliges Rätsel. Eigentlich ist es ziemlich deprimierend.
Einige Sicherheitsprobleme sind jedoch derart komplex, dass sogar gut ausgebildeten Technikern Fehler unterlaufen. Fehler sind menschlich.
Das Visual C++-Team von Microsoft befasst sich schon seit einiger Zeit mit dem Problem, besseren Schutz in Compiler zu integrieren. Mit der Hilfe unseres Sicherheitsteams wurden im Laufe der Jahre Verbesserungen erzielt. In diesem Artikel werden Möglichkeiten des Pufferüberlaufschutzes erläutert, die in Visual C++® 2005 und später verfügbar sind. Es sollte erwähnt werden, dass einige andere Compiler Schutz bieten. Visual C++ hat jedoch gegenüber GCC zwei Hauptvorteile. Der erste besteht darin, dass alle Schutzmechanismen standardmäßig im Toolset verfügbar sind. Sie müssen nicht extra Add-Ins herunterladen. Zweitens sind die Optionen einfach zu verwenden.
Das Visual C++-Toolset bietet folgende Schutzmechanismen (in beliebiger Reihenfolge):
- Stapelbasierte Pufferüberlauferkennung (/GS)
- Safe Exception Handling (/SafeSEH)
- Kompatibilität mit Datenausführungsverhinderung (Data Execution Prevention, DEP) (/NXCompat)
- Image Randomization (/DynamicBase)
- Automatische Verwendung sichererer Funktionsaufrufe
- C++ operator::new
Bevor auf die einzelnen Mechanismen näher eingegangen wird, möchte ich darauf hinweisen, dass sie nicht als Ersatz für unsicheren Code dienen dürfen. Ihr Ziel sollte immer darin bestehen, möglichst sicheren Code zu erstellen. Wenn Sie nicht wissen, wie Sie dabei vorgehen müssen, sollten Sie zunächst einige der ausgezeichneten Bücher lesen, die zu diesem Thema verfügbar sind.
Typischer Stapel im Vergleich zu einem mit /GS kompilierten Stapel (Klicken Sie zum Vergrößern auf das Bild)
Außerdem soll darauf hingewiesen werden, dass es sich hier durchweg um Anforderungen des Sicherheitsentwicklungszyklus (Security Development Lifecycle, SDL) bei Microsoft handelt. Der C- und C++-Code ist also nur lieferfähig, wenn diese Optionen verwendet werden. Gelegentlich gibt es Ausnahmen, allerdings nur relativ selten. Daher wird darauf nicht näher eingegangen.
Schließlich gilt es noch diesen wichtigen Punkt zu bedenken: Je nach Code ist es möglich, diese ausgeklügelten Schutzmechanismen zu umgehen. Je mehr der Code davon verwendet, desto schwieriger ist es, sie zu umgehen. Kein Verteidigungsmechanismus ist jedoch perfekt. Diese Mechanismen wirken wie Bremsschwellen, die die Möglichkeit verringern, dass ein Exploit zum Erfolg führt. Sie sind also gewarnt! Als echte Schutzmechanismen wirken nur sicherere Funktionsaufrufe. Dadurch lassen sich Sicherheitsrisiken beseitigen. Im Folgenden werden die einzelnen Schutzmechanismen ausführlicher betrachtet.
Stapelbasierte Pufferüberlauferkennung (/GS)
Die stapelbasierte Pufferüberlauferkennung ist die älteste und bekannteste Verteidigung, die in Visual C++ zur Verfügung steht. Das Ziel des /GS-Compilerkennzeichens ist einfach: Verringern der Möglichkeit, dass schädlicher Code korrekt ausgeführt wird. Die /GS-Option ist in Visual C++ 2003 und höher standardmäßig aktiviert und erkennt bestimmte Arten von Stack Smash zur Laufzeit. Hierzu wird im Stapel einer Funktion unmittelbar vor der Absenderadresse eine Zufallszahl eingefügt. Bei Rückgabe der Funktion überprüft der Funktionsepilogcode diesen Wert, um sicherzustellen, dass er nicht geändert wurde. Wenn das Cookie beim Aufruf eine Änderung aufweist, wird die Ausführung angehalten.
Der Funktionsprologcode, von dem das Cookie festgelegt wird, sieht folgendermaßen aus:
sub esp, 8
mov eax, DWORD PTR ___security_cookie
xor eax, esp
mov DWORD PTR __$ArrayPad$[esp+8], eax
mov eax, DWORD PTR _input$[esp+4]
Im Folgenden wird der Funktionsepilogcode gezeigt, der das Cookie überprüft:
mov ecx, DWORD PTR __$ArrayPad$[esp+12]
add esp, 4
xor ecx, esp
call @__security_check_cookie@4
add esp, 8
Visual C++ 2005 verschiebt außerdem Daten im Stapel, um die Beschädigung von Daten zu erschweren. So werden beispielsweise Puffer in höhere Speicher verschoben als Nichtpuffer. Dieser Schritt kann helfen, Funktionszeiger im Stapel zu schützen Durch das Verschieben von Zeiger- und Pufferargumenten in untere Speicher zur Laufzeit lässt sich das Risiko mehrerer Pufferüberlaufangriffe verringern. Das Diagramm zeigt einen Vergleich eines typischen Stapels mit einem /GS-Stapel.
In den folgenden Situationen wird die /GS-Compileroption nicht angewendet:
- Funktionen enthalten keinen Puffer.
- Optimierungen sind nicht aktiviert.
- Für Funktionen sind Variablenargumentlisten festgelegt.
- Funktionen sind mit dem naked-Schlüsselwort (C++) markiert.
- Funktionen enthalten in der ersten Anweisung Inlineassemblycode.
- Der Puffer ist nicht vom 8-Byte-Typ, und seine Größe beträgt weniger als 4 Byte.
Visual C++ 2005 SP1 wurde eine Option zur Erhöhung der Aggressivität der /GS-Heuristik hinzugefügt, damit mehr Funktionen geschützt werden können. Microsoft hat sich zu diesem Schritt entschlossen, da eine kleine Anzahl von Sicherheitsbulletins in Code mit stapelbasierten Pufferüberläufen veröffentlicht und der Code, obgleich mit /GS kompiliert, nicht durch ein Cookie geschützt wurde. Durch diese neue Option erhöht sich die Anzahl der geschützten Funktionen erheblich.
Verwenden Sie diese Option, indem Sie die folgende Zeile in Module einfügen, die zusätzlich geschützt werden sollen, z. B. Code, der Daten aus dem Internet verarbeitet:
#pragma strict_gs_check(on)
Dieses Pragma ist nur ein Beispiel für die ständige Weiterentwicklung der /GS-Funktion durch Microsoft. Die ursprüngliche Version in Visual C++ 2003 war ziemlich einfach und wurde dann in Visual C++ 2005 SP1 und nochmals in Visual C++ 2008 aktualisiert, als neue Angriffe sowie neue Möglichkeiten zur Umgehung vorhandener Angriffe bekannt wurden. Unsere Analyse hat ergeben, dass /GS keine ernst zu nehmenden Kompatibilitäts- oder Leistungsprobleme verursacht.
Safe Exception Handling (/SafeSEH)
Der CodeRed-Wurm, der Internet Information Server (IIS) 4.0 betraf, wurde durch einen stapelbasierten Pufferüberlauf verursacht. Interessanterweise hätte /GS das vom Wurm ausgenutzte Problem nicht entdeckt, da der Code keinen Überlauf der Absenderadresse der betroffenen Funktion verursachte. Vielmehr wurde ein Ausnahmehandler im Stapel beschädigt. Dieses Beispiel verdeutlicht, warum Sie immer möglichst sicheren Code schreiben sollten, und sich nicht allein auf die Verteidigung auf Compilerbasis verlassen dürfen.
Ein Ausnahmehandler ist Code, der ausgeführt wird, wenn eine außergewöhnliche Bedingung eintritt, z. B. eine Teilung durch Null. Die Adresse des Handlers wird auf dem Stapelrahmen der Funktion gehalten und ist deshalb möglicherweise Beschädigungen ausgesetzt. Der Linker ist in Visual Studio® 2003 und späteren Versionen enthalten und bietet eine Option zum Speichern der Liste gültiger Ausnahmehandler im PE-Header des Abbilds zur Kompilierzeit. Wenn zur Laufzeit eine Ausnahme ausgelöst wird, überprüft das Betriebssystem den Abbildheader, um zu bestimmen, ob die Ausnahmehandleradresse korrekt ist. Ist dies nicht der Fall, wird die Anwendung beendet. Dies hätte eine Beschädigung durch den CodeRed-Wurm verhindert, wenn die Technologie zu dem Zeitpunkt zur Verfügung gestanden hätte. Die /SafeSEH-Option führt zu keinerlei Leistungseinbußen, abgesehen von den Fällen, in denen eine Ausnahme ausgelöst wird. Daher sollten Sie immer eine Verknüpfung mit dieser Option herstellen.
DEP-Kompatibilität (/NXCompat)
Fast jede heute hergestellte CPU unterstützt eine „no execute“-Funktion (NX). Das heißt, dass die CPU keine Nicht-Codepages ausführt. Denken Sie einen Moment darüber nach, was dies bedeutet: Fast alle Sicherheitsrisiken durch Pufferüberlauf sind Datenfehler. Der Angreifer fügt dann über den Pufferüberlauf Daten in den Prozess ein und setzt die Ausführung innerhalb des schädlichen Datenpuffers fort. Warum aber führt die CPU Daten aus?
Durch Verknüpfung mit der /NXCompat-Option wird die ausführbare Datei von der „no execute“-Funktion der CPU geschützt. Das Sicherheitsteam von Microsoft konnte bis jetzt nur sehr wenige Kompatibilitätsprobleme beobachten, die sich auf diese Option zurückführen ließen. Eine Leistungsverschlechterung findet nicht statt.
In Windows Vista® SP1 steht darüber hinaus eine neue API zur Verfügung, die DEP für den ausgeführten Prozess aktiviert. Diese API kann nicht wieder entfernt werden, wenn sie erst einmal festgelegt wurde:
SetProcessDEPPolicy(PROCESS_DEP_ENABLE);
Image Randomization (/DynamicBase)
Windows Vista und Windows Server® 2008 unterstützen Image Randomization. Das bedeutet, beim Systemstart werden Betriebssystemabbilder im Speicher nach dem Zufallsprinzip verschoben. Der Zweck dieses Features besteht darin, die Vorhersagbarkeit zu verringern und damit Angriffe zu erschweren. Dies wird auch Address Space Layout Randomization (ASLR) genannt. Wenn ASLR von Nutzen sein soll, müssen Sie außerdem DEP aktivieren.
Standardmäßig schiebt Windows® nur Systemkomponenten hin und her. Wenn das Abbild vom Betriebssystem verschoben werden soll (ausdrücklich empfohlen), dann sollten Sie eine Verknüpfung mit der /DynamicBase-Option herstellen. (Diese Option ist im Toolset von Visual Studio 2005 SP1 und höher verfügbar.) Bei der Verknüpfung mit der /DynamicBase-Option gibt es einen interessanten Nebeneffekt: Auch der Stapel wird vom Betriebssystem zufällig angeordnet, was dazu beiträgt, die Vorhersagbarkeit zu verringern. Damit wird es Angreifern wesentlich erschwert, ein System erfolgreich zu beeinträchtigen. Beachten Sie, dass auch der Heap in Windows Vista und Windows Server 2008 zufällig angeordnet wird. Dies geschieht dort jedoch standardmäßig. Es besteht keine Notwendigkeit der Kompilierung oder Verknüpfung mit besonderen Optionen.
Sicherere Funktionsaufrufe
Betrachten Sie die folgenden Codezeilen:
void func(char *p) {
char d[20];
strcpy(d,p);
// etc
}
Wenn angenommen wird, dass *p nicht vertrauenswürdige Daten enthält, stellt dieser Code ein Sicherheitsrisiko dar. Das Merkwürdige an diesem Code besteht darin, dass der Compiler unter Umständen den Aufruf von „strcpy“ in einen sichereren Funktionsaufruf hätte umwandeln können, der den Kopiervorgang auf die Größe des Zielpuffers begrenzt. Warum? Weil die Puffergröße statisch und zum Zeitpunkt der Kompilierung bekannt ist!
Mit Visual C++ können Sie der stdafx.h-Headerdatei die folgende Zeile hinzufügen:
#define _CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES 1
Der Compiler gibt dann den folgenden Code aus der ursprünglichen, unsicheren Funktion aus:
void func(char *p) {
char d[20];
strcpy_s(d,__countof(d), p);
// etc
}
Wie Sie sehen, ist der Code jetzt sicher. Dabei hat der Entwickler lediglich „#define“ hinzugefügt. Dies ist eine meiner Lieblingserweiterungen von Visual C++, da ungefähr 50 Prozent der unsicheren Funktionsaufrufe automatisch in sicherere Aufrufe umgewandelt werden können.
C++ operator::new
Schließlich verfügt Visual C++ 2005 und später über einen Schutz, der Ganzzahlüberlauf erkennt, wenn „operator::new“ aufgerufen wird. Schreiben Sie zunächst Code wie diesen:
CFoo *p = new CFoo[count];
Das Ergebnis der Kompilierung durch Visual C++ sieht so aus:
00004 33 c9 xor ecx, ecx
00006 8b c6 mov eax, esi
00008 ba 04 00 00 00 mov edx, 4
0000d f7 e2 mul edx
0000f 0f 90 c1 seto cl
00012 f7 d9 neg ecx
00014 0b c8 or ecx, eax
00016 51 push ecx
00017 e8 00 00 00 00 call ??2@YAPAXI@Z ; operator new
Nachdem die zuzuordnende Speichermenge berechnet wurde (mul edx), wird das CL-Register abhängig vom Wert des overflow-Kennzeichens nach der Multiplikation entweder festgelegt oder nicht festgelegt. ECX lautet daher 0x00000000 oder 0xFFFFFFFF. Aufgrund des nächsten Vorgangs (oder ecx) hat das ECX-Register entweder den Wert 0xFFFFFFFF oder den in EAX gespeicherten Wert, der das Ergebnis der ursprünglichen Multiplikation ist. Dieser wird dann an „operator::new“ übergeben, was angesichts der 2^N-1-Zuordnung fehlschlägt.
Dieser Schutz erfolgt automatisch. Es ist kein Compilerschalter erforderlich, sondern der Compiler verhält sich automatisch auf diese Weise.
Was passiert bei einem Fehlschlagen?
Eine äußerst heikle Frage! Die Auslösung eines der oben genannten Schutzmechanismen hat ziemlich unangenehme Folgen: Die Anwendung wird beendet. Das ist zwar alles andere als optimal, aber immer noch besser als das Ausführen des schädlichen Codes des Angreifers.
Aufgrund der hohen Anzahl von Angriffen und der Tatsache, dass nicht nachweisbar ist, dass ein Code absolut keine Sicherheitsrisiken enthält, muss nach SDL-Anforderungen neuer Code unter Beachtung all dieser Verteidigungsoptionen geschrieben werden. Einer der Leitgedanken von SDL ist es, in Betracht zu ziehen, was bei einem Fehlschlagen von Code passieren soll. In einem solchen Fall sollen Sie sich zur Wehr setzen! Überlassen Sie dem Code des Angreifers nicht das Feld, sondern machen Sie es dem Angreifer schwer, Exploits auszunutzen. Geben Sie nicht auf!
Verwenden Sie zum Kompilieren daher die aktuelle Version des C++-Compilers, um /GS zu verbessern, und stellen Sie Verknüpfungen mit dem aktuellen Linker her, um die Schutzmechanismen von CPU und Betriebssystem nutzen zu können.
Senden Sie Ihre Fragen und Kommentare (in englischer Sprache) an briefs@microsoft.com.
Michael Howard ist leitender Programmmanager für Sicherheit bei Microsoft und konzentriert sich auf die Verbesserung der Prozesssicherheit sowie auf bewährte Methoden. Er ist Mitautor von fünf Büchern über Sicherheit, darunter
Writing Secure Code for Windows Vista,
The Security Development Lifecycle,
Writing Secure Code und
19 Deadly Sins of Software Security. Besuchen Sie seinen Blog unter
blogs.msdn.com/michael_howard.