So spüren Sie Schadsoftware in Entwicklungssystemen auf

Autor / Redakteur: Paul Anderson * / Margit Kuther

Die Taint-Analyse-Technik legt offen, wie gefährliche Daten von einem Programmteil zum nächsten fließen.

Anbieter zum Thema

Taint-Analysetool: Taint Sources (rot), Taint Sinks (blau)
Taint-Analysetool: Taint Sources (rot), Taint Sinks (blau)
(Bild: GrammaTech)

Mit der Funktionalität von Software steigt das Risiko für Programmierfehler, die zu Sicherheitslücken in Embedded-Systemen führen können. Gefährdet sind vor allem Systeme mit Code verschiedener Anbieter.

Laut Forschungsergebnissen treten Fehler, die zu Sicherheitslücken führen, vermehrt an Schnittstellen zwischen Modulen auf, oft wegen Meinungsverschiedenheiten bei der Interpretation der Schnittstellenspezifizierung.

Viele Sicherheitsschwachstellen lassen sich leicht ausbeuten, weil die Programmierer von der irrigen Annahme vertrauenswürdiger Datenquellen ausgehen. Sie setzen vernünftig formatierte Daten voraus und rechnen nicht damit, dass ein Hacker den Input steuert. Aber dieselben Techniken, die vor Sicherheitslücken schützen, eignen sich auch zur Abwehr fehlerhafter Datenwerte und zum Auffinden von Fehlern, weil sie die gefährlichsten Codeteile verstehen.

Bildergalerie

Programmierer können sich schützen, indem sie Input aus potenziell gefährlichen Kanälen als riskant einstufen, bis die Daten validiert wurden.

In der Sprache des sicheren Programmierens nennt man diese Eingabewerte ‚Tainted‘ (gefährlich). Ob ein Programm Tainted-Daten korrekt verarbeitet, lässt sich meistens schwer feststellen, weil man dazu den Datenfluss durch die Codestruktur nachverfolgen muss. Sogar für relativ kleine Programme ist das eine mühsame Aufgabe, und im Allgemeinen ist sie für die meisten Echtzeit-Anwendungen manuell unausführbar.

Die nachfolgend beschriebene Visual-Taint-Analyse als Teil moderner, statischer Analysetechnik zeigt automatisch auf, wie potenziell gefährlicher Input durch das Programm zu empfindlichen Codeteilen fließt und ein Sicherheitsproblem auslösen kann.

Das größte Risiko beim Einsatz von Werten aus einem riskanten Datenkanal ist, dass ein Angreifer diesen Kanal nutzt, um ein Sicherheitsleck oder einen Programmabsturz auszulösen. Tainted-Daten können viele unterschiedliche Fehler auslösen, beispielsweise Buffer Overruns, SQL Injection, Command Injection, Cross-Site Scripting, Arithmetic Overflow und Path Traversal. (Details siehe CWE).

Buffer Overrun, einer der verbreitetsten Angriffe

Viele der schlimmsten Online-Angriffe der letzten zwei Dekaden sind auf den berüchtigten Buffer Overrun zurückzuführen. Weil diese weit verbreitete Schwachstelle die Wichtigkeit der Taint-Analyse so gut aufzeigt, wird sie hier genauer betrachtet.

Ein Buffer Overrun bietet einem Angreifer mehrere Ansätze, im klassischen Fall übernimmt er den Prozess und lässt ihn beliebigen Code ausführen. In nachfolgenden Beispiel befindet sich der Buffer auf dem Stack:

  • struct sample { int nbytes; char bytes[100]; };
  • void get_item(int fd)
  • {
  • struct sample item;
  • int count;
  • read(fd, &item.nbytes, sizeof(int));
  • read(fd, &item.bytes[0], item.nbytes);
  • }

Das struct sample soll Daten variabler Längen speichern. Das erste Feld zeigt an, wie viele Bytes im zweiten Feld benutzt werden. Der erste Aufruf auf read() erhält die Anzahl der zu lesenden Bytes, und der zweite Aufruf liest diese Byte-Anzahl in das zweite Feld der Datenstruktur ein.

Zu beachten ist, dass der Datentyp nicht mehr als 100 Bytes in diesem Feld speichern kann. Außerdem gibt es im Code keine Überprüfung die sicherstellt, dass dieser Buffer nicht überläuft. Hat der Angreifer diese Eingabequelle unter Kontrolle, löst das Einlesen von mehr als 100 Bytes einen Buffer Overrun aus.

Weil die automatische Variable item als Teil des Aktivierungssatzes für den Prozess auf dem Stack hinterlegt ist, werden alle Bytes nach den ersten 100 auf die Teile des Programstacks jenseits der buf-Grenze geschrieben. Die Variable count kann überschrieben werden (abhängig davon, wie der Compiler den Speicherplatz auf dem Stack zugewiesen hat). Dann ist der Wert dieser Variablen unter der Kontrolle des Angreifers – der Stack enthält die Adresse, auf die das Programm nach Ausführung des Prozesses springt.

Nun kann der Angreifer eine Byte-Sequenz seiner Wahl mit einer Returnadresse seiner Wahl in den Kanal eingeben. Erreicht die CPU das Funktionsende, kehrt sie zu genau dieser Adresse zurück, anstatt zu der aus dem Funktionsauslöser. Wird der Code in einer Umgebung ausgeführt, in welcher der Angreifer die Umgebungsvariable nicht steuert, kann er diese Schwachstelle ausschöpfen. Dennoch ist der Code offensichtlich sehr gefährlich und bleibt ein Risiko. So könnte etwa ein Programmierer diesen unsicheren Code in einem anderen Programm laufen lassen.

Taint Sources, Sinks und Cleanser

In der Terminologie der Taint-Analyse ist eine ‘Taint Source‘ ein Ort im Programm, wo Daten aus einer gefährlichen Quelle einfließen. Im oben beschriebenen Beispiel ist es der Aufruf read(). Eine ‚Taint Sink‘ ist der Ort, den die Tainted Daten keinesfalls erreichen sollten, es sei denn sie wurden validiert. Wurde ein Wert geprüft, bezeichnet man ihn ‚cleansed‘.

Die meisten Programme beziehen ihren Input aus mehreren Quellen, und die Umgebung, in der das Programm ausgeführt wird, bestimmt das Risiko jeder einzelnen Quelle. Eine Klassifizierung von Taint Sources könnte aussehen wie folgt:

  • Die Umgebung, die Kommandozeilenargumente eines Programms und dessen Umgebungsvariablen enthält,
  • Datei-Inhalte,
  • Datei-Metadaten wie Datenzugriffsrechte oder Zeitstempel,
  • das Netzwerk,
  • Netzwerk-Services, z.B. die Ergebnisse einer DNS-Anfrage,
  • die System Clock,
  • das Registry, wie bei Windows-Systemen.

Natürlich kann ein Programm auch andere potenziell gefährliche Eingabequellen haben, z.B. sollte ein Programm, das Daten aus einem Gerät mit Infrarot-Sensor ausliest, diesen Kanal als gefährlich einstufen. Sicherheitsanalysten sprechen von der Angriffsfläche (‚Attack Surface‘) eines Programms als den Einwirkungsstellen für einen Hacker. Um das Risiko eines Programms einschätzen zu können, sollte man zunächst dessen Angriffsfläche kennen.

Diese entspricht weitgehend den Taint Sources eines Programms. Weil es schwierig sein kann, für Tainted-Werte empfindliche Programmfehler zu entdecken, ist die Automatisierung (wie nachfolgend beschrieben) der beste Ansatz.

Die Taint-Analyse ist eine Form der statischen Analyse, wobei nur eine bestimmte Klasse der statischen Analysetools sie umsetzen kann.

Taint-Analyse, Auswertung von Eingabedateien

Diese Analysetools funktionieren, vereinfacht dargestellt, wie folgt: Zunächst erstellen sie durch Lesen und Analysieren jeder einzelnen Eingabedatei ein Modell des gesamten Programms. Dieses besteht aus Darstellungen wie Abstract Syntax Trees (AST) für jede Kompilierungseinheit, Flusskon- trollgraphen für jedes Unterprogramm, Symboltabellen und dem Call Graph.

Checker, die Fehler auffinden, sind in Form von unterschiedlichen Fragen auf diesen Repräsentationen eingebaut. Mustererkennung auf dem AST oder den Symboltabellen spüren oberflächliche Bugs auf. Die wirklich ernsten Fehler führen zur Fehlfunktion des Programms, z.B. Null-Pointer-Dereferenzierungen oder Buffer Overruns.

Bildergalerie

Nur anspruchsvolle Abfragen können sie entdecken, quasi als abstrakte Simulationen. Der Analysator simuliert die Programmausführung, nutzt allerdings anstelle von konkreten Werten Gleichungen, die den abstrakten Programmstatus abbilden. Bei Unregelmäßigkeit wird eine Warnung ausgelöst (Bild 1; siehe Bildergalerie).

Die modernen Analysetools sind so nützlich, weil sie schon sehr früh im Entwicklungsprozess Defekte aufspüren, die nur unter ungewöhnlichen Umständen auftreten. Damit rechnet sich ihr Einsatz bereits, bevor der Code zum Testen bereitsteht. Sie sollen traditionelle Testmethoden nicht ersetzen, sondern ergänzen.

Strings und Pointer erschweren die Analyse

Den Fluss von Tainted-Daten durch ein Programm nachzuverfolgen, kann schwierig sein. Denn das beinhaltet das Aufspüren des Wertes, wie er von einer Variablen zur nächsten kopiert wird, unter Umständen über abgegrenzte Abläufe hinaus und über mehrere Umwege. Ein Beispiel hierfür ist ein Programm, das einen String aus einem riskanten Netzwerkport liest.

Weil Strings in C normalerweise von Pointern (Zeigern) verwaltet werden, muss die Analyse sowohl die Inhalte des Strings als auch den Wert aller Pointer, die sich auf den String beziehen, prüfen. Die Zeichen selbst gelten als Tainted, während der Zeiger als ‚point to taintedness‘ bezeichnet wird (‚zeigt auf gefährlichen Code‘). Werden die Inhalte des Strings kopiert, z.B. über strcpy(), dann wird das taintedness (‚gefährliche‘) Objekt auf den neuen String übertragen. Werden die Pointer kopiert, dann ist auch die Übertragung des ‚points-to-taint‘-Objekts auf den neuen Pointer erforderlich.

Natürlich könnte es im Gegenzug auch Pointer auf diese Pointer geben, und sogar Pointer darauf – die Analyse muss auch diese nachverfolgen. Letztendlich läuft es auf eine Art Alias-Analyse hinaus, die feststellt, welche Variablen auf dieselben Speicherplätze zugreifen. Ein Beispiel würde hier zu weit führen, findet sich aber unter: http://en.wikipedia.org/wiki/Alias_analysis.

Den Taint-Fluss nachvollziehen und verstehen

Weil Taint in unerwarteten Wegen durch ein Programm fließen kann, ist es so wichtig, diese Kanäle zu verstehen. Die Lage von Taint Sources und Sinks lässt sich visualisieren. Zusätzlich zur regulären Codeansicht können im Fluss involvierte Programmelemente überlagert werden. Diese Visualisierung unterstützt die Programmierer bei der Einschätzung, wie riskant ihr Code ist, und bei der Entscheidung, wie sie ihn verändern und die Schwachstelle sichern können.

In Bild 2 (siehe Bildergalerie) zeigt die rote Unterstreichung auf Zeile 467, dass der Wert der Variablen msg Tainted ist. Die Unterstreichung in den vorhergehenden Zeilen weist darauf hin, dass der Taint über den Parameter epd_line in den Ablauf namens SolvePosition übertragen wird. Der Ausschnitt rechts enthält Details über diese Variable. Man sieht, dass sie den Taint aus dem Aufruf strcpy auf Zeile 77 von epd.c erhalten hat.

Eine alternative Betrachtungsart des Taint-Flusses durch ein Programm liefert das Belegungsdiagramm in Bild 3 (siehe Bildergalerie). Hier hebt der Anwender das Modul mit Taint Sources mithilfe roter Farbe hervor. Diese Ansicht liefert einen vernünftigen Annäherungswert an die Angriffsfläche des Programms. Mittels verschiedener Darstellungsmöglichkeiten können Programmierer das exakte Verhalten ihres Codes in Embedded-Anwendungen besser nachvollziehen.

Wegen der steigenden Anforderungen der Embedded-Industrie brauchen Entwickler automatisierte Analysetools dringender als zuvor. Die Taint-Analyse-Technik als Teil moderner statischer Analysetools vermittelt, wie gefährliche Daten von einem Programmteil zum nächsten fließen. So kann man die Angriffsfläche eines Programms besser einschätzen, Fehler früh im Entwicklungszyklus auffinden und korrigieren.

* Dr. Paul Anderson ist Vice President of Engineering bei GrammaTech

(ID:42937114)