An der Quelle

Statische Quellcode-Analyse ist ein mächtiges Verfahren, um nach Schwachstellen im Sourcecode einer Applikation zu suchen, ohne diese auszuführen. Automatisierte Quellcode-Scanner unterstützen dabei durch die Verfolgung des Datenflusses und das Erkennen von Schwachstellen. Wo liegen die Möglichkeiten, wo die Grenzen dieser Werkzeuge?

vorlesen Druckansicht 4 Kommentare lesen
Lesezeit: 8 Min.
Von
  • Andreas Kurtz
Inhaltsverzeichnis

Die meisten Schwachstellen innerhalb einer Applikation entstehen schon während der Entwicklung. Anwendungen werden häufig unter hohem Zeit- und Kostendruck entwickelt, Sicherheit spielt daher nur eine untergeordnete Rolle oder geht in der Hektik der Implementierung neuer Funktionen unter.

Bisher versuchte man, Schwachstellen vorwiegend durch Angriffssimulationen, sogenannte Penetrationstests, aufzudecken. Allerdings bleibt bei derartigen Untersuchungen die interne Struktur einer Applikation verborgen. Es werden nur solche Lücken aufgedeckt, die einen von außen ermittelten feststellbaren Effekt nach sich ziehen. Derzeit gehen immer mehr Unternehmen dazu über, Sicherheitsmaßnahmen in die Entstehungsphase einer Applikation zu verlagern, um Fehler und Sicherheitslücken von vornherein zu vermeiden. Richtlinien für sichere Programmierung sind eine Maßnahme in dieser Richtung, regelmäßige Code-Reviews eine andere.

Automatisierte Quellcode-Scanner sollen helfen, die gesamte Codebasis schon während der Entwicklungsphase regelmäßig auf Schwachstellen zu durchsuchen. Dieser Ansatz ist nicht zuletzt aus wirtschaftlicher Sicht interessant: Defekte während der Entwicklung zu beheben ist bekanntlich deutlich günstiger als dies zu einem späteren Zeitpunkt zu tun.

Quellcode-Scanner werden in zwei unterschiedlichen Szenarien eingesetzt: Sie unterstützen Auditoren bei der manuellen Quellcode-Analyse, und sie sind direkt in die Entwicklungsumgebung der Programmierer integriert, um schon während der Entwicklung auf mögliche Probleme hinzuweisen. Die folgenden Abschnitte beschreiben Möglichkeiten und Grenzen der automatisierten Quellcode-Analyse, eine Übersicht ausgewählter Produkte ist im Textkasten zu finden. Einen detaillierten Blick auf frei verfügbare Tools für Java bietet der Artikel ab Seite 48. Konzeptionelle Ansätze, die auf die Beschränkung der möglichen Folgen von Fehlern abstellen, beschreibt der Artikel ab Seite 56. Es geht dabei um das mit Java eingeführte Sandbox-Prinzip, das in Microsofts .Net Framework weiterentwickelt wurde.

Alan Turing zeigte bereits 1936, dass das Halteproblem nicht entscheidbar ist. Das heißt, für einen Algorithmus lässt sich nicht im Vorfeld entscheiden, ob er terminiert. In anderen Worten: Man kann nur herausfinden, was ein Programm macht, wenn man es laufen lässt. Ergo existiert kein allgemeingültiges Verfahren, um automatisiert zu entscheiden, ob ein Programm Fehler enthält. Genau dies sollten Quellcode-Scanner allerdings tun.

Ein Widerspruch zur theoretischen Informatik? Die Praxis zeigt, dass die Technik tatsächlich funktioniert und Quellcode-Scanner heutzutage sinnvolle Ergebnisse liefern. Dennoch sind die Befunde nicht immer perfekt und mitunter mit falsch-positiven Meldungen behaftet. Deshalb sollten Auditoren alle gefundenen Schwachstellen nachträglich verifizieren.

Ăśberblick ĂĽber die Funktionsweise von Quellcode-Scannern (Abb. 1)

Nahezu alle Quellcode-Scanner arbeiten nach dem selben Prinzip [1] (siehe Abbildung 1). Sie setzen den Quellcode einer Anwendung zunächst um in ein Modell, das den Code in einer abstrahierten Form repräsentiert. Auf diesen Modellen werden später unter Zuhilfenahme umfangreicher Regelwerke die Analysen zum Auffinden von Schwachstellen durchgeführt.

Viele der im Rahmen der statischen Quellcode-Analyse eingesetzten Methoden stammen aus der Welt der Compiler. Zunächst geht es um das Erstellen eines Modells. In der ersten Phase erfolgt die sogenannte lexikalische Analyse. Der Scanner zerlegt den eingelesenen Quellcode in einzelne Token (Schlüsselwörter, Bezeichner, Operatoren) und kann so unter anderem für die Untersuchung unwichtige Informationen wie Leerzeichen und Kommentare verwerfen. Im nächsten Schritt versucht ein Parser, im Rahmen einer syntaktischen Analyse die einzelnen Token auf die Regeln einer kontextfreien Grammatik abzubilden. Das soll sicherstellen, dass der eingelesene Quellcode syntaktisch korrekt ist und der Grammatik der Quellsprache entspricht.

Aus der Abfolge der Token und den Ableitungsregeln der Grammatik werden ein Syntaxbaum und im nächsten Schritt die sogenannte Symboltabelle erstellt. Diese Tabelle speichert für jeden einzelnen Bezeichner (Variable) den exakten Typ und den Ort der Deklaration im Quellcode. Mithilfe einer abstrahierten Version des Syntaxbaums und der Symboltabelle ist es nun möglich, den Bezeichnern eine Bedeutung zuzuordnen – was zur Bezeichnung dieser Phase als „semantische Analyse“ führt. Für einen Quellcode-Scanner ist die Kenntnis der jeweiligen Variablentypen wichtig: In objektorientierten Sprachen entscheidet schließlich der Typ eines Objekts darüber, welche Funktionen zur Verfügung stehen. Im letzten Schritt erzeugen die Quellcode-Scanner aus dem Syntaxbaum und den zugehörigen Typinformationen der Symboltabelle ein internes Programmmodell, das auf Schwachstellen untersucht werden kann. Diese Modelle abstrahieren weitgehend von der verwendeten Quellsprache, um eine möglichst generische Suche nach Schwachstellen zu ermöglichen.

Um Schwachstellen identifizieren zu können, müssen Quellcode-Scanner in der Lage sein, dem Kontroll- und Datenfluss eines Programms möglichst präzise zu folgen. Lässt sich der Weg einer Benutzereingabe nicht vollständig nachvollziehen, wird eine Schwachstelle möglicherweise nicht erkannt. Um das zu verhindern, erstellt der Scanner zunächst einen sogenannten Aufrufgraphen (engl. call graph). Dieser bildet die Aufrufhierarchie der einzelnen Funktionen eines Programms ab. Jeder Knoten dieses Graphs repräsentiert eine im Quellcode definierte Funktion. Eine Kante dazwischen verdeutlicht den Sachverhalt, dass innerhalb einer Funktion eine weitere aufgerufen wird. Ein Zyklus veranschaulicht das Auftreten einer Rekursion.

Kontrollflussgraph zur Bestimmung der möglichen Ausführungspfade (Abb. 2).

Durch diese Aufrufgraphen lässt sich der Kontrollfluss über Methodenaufrufe hinweg verfolgen. Um darüber hinaus dem Kontrollfluss innerhalb eines Programmabschnitts folgen zu können, wird ein sogenannter Kontrollflussgraph (siehe Abbildung 2) erstellt. Die Wurzel des Graphen entspricht meist dem Beginn einer Funktion, Kanten innerhalb des Graphen verdeutlichen den Kontrollfluss. Eine bedingte Verzweigung innerhalb des Programms ist im Kontrollflussgraphen durch zwei unterschiedliche Pfade gekennzeichnet. Schleifen finden sich als Zyklen wieder.

Auf Basis der beschriebenen Modelle erfolgt nun die Suche nach Schwachstellen. Das grundlegende Prinzip dahinter ist relativ einfach: Quellcode-Scanner suchen nach Eingabemöglichkeiten – nur darüber kann ein Angreifer manipulierte Werte einschleusen. Sämtliche Eingabemöglichkeiten werden zunächst als „nicht vertrauenswürdig“ markiert. Durch Verfolgen des Datenflusses lässt sich nun genau nachvollziehen, welche Bereiche eines Programms die getätigten Eingaben durchlaufen. Erreicht ein vom Angreifer manipulierter Wert ungefiltert eine verwundbare Funktion, ist eine Schwachstelle entdeckt.

Hinter diesem einfachen Grundprinzip stecken komplexe Algorithmen. Die Schwierigkeit bei der statischen Quellcode-Analyse besteht in der Simulation der typischen Umgebungsbedingungen für die Ausführung eines Programmabschnitts. Im Rahmen einer globalen Analyse werden deshalb unter Zuhilfenahme des Aufrufgraphen zunächst alle Funktionen und deren Abhängigkeiten identifiziert. Auf diese Weise sollen die Werte globaler Variablen möglichst genau simuliert und Eingaben mit potenziellem Angriffscode auch über Funktions- und Objektgrenzen hinweg verfolgbar sein.

Mithilfe des Syntaxbaums und des Kontrollflussgraphen kann das Analyseprogramm nun die einzelnen Programmabschnitte genauer untersuchen. Diese Phase der Quellcode-Analyse basiert – vereinfacht dargestellt – auf dem Treffen von Annahmen. Für alle Eingaben, die später durch kritische Funktionen weiterverarbeitet werden, lassen sich bestimmte optimistische Bedingungen voraussetzen; zum Beispiel, die Länge einer Eingabe sei kleiner als der dafür reservierte Speicherbereich und überschreibe somit keine benachbarten Speicherstellen. Stellt sich heraus, dass innerhalb des Programms keine Maßnahmen getroffen wurden, um diese Bedingung zu erfüllen, führt dies unmittelbar zur Meldung einer Schwachstelle.

Ein kurzes Beispiel soll diese Analysemethoden verdeutlichen: Angenommen, ein Serverdienst besitzt eine Buffer-Overflow-Schwachstelle. Darüber ist es einem Angreifer möglich, Speicherbereiche durch überlange Eingaben zu überschreiben und eigenen Schadcode einzuschleusen, der anschließend zur Ausführung kommt.

Das nachfolgende Quellcode-Listing demonstriert diese Schwachstelle: Über einen Kommandozeilenparameter (argv[]) können Werte an ein Programm übergeben werden. Diese Eingaben verarbeitet eine Unterfunktion (function()) weiter, und die C-Funktion strcpy() kopiert sie in einen dafür reservierten Speicherbereich. Da das Programm keinerlei Längenbeschränkungen der zu kopierenden Zeichen vorsieht, kann ein Angreifer angrenzende Speicherbereiche durch überlange Eingaben überschreiben.

void
function (char * arg)
{
char buff[24];
strcpy (buff, arg);
}
int
main (int argc, char* argv[])
{
function (argv[1]);
return 0;
}

Um diese Schwachstelle zu erkennen, folgen Quellcode-Scanner zunächst dem Daten- und Kontrollfluss innerhalb der Verarbeitungslogik und stellen somit eine Verbindung zwischen der Eingabe und einer verarbeitenden Funktion her. Stellen sie anschließend fest, dass die Eingaben ungefiltert an eine kritische Funktion übergeben werden, so meldet der Quellcode-Scanner eine Schwachstelle – in diesem Beispiel die Gefahr eines Buffer Overflow.

Die vollständige Printausgabe der iX (ab 22. Januar am Kiosk) erhalten Sie im gut sortierten Zeitschriftenhandel und ohne Versandkosten hier.

Mehr Infos

iX-TRACT

  • Die statische Codeanalyse ermöglicht das Aufdecken von Sicherheitsproblemen schon während der Entwicklung.
  • Die Arbeitsweise der entsprechenden Werkzeuge ähnelt der von Compilern.
  • Der Einsatz automatischer Schwachstellenscanner ersetzt nicht den Test an der laufenden Applikation.

(js)