MSDN Magazin > Home > Ausgaben > 2007 > November >  Codeüberprüfungen: Erkennen und Korri...
Codeüberprüfungen
Erkennen und Korrigieren von Sicherheitsrisiken vor der Auslieferung Ihrer Anwendung
Michal Chmielewski and Neill Clift and Sergiusz Fonrobert and Tomasz Ostwald

Themen in diesem Artikel:
  • Der Codeüberprüfungsprozess
  • Priorisieren des Codes für die Überprüfung
  • Arten von Sicherheitsrisiken
  • Verwendung der Überprüfungsergebnisse
In diesem Artikel werden folgende Technologien verwendet:
In der Softwareentwicklung kann ein kleiner Codefehler zu einem großen Sicherheitsrisiko führen, das die Sicherheit eines ganzen Systems oder Netzwerks gefährdet. Oftmals wird ein Sicherheitsrisiko jedoch nicht durch einen einzelnen Fehler, sondern durch eine Folge von Fehlern verursacht, die im Lauf des Entwicklungszyklus eintreten: Ein Codefehler wird eingeführt, während der Testphasen nicht entdeckt, und verfügbare Abwehrmechanismen unterbinden einen erfolgreichen Angriff nicht.
Sicherheit muss eine Priorität in allen Phasen der Softwareentwicklung sein. Softwaresicherheitsrisiken müssen verhindert werden. Natürlich müssen Sicherheitsrisiken vor Veröffentlichung der Software entdeckt werden, zudem müssen aber auch praktische Auswirkungen potenzieller Risiken limitiert werden (beispielsweise durch Reduzieren der Angriffsfläche). Bei Microsoft wird dieser ganzheitliche Ansatz durch den Sicherheitsentwicklungszyklus (Security Development Lifecycle, SDL) implementiert, der alle Hauptphasen der Softwareentwicklung, einschließlich Entwicklerschulung, Entwurfsverbesserung und Einsatz von Codier- und Testmethoden, sowie die Erstellung von Notfallmaßnahmen nach der Veröffentlichung eines Produkts abdeckt (siehe Abbildung 1). SDL ist nicht die einzige Möglichkeit für die Codeüberprüfung, bildet jedoch die Grundlage einer Vielzahl der Dinge, die hier behandelt werden.
Abbildung 1 Der Sicherheitsentwicklungszyklus (Klicken Sie zum Vergrößern auf das Bild)
In diesem Artikel geht es um manuelle Sicherheitscodeüberprüfungen, die von Entwicklern oder Sicherheitsfachleuten durchgeführt werden. In einem gemäß SDL definierten Prozess werden solche Aufgaben gewöhnlich während einer Sicherheitsinitiative oder eines Penetrationstests durchgeführt und sind mit einer abschließenden Sicherheitsüberprüfung verbunden. Codefehler können mithilfe verschiedener Ansätze aufgedeckt werden, doch selbst im Vergleich zu hoch entwickelten Tools haben manuelle Codeüberprüfungen ihren Wert hinsichtlich Präzision und Qualität unter Beweis gestellt. Leider sind manuelle Codeüberprüfungen auch am teuersten.
Hier sollen auch die Vor- und Nachteile von Sicherheitsüberprüfungen von Code im Kontext großer Softwareprojekte detailliert erörtert werden. Dieser Artikel basiert auf den im Lauf der Zeit bei Überprüfungen von Hauptprodukten, die in den letzten Jahren von Microsoft veröffentlicht wurden, gesammelten Erfahrungen.

Softwaresicherheitsrisiken
Bei einem Softwareprodukt einer gewissen Größe und Komplexität darf nie davon ausgegangen werden, dass es frei von Sicherheitsrisiken ist. Es müssen alle erdenklichen Schritte zur Beschränkung der Anzahl von Codefehlern und zur Reduzierung von deren praktischen Auswirkungen unternommen werden, aber zumeist wird dabei etwas übersehen. Softwarefehler, die sich auf die Sicherheit auswirken (und als Sicherheitsrisiken bezeichnet werden), können auf verschiedenen Ebenen der Anwendung vorhanden sein und in verschiedenen Phasen des Entwicklungszyklus eingeführt werden.
Sicherheitsrisiken sind nicht auf den Code beschränkt. Sie können bereits bei der Formulierung der Anforderungen in Form einer Anforderung eingeführt werden, die nicht auf sichere Weise implementiert werden kann. Der Grundentwurf eines Produkts kann ebenfalls Fehler enthalten. So können beispielsweise ungeeignete Technologien ausgewählt werden, oder ihre Verwendung kann vom Sicherheitsstandpunkt aus falsch sein. Im Idealfall werden alle diese Probleme bei Entwurfsüberprüfungen oder bei der Bedrohungsmodellierung während der Frühphasen der Produktentwicklung identifiziert.
Sicherheitsüberprüfungen des Codes zielen hauptsächlich auf die Suche nach Problemen auf Codeebene ab, die nach wie vor die Mehrzahl von Sicherheitsrisiken verursachen. Diese Bemühungen können auch zur Identifizierung von Entwurfsproblemen führen, aber solche Probleme könnten mit erforderlichen Verbesserungen bei der Bedrohungsmodellierung und anderen Aspekten des Entwicklungsprozesses im Zusammenhang stehen. Quellcodeüberprüfungen können ebenfalls mit Prioritäten durchgeführt werden, die nicht auf die Sicherheit bezogen sind. Doch in diesem spezifischen Kontext geht es um die Suche nach Codeschwachstellen, die zur Verletzung der Sicherheitsgarantien des Produkts oder zur Beeinträchtigung der Sicherheit des Systems genutzt werden könnten.
Ein Codefehler, der potenziell Sicherheitsprobleme verursachen kann (beispielsweise Probleme aufgrund mangelnder Prüfung), muss bestimmte Bedingungen erfüllen, um ein Sicherheitsrisiko darzustellen. Es muss eine Sicherheitsgrenze vorhanden sein, die angegriffen werden kann, und ein Angreifer muss eine gewisse Kontrolle über die Daten oder die Umgebung haben. Probleme in Code, der innerhalb desselben Sicherheitskontexts wie der Angreifercode ausgeführt wird, bieten keinen potenziellen Berechtigungsgewinn beim Ausnutzen der Schwachstelle. In anderen Fällen sind Sicherheitsrisiken vorhanden, die sich jedoch in Code befinden, der nicht ausgeführt werden kann, weil er dem Angreifer nicht zugänglich ist. Diese Codefehler können sich zwar auf die Produktzuverlässigkeit auswirken, sollten jedoch nicht als eigentliche Schwachstellen betrachtet werden. Das Endziel bei Sicherheitsüberprüfungen ist das Aufdecken von Codeschwachstellen, die für einen Angreifer zugänglich sind und es diesem evtl. ermöglichen, eine Sicherheitsgrenze zu umgehen.
Der Zugriff auf eine Schwachstelle ist dabei nicht gleichbedeutend mit dessen Ausnutzbarkeit. Ein erfolgreicher Angriff kann durch Plattformverbesserungen wie das Kennzeichen „/GS“, das Kennzeichen „/SafeSEH“ oder Address Space Layout Randomization (ASLR) immer noch entschärft werden.
Die Ausnutzbarkeit einer Codeschwachstelle gehört nicht zum Umfang der Codeüberprüfung und zwar hauptsächlich deshalb, weil es normalerweise unmöglich ist nachzuweisen, dass verfügbare Gegenmaßnahmen gegen eine Ausnutzung ausreichen. Die Verantwortlichkeiten des Codeprüfers enden mit der Bestätigung, dass eine Codeschwachstelle vorhanden ist und dass diese angemessen selektiert wurde. Von diesem Augenblick an, sollte der Fehler einfach als ein weiteres Problem betrachtet werden, das behoben werden muss.
Die Identifizierung von Codeschwachstellen ist ein Hauptziel der Codeüberprüfung, doch zusätzliche Ergebnisse sind möglich. So kann der Prüfer Feedback zur Gesamtqualität des Codes, Redundanz, zu totem Code oder unnötiger Komplexität geben. Prüfer können auch Empfehlungen für Verbesserungen zur Verminderung der Angriffsfläche, Datenüberprüfung, Codebereinigung oder Lesbarkeit des Codes (beispielsweise die Verbesserung von Kommentaren) geben. Da die Dokumentierung solcher Ergebnisse jedoch Zeit erfordert, sollten Sie zu Beginn entscheiden, ob die Ergebnisse auch solche Empfehlungen umfassen sollen oder ob allein die Identifizierung von Sicherheitsproblemen im Mittelpunkt stehen soll.

Suche nach Codefehlern
Es gibt verschiedene Ansätze bei der Suche nach Codefehlern. Da jeder sowohl eindeutige Vorteile als auch praktische Einschränkungen hat, ist es wichtig, den Unterschied zwischen Codeüberprüfungen und anderen Optionen zu verstehen. In diesem Artikel wird davon ausgegangen, dass sowohl der Quellcode als auch die Entwurfsdokumentation zur Verfügung stehen und dass eine so genannte Whiteboxanalyse durchgeführt wird. Dabei handelt es sich um eine interne Analyse im Gegensatz zum Blackboxansatz, bei dem nur extern sichtbares Verhalten im Mittelpunkt steht.
Codeüberprüfungen können als manuelle statische Methode zur Suche von Codefehlern beschrieben werden. Analog dazu können zwei weitere gebräuchliche Methoden als automatisierte statische Methode und als automatisierte dynamische Methode beschrieben werden. Die automatisierte statische Methode hat gewöhnlich die Form von statischen Codeanalysetools, die den Quellcode untersuchen, um bekannte Arten von definierten Problemen mithilfe von Mustern zu diagnostizieren. PREfix und PREfast sind Beispiele für diese Methode. Bei der automatisierten dynamischen Methode kommen automatisierte Codetesttechniken (beispielsweise Testen mit zufälligen Daten) zum Einsatz, die hauptsächlich auf Dateien, Protokolle und APIs abzielen. Obwohl diese Lösungen auch in einem Blackboxtest angewendet werden können, lassen sich oft viel bessere Ergebnisse erzielen, wenn Informationen zu internen Elementen wie Dateiformaten zur Verfügung stehen und entsprechend verwendet werden.
Jede Methode besitzt bestimmte praktische Vorteile und Einschränkungen. Mit Tools für die statische Codeanalyse kann mehr Code durch Automatisierung verarbeitet werden, doch die Ergebnisse sind durch den Satz vordefinierter Muster für bekannte Arten von Problemen stark eingeschränkt. Die Ergebnisse enthalten zudem oft eine große Anzahl falscher Positivdiagnosen, die die Problembehebung mit begrenzten Ressourcen schwierig gestalten. Tests mit zufälligen Daten können leicht automatisiert und fortwährend durchgeführt werden, doch sie arbeiten zumindest teilweise zufällig, und es kann problematisch sein, in tiefere Bereiche des Codes vorzudringen. In den meisten Fällen ist es verhältnismäßig einfach, einen grundlegenden Test mit zufälligen Daten durchzuführen, es ist jedoch wesentlich schwieriger, kritische Codepfade vollständig abzudecken.
Verglichen mit automatisierten Methoden stehen die Vor- und Nachteile von manuellen Quellcodeüberprüfungen hauptsächlich mit der direkten Beteiligung eines Prüfers im Zusammenhang. Die Ergebnisse solcher Überprüfungen können sehr unterschiedlich ausfallen, da sie von der Erfahrung des Prüfers im Einsatz bestimmter Technologien, Architekturen und Szenarios abhängen. Gewöhnlich kann davon ausgegangen werden, dass ein Prüfer immer noch Vorteile gegenüber einer computerbasierten Lösung bietet. Der Prüfer kann im Verlauf des Prozesses dazulernen, den Kontext von Softwarekomponenten verstehen und mit Designern und Entwicklern interagieren. Zudem kann er Feedback geben, das nicht auf einen ausführlichen Problembericht begrenzt ist, sondern auch Empfehlungen auf hoher Ebene umfassen kann.
Gleichzeitig ist ein Prüfer jedoch anfällig für Fehler, Ermüdung oder Langeweile. Der Umfang einer Prüfung ist gewöhnlich begrenzt, und die Bewertung der Überprüfungsqualität kann schwierig sein, da sie normalerweise auf der subjektiven Zuversicht des Prüfers beruht.
Doch diese Nachteile können überwunden werden. Es kann eine aufgabenspezifische Überprüfung entworfen werden, die auf die Analyse eines Codeblocks in mehreren Durchläufen für jeweils nur eine Fehlerart abzielt. Bei einem anderen Ansatz können mehrere Prüfer unabhängige Überprüfungen eines kritischen Codeabschnitts durchführen, sodass die Wahrscheinlichkeit menschlicher Fehler begrenzt wird. Codeüberprüfungen sind einer der spezifischen Fälle, bei denen Redundanz großen potenziellen Wert hat, da die Grenzen menschlicher Beteiligung überwunden werden.
Die manuelle Codeüberprüfung sollte nie als die beste Lösung bei der Suche nach Codesicherheitsrisiken oder als Ersatz für andere Ansätze betrachtet werden. Vielmehr stellt sie eine komplementäre Lösung dar. Eine Codeüberprüfung darf nie die Bedrohungsmodellierung, Tests mit zufälligen Daten oder die Durchsetzung bewährter Codiermethoden ersetzen. Verglichen mit anderen Ansätzen ist die Codeüberprüfung normalerweise teurer, sodass sie hauptsächlich in den kritischsten Bereichen und bei Problemen verwendet werden sollte, bei denen die Wirksamkeit anderer Ansätze begrenzt ist.

Der Codeüberprüfungsprozess
Die Codesicherheitsüberprüfung ist am erfolgreichsten, wenn sie geplant und im Kontext anderer sicherheitsbezogener Initiativen, beispielsweise zusammen mit der Bedrohungsmodellierung ausgeführt wird (siehe Abbildung 2). Außerdem können die Ergebnisse von Codeüberprüfungen zusätzlichen Wert bieten, indem andere Sicherheitsaufgaben wie Tests und Entwurf verbessert werden.
Abbildung 2 Codeüberprüfungen 
Der Wert guter Bedrohungsmodelle für die Codeüberprüfung kann nicht überschätzt werden. Zur Durchführung einer erfolgreichen Codeüberprüfung muss ein Prüfer die Ziele eines Produkts, dessen Aufbau und die zur Implementierung verwendeten Technologien gut kennen. Die ersten beiden Bereiche werden ebenfalls von der Bedrohungsmodellierung abgedeckt, und obwohl sie sich auf die Suche nach Problemen auf hoher Ebene konzentrieren, umfassen sie Untersuchungen, die auch bei der Codeüberprüfung nützlich sind. Diese beiden Arten von Sicherheitsbemühungen ergänzen einander normalerweise. Die Bedrohungsmodellierung hilft bei der Identifizierung eines kritischen Codebereichs, der dann eingehender überprüft wird. Ergebnisse aus einer Codeüberprüfung können ebenfalls verwendet werden, um in einem Bedrohungsmodell spezifizierte Sicherheitsannahmen zu überprüfen (oder in Frage zu stellen).
Im Idealfall beginnt eine Codesicherheitsüberprüfung mit einer Überprüfung der Modelle der Qualitätsbedrohung und Entwurfsspezifikationen und geht dann zum Quellcode über. Zuvor sollte die gesamte Entwicklungsarbeit am Code, der überprüft werden soll, abgeschlossen sein. Zur Diagnose einfacher Probleme sind verfügbare Sicherheitstools am Code auszuführen, bevor die manuelle Überprüfung beginnt. Damit nicht nach bereits bekannten Problemen gesucht wird, müssen alle zuvor identifizierten Sicherheitsfehler behoben werden.
Das Sicherheitsteam sollte Codeüberprüfungsaufgaben bereits frühzeitig im Entwicklungslebenszyklus planen, um den besten Zeitpunkt für deren Ausführung zu identifizieren. Der Plan selbst muss auch organisatorische Entscheidungen berücksichtigen, die sich auf die Gesamtrendite auswirken können. Es gibt beispielsweise drei wahrscheinliche Optionen für die Auswahl der Teilnehmer bei einer Codeüberprüfung. Zum einen kann es sich um externe Prüfer handeln, die mit dem Code und dem Produkt nichts zu tun haben (Codeüberprüfungsfachleute aus einem Penetrationstestteam). Zweitens könnte es ein Prüfer sein, der nichts mit dem Code zu tun hat, aber mit dem Produkt vertraut ist (ein Entwickler aus einem anderen Team in derselben Organisation). Schließlich können Prüfer sowohl mit dem Code als auch dem Produkt vertraut sein (beispielsweise ein Entwickler, der an dem Code arbeitet).
Jede Option hat sowohl Vor- als auch Nachteile. Im Fall von Prüfern, die selbst nichts mit einem Produkt zu tun haben, werden normalerweise sachkundige Sicherheitsfachleute ausgewählt, die aufgrund ihrer besonderen Erfahrung und Perspektive einen wichtigen Beitrag leisten können. In den meisten Fällen verfügen sie jedoch im Vergleich zu den Mitgliedern des Produktteams über weniger Kenntnisse hinsichtlich der Interna und Implementierungsdetails eines Produkts. Entwickler, die die betreffenden Codefragmente geschrieben haben, verstehen sie gewöhnlich am besten, aber sie können als Ersteller auch besonders voreingenommen sein und bedeutsame Codefehler einfach übersehen.
Keine dieser Optionen ist die endgültige Lösung, die automatisch für jeden Fall angewendet werden kann. Die Codeüberprüfung hängt mehr von den menschlichen Teilnehmern ab als von technischen Lösungen und muss folglich von dem Team, dem diese Aufgabe übertragen wurde, angenommen werden. Einige Teams erreichen die besten Ergebnisse durch Erörterung und Analyse des Codes in Gruppenbesprechungen. In anderen Fällen erzielen Entwickler die besten Ergebnisse, wenn sie den Code allein analysieren. Das Team sollte die Strategie auswählen, welche bei der Produktüberprüfung die größtmögliche Wirksamkeit bietet. Diese allgemeine Regel gilt nicht nur für die Planung und Organisierung einer Codeüberprüfung, sondern für alle Herausforderungen, die dieser Prozess mit sich bringt.

Priorisieren der Überprüfungsbemühungen
Die wichtigste Regel bei der Codeüberprüfung lautet, dass nie genug Zeit für den gesamten zu überprüfenden Code vorhanden ist. Eine der größten Herausforderungen besteht daher in der Auswahl und Priorisierung der Komponenten oder Codefragmente des Produkts, die Teil des primären oder zumindest anfänglichen Analyseumfangs sein sollen. Dafür ist es notwendig, den Entwurf eines Produkts und die Rolle bestimmter zu dessen Implementierung verwendeter Technologien zu kennen. Bei der Priorisierung handelt es sich um eine Analyse auf hoher Ebene, die den Rahmen des gesamten Prozesses definiert.
Die Priorisierung beginnt mit der Analyse von Bedrohungsmodellen und anderer Dokumentation, die für ein Produkt zur Verfügung stehen. Bei ordnungsgemäßer Durchführung kann ein Bedrohungsmodell viele nützliche Daten über Beziehungen zwischen verschiedenen Komponenten, Einstiegspunkte, Sicherheitsgrenzen, Abhängigkeiten und Annahmen im Entwurf oder in der Implementierung liefern. Da das Ziel der Priorisierung darin besteht, die Implementierungsdetails zu verstehen, müssen Informationen aus einem Bedrohungsmodell dann mit dem Code selbst verglichen werden, wobei Informationen zur Struktur, Qualität oder zum Entwicklungsprozess des Codes analysiert werden.
Die Priorisierung kann in vier Hauptaufgaben unterteilt werden. Diese sind in Abbildung 3 zusammen mit Beispielfragen, die bei der Suche nach den erforderlichen Informationen hilfreich sein können, dargestellt.

Kenntnis der zur Implementierung des Produkts verwendeten Umgebung und Technologien
Ist es eine TCB-Komponente (Trusted Computing Base): Kerneltreiber, Subsystem oder berechtigter Dienst?
Wird es standardmäßig gestartet, zusammen mit anderen gehostet oder als dedizierter Prozess ausgeführt?
Handelt es sich um ein wieder verwendbares Plug-In, einen Treiber, Codec, Protokollhandler oder Dateiparser?
Wird es geladen und ruft direkt anderen Code (Drittanbieterbibliotheken) auf?
Welche zugrunde liegenden Technologien werden verwendet (RPC, DCOM, ActiveX, WMI, SQL)?
Welche Sprachen (C/C++, C#, Visual Basic) und Bibliotheken (STL, ATL, WPF) werden verwendet?
Aufstellung aller Quellen nicht vertrauenswürdiger Eingabedaten (Einstiegspunkte)
Ist es über ein Netzwerk (TCP, UDP, SMB/NetBIOS, Named Pipes) verfügbar?
Wird IPC-Kommunikation (LPC, freigegebene Abschnitte, globale Objekte) verwendet?
Kann es programmatisch gesteuert werden (Automatisierung oder Skripterstellung)?
Welche Ressourcen werden verwendet (Registrierung, Dateien, Datenbanken)?
Verfügt es über eine Benutzeroberfläche, analysiert es Befehlszeilenargumente oder verwendet es Umgebungsvariable?
Kommuniziert es direkt mit externer Hardware (USB-Treiber)?
Bestimmung, wer auf Einstiegspunkte zugreifen kann und unter welchen Bedingungen
Ist es eine Sicherheitsgrenze, die möglicherweise eine Rechteerhöhung zulässt?
Ist dieser Einstiegspunkt auf Remotezugriff, lokalen oder physischen Zugriff begrenzt?
Gibt es Zugriffseinschränkungen auf Transport-, Schnittstellen- oder Funktionsebene?
Ist Authentifizierung erforderlich (anonym, authentifiziert, Dienst, Administrator)?
Welche Methoden dienen zur Authentifizierung, zum Speichern vertraulicher Daten und so weiter?
Gibt es andere Einschränkungen (Zulassen/Verweigern von Listen, Zertifikaten)?
Berücksichtigung des nichttechnischen Kontexts des Codes
Ist es neuer oder veralteter Code? Befindet er sich noch in der Entwicklung oder bereits im Pflegemodus?
Wurde der Sicherheitskontext geändert (Code wurde vom Benutzer- in den Kernelmodus verschoben)?
Wie sieht der Entwicklungsgang der Sicherheitsbemühungen in diesem Produktteam oder in der Organisation aus?
Wie sieht die Entwicklung der Sicherheit des Produkts aus?
Wie ist es um das Sicherheitsbewusstsein des Teams oder der Organisation bestellt?
Zu welchen Ergebnissen haben vorherige Codeüberprüfungen geführt?
Welche Probleme wurden mithilfe automatisierter Tools erkannt? In welchen Bereichen?
Bei der ersten Aufgabe geht es um das Verständnis des technologischen Kontexts des Codes. Dieser Kontext deckt nicht nur die speziellen, in einem Produkt verwendeten Technologien, sondern auch das Betriebssystem und Drittanbieterabhängigkeiten sowie die in der Entwicklung verwendeten Tools ab. Das Ziel dieser Aufgabe besteht in der Ermittlung von Beziehungen zwischen einem Produkt und anderen Systemen, Anwendungen oder Diensten. Auf der Grundlage dieser Beziehungen ist es möglich zu bestimmen, auf welche Komponenten ein Produkt angewiesen ist und welche andere Software von dem Produkt abhängig ist. Im Kontext der Sicherheit legen diese Beziehungen fest, wie sich ein Produkt auf das übrige System auswirkt und umgekehrt. Einige risikoreiche Bereiche werden bei diesem Prozess sichtbar.
Die risikoreichen Bereiche stehen normalerweise im Zusammenhang mit Begriffen wie Sicherheitsgrenzen und mögliche Angriffsvektoren. In der Praxis lässt sich dies alles auf Daten reduzieren, die von einem Angreifer gesteuert werden und als nicht vertrauenswürdig betrachtet werden müssen, sowie auf Einstiegspunkte, aus denen diese Daten stammen können. Das Team muss jeden dieser Einstiegspunkte in Bezug auf Garantien und Annahmen über eingehende Daten analysieren. Eine Hauptfrage ist, wann und wie Daten überprüft werden. Diese Frage kann oft auf eigentliche Datenmerkmale bezogen sein (können sie asynchron geändert oder in der falschen Reihenfolge gesendet werden)? Sie kann sich auch auf die Merkmale eines Einstiegspunkts beziehen, beispielsweise, ob dieser standardmäßig verfügbar ist (ein Netzwerkdienst oder eine programmatische Benutzeroberfläche) oder ob er infolge der Aktion eines Benutzers erstellt wurde (durch einen Klick oder das Öffnen einer Datei).
Die Merkmale eines Einstiegspunkts zählen auch zum Umfang der nächsten großen Aufgabe: die Analyse von Vertrauensstellungen und Zugriffssteuerung. Ein Produkt kann viele verschiedene Einstiegspunkte haben, aber seine eigentliche Angriffsfläche wird nur von jenen gebildet, die einem Angreifer über eine Sicherheitsgrenze hinweg zugänglich sind. Jeder einzelne Einstiegspunkt sollte daher untersucht werden, um zu überprüfen, wer auf ihn unter welchen Bedingungen zugreifen kann. Nicht authentifizierter Zugriff auf breit angelegte Funktionalität sind für einen Angreifer offensichtlich interessanter als Einstiegspunkte, die nur mit Administratorrechten zugänglich sind. Im letzteren Fall kann die Überprüfung nur auf den Codeabschnitt begrenzt werden, der für die eigentliche Authentifizierung verantwortlich ist (einschließlich des vorhergehenden Codes).
Schließlich können nichttechnische Quellen ebenfalls Daten bereitstellen, die für die Priorisierung der Codeüberprüfung nützlich sind. Code wird in der Regel von Entwicklerteams erstellt, und Teams ändern sich normalerweise im Lauf der Zeit, wenn neue Mitglieder dazu kommen und andere das Team verlassen. Code hat auch seinen eigenen Sicherheitsverlauf, manchmal nicht nur im Hinblick auf ein bestimmtes Produkt. In den meisten Fällen kann davon ausgegangen werden, dass neuer Code von besserer Qualität als veralteter Code ist, aber es kann Ausnahmen geben. Gespräche mit Entwicklern, die Überprüfung der Dokumentation aus vorherigen Überprüfungen oder das Ausprobieren von Code können sehr nützliche Anhaltspunkte bieten. Zur bestmöglichen Nutzung verfügbarer Ressourcen sollte das Sicherheitsteam alle Daten verwenden, die die Effektivität von Prüfern während des gesamten Prozesses gewährleisten.

Taktiken der Codeüberprüfung
Die Herausforderungen im Zusammenhang mit Codeüberprüfungen bei wichtigen Anwendungen sind hauptsächlich das Ergebnis der Größe und Komplexität der Software. Moderne Produkte werden mithilfe einer Vielzahl verschiedener Technologien, Programmiermodelle, Codierstile und Codeverwaltungsrichtlinien erstellt. In der Praxis steht die vollständige und aktuelle Dokumentation selten zur Verfügung. Ein Prüfer sieht sich in der Regel viel mehr Informationen gegenüber als er innerhalb eines akzeptablen Zeitrahmen verarbeiten kann. Um einem Zuviel an Informationen während der Überprüfung komplexer Produkte entgegenzuwirken, wurden bestimmte, als Taktiken bezeichnete Praktiken entwickelt. Nachfolgend werden die beiden gebräuchlichsten erörtert.
Die kontextbezogene Codeanalyse ist die natürliche Folge der Priorisierung und Analyse auf hoher Ebene. Ihr liegt die präzise und kontextbewusste Analyse von Datenflüssen in den Codebereichen zugrunde, die direkt an der Verarbeitung nicht vertrauenswürdiger Daten, die über eine Sicherheitsgrenze hinaus übergeben wurden, beteiligt sind. Der Vorteil bei einem zielgerichteten Ansatz dieser Art ist die Möglichkeit, sich auf den Code zu konzentrieren, bei dem ein Angriff wahrscheinlicher ist, und folglich eine gute Abdeckung der kritischen Codepfade zu erreichen.
Die kontextbezogene Codeanalyse lässt sich in zwei Durchläufe unterteilen. Der erste beginnt mit einem Einstiegspunkt und der Ermittlung des Ausmaßes an Steuerungsmöglichkeiten, die ein Angreifer über Eingabedaten erlangen kann. Von diesem Punkt aus führt der Entwickler eine Datenflussanalyse durch, mit der verfolgt werden soll, wie sich Variable und Datenstrukturen mit der Ausführung des Programms ändern. Die Analyse wird pro Funktion durchgeführt. Sie beginnt mit den Eingabeargumenten und analysiert dann alle Ausführungspfade, wobei gleichzeitig Änderungen bei den Zuständen von Variablen und Strukturen überwacht werden.
Jede Funktion, die aufgerufen wird und von einem Angreifer gesteuerte Daten übernimmt (die von einem Einstiegspunkt kommen und noch nicht überprüft wurden), ist auf dieselbe Weise zu analysieren. Ähnlich sollte die Weiterleitung von Daten in globale Datenstrukturen gekennzeichnet werden, damit jeder Verweis auf sie später analysiert werden kann. An dieser Stelle zielt die Navigation durch den Code auf besseres Verstehen des Codes, die Ermittlung seiner Struktur und die Kennzeichnung von Stellen für weitere ausführliche Untersuchungen ab.
Im Fall von Codesicherheitsüberprüfungen muss natürlich jenen Codebereichen besondere Aufmerksamkeit geschenkt werden, bei denen Sicherheitsprobleme wahrscheinlicher sind. Beispiele für solche Stellen sind in Abbildung 4 aufgeführt.

Benutzeridentifikation, Authentifizierung und Datenschutz
Autorisierungs- und Zugriffsprüfungen
Code, der nicht vertrauenswürdige Daten vorverarbeitet (Netzwerkpaketparser, Formatleser)
Nicht sichere Vorgänge in Puffern, Zeichenfolgen, Zeigern und im dynamischen Speicher
Prüfungsebenen, die sicherstellen, dass nicht vertrauenswürdige Daten ein gültiges Format haben
Code, der für die Konvertierung nicht vertrauenswürdiger Daten in interne Datenstrukturen verantwortlich ist
An der Interpretation nicht vertrauenswürdiger Daten beteiligte Logik
Bereiche, die Annahmen über die Daten selbst sowie über das Verhalten ihrer Quelle machen
Code, der an der Behandlung von Fehlerbedingungen beteiligt ist
Verwendung von Betriebssystemressourcen und Netzwerk (Dateien, Registrierung, globale Objekte, Sockets)
Problematische Bereiche, die typisch für die Umgebung sind, in der der Code ausgeführt wird
Verwendung problematischer APIs oder Verstöße gegen API-Verträge
Wenn bestimmter Code keine Daten verarbeitet, die von einem Einstiegspunkt stammen können, oder wenn er Daten verarbeitet, die bereits überprüft wurden, ist er vom Sicherheitsstandpunkt aus nicht mehr relevant, und der Prüfer sollte zu anderen Bereichen übergehen. Schließlich muss der Prüfer über allgemeine Kenntnisse des Codes einer Komponente und der Liste als potenziell interessant gekennzeichneter Codestellen verfügen. Mithilfe dieser Liste kann er den zweiten Durchlauf der kontextbezogenen Analyse beginnen, die sich auf eine detaillierte Untersuchung ausgewählter Codebereiche konzentriert.
Die zweite Taktik der Codeüberprüfung ist die Codeanalyse mit Musterfokussierung. Hier beginnt der Prüfer an einer willkürlich gewählten Stelle im Code und sucht nach bekannten Typen möglicher Sicherheitsrisiken. Obwohl verschiedene unterstützende Tools angewendet werden können, muss sich „bekannt“ nicht auf formelle Muster beziehen, vielmehr geht es dabei um die individuelle Erfahrung und Intuition des Prüfers. Bei jedem Risikokandidaten verfolgt der Prüfer alle Codepfade, um zu bestimmen, ob der Codefehler tatsächlich eine Schwachstelle darstellt, d. h. ob er Daten verarbeitet, die von einem Angreifer über eine Sicherheitsgrenze hinweg gesteuert werden können. Wenn auf irgendeiner Ebene eine einwandfreie Prüfung festgestellt wird, muss der Fehler nicht als Sicherheitsrisiko betrachtet werden, obwohl er immer noch ein Tiefenverteidigungs- oder ein nicht sicherheitsrelevantes Problem sein kann, das behoben werden muss.
Die Codeanalyse mit Musterfokussierung kann auch bei begrenzter Kenntnis einer Anwendung eingesetzt werden, da sich der Prüfer auf die Qualität und Richtigkeit ausgewählter Codefragmente statt auf ihre Rolle oder Position innerhalb eines Systems konzentrieren kann. Dies ermöglicht die Abdeckung einer signifikanten Codemenge auf der Suche nach bestimmten Mustern wie schlechten Codekonstrukten oder problematischen API-Aufrufen. Die Analyse potenzieller Codefehler isoliert vom Kontext einer gesamten Anwendung bietet jedoch normalerweise nicht genug Informationen, um zu ermitteln, ob es sich wirklich um ein Problem handelt. Dies kann dazu führen, dass ernste Fehler übersehen oder nicht relevante behoben werden, was zusätzliche langfristige Konsequenzen für die Anwendung hat. Wenn ein Fehler nur im lokalen Kontext beschrieben wird, kann es verlockend sein, eine lokale Problembehebung anstelle einer vollständigeren Prüfung auf einer höheren Ebene durchzuführen. Ein solcher Ansatz beim Beheben von Codefehlern kann zu überflüssigen Prüfungen, vermehrtem Chaos, geringerer Leistung und allgemeinen Problemen mit der Codeverwaltung führen.

Verschiedene Typen von Sicherheitsrisiken
Versuchen Sie, sich bei Codesicherheitsüberprüfung in den Angreifer hineinzudenken, und untersuchen Sie alle Bereiche, die seiner Kontrolle unterliegen könnten. Die Durchführung von Codeüberprüfungen darf sich nicht auf die Betrachtung gefährlicher APIs und Datenkopiervorgänge beschränken. Bei der Sicherheit zählen die Details. Die Erfahrung zeigt, dass selbst kleinste Fehler ausgenutzt werden können. Dieser Abschnitt enthält Beispiele für einige Arten von Codefehlern, aber es können an dieser Stelle nicht alle abgedeckt werden. Die Randleiste „Sicherheitsressourcen“ verweist auf umfassendere Informationsquellen zu wahrscheinlichen Sicherheitsrisiken.
Das allgemeine Konzept von Sicherheitsrisiken bei Code wird oft mit Pufferüberläufen in Zusammenhang gebracht. Im Kontext der Verarbeitung nicht vertrauenswürdiger Daten sind Codefehler in Bezug auf den Bereich und Datentyp noch immer eine häufige Quelle von Sicherheitsproblemen. Diese Gruppe von Sicherheitsrisiken ist jedoch nicht nur auf Pufferüberläufe begrenzt. Ein Pufferüberlauf steht in Verbindung mit dem Kopieren von Daten in den Stapel, in Datensegmente oder den Heap unabhängig von deren Größe, was zum Schreiben von Code, der über den definierten Bereich hinausgeht, und gefährlichen Situationen führen kann, die aus potenziellen Überschreibungen von Strukturen resultieren, die den Programmfluss oder andere vertrauliche Daten steuern.
Gleichermaßen häufige und schwerwiegende Beispiele von Codefehlern im Zusammenhang mit der Bereichsprüfung sind Lese- und Schreibvorgänge bei Pufferüberlauf. Zu diesen speziellen Problemen kommt es oft beim Verwenden von Zeigern, die von einer nicht vertrauenswürdigen Quelle eingehen (die Zeiger als Cookies oder Ziehpunktwerte verwendet), oder von falsch berechneten Indexen und Offsets. Sie ermöglichen es Angreifern, auf beliebige Prozessspeicher wie Datenvariable oder Funktionszeiger zuzugreifen, und verursachen Änderungen im Programmverhalten einschließlich Ausführung von beliebigem Code.
Bei Berechnungen können viele Transformationen auf numerischen Typenvariablen (Addition, Subtraktion, Multiplikation und so weiter) dazu führen, dass die Berechnung den definierten Bereich verlässt und unbemerkt umbricht, was zu Ganzzahlenüberläufen oder -unterläufen führt. Dies wird zum Problem, wenn die Variable nach der Transformation verwendet und gleichzeitig an anderer Stelle untransformiert oder auf andere Art transformiert verwendet wird. Dadurch können sich vorhandene Überprüfungen als unzureichend erweisen, was zu Pufferüberläufen oder Lese- und Schreibvorgängen bei Pufferüberlauf führt.
Andere Probleme stehen im Zusammenhang mit dem Zeichen der numerischen Typvariablen. Es gibt viele abstrakte Entitäten, die von einem Programm behandelt werden und die keine definierte Bedeutung für negative Werte haben. Zeichenfolgenbytelängen oder Arrayelementanzahlen sind beispielsweise als negative Werte eigentlich nicht sinnvoll, und die Verwendung von signierten Typen zum Manipulieren dieser Werte führt zu einer Reihe von problematischen Fällen.
Die (explizite oder implizite) Typumwandlung ist ein weiteres Beispiel für die potenziell unsichere Transformation von Datenelementen. Die Umwandlung kann Elemente (hinsichtlich der Bitzahl) vergrößern und daher die Weiterleitung von signierten Bits in neue Werte beinhalten. Ebenso könnte die Typumwandlung auch die Kürzung eines Werts verursachen. Probleme tauchen wieder dann auf, wenn das Datenelement in einem transformierten Zustand in einem Teil des Programms verwendet wird, während es in einem untransformierten oder anders transformierten Zustand in einem anderen verwendet wird.
Es gibt bestimmte Sicherheitsrisiken für Code bei Verwendung eines dynamischen Speichers. Das Programmverhalten mit nicht initialisierten Datenelementen kann undefiniert sein. Ein Angreifer könnte in der Lage sein, einige nicht initialisierte Variable durch Aufrufe von APIs zu steuern, um die nicht initialisierten Variablen vor der API, die sie initialisiert, zu erhalten oder festzulegen. Die Rückgabe von nicht initialisiertem Speicher an einen Angreifer kann eine Reihe von Problemen verursachen. Da Heap-Zuordnungen von einer Ressource kommen, die für verschiedene Threads, die mehrere Sicherheitskontexte (Clients) bedienen, freigegeben ist, können vertrauliche Informationen von einem Client an den anderen freigegeben werden. Dadurch kann ein Angreifer auch Informationen erhalten, die bei einer breiter angelegten Ausnutzung anderer Sicherheitsfehler nützlich sind, indem dem Angreifer beispielsweise beim Vorhersagen von Adressen geholfen wird. Andere Probleme können im Zusammenhang mit der doppelten Freigabe von Speicher, mit der Verwendung von bereits freigegebenem Speicher, Speicherverlusten oder Speicherauslastung stehen. Die möglichen Auswirkungen reichen von Denial-of-Service-Angriffen und Offenlegung von Informationen bis zur Ausführung schädlichen Codes.
Eine ganz andere Gruppe von Sicherheitsrisiken bei Code bezieht sich auf die Synchronisierung und Zeitfehler. Gute Beispiele sind TOCTOU-Racebedingungen (Time-of-Check, Time-of-Use). Sicherheits- und Bereinigungsüberprüfungen sind wertlos, wenn sie an Daten oder Ressourcen durchgeführt werden, die von einem Angreifer nach der Überprüfung, aber vor der eigentlichen Verwendung geändert werden können. Die Prüfung muss an privaten Kopien der Daten erfolgen, die sich nicht asynchron ändern können. Auf Ressourcen muss richtig verwiesen werden, um sicherzustellen, dass sie nicht gelöscht oder ersetzt werden.
Multithreadumgebungen erlegen Code besonders schwere Anforderungen (Dienste, Kernelkomponenten) hinsichtlich der Synchronisierung von Zugriff auf freigegebene Objekte und Ressourcen auf. Programmatische Benutzeroberflächen, die Angreifern Zugriff auf gleichzeitige Funktionsaufrufe oder deren asynchronen Abbruch ermöglichen, können Racebedingungen ermöglichen. Probleme, die durch fehlende Sperren, das Wegfallen von Sperren und die Annahme des beibehaltenen Zustands, den Missbrauch gesperrter Vorgänge oder die Verwendung eines Satzes unzusammenhängender Sperren verursacht werden, können zu unsicheren Speichervorgängen oder Deadlocks führen.
Wenn Code im Fall von Problemen bei der Objektlebensdauerverwaltung anderen Threads ein neues Objekt zur Verfügung stellt, indem globale Daten manipuliert werden (Veröffentlichung), muss er sich in einem konsistenten (normalerweise völlig initialisierten) Zustand befinden. Die richtige Verwaltung der Lebensdauer eines Objekts wird durch einen angemessenen Verweiszählermechanismus festgelegt. Die Zerstörung eines fehlerhaften Objekts und der Bereinigungslogik können einem Angreifer die Möglichkeit bieten, Code frühzeitig zu entladen (beispielsweise ein Plug-In) und noch verwendeten Speicher freizugeben.
Viele Sicherheitsfehler beim Codieren stehen nicht direkt im Zusammenhang mit Manipulationen an nicht vertrauenswürdigen Daten, sondern eher mit ihrer eigentlichen Interpretation oder ihrem Einfluss auf Programmverhalten und Ergebnisse. Klassische Beispiele sind Einschleusungsangriffe, zu denen es kommen kann, wenn von einem Angreifer gesendete Daten zum Parametrieren von anderem Inhalt, beispielsweise einem Skript, einer Webseite, Befehlszeile oder Formatzeichenfolge, verwendet wird. Mithilfe spezieller Escapezeichen oder Steuerungssequenzen kann ein Angreifer die Interpretation von nicht vertrauenswürdigen Daten als Teil eines ausgeführten Skripts oder Befehls erzwingen. Probleme entstehen auch, wenn nicht vertrauenswürdige Daten zum Erstellen von Namen und Pfaden zu Ressourcen verwendet werden, die zu erstellen oder zu verwenden sind (Dateien, Sockets, Registrierung, freigegebene Abschnitte, globale Objekte). Directory Traversal- und Kanonisierungsprobleme machen Code anfällig für alle möglichen Umleitungen, die zur Verwendung von Ressourcen, die der Kontrolle von Angreifern unterstehen, oder zur Veröffentlichung vertraulicher Informationen führen können (beispielsweise Benutzergeheimnisse oder Netzwerkanmeldeinformationen).
Sicherheitsrisiken können auch das Ergebnis fehlerhafter Annahmen über den Ursprung und das Ziel von Daten sein (Client-ID, Netzwerkadresse, Sitzungs-ID oder Kontexthandle), die Reihenfolge, in der sie eingehen (Netzwerknachrichten, API-Aufrufe), und ihre Menge (zu viele oder zu wenige Daten). Oft kann ein Angreifer in eingeschränktem Maß die Umgebung steuern, in der Code ausgeführt wird, indem benannte Objekte erstellt werden, bevor das eigentliche Produkt sie erstellt oder verwendet (Name-Squatting-Angriffe), Speicherplatz gefüllt oder die Netzwerkkommunikation gesperrt oder umgeleitet wird. Wenn diese Probleme nicht gelöst werden, kann Code anfällig für Angriffe mit Berechtigungserweiterung oder Denial-of-Service-Angriffe sein. Andere häufige Probleme sind das Ergebnis von Annahmen über die vom Code verwendeten Technologien: entweder über ihre Sicherheitsgarantien (Integrität des Kommunikationskanals) oder über die inneren Funktionsweisen ihrer APIs (Bereitstellung einer falschen Kombination aus Parametern und keine Überprüfung der Rückgabewerte anstelle der Berücksichtigung von API-Verträgen).
Die Ergebnisse von Codeüberprüfungen sind oft nicht darauf beschränkt, Probleme zu suchen, die als Probleme auf Codeebene klassifiziert sind. Prüfer arbeiten auf der eigentlichen Implementierungsebene, und obwohl die Verwendung der Entwurfsdokumentation einschließlich Spezifikationen und Bedrohungsmodellen für sie von Vorteil sein kann, sollten sie sich nicht auf diese Daten verlassen. Folglich führen Codeüberprüfungen zur Identifizierung von Problemen auf der Entwurfsebene oder Inkonsistenzen zwischen Spezifikation und Implementierung. Zu häufig gefundenen Problemen gehören die Offenlegung gefährlicher Funktionalität, falsche Protokollimplementierung, Verwendung benutzerdefinierter Pseudosicherheitsmechanismen (beispielsweise Authentifizierungsschemas und Zugriffsprüfungen) und die Identifizierung von Möglichkeiten zur Umgehung von Sicherheitsbarrieren.

Ergebnisverarbeitung
Eine Codesicherheitsüberprüfung ist erst dann erfolgreich, wenn die Sicherheit eines Produkts verbessert wird. Die Codeüberprüfung sollte Ergebnisse bieten, mit deren Hilfe Entwicklungsteams sinnvolle Änderungen an einem Produkt durchführen können. Das Erzielen dieser Verbesserung auf der Grundlage einer Überprüfung ist mit zwei praktischen Anforderungen verbunden: vollständige Dokumentation und angemessene Selektierung.
Die Dokumentation für jedes identifizierte Sicherheitsrisiko muss alle zum Lokalisieren und Verstehen des Problems erforderlichen Einzelheiten enthalten. Diese Einzelheiten müssen Hinweise auf fehlerhaften Code, eine Erklärung des Problems und eine Begründung dafür enthalten, warum es sich um ein Sicherheitsrisiko handelt. Empfehlungen für eine Problembehebung sind nützlich, doch die Auswahl und Erstellung der eigentlichen Lösung fällt in den Verantwortungsbereich der Codebesitzer. Wenn Daten fehlen oder nicht klar ist, warum ein Codefehler ein Sicherheitsrisiko darstellt, sind zusätzliche Untersuchungen durch das Produktteam erforderlich, was aufgrund von Ressourcen- oder Zeitlimitierungen nicht immer möglich ist.
Eine andere wichtige Anforderung steht im Zusammenhang mit der exakten Selektierung der Codeschwachstellen. Wenn der Schweregrad eines Problems zu niedrig angesetzt wird, kann es von einem Produktteam möglicherweise nicht behoben werden. Wenn andererseits der Schweregrad zu hoch angesetzt wird, könnte das Problem anstelle eines anderen Problems mit höherer praktischer Auswirkung zur Fehlerbehebung ausgewählt werden. Der Selektierungsprozess hängt stark von der Qualität der Sicherheitsfehlerschwelle ab, aber auch vom Verstehen der Prioritäten der Codeprüfer (die die Selektierung durchführen) und der Entwickler (die auf der Grundlage der Selektierung aktiv werden). Wie bereits erwähnt, darf sich die Verfügbarkeit von Mechanismen zur Fehlerausnutzungsverhinderung nicht auf die Selektierung von Sicherheitsfehlern beim Code auswirken.

Zusammenfassung
Codeüberprüfungen können nützlichere Informationen bereitstellen als nur eine Liste von Sicherheitsproblemen. Wenn möglich, sollten Prüfer auch die Codeabdeckung, das in bestimmte Codebereiche gesetzte Vertrauen und allgemeine Empfehlungen für Neugestaltung und Bereinigen von Code dokumentieren. Codeüberprüfungen bieten auch eine einmalige Gelegenheit zur Bereicherung des Wissens in einer Organisation, zur Steigerung des Sicherheitsbewusstseins und zur Verbesserung der Wirksamkeit von Sicherheitstools. Schließlich können Entwicklungsteams die Ergebnisse der Codeüberprüfungen verwenden, um bei zukünftigen Produktsicherheitsinitiativen Prioritäten zu setzen.
Software wird immer mit Sicherheitsrisiken behaftet sein, obwohl sich deren Art und die praktischen Auswirkungen im Lauf der Zeit ändern werden. Automatisierte Sicherheitstools können immer mehr Codefehler ermitteln, aber einige Sicherheitsrisiken werden weiterhin übersehen werden (da sie entweder unentdeckt bleiben oder sich hinter einer großen Anzahl falscher Positivdiagnosen verstecken). Die manuelle Quellcodeanalyse ist kein Ersatz für diese bewährten Tools, aber sie kann oft vorteilhaft mit ihnen kombiniert werden.
Die manuelle Codeüberprüfung ist teuer, schwierig und stark von der Erfahrung und dem Engagement der Beteiligten abhängig. Doch in vielen Situationen ist diese Investition für ein Projekt erforderlich, damit die Sicherheit eines Produkts oder seiner kritischen Komponenten angemessen zuverlässig ist. Ein erfahrener Prüfer ist in der Lage, Probleme zu identifizieren, die von Tools übersehen würden. So lange Menschen die Ursache von Sicherheitsproblemen sein können, müssen sie auch Teil der Lösung sein.

Michal Chmielewski ist als Security Software Engineer im SWI Attack Team bei Microsoft tätig. Er ist auf die Analyse von Produkten auf Sicherheitsrisiken spezialisiert.

Neill Clift ist als Security Software Engineer im SWI-Team bei Microsoft tätig. Er ist Fachgebietsexperte für Kernel- und Treiberprobleme.

Sergiusz Fonrobert ist derzeit als Security Software Engineer im SWI Attack-Team tätig und für Penetrationstest bei verschiedenen Microsoft-Produkten verantwortlich.

Tomasz Ostwald ist derzeit als Security Program Manager im SWI-Team tätig, wo er abschließende Sicherheitsüberprüfungen an den von Microsoft veröffentlichten Produkten durchführt.

Page view tracker