Source Code Analyse - Eine Einführung

Source Code Analyse

Eine Einführung

Marc Ruef
von Marc Ruef
Lesezeit: 30 Minuten

Sicherheitsüberprüfungen können auf viele verschiedene Arten stattfinden. Traditionellerweise werden netzwerkbasierte Analysen (z.B. Vulnerability Scans oder Penetration Tests) vorangetrieben. Es können aber auch andere Herangehensweisen, wie lokale Audits, Config Reviews und Firewall Rule Reviews umgesetzt werden.

Eine sehr mächtige Methode einer Sicherheitsüberprüfung stellt die Source Code Analyse (SCA) dar. Bei dieser wird der Programmcode einer Software auf etwaige Schwachstellen hin untersucht. Dieser Beitrag diskutiert die Grundlagen einer solchen Review.

Ausgangslage und Ziel

Die Ausgangslage für eine SCA ist das Vorhandensein des Programmcodes der zu untersuchenden Software. Closed-source Lösungen können also nicht ohne weiteres einer SCA unterzogen werden. In diesem Fall müsste ein Reverse Engineering angegangen werden. Dieses ist in der Regel mit einem bedeutenden Mehr an Aufwand verbunden.

Je nachdem kann versucht werden, in den Besitz der Sources zu kommen. Gewisse Projekte bieten Zugriff darauf an. Und gewisse Hersteller liefern – wenigstens punktuell – den Quelltext an ausgewählte Kunden aus. Sollte die Software als Auftrag durch eine Drittfirma entwickelt worden sein, sollte schon im gegebenen Rahmenvertrag definiert werden, dass der Quelltext ausgehändigt oder Zugriff auf diesen möglich gemacht wird.

Steht der Quelltext zur Verfügung, dies ist bei quelloffener Software immer der Fall, steht einer SCA nichts mehr im Weg. Im Rahmen dieser sollen Schwachstellen in der gegebenen Applikation gefunden werden.

Eintrittspunkte

Eine Software stellt in der Regel verschiedene Eintritts- und Austrittspunkte zur Verfügung:

Eintrittspunkte sind typischerweise Benutzereingaben. Diese werden bei zeilenbasierter Software als Argumente bzw. Parameter übergeben und können während der Laufzeit als interaktive Eingaben erfolgen. Bei Software mit grafischer Oberfläche (GUI) können dies zusätzlich Eingaben durch Maus oder Gestensteuerung sein.

Eine Software greift aber auch noch auf andere Eingabemöglichkeiten, vorzugsweise als Schnittstelle zu nicht-menschlichen Quellen zurück. So können zum Beispiel Umgebungsvariablen berücksichtigt, Dateien eingelesen oder Datenbankabfragen gemacht werden. Diese Zugriffe erfordern in der Regel keine Interaktivität durch einen Benutzer.

Eintrittspunkte identifizieren

Im Rahmen der SCA ist es in der Regel von grosser Wichtigkeit, die Eintrittspunkte vollumfänglich zu identifizieren. Diese lassen sich anhand von Parametern, Funktionsaufrufen oder Objekten ausmachen. Diese sind jeweils sprachabhängig aber üblicherweise und zu grossen Teilen innerhalb der Sprache standardisiert. Die folgende Tabelle listet die typischen Eintrittspunkte für verschiedene Sprachen.

Eintrittspunkt PHP ASP JSP Ruby
Argumente $argv, $_SERVER[‘argv’] ARGV
Interaktive Eingabe fgets() gets(), stdin.read()
Umgebungsvariablen $_ENV, getenv(), apache_getenv(), $_SERVER Request.ServerVariables, oShell. ExpandEnvironmentStrings() System.getenv(), System.getProperty() ENV
Dateien fread(), fgets(), file(), file_get_contents() file.OpenAsTextStream(), oBinRead.readBinFile() InitialContext().lookup() target.read(), File.readlines()
MySQL Query mysqli_query(), mysqli_multi_query(), mysqli_real_query(), mysqli_send_query(), mysqli_stmt_execute() oConn.Execute() executeQuery(), executeUpdate(), execute() Active Record: Model.find(), Model.take(), Model.first(), Model.last(), Model.find_by()
HTTP Dateiuploads $_FILES FileUploadControl, oUpload(“foo”).SaveAs request.getPart() params
HTTP GET- Parameter $_GET, $_REQUEST, $_SERVER [‘QUERY_STRING’], $HTTP_GET_VARS Request.QueryString getParameter(), getParameterValues(), ${param[‘foo’]}, ${param.foo} params, GET, query_parameters()
HTTP POST- Parameter $_POST, $_REQUEST, $HTTP_RAW_POST_DATA, $HTTP_POST_VARS Request.Form, Request[“foo”] getInputStream(), getReader(), ${param[‘foo’]}, ${param.foo} POST, request_parameters(), raw_post()
HTTP Cookies $_COOKIE Request.Cookies request.getCookies(), ${cookie[‘foo’]}, ${cookie.foo} Client: cookie_jar()
HTTP Sessions $_SESSION Session.Contents, Session(“foo”) session.getAttribute()

Ist man sich der eingesetzten Sprache und der darin genutzten Konstrukte bewusst, können die Eintrittspunkte durch eine Text-Suche identifiziert werden. Zum Beispiel mit der Hilfe von grep:

maru@debian:~$ grep -H -n -r '$_GET\|$_POST\|$_SERVER\|$_COOKIE\|$_FILE' *.php
foo.php:3:if($_GET['a']  'foo'){
foo.php:5:}elseif($_POST['b']  'bar'{
foo.php:6:    echo htmlentities($_POST['c']);

Diese Eintrittspunkte sollte man sich notieren (Zeilennummer oder Funktionsname), um in einer weiteren Analyse und im Rahmen der Dokumentation auf sie referenzieren zu können.

Schwierigkeit alternativer Referenzierung

Die grösste Schwierigkeit bei der Identifikation von Eintrittspunkten (dies gilt auch für Austrittspunkte), ist die Gegebenheit, dass alternative Referenzierungen möglich sind. Nämlich dann, wenn Eingaben aus einem Kanal über verschiedene Mechanismen erfolgen können.

PHP bietet hier für HTTP GET-Parameter ein sehr gutes Beispiel. Wird die URL http://example.com/?foo=bar aufgerufen, dann kann auf den Parameter foo mit $_GET[‘foo’] zugegriffen werden. Die Suche nach $_GET wird also sämtliche Eintrittspunkte dieser Art ausfindig machen lassen können. Zeitgleich kann aber auch über $_REQUEST[‘foo’] oder $_SERVER[‘QUERY_STRING’] (es wäre ein substr() oder eine Regex erforderlich) auf den besagten Parameter referenziert werden. In alten Installationen gar ebenfalls als $HTTP_GET_VARS und bei eingeschaltetem register_globals als einzelne Variablen (in diesem Fall schlicht $foo). Beide der letztgenannten Mechanismen sind aber in aktuellen Versionen von PHP nicht mehr vorhanden.

Bei JSP gibt es ebenfalls den Sonderfall der Unified Expression Language. Diese sieht vor, dass anstatt mit einer Funktion durch simplifizierte Tags auf die einzelnen Objekte zugegriffen werden kann: Anstelle von getParameter("foo") können ${param[‘foo’]} und ${param.foo} verwendet werden.

Dies zeigt auf, dass das Verständnis für die eingesetzte Sprache relativ hoch sein muss, um alle Varianten eines Mechanismus erkennen und identifizieren zu können. Der Aufwand der Analyse steigt also mit der Flexibilität einer Sprache. Gerade PHP-Software ist bei fehlender Einheitlichkeit für ihre enorm schwierige Nachvollziehbarkeit bekannt. Andere Sprachen, wie zum Beispiel JSP oder Perl, können aber genauso dazu tendieren. Ein simples Suchen mit grep gestaltet sich bedeutend schwieriger, da mit komplexen regulären Ausdrücken gearbeitet werden müsste.

Zudem illustriert der Sonderfall mit register_globals, dass eine deterministische SCA nur dann möglich ist, wenn die Plattform, auf der die Software schlussendlich laufen soll, ebenfalls mitberücksichtigt wird.

Eintrittspunkte ermöglichen Manipulationen

Die ausgemachten Eintrittspunkte müssen im weiteren Verlauf der SCA berücksichtigt werden. Sie definieren in den meisten Fällen die Ausgangslage für einen Angriff, denn durch sie wird erst eine bewusste Manipulation möglich. Der Tractatus Logico-Philosophicus Instrumentum Computatorium hält dies durch Herleitung 1.1.1 wie folgt fest:

Sicherheit ist ein Zustand. Er sieht eine wohldefinierte Anzahl an erlaubten Handlungen vor. Eine Handlung ist eine ausgeführte Tätigkeit.

Ein Eintrittspunkt ist dementsprechend bezüglich Exponiertheit und Möglichkeiten zu beurteilen. Ein HTTP GET-Parameter ist zum Beispiel sehr einfach und durch jedermann, der mit einer Webapplikation interagieren kann, zugänglich. Er bietet damit eine einfache und unkomplizierte Möglichkeit, Einfluss auf das Verhalten einer Webanwendung zu nehmen. Nachfolgende Tabelle versucht im Sinn eines generischen Base Score die jeweiligen Eintrittspunkte zu klassifizieren. Diese können je nach eingesetzter Technologie und verwendeten Mechanismen (z.B. Komplexität der jeweiligen Datenpunkte) abweichen und sind Produkt-/Kundenspezifisch zu erweitern.

Eintrittspunkt Exponiertheit Einfachheit
Argumente mittel-hoch hoch
Umgebungsvariablen gering hoch
Dateien mittel-hoch gering-hoch
Datenbank gering-hoch gering-hoch
HTTP Dateiuploads mittel-hoch hoch
HTTP GET-Parameter hoch mittel-hoch
HTTP POST-Parameter mittel-hoch mittel-hoch
HTTP Cookies mittel mittel-hoch
HTTP Sessions gering gering

Stützt sich eine Anwendung hingegen auf Umgebungsvariablen ab, müssen ganz andere Voraussetzungen erfüllt sein, um in dieser Hinsicht eine Manipulation vornehmen zu können. Umgebungsvariablen sind in der Regel lediglich im Rahmen lokaler Zugriffe anpassbar und erfordern einen entsprechenden interaktiven Login auf dem Zielsystem.

Eintrittspunkte via Umgebungsvariablen lassen sich also nur indirekt manipulieren. Genauso verhält es sich, wenn die Ausgaben von Datenbankabfragen herangezogen werden sollen. Dies ist typischerweise bei einer persistenten Cross Site Scripting-Schwachstelle der Fall. Dort muss zuerst der Payload in die Datenbank geschrieben werden, um in einem weiteren Schritt diesen wieder auszulesen und darzustellen. Ein Angriff durchläuft hierbei also zwei Phasen und ist im Rahmen der SCA auch schwieriger nachzuvollziehen.

Austrittspunkte

Normalerweise nimmt eine Software ihre Eingaben entgegen, verarbeitet diese und gibt das Resultat aus. In einem zweiten Schritt der Analyse ist es deshalb wichtig zu verstehen, welche Austrittspunkte durch eine Anwendung zur Verfügung gestellt wird. Hierbei wird in ähnlicher Weise verfahren, wie beim Suchen von Eintrittspunkten. Auch hier gibt es sprachabhängige Konstrukte, die gefunden und ausgewertet werden können.

Austrittspunkt PHP ASP JSP Ruby
Ausgabe echo, print, printf(), fprintf(), sprintf(), vprintf(), print_r() Response.Write() out.println(), out.print() puts(), print(), printf(), putc(), ios.write(), ios.puts()
Datei schreiben fwrite(), file_put_contents() file.write(), file.WriteLine() InitialContext().bind() target.write()
MySQL Query mysqli_query(), mysqli_multi_query(), mysqli_real_query(), mysqli_send_query(), mysqli_stmt_execute() oConn.Execute() executeQuery(), executeUpdate(), execute() Model.find(), Model.take(), Model.first(), Model.last(), Model.find_by()
Umgebungsvariablen putenv() System.setenv(), System.setProperty() ENV

Eintrittspunkte erschliessen Schwachstellen

Eine Schwachstelle innerhalb einer Software erschliesst sich erst dann, wenn eine Manipulation – ausgelöst durch einen Eintrittspunkt – bis und mit dem Austrittspunkt durchgesetzt werden kann. Dadurch ergeben sich die klassischen Angriffsklassen:

Beispiel einer Cross Site Scripting-Attacke

Nehmen wir als Beispiel eine Cross Site Scripting-Schwachstelle. Bei dieser ist ein Angreifer darum bemüht, durch eine Manipulation einer Applikation, die Webausgabe weitreichend zu verändern. Durch das Injizieren von HTML- und JavaScript-Elementen sollen indirekte Angriffe auf einen Benutzer durchgesetzt werden.

Um einen solchen Angriff bewusst ausführen zu können, muss nun ein Eintrittspunkt vorhanden sein, um den Payload initiieren zu können. Hierfür kann klassischerweise ein GET-Parameter herhalten. Ein Angriff sieht dann zum Beispiel so aus:

http://example.com/?foo=<script>alert('xss');</script>

Falls die Schwachstelle vorhanden ist und beim Aufruf dieser URL das Script injiziert wird, ist davon auszugehen, dass im Groben die Ausgabe wie folgt stattfindet:

echo $_GET['foo'];

Forward und Backward Slicing

Um die meisten Schwachstellen zu finden, kann also sowohl ein Forward Slicing als auch ein Backward Slicing stattfinden. Man sucht sich also einen Endpunkt und verfolgt die Datenverarbeitung bis zum gegenüberliegenden Endpunkt:

Naturbedingt ist das Applizieren und Nachvollziehen von Forward Slicing bedeutend einfacher, da der Code eher in der Reihenfolge der sequentiellen Abarbeitung gelesen werden kann.

Verschiedene Klassen von Schwachstellen lassen sich jedoch schneller identifizieren, wenn man ein Backward Slicing anstrebt. Es stellt sich dann nur die Frage, ob und welche Variablen zu beeinflussen sind, um eine konkrete Ausnutzung durchzusetzen.

Funktionen und Methoden

Bisher wurden Eintritts- und Austrittspunkte lediglich im Kontext der Software als Gesamtes betrachtet. Moderne Programmiersprachen bieten jedoch die Möglichkeit, Programmcode in unterschiedliche Routinen zu unterteilen. Bei höheren Programmiersprachen werden hierzu hauptsächlich Funktionen und Methoden verwendet. In der objektorientierten Programmierung kommen zusätzlich Klassen hinzu. Hier sind also die Paradigmen der zugrundeliegenden Sprache zu berücksichtigen.

Ein Eintrittspunkt ist also nicht nur das Argument, das ein Benutzer während dem Programmaufruf mitgibt. Ein softwareinterner Eintrittspunkt ist auch dann gegeben, wenn eine Funktion oder Methode mit einem Argument aufgerufen wird. Der grosse Vorteil softwareinterner Eintrittspunkte gegenüber externer Eintrittspunkt ist, dass sich erstere bedeutend einfacher identifizieren lassen, da sie sehr stark normiert sind. Ein Funktionsaufruf in ANSI C sieht immer gleich aus. Beispiel:

int foo(int param1, int param2){
   //irgendwelcher Code
}

Hierbei wird die Funktion foo() definiert. Diese liefert aufgrund des vorangesetzten int als Rückgabewert einen Integer-Datentyp zurück. In den Klammern erwartet die Funktion zwei Parameter, die ihrerseits ebenfalls als Integer übergeben werden. Die Parameter werden funktionsintern (lokal) als param1 und param2 referenziert.

Austrittspunkte bei Funktionen sind ebenfalls sprachabhängig definiert. Das zuvor genannte Beispiel in ANSI C illustriert die Typensicherheit der Sprache. Es wird durch die Deklaration der Funktion angegeben, welcher Datentyp zurückgeliefert wird. Andere Sprachen, wie zum Beispiel PHP, verzichten darauf.

Eine Funktion ist per Definition um eine Rückgabe bemüht. Bei den meisten Sprachen geschieht dies durch eine Anweisung namens return (z.B. PHP).

Die meisten Programmiersprachen auferlegen den einzelnen Subroutinen einen Scope. Dadurch wird der Sichtbarkeitsbereich für Objekte definiert. Funktionen können also nicht ohne weiteres auf Variablen ausserhalb ihres Scopes zugreifen. Bei PHP können globale Variablen durch global $foo eingebunden werden. Dadurch wird die direkte Manipulation der globalen Variable möglich, wodurch ein neuer Eintrittspunkt innerhalb der Funktion, losgelöst von den Argumenten des Funktionsaufrufs, generiert wird.

Logische Abläufe

Bisher haben wir in erster Linie Eintritts- und Austrittspunkte besprochen. Wir haben zwar einen kurzen Exkurs zu Subroutinen gemacht, sind aber noch nicht auf den logischen Ablauf innerhalb einer Software eingegangen.

Eine Software funktioniert selten statisch linear. Stattdessen werden oftmals Entscheidungen eingebaut, die den weiteren Verlauf der Datenverarbeitung beeinflussen. Zum Beispiel könnte eine Ausgabe nur dann ausgegeben werden, wenn die Eingabe der Zahl 1 entsprach:

if($_GET['foo'] == 1){
   echo $_GET['foo'];
}

Eine Schwachstelle manifestiert sich erst dann, wenn der logische Ablauf eben diese zulässt. Das zuvor genannte Beispiel der XSS-Schachstelle setzt voraus, dass die Eingabe bis zur Ausgabe weitergereicht wird, ohne dass ein für den erfolgreichen Angriff negativen Einfluss stattfindet. Wäre zwischenzeitlich ein htmlentities($foo) zum Einsatz gekommen, hätte sich die Schwachstelle nicht mehr manifestieren können:

$foo_sanitized = htmlentities($foo);
echo $foo_sanitized;

Logische Fehler

Abgesehen davon, dass der logische Ablauf eine Schwachstelle erhalten kann, kann es eine solche auch generieren. Es gibt eine Vielzahl an Schwachstellen, die auf diese Art entstehen können. Diese abschliessend aufzuführen ist unmöglich, da sie sich auf individuelle Eigenheiten entsprechender Software abstützen. Nachfolgend sollen deshalb lediglich exemplarisch einige mögliche Formen logischer Fehler illustriert werden.

Beispiel 1: Zuweisung anstatt Vergleich

Ein klassischer Fehler begründet sich in erster Linie in einem Flüchtigkeitsfehler. In den meisten höheren Programmiersprachen wird einer Variabel ein Wert zugewiesen, indem ein einfaches Gleichheitszeichen verwendet wird: $foo = 'bar'; Und ein Vergleich innerhalb einer Expression wird mit einem doppelten Gleichheitszeichen durchgeführt: if($foo == 'bar')

Durch Nachlässigkeit beim Schreiben von Code kann es passieren, dass bei einem Vergleich nun ebenfalls versehentlich ein einfaches anstatt eines doppelten Gleichheitszeichens verwendet wird:

if($foo = 'bar'){
   //wird immer ausgeführt
}else{
   //wird nie ausgeführt
}

Dies führt dazu, dass einerseits der Inhalt der Variabel beim vermeintlichen Vergleich stets überschrieben wird. Sofern dieser Schreibzugriff erfolgreich ist, wird der Ausdruck stets TRUE zurückliefern. Ein elseif oder else wird also gar nie zum Zug kommen können.

Schwachstellen dieser Art lassen sich durch Mustererkennenung relativ einfach finden. Schwieriger ist es nur bei Sprachen, die sowohl für die Zuweisung als auch für den Vergleich ein einfaches Anführungszeichen einsetzen. Dazu gehören zum Beispiel die Sprachen der Familie Visual Basic:

Beispiel 2: Toter Code

Ein anderer klassischer Fehler liegt in totem Code (Dead Code). Dabei handelt es sich um Codeteile, die nie ausgeführt werden. Dies liegt daran, dass sie im Rahmen des logischen Ablaufs unmöglich zugänglich sind, da die Voraussetzungen nie erfüllt werden können. Im Rahmen der Optimierung durch Compiler könnten diese zwar automatisch eliminiert werden. Es gibt dennoch Situationen, in denen toter Code nicht deterministisch identifiziert werden kann oder die eingesetzte Entwicklungsumgebung derartige Optimierungen gar nicht anbietet. Nachfolgend wird ein Code-Fragment gezeigt, bei dem die Eingabe $foo auf ihren Wert geprüft wird:

if($foo > 0){
   //kann ausgeführt werden
}elseif($foo > 10){
   //kann niemals ausgeführt werden
}else{
   //kann ausgeführt werden
}

Die elseif-Condition kann niemals TRUE sein, da bei positiven Werten immer die erste Expression greift. Dadurch kann es zu sonderbaren Effekten bei der Ausführung kommen, da Situationen, die erwartet werden, gar nie entstehen können.

Beispiel 3: Typenunsichere Prüfungen

Ein anderes Problem tritt bei Sprachen auf, die nicht typensicher (type safe) sind. Einmal mehr greifen wir auf das XSS-Beispiel auf der Basis von PHP zurück. Eine Validierungs-Funktion könnte nachfolgende Prüfung benutzen, um spitze Klammern erkennen zu können. Dabei wird die Funktion strpos() eingesetzt, die im Fall eines Treffers die Position dessen ausgibt. Hier wird angenommen, dass im Fall eines Treffers die erste Condition zutrifft, da sie wahr sein wird. Dies funktioniert auch, sofern das gesuchte Zeichen nicht an erster Stelle des geprüften Strings steht. Ist dem nämlich so, wird der Rückgabewert 0 lauten. Aufgrund der Typenunsicherheit von PHP entspricht dies in diesem Fall einem FALSE, weshalb der else-Codeblock ausgeführt wird.

if(strpos($foo, '<')){
   //nur falls an Position >0
}else{
   //auch falls an Position 0
}

Dem kann nur entgegnet werden, indem eine typensichere Prüfung auf der Basis von === bzw. !== stattfindet. Ein ähnlicher Effekt entsteht, wenn eine Funktion keinen Rückgabewert liefert. Standardmässig wird bei einer Funktion in PHP ohne eigens definierten Rückgabewert der Wert NULL zurückgegeben, was bei einer typenunsicheren Prüfung einem FALSE entspricht. Diese Art von Schwachstellen ist also abhängig von den Paradigmen der zugrundeliegenden Programmiersprache.

Ein ähnlicher Effekt entsteht bei JavaScript. Die Anweisung "5" - 2 hat das Resultat 3 zur Folge, da das Minuszeichen als arithmetischer Operator verstanden wird. Wird hingegen "5" + 2 verwendet, lautet das Resultat nicht 7 sondern 52. Dies liegt daran, dass im Zusammenhang mit dem Pluszeichen beide Werte als Strings verstanden und deshalb zusammengeführt werden.

Geschäftsabläufe werden wichtig

Für das Erkennen von logischen Fehlern muss man sich das Wissen um die Funktionsweise der Anwendung aneignen. Dies muss in erster Linie innerhalb der Software selber stattfinden. Ein vollumfängliches Verständnis ist aber meist nur dann möglich, wenn auch die zugrundeliegenden Geschäftsprozesse verstanden werden. Zum Beispiel können gewisse Prüfungen innerhalb eines Devisenhandelssystems nur dann erfolgreich durchgeführt werden, wenn man sich um die Wichtigkeit von Margen und den damit verbundenen Sell-Outs bewusst ist. Eine gute SCA fokussiert sich also nicht nur auf einzelne Codezeilen, sondern versucht die Anwendung innerhalb des Unternehmens zu betrachten.

In PHP kann bei strpos die Position 0 als FALSE missverstanden werden

Datenverarbeitung

Neben den logischen Abläufen wird die Funktionsweise einer Software ebenso durch die Behandlung der bereitgestellten und benutzten Daten beeinträchtigt. Die in Variablen eingesetzten Daten werden durch verschiedene Funktionen bearbeitet. Hierbei können Built-In Funktionen, die durch die Programmiersprache selbst angeboten werden, zum Einsatz kommen. Oder es werden eigene Funktionen, diese können auch über externe Komponenten (Bibliotheken oder APIs) zur Verfügung gestellt werden, genutzt. Je nachdem wie eine Variable bearbeitet wird, können sich dadurch Schwachstellen etablieren oder halt eben nicht.

Konstruierte Datensätze

Bei Strings können verschiedene generische Funktionen zum Einsatz kommen, die im das Verhalten der Software massgeblich verändern können. Durch substr() oder explode() könnten Teil-Strings angelegt werden. Wird zum Beispiel bei einem XSS-Zugriff ein explode('<', $foo) eingeschoben, verringern sich die Chancen massgeblich, dass damit ein erfolgreicher oder wenigstens koordinierter Angriff realisiert werden kann.

Erweiterte Funktionen

Ebenso können auch eigens entwickelte Funktionen zum Tragen kommen, die bei der Prüfung und Sicherung der Eingaben helfen können. Im Code ist dementsprechend besonders nach Funktionen mit Hinweisen auf Validation und Sanitation zu suchen. Diese Funktionen müssen besonders gut analysiert werden, da ein Fehler in ihnen erst die Möglichkeit der Erschliessung einer Schwachstelle gewährt.

Moderne Sprachen bieten bisweilen Funktionen an, die genau zum Ziel haben, typische Angriffstechniken abwenden zu können. Auf diese wird bevorzugt zurückgegriffen, da sie sich in der Regel bewährt haben.

Angriffstechnik PHP ASP JSP
Cross Site Scripting htmlentities(), htmlspecialchars() Server.HTMLEncode() escapeHtml()
Directory Traversal basename(), realpath()
SQL Injection mysql_real_escape_string(), mysqli_real_escape_string(), sqlite_escape_string(), addslashes(), PDO::quote()

Assertions

In ähnlicher Weise funktionieren Assertions. Bei diesen werden die zu erwartenden Voraussetzungen forciert oder Widersprüche erkannt, um eine spezifische Fehlerbehandlung anzustreben. Nachfolgendes Beispiel zeigt eine Assertion in ANSI C, die sich dank glibc (C99) sehr einfach umsetzen lässt. Falls die Assertion unwahr ist, wird eine Fehlermeldung ausgegeben.

#include <assert.h>
x = 1;
y = x + 2;
assert(y > 1);

Assertions sind relativ unpopulär. Diese werden in erster Linie in hochprofessionellen Projekten verwendet, bei denen die Prüfbarkeit der Datenverarbeitung einen sehr hohen Stellwert geniesst.

Entsprechend ist im Rahmen des Program Slicing zu prüfen, ob die Datenverarbeitung auf sicherheitstechnisch wichtige Funktionen verzichtet, wodurch die Durchführung einer Attacke ermöglicht wird.

Datenmanipulation erschliesst Schwachstellen

Im Gegenzug kann natürlich genauso gegeben sein, dass eine bestimmte Datenmanipulation erst überhaupt eine Schwachstelle zulässt. Nachfolgend wird ein Code abgedruckt, der zwei Eingaben $var1 und $var2 erwartet. Diese werden über die jeweiligen HTTP GET-Parameter eingelesen. Dabei darf die Eingabe pro Variable aber maximal 15 Zeichen lang sein. Dies reicht nicht aus, um eine klassische XSS-Schwachstelle durchzuführen:

$var1 = substr($_GET['foo'], 0, 15);
$var2 = substr($_GET['bar'], 0, 15);
echo $var1.$var2;

Da die beiden Strings aber bei der Ausgabe zusammengeführt werden, kann der Payload gleichmässig auf die beiden Variablen verteilt werden. Hier müssen also mehrere Gegebenheiten zusammenspielen, damit der Fehler auftritt:

http://example.com/?foo=<script>alert('&bar=xss');</script>

Derlei Schwachstellen finden bevorzugt dort statt, wo Datenquellen aufeinander aufbauen oder zusammengeführt werden. Dazu gehören explizite Funktionen wie implode(), array_merge() (gerade bezüglich Array-Funktionen wartet PHP mit einer Vielzahl an möglichen Fehlerquellen auf). Aber auch simple String Concatenation kann als Grundlage für einen solchen Angriff herhalten.

Analyse grafischer Schnittstellen

Eintrittspunkte bei prozeduralen Projekten zu identifizieren, ist verhältnismässig einfach. Bedeutend schwieriger kann es werden, wenn eine Anwendung eine grafische Oberfläche zur Verfügung stellt. Die jeweiligen Objekte werden im Code oftmals abstrahiert und erfordern zusätzlichen Aufwand, um sie im Kontext der effektiven Verarbeitung des Codes verstehen zu können. Durch die Eigenschaft von Software mit grafischer Oberfläche können sich damit neue Schwachstellen auftun.

Fehlerhafte Events

Die Objekte weisen dabei Events auf, deren Eintritt eine Aktion auslöst. Für die meisten Objekte kann zum Beispiel der Event Click definiert werden. Wird also eine Schaltfläche namens cmdButton angeklickt, wird bei Visual Basic folgender Codeblock ausgeführt:

Private Sub cmdButton_Click()
   MsgBox "foo"
End Sub

Der Begriff Click impliziert in diesem Zusammenhang, dass ein Mausklick stattfindet. Dieser kann aber auch durch eine Tastatureingabe – bei Windows bieten sich die Leertaste bei selektiertem Objekt oder die Enter-Taste bei aktiviertem Default = Yes an – stattfinden. Falls also zum Beispiel der Mauszeiger deaktiviert wird, heisst dies noch lange nicht, dass ein Benutzer nicht stattdessen mit einer anderen Eingabemöglichkeit diesen Event auslösen kann.

Diese Eigenschaft hat schon so manche Anwendung unsicher gemacht, wenn es zum Beispiel um das Verhindern von Eingaben geht. Es wird zum Beispiel eine Textbox definiert, die mit dem Event KeyPress durchsetzt, dass nur Zahlen eingegeben werden können:

Private Sub txtTextbox_KeyPress(KeyAscii As Integer)
   Select Case KeyAscii
      Case vbKey0 To vbKey9
      Case vbKeyBack, vbKeyClear, vbKeyDelete
      Case vbKeyLeft, vbKeyRight, vbKeyUp, vbKeyDown, vbKeyTab
      Case Else
         KeyAscii = 0
         Beep
   End Select
End Sub

Wird nun aber ein String in die Zwischenablage kopiert und dieser mittels rechter Maustaste in die limitierte Textbox eingefügt, wird der Event nicht ausgelöst. Die unliebsame Eingabe kann in diesem Fall also stattfinden. Im Idealfall findet eine Prüfung vor dem Weiterverarbeiten der Eingabe, dies kann im Rahmen des Events Validate oder spätestens bei einem LostFocus geschehen, statt.

Parallelisierung, Multi-Threading und Multi-Tasking

Hinzu kommt, dass die Abarbeitung von Software mit grafischer Oberflächen oftmals Parallelisierung anbietet und gar in einem Multi-Tasking Umfeld stattfinden. Dort kann keine serialisierte Abarbeitung gewährleistet werden. Somit treten ähnliche Laufzeitprobleme auf, wie man sie von Multi-Threading Lösungen her kennt.

Folgender Code hat zum Ziel, nach dem Öffnen einer Anwendung zuerst den Titel des Frames mittels Me.Caption festzulegen, um danach die für den Debug-Modus vorgesehene Schaltfläche tlbMenu.Buttons.Item(3).Enabled zu deaktivieren. Sie kann nicht mehr betätigt werden:

Private Sub Form_Load()
   Me.Caption = "Foo 2.0"
   Screen.MousePointer = VbHourglass
   With tlbMenu.Buttons
      .Item(1).Enabled = True
      .Item(2).Enabled = True
      .Item(3).Enabled = False 'deaktiviere Debug Möglichkeiten
   End With
   Screen.MousePointer = vbDefault
End Sub

Für den Benutzer finden diese Ereignisse subjektiv gesehen zeitgleich statt. Er wird nicht bemerken, dass zuerst der Titel geändert und danach der Reihe nach die Buttons aktiviert bzw. deaktiviert werden. Wird das System jedoch stark ausgelastet oder weisen die einzelnen Anweisungen zusätzliche Abhängigkeiten auf, die zeitlich hinausgezögert werden können, kann dies zu Problemen führen. Schafft es der Benutzer nämlich vor der Anweisung .Item(3).Enabled = False einen Klick auf den besagten Button durchzuführen, wird dieser noch vor dem Deaktivieren desselbigen initiiert werden. Man hat es hier also mit möglichen Race Conditions zu tun.

Ein Einschränken dieser Probleme ist meistens nie ganz möglich. Man kann aber versuchen, einen strengen serialisierten Ablauf voranzutreiben, indem man auf parallelisierende Mechanismen verzichtet und die abschliessende Ausführung einzelner Anweisungen durch ein DoEvents forciert. Gerade letzteres ist aber eine Eigenheit der Visual Basic-Familie.

Zusätzlich kann man versuchen, die sicherheitsrelevanten Aufgaben zuerst zu erledigen. Also dass man als erstes den Button deaktiviert, und erst dann den Titel des Frames anpasst und die anderen Buttons aktiviert. Sollte eine träge Ausführung stattfinden, treten zwar optische Unschönheiten auf. Die sicherheitstechnische Integrität des Programmablaufs ist damit aber optimiert.

Den maximalen Schutz erreicht man jedoch, indem man nicht benötigte Objekte frühestmöglich ausblendet (hiding) oder sperrt (locking). Im Idealfall wird der Debug-Button also schon bei der Instanziierung als gelockt definiert, so dass kein nachträgliches Sperren mehr erforderlich wird. Erst ein explizites Entsperren würde den Zugriff bei Bedarf erlauben. Es wirklich also quasi ein Whitelist-Ansatz für die Freigabe von Objekten angestrebt.

Dokumentation der Resultate

Wenn Schwachstellen ein einem Quelltext gefunden wurden, müssen diese in der Regel dokumentiert werden. Diese Dokumentation dient dann als Grundlage, dass die Fehler in einem nächsten Schritt korrigiert bzw. adressiert werden können.

Wichtig dabei ist, dass die betroffenen Codeteile möglichst genau referenziert werden. Dabei werden im Idealfall die folgenden Datenpunkte verwendet:

Datenpunkt Beispiel
Softwarename Foo Forum 2.0
Dateiname post.php
Funktionsname createNewPost()
Codezeilen Lines 23-42

Zusätzlich kann der betroffene Codeblock direkt abgebildet werden, um eine Prüfung und Referenzierung zusätzlich vereinfachen zu können.

Weitere Informationen zur Schwachstelle (Schwachstellenklasse, Beschreibung, Angriffsszenario, Exploit-Beispiel) bringen zusätzliche Qualität mit. Dabei ist nach Möglichkeiten auch gleich eine Gegenmassnahme vorzuschlagen. Diese kann optimalerweise als Codebeispiel erfolgen, so dass ein möglichst konkreter Ansatz vorgetragen werden kann.

Zusammenfassung

Source Code Analysen sind eine wichtige Alternative oder Ergänzung zu anderen Formen der Sicherheitsüberprüfung. Wenn der Quelltext einer Software zur Verfügung steht, können sehr effizient und zuverlässig Schwächen in dieser identifiziert werden.

Dabei sollten die jeweiligen Eintritts- und Austrittspunkte gefunden werden, um Manipulationsmöglichkeiten und Auswirkungen identifizieren zu können. Der logische Ablauf und die Datenverarbeitung müssen zusätzlich berücksichtigt werden, um effektive Angriffsmöglichkeiten identifizieren zu können.

Über den Autor

Marc Ruef

Marc Ruef ist seit Ende der 1990er Jahre im Cybersecurity-Bereich aktiv. Er hat vor allem im deutschsprachigen Raum aufgrund der Vielzahl durch ihn veröffentlichten Fachpublikationen und Bücher – dazu gehört besonders Die Kunst des Penetration Testing – Bekanntheit erlangt. Er ist Dozent an verschiedenen Fakultäten, darunter ETH, HWZ, HSLU und IKF. (ORCID 0000-0002-1328-6357)

Links

Sie wollen die Sicherheit Ihrer Firewall prüfen?

Unsere Spezialisten kontaktieren Sie gern!

×
Konkrete Kritik an CVSSv4

Konkrete Kritik an CVSSv4

Marc Ruef

scip Cybersecurity Forecast

scip Cybersecurity Forecast

Marc Ruef

Voice Authentisierung

Voice Authentisierung

Marc Ruef

Bug-Bounty

Bug-Bounty

Marc Ruef

Sie wollen mehr?

Weitere Artikel im Archiv

Sie brauchen Unterstützung bei einem solchen Projekt?

Unsere Spezialisten kontaktieren Sie gern!

Sie wollen mehr?

Weitere Artikel im Archiv