Bedrohung

Buffer Overflow Attacks

Buffer Overflow – leichter als gedacht

Von Stefan Strobel, München

Alle Nase lang dasselbe Hickhack: Buffer-Overflow-Alarm in Applikation XY, Hersteller beteuert "keine reale Gefahr", Exploit erscheint, ... Wie leicht ist es aber wirklich, solche Fehler auszuschöpfen und wie funktioniert das? Eine Studie gab unlängst Antwort.

Egal ob Solaris, HP/UX oder Windows 2000: Zu jedem Betriebssystem kommen regelmäßig neue Meldungen über Sicherheitslücken und nicht selten steht in der Erklärung, dass es sich um einen Pufferüberlauf (Buffer Overflow) handelt, der einem Angreifer letztlich sogar Systemverwalter-Berechtigung auf der Zielmaschine verschaffen kann.

Die Reaktion der Hersteller besteht dann häufig aus einem nicht ganz zutreffenden Hinweis, dass es sich bei Buffer Overflows um eine rein theoretische Verwundbarkeit handle. Hacker wiederum fühlen sich durch diese Hinweise genötigt, so genannte Exploits zu entwicklen, mit denen sich die Sicherheitslücken nahezu automatisch ausnutzen lassen.

Sobald ein solcher Exploit im Internet für jedermann verfügbar ist, gilt eine Sicherheitslücke natürlich als unbestreitbar reell. Eine weitere unangenehme Folge davon ist aber auch, dass nun Hunderttausende oder sogar Millionen von technisch unbegabten Hobby-Hackern – so genannte Script-Kiddies – mithilfe des Exploits in die Lage versetzt werden, tatsächlich in Server einzubrechen.

In dieser Kette von Ereignissen und Argumenten, die man nahezu wöchentlich beobachten kann, bleibt für viele unklar, was tatsächlich hinter einem solchen Pufferüberlauf steckt und wie groß die Gefahr ist, die davon ausgeht. Entscheidend dabei ist auch, ob es – wie häufig behauptet wird – tatsächlich nur eine Handvoll hochbegabter Hacker gibt, die in der Lage sind einen Exploit zu entwickeln, oder ob das Potenzial möglicher Angreifer auch ohne einen öffentlich verfügbaren Exploit sehr groß ist.

[strcpy() bietet bisweilen Angriffsfläche]
Bestimmte C-Funktionen, besonders strcpy(), begünstigen Progammierfehler, die zu Buffer Overflows führen können.

Eine Untersuchung in Form einer Studienarbeit, die an drei Studenten der Berufsakademie Mosbach vergeben und vom Labor der Sicherheitsfirma Articon-Integralis betreut wurde, hatte diese Frage zum Thema. Die Aufgabenstellung der Studienarbeit war relativ kurz: Zunächst sollten die Studenten die Ursachen beziehungsweise Programmierfehler analysieren, die einen Pufferüberlauf ermöglichen. Danach galt es für einen einfachen, in der Programmiersprache C geschriebenen Server, der einen entsprechenden Programmierfehler enthält, einen Exploit zu erstellen, mit dem man beliebige Befehle auf dem Zielsystem ausführen kann.

Um einen solchen Exploit zu erstellen, sind neben Kenntnissen der Programmiersprache C vor allem ein Verständnis der Prozessorarchitektur, Speicherverwaltung und der jeweiligen Maschinensprache des Serversystems nötig. Obwohl sich die Studenten die notwendigen Kenntnisse in Bezug auf IT-Sicherheit erst aneignen mussten, gelang es ihnen, die Aufgabe innerhalb von rund sechs Wochen neben ihren normalen Vorlesungen zu lösen. Ohne die Einarbeitungszeit und die täglichen Vorlesungen hätten sie ihre Arbeit sicherlich in weniger als zwei Wochen fertig stellen können. Alle dafür nötigen Informationen stehen in der Standardliteratur über Assembler-Programmierung sowie in frei verfügbaren Anleitungen auf den etablierten Hacker-Webservern im Internet zur Verfügung.

Die Untersuchung hat somit gezeigt, dass ein Pufferüberlauf nicht erst dann zur Gefahr wird, wenn ein fertiger Exploit jedermann zur Verfügung steht. Wenn schon Studenten ohne Vorkenntnisse einen Exploit in vier bis sechs Wochen neben den Vorlesungen entwickeln können, dann dürfte dies für einen Profi eine Affäre von wenigen Stunden oder Tagen sein.

Hintergrund

Wie kommt es eigentlich zu einem Pufferüberlauf und wie lässt er sich für einen Angriff ausnutzen? Wie der Name schon sagt, wird bei einem solchen Programmierfehler ein Puffer zum Überlaufen gebracht. Meist ist das ein Speicherbereich, in dem Eingaben für die weitere Verarbeitung abgelegt werden. Diese Eingaben kommen entweder aus Übergabeparametern beim Aufruf eines Programms von der Kommandozeile, aus Dialogeingaben oder Netzwerkprotokollen. Im Fehlerfall ist ein Eingabewert länger, als es das Programm erwartet. Er überschreibt damit den Speicherbereich einer Variablen und die im Speicher darauf folgenden Werte. Voraussetzung hierfür ist natürlich, dass der Programmierer vergessen hat, die maximal zulässige Länge der Eingabewerte zu überprüfen. Klassische Beispiele solcher Fehler traten in Webservern auf, die als Eingabe den Pfad einer (über)langen URL von dem anfragenden Browser bekommen.

Dies allein liefert jedoch noch keine Einbruchsmöglichkeit, da bei fast jeder Prozessorarchitektur das ausführbare Programm getrennt von den Variablen gespeichert wird. Um in die Maschine einzubrechen, möchte der Angreifer jedoch nicht einfach den Wert von Variablen überschreiben, sondern eigenen Programmcode einschleusen, der anschließend auch ausgeführt wird.

Riskante Überlauf-Probleme treten bei lokalen Variablen auf. Lokale Variablen sind nur innerhalb einer Unterroutine oder Funktion gültig und werden daher zusammen mit der Rücksprungadresse der Funktion auf dem so genannten Stack gespeichert. Der Stack ist ein Speicherbereich, der wie ein Stapel wächst. Werte können "oben" auf den Stapel gelegt und wieder vom Stapel "herunter" genommen werden. Dadurch lassen sich die Rücksprungadressen vieler hierarchisch verschachtelter Unterprogramme sehr elegant verwalten: Der Prozessor legt beim Aufruf eines Unterprogramms die aktuelle Adresse als Rücksprungadresse auf den Stack. Ruft das Unterprogramm seinerseits ein weiteres Unterprogramm auf, so wird wieder die aktuelle Adresse auf den Stack gelegt – der Stack wächst. Nachdem ein Unterprogramm beendet ist, liegt die Adresse, an der es im übergeordneten Programm weitergeht, oben auf dem Stack. "Oben" auf dem Stack bedeutet dabei eine niedrigere Speicheradresse, da der Stack von höheren Speicheradressen zu niedrigeren Adressen hin wächst.

[Speicheraufteilung im RAM]
Der Stack wächst im Speicher von höheren zu niedrigeren Adressen hin.

Lokale Variable, die nur innerhalb eines Unterprogramms gültig sind, können ebenfalls elegant auf dem Stack gespeichert werden. Beim Verlassen des Unterprogramms gibt der Prozessor ihren Speicherplatz automatisch wieder frei. Falls nun die Länge einer Zeichenkette, die in einer lokalen Variable gespeichert werden soll, größer ist als der dafür vorgesehene Platz, so überschreibt eine ungeprüfte Speicherung dieses Wertes nicht nur andere lokale Variablen, sondern eventuell auch die Rücksprungadresse der aktuellen Unterroutine.

[Buffer Overflow im Stack]
Wenn strcpy() eine lokale Variable in den Stack schreibt, die länger als der vorgesehene Speicherplatz ist, so überschreibt die Funktion gegebenenfalls auch die Rücksprungadresse des Unterprogrammaufrufs.

Bestimmte Funktionen der Programmiersprache C begünstigen dies; sie kommen in unzähligen Programmen zum Einsatz. Es handelt sich dabei vor allem um die Zeichenkettenverarbeitungsfunktion strcpy(). Sie hat zwei Parameter: eine Quell- und eine Ziel-Adresse, an der die Funktion jeweils Speicherplatz für einen String erwartet. Sie kopiert Zeichen für Zeichen des Quell-Strings in den Ziel-String und hört erst dann auf, wenn das Zeichen \0 (der ASCII-Wert 0) den Quell-String beendet.

Das Null-Zeichen als Zeichenkettenende ist in C durchaus üblich. Wenn ein Programmierer davon ausgeht, dass bestimmte Eingabewerte maximal 80 Zeichen lang sind, und er diesen Wert mit strcpy() in einen Puffer kopiert, für den er 100 Bytes reserviert hat, dann prüft die Funktion strcpy() dieses Maximum nicht. Falls der Quell-String erst nach 200 Zeichen eine Null enthält, dann überschreibt die Funktion eben weitere Speicherbereiche. Ist der Ziel-String eine lokale Variable, dann kann dieses Prozedere auch die Rücksprungadresse der aktuellen Funktion auf dem Stack überschreiben.

Fataler Rücksprung

Wenn der Prozessor nach dem Beenden der aktuellen Funktion mit dem aufrufenden Programm fortfahren will, dann liest er aus dem Speicher der Rücksprungadresse einen Teil des kopierten Textes und interpretiert ihn als Adresse. Da an dieser Adresse bei unbeabsichtigten Überläufen meist kein sinnvolles Programm steht, führt dieser Zustand häufig zu einem Absturz mit der Meldung "Segmentation Fault" (Speicherzugriffsschutz-Fehler). Falls an der neuen Adresse jedoch ein funktionsfähiger Maschinencode steht, so wird dieser ausgeführt.

Genau dies ist das Ziel des Angreifers. Er sucht nach Stellen in Pogrammen oder Netzwerkdiensten, die Eingabewerte mit Funktionen wie strcpy() bearbeiten, und versucht dann über den Eingabewert Maschinencode einzuschleusen und gleichzeitig die Rücksprungadresse derart zu überschreiben, dass sie auf den gerade eingeschleusten Code zeigt.

[Stackinhalt: vorher - nachher]
Um einen Pufferüberlauf (der lokalen Variablen Y) für einen Angriff auszunutzen, ergänzt man Füller für weitere Variablen (X) und setzt eine entsprechende Rücksprungadresse ein, die zur Ausführung des eingeschleusten Programmcodes führt (links im Bild der vorgesehene Zustand des Stacks, rechts der Zustand nach dem Überlauf).

Das klingt kompliziert und ist auch in der Praxis nicht trivial. Bei genauerer Überlegung fallen dem geübten Softwareentwickler sogar viele Argumente ein, warum ein Ausnutzen einer solchen Schwachstelle in der Praxis nahezu unmöglich sein sollte. Leider fallen dem geübten Hacker ebenso viele Tricks ein, wie er diese Probleme bewältigen kann, um den Pufferüberlauf doch zu missbrauchen und eigenen Code auszuführen.

Das erste und offensichtlichste Problem ist der begrenzte Platz, der einem Angreifer für seinen einzuschleusenden Code zur Verfügung steht. Dieser ist beispielsweise von der Größe des Stacks und der maximalen Länge der eingelesenen Eingabe begrenzt. Ein Angreifer hat keine Möglichkeit über Buffer Overflows Programme mit mehreren Kilobytes oder sogar Megabytes einzuschleusen. Betrachtet man heutige Software, so könnte man meinen, dass sogar minimale Programme mindestens mehrere Megabytes groß sind. Dieser Eindruck täuscht jedoch – die großen Programmdateien kommen eher von uneffektiven Programmiersprachen oder aufgeblasenen Bibliotheken.

In einem reinem Assemblercode lassen sich höchst elegante und sehr kleine Programme schreiben. Hinzu kommt, dass ein Angreifer nicht jede Funktion mitliefern muss: In einem typischen Windows-System sind jede Menge von Bibliotheken (DLLs) bereits in den Hauptspeicher geladen. Häufig sind sie sogar immer an denselben Adressen im Speicher und ein Angreifer kann sie wie selbstverständlich aufrufen. Damit lassen sich beispielsweise TCP/IP-Verbindungen öffnen oder Befehle über den Kommandozeilen-Interpreter des Systems ausführen.

Ein anderes Problem ist, dass der eingeschleuste Code in der Eingabe kein Null-Zeichen enthalten darf, da sonst die strcpy() Funktion davon ausgeht, dass der Eingabe-String zu Ende ist, und mit dem Kopieren aufhört. Um dies zu verhindern, kann der Angreifer sehr vorsichtig bei der Auswahl seiner Befehle sein. Anstelle eines Befehls, der den Wert 0 in ein Prozessorregister lädt, kann er auch den Wert 1 in das Register laden und ihn dann um 1 herunterzählen. Auf diese Weise kommt der Wert 0 im Programmcode nicht vor. Alternativ kann man auch das gesamte einzuschleusende Programm so umkodieren, dass kein 0-Wert vorkommt, und es dann beim Start wieder zurückkodieren. Dies lässt sich zum Beispiel einfach durch eine XOR-Verknüpfung des Programms mit einer geeigneten Zahl realisieren.

Selbstverständlich ist es schwierig oder nahezu unmöglich, die Adresse zu kennen, an der das eingeschleuste Programm in den (Stack-)Speicher kommt. Der Angreifer kann zwar versuchen, die Adresse mithilfe eines Debuggers herauszufinden, dafür benötigt er aber Zugang zu der Software, die auf dem Zielsystem läuft. Bei vielen bekannten Servern ist das zwar kein Problem. Dennoch versuchen Angreifer eher, den eingeschleusten Code möglichst adressunabhängig zu schreiben. Anstelle von absoluten Sprüngen verwenden sie dann beispielsweise relative Sprünge.

Zusammengefasst kann man sagen, dass ein Exploit für einen Pufferüberlauf keine triviale Angelegenheit ist. Der Angreifer muss sich intensiv mit der Speicherverwaltung und der Maschinensprache seines Zielsystems beschäftigen. Mit etwas Übung und geeigneten Werkzeugen lässt sich dies jedoch auch von Anfängern in relativ kurzer Zeit realisieren, wie die Studie der Berufsakademie Mosbach belegt. Erleichtert wird der Angriff vor allem, wenn der Angreifer Zugang zu der Software hat, in der er einen Pufferüberlauf ausnutzen will.

Gegenmaßnahmen

Man ist solchen Angriffen jedoch nicht völlig hilflos ausgeliefert. Der Ursprung deratiger Schwachstellen liegt in der mangelnden Qualität der eingesetzten Software. Gute Programmierer prüfen alle Eingabewerte, egal ob sie über das Netz, in WWW-Formularen oder per Kommandozeile kommen. Falls man eigene Software verwendet, können solche Fehler durch externe Code-Reviews gefunden und beseitigt werden. Leider bietet sich diese Möglichkeit bei Standardsoftware nicht. In diesem Fall kann man lediglich versuchen, den Pufferüberlauf vom Betriebssystem her zu verhindern oder zumindest seine Auswirkungen einzudämmen.

Bei Solaris gibt es beispielsweise die Möglichkeit das System so einzustellen, dass es im Stacksegment keine Befehle ausführt. Diese Einstellung ist jedoch nicht mit allen Anwendungen kompatibel und sie verhindert nicht alle Varianten von Pufferüberläufen, aber es ist bereits ein großer Schritt in die richtige Richtung.

Eine weitere Möglichkeit ist die Verwendung eines Trusted Operating Systems (TOS), beispielsweise Pittbull, welches das Betriebssystem in mehrere so genannte Compartments zerlegt. Damit kann ein Hacker zwar einen Pufferüberlauf ausnutzen, um Zugriff auf eine Anwendung zu erlangen. Er bleibt dabei aber in einem kleinen Teil des Systems eingesperrt und kann nicht den gesamten Server unter seine Kontrolle bringen.

Stefan Strobel arbeitet im Strategic Development Team der Articon-Integralis AG in Heilbronn und ist Dozent an der Berufsakademie Mosbach und der FH Heilbronn.

© SecuMedia-Verlags-GmbH, D-55205 Ingelheim,
KES 5/2001, Seite 6