Systematische Eingabeüberprüfungen Marc Ruef | 08.04.2007 iDieser Beitrag stellt ein Transkript von Computec Radio, Folge 6: Systematische Eingabeüberprüfungen (http://www.computec.ch/download.php?view.872) dar. Die Audiofassung steht zum freien Download zur Verfügung./i Ein Computerprogramm muss man sich wie eine mathematische Funktion vorstellen. Diese lautet der Einfachheit halber f. Ihre Aufgabe ist es, die Quadratwurzel eines ihr übergebenen Werts zu berechnen. Die Funktion erhält also eine Eingabe, auf die der durch f definierte Algorithmus, nämlich das Wurzelziehen, angewendet wird. Damit liefert sie die Wurzel der Eingabe als Ausgabe zurück. Die Eingabe von E führt also zu f(E), was zur Ausgabe A führt. Definiert sich die Eingabe E durch 16, ergibt dies 4 = f(16). Nun könnte genau diese Funktion in einem Computerprogramm implementiert werden. Sprich, diese Software soll genau das machen, was nun am formalen Beispiel aufgezeigt wurde. Die Anwendung namens wurzel.c erwartet also beim Programmaufruf die Übergabe eines Arguments. Dieses dient als Eingabe für die Funktion, die in bekannter Weise die Wurzel ausrechnet und das Resultat davon auf dem Bildschirm ausgibt. Eine solche Anwendung ist, unabhängig von der genutzten Programmiersprache, relativ einfach umgesetzt. Hochsprachen wie C oder Java kommen mit internen Funktionen daher, wodurch sich eine übersichtliche Implementierung in etwa einem halben Duzend Zeilen realisieren lässt. Um nun herauszufinden, ob und inwiefern wurzel.c eine von einem Angreifer ausnutzbare Schwachstelle aufweist, müssen zuerst die vom Anwender beeinflussbaren Gegebenheiten identifiziert werden. Offensichtlicherweise sind dies die Eingabe, die er vor und während der Programmausführung tätigen kann. Die beschriebene Anwendung ist zeilenbasiert und erwartet lediglich bei ihrem Aufruf die Übergabe eines einzigen Arguments. Dies ist damit der einzige Parameter, den der Anwender unmittelbar beeinflussen kann. Dass wurzel.c bei der Übergabe des Parameters 16 den korrekten Wert 4 zurückliefern sollte, ist nichts besonderes. Dabei ist zu bemerken, dass es sich sowohl bei der Eingabe 16 als auch bei der Ausgabe 4 um einen ganzzahligen Wert handelt. Es würde nun interessieren, ob und inwiefern die Anwendung mit reellen Zahlen umgehen kann. Der erste Test könnte also die Parameterdefiniton 1,44 vorsehen. Verschiedene Reaktionen können nun beobachtet werden. Erstens könnte die Anwendung die Eingabe korrekt interpretieren und deshalb die richtige Quadratwurzel 1,2 zurückliefern. Andernfalls könnte eine Eingabeüberprüfung die Eingabe als nicht ganzzahliger Wert identifizieren und eine gerichtete Fehlermeldung ausgeben. Andererseits könnte die Eingabe aber auch zu einem undefinierten Zustand führen, sollte die Eingabe zwar zugelassen werden, die Abarbeitungsfunktionen aber ausschliesslich auf ganzzahlige Werte ausgelegt sein. Wird die Eingabe als solche aufgrund offensichtlicher Missachtung der Vorgabe eines ganzzahligen Werts verweigert, kann in einem nächsten Schritt versucht werden die Zahl 0 einzugeben. Diese weist kein Trennzeichen, welches auf eine reelle Zahl hindeuten würde, auf. Achtet die Eingabeüberprüfung nur auf das Vorkommen von Komma oder Punkt, kann diese damit umgangen werden. Die damit eingebrachte Eingabe führt nun dazu, dass aus dem Wert 0 dessen Quadratwurzel errechnet werden möchte. Die Ziffer 0 nimmt seit jeher einen ganz speziellen Platz in der Mathematik und damit auch in der Informatik ein. Sie ist als einzige darum bemüht etwas auszudrücken, was nicht existiert. Es wird nun also schwierig aus etwas nichtexistierendem dessen Wurzel zu berechnen. Je nach Implementierung liefert der Funktionsaufruf nun wiederum 0, undefiniert oder fehlerhaft zurück. Es gibt eine Vielzahl an arithmetischen Funktionen, die allergisch auf bestimmte Eingaben oder Werte reagieren. Ein bekanntes und seit vielen Jahren umstrittenes Beispiel ist die Berechnung von 0^0. Da sind die Rückgabewerte 0, 1, unbestimmt oder fehlerhaft zu beobachten. Für Nichtmathematiker ist erstaunlich, dass die richtige Lösung in diesem Fall 1 zu lauten scheint. Verschiedene Mathematiker haben in einer konvergenten Iteration bewiesen, dass dies die einzig richtige Lösung ist. Diese wurde dann auch auf verschiedene Standards, wie zum Beispiel IEEE 754 oder C99, übertragen. Dennoch halten sich nicht zwingend alle Implementierungen an diese Vorgaben, weshalb nach wie vor mit Abweichungen gerechnet werden kann und muss. Gerade IEEE 754 ist ein interessantes Dokument, was Eingabeüberprüfungen betrifft. In diesem wird in erster Linie das Rundungsverhalten von Anwendungen beschrieben. Die Speichermöglichkeiten von Computersystemen sind naturbedingt beschränkt. Aus diesem Grund lassen sich reelle Zahlen nur mit einer begrenzten Genauigkeit, also mit einer limitierten Anzahl Stellen nach dem Komma, anzeigen. Wie nun mit den überschüssigen Stellen verfahren werden soll, ist massgeblich für die Verarbeitung und Genauigkeit eines Systems, also die unliebsamen Rundungsfehler, mitverantwortlich. Primitive Lösungen sehen ein Abschneiden der überschüssigen Stellen vor. In IEEE 754 wird grundsätzlich zwischen binären und binärdezimalen Rundungen unterschieden. Bei einer binären Rundung muss zur nächsten darstellbaren Zahl gerundet werden. Wenn diese nicht eindeutig definiert ist, da sie sich genau in der Mitte zwischen zwei darstellbaren Zahlen befindet, muss in Richtung zur nächstgelegenen geraden Zahl gerundet werden. Der statistische Drift, eine unliebsame Abweichung der Gesamtbetrachtung, wird damit minimiert. Sicherheitsprobleme aufgrund von Rundungsfehlern sind jedoch sehr selten. Anders sieht es wiederum beim Verhalten von vorzeichenbehafteten Werten aus. Ein vorzeichenloser Wert ist lediglich im positiven Bereich gleich gross oder grösser als 0 zu finden. Ein Wert mit einem Vorzeichen lässt jedoch auch negative Werte, denen ein Minuszeichen vorangestellt wird, zu. Wird nun wurzel.c mit der negativen Eingabe -16 aufgerufen, wird dies voraussichtlich zu einer internen Fehlermeldung führen. Ein Sonderfall dieser Art ist übrigens der Wert -0. Manche Systeme unterscheiden diesen tatsächlich vom positiven Wert +0, obschon beide in ihrem Wert equivalent betrachtet werden sollten. Auch dies könnte zu Problemen, zum Beispiel bei Rechteprüfungen mit User-ID (UID), führen. Vorzeichen eignen sich zudem sehr gut, um einen sogenannten (arithmetischen) Underflow durchzuführen. Dabei wird unterhalb die Untergrenze eines Wertbereichs gegangen. Handelt es sich beim Wertbereich um positive Zahlen, kann dies also ganz einfach durch den Wert 0-1 geschehen. Einige Systeme erkennen diesen Underflow und brechen den Zugriff ab. Andere akzeptieren ihn, zählen den Minuswert jedoch also höchsten Wert des definierten Wertbereichs. Damit wird also ein Rundlauf realisiert. Durch den Aufruf von -1 kann zum Beispiel damit der Wert max-1 angesprochen werden. Genauso wie sich durch max+1 der Wert +1 angehen lässt. Überhaupt nehmen Sonderzeichen in praktisch allen Umgebungen eine zentrale Rolle ein. Desweiteren könnte nun versucht werden, bei wurzel.c die Eingabe der Form 10% durchzuführen. Das Prozentzeichen könnte als Deklaration des Werts verstanden werden. Gleichzeitig könnte es aber auch einem der internen Mechanismen zur Kontrolle dienen. Zum Beispiel werden bei einigen C-Funktionen mit %x innerhalb von Format Strings die Anzeige von hexadezimalen Werten definiert. Dadurch liesse sich die Datenverarbeitung umfassend beeinflussen. Um zielgerichtet mit Sonderzeichen arbeiten zu können, sind im besten Fall also die eingesetzten Mechanismen bekannt. Daraus lässt sich nun der anzuwendende Zeichensatz ermitteln. Bei Dateizugriffen sind dies Punkt (.), Slash (/), Strichpunkt (;) und das Pipe-Zeichen (|). Bei Datenbanken kommen mitunter das Hochkomma ('), Vergleichsoperatoren (=) sowie runde Klammern (, ) zum Einsatz. Und bei Webanwendungen spielen besonders das Kleiner-als- () sowie Anführungszeichen (") eine wichtige Rolle. Bisher wurde stets darauf geachtet, beim Aufruf von wurzel.c die erwartete und damit korrekte Anzahl Argumente zu übergeben. Doch auch diese Definiton kann missachtet werden. So lassen sich nun absichtlich weniger oder mehr Parameter einsetzen. Dies führt jenachdem wieder zu wohldefinierten oder internen Fehlermeldungen, die sich in irgendeiner Weise ausnutzen lassen. Gerade Attacken auf Entscheidungsprüfungen und Speicherverwaltungen lassen sich damit initiieren. Diese Diskussion hat sich auf die Kommandoeingabe als Parameter eines zeilenbasierten Programms fokussiert. Die gleichen Betrachtungen und Methoden können jedoch bei jeder Schnittstelle, die eine Anwendung zur Verfügung stellt, angewendet werden. Eingaben auf grafischen Oberflächen, Rückgabewerte von externen Libraries, Daten in Umgebungsvariablen oder Textblöcke von Konfigurationsdateien sind nur einige Beispiele für Angriffs- und Injektionsvektoren.