Exploits entmystifiziert, Teil 2 Einen Stack-basierten Pufferüberlauf provozieren

Autor / Redakteur: Thorsten Henning* / Stephan Augsten

Das Ausnutzen von Speicherfehlern ist ein gängiger Weg für einen Schwachstellen-Exploit. Doch wie werden Speicherbereiche überschrieben und Speicherabfragen auf genau diese Bereiche umgebogen? Dies wollen wir uns am Beispiel stapelbasierter Pufferüberlauf-Schwachstellen genauer ansehen.

Firma zum Thema

Der stapelbasierte Pufferüberlauf ist einer der Klassiker, wenn es um Schwachstellen-Exploits geht.
Der stapelbasierte Pufferüberlauf ist einer der Klassiker, wenn es um Schwachstellen-Exploits geht.
(Bild: Archiv)

Im ersten Teil dieser Serie haben wir die Grundlagen der Ausnutzung von Speicherfehlern behandelt. Der Exploitation-Prozess setzt sich somit aus den folgenden Teilen zusammen:

  • Überschreiben einer Adresse im Prozess-Speicherraum
  • Umleiten der Ausführungsabfolge zur Shellcode-Adresse
  • Ausführen des Shellcodes

Im Folgenden geht es um die eigentliche Umsetzung des Überschreibens und Umleitens. In seiner einfachsten Form wird der Speicherplatz in den Bereich des ausführbaren (Executable) Codes und den Datenbereich unterteilt.

Der Executable-Bereich enthält sowohl einzigartigen Code des Programms als auch DLLs (Dynamic Link Libraries; Dynamische Programmbibliotheken), die das Betriebssystem für alle Prozesse bereitstellt. Der Datenbereich enthält, wie der Name schon sagt, die Daten, auf dem der Code arbeitet.

Der Datenbereich besteht aus dem Stack (Stapel) und dem Heap (Haufen), was im Folgenden noch näher beschrieben werden soll. Der Angreifer interessiert sich für eben diesen Datenbereich, da der Shellcode in das eingebettet ist, was hier hinein geladen werden soll. Das heißt, sobald die Datei mit dem Shellcode ausgeführt wird, befindet sich der Shellcode entweder in einem Stack oder in einem Heap.

Herausforderung und Lösung aus Angreifer-Perspektive

Aus der Sicht des Angreifers ist es erforderlich, den Shellcode in den Datenbereich zu injizieren. Die Rolle des Shellcodes ist es, ausgeführt zu werden und eine Verbindung zwischen dem Angreifer und dem Zielcomputer zu öffnen. Dies ist die grundlegende Herausforderung bei einem Exploit:

  • 1. Der Shellcode wird standardmäßig in den Datenbereich geladen.
  • 2. Der Shellcode muss ausgeführt werden.
  • 3. Dadurch dass er im Datenbereich residiert, können die „besetzten“ Speicheradressen nie zur Ausführung von der CPU abgerufen werden.

Hauptziel des Angreifers ist es, die CPU so zu manipulieren, dass Inhalte von Speicheradressen ausgeführt werden, die unter normalen Umständen nicht ausgeführt würden. An dieser Stelle kommen Schwachstellen ins Spiel.

Was es genau bedeutet, wenn man davon spricht, dass eine Anwendung eine Sicherheitslücke oder Schwachstelle enthält? Damit ist gemeint, dass eine in Handarbeit erstellte Eingabedatei den Ausführungsfluss von seinem vorbezeichneten Kurs ablenkt. In anderen Worten, die CPU wird abgeholt zu einer Adresse, die sie nicht empfangen sollte.

Das alles gilt es nun, zu verbinden: Der Angreifer hat es geschafft, einen Shellcode im Datenbereich des Prozessspeichers einzufügen. Jetzt sucht er nach einer Möglichkeit, den Shellcode ausgeführt zu bekommen. Um das zu erreichen, wird der Angreifer die Datei so anpassen, dass die abgelenkte Adresse eine Anleitung enthält, in die Shellcode-Adresse zu springen. Nun empfängt die CPU eine Adresse, die ausführbaren Code enthält, und wird den Anweisungen folgen, zur Shellcode-Adresse springen und diese ausführen.

Schwachstellen kurz erklärt

Schwachstellen sind fest an den Overwrite/Überschreiben-Teil im Exploitation-Fluss gebunden. Verschiedene Schwachstellen ermöglichen es dem Angreifer, Adressen in verschiedenen Teilen des Prozess-Adressbereichs zu überschreiben.

Die erste Art von Schwachstelle, die hier beschrieben werden soll, ist ein Stapel-basierter Pufferüberlauf (stack-based buffer overflow). Dieser Schwachstellen-Typ einer der ältesten, der in freier Wildbahn genutzt wird, und stellt immer noch einen bedeutenden Teil der aktuellen Bedrohungslandschaft dar.

Der Stack/Stapel

Ein typisches Computerprogramm besteht aus einem Hauptprogramm und Funktionen oder Unterprogrammen. Wenn ein Unterprogramm aufgerufen wird, führt es seine Aufgabe aus und gibt die Steuerung an das Hauptprogramm zurück. Aus der Perspektive des Speicheradressenraums befinden sich die Adressen des Hauptprogramms im Code-Bereich.

Beim Aufruf eines Unterprogramms wird ein Stapel aktiviert, um die lokalen Variablen zu speichern (was in etwa dem entspricht, was wir als Daten bezeichnen). Das Unterprogramm führt dann seine bestimmte Aufgabe aus. Wenn es fertig ist, übergibt es die Steuerung wieder an das Hauptprogramm.

Aus der Perspektive des Angreifers gibt es drei interessante Funktionen:

  • 1. Feste Größe: Die Größe des Stapels ist festgelegt und bestimmt den Zeitpunkt des Aufrufs. Zum Beispiel wird im Unterprogramm ein Array von 10 Zeichen deklariert. Dies wird die Größe des Stapels sein, unabhängig von den Argumenten, die an den Stapel weitergegeben werden.
  • 2. Rücksendeadresse: Der Return-Kontrollmechanismus funktioniert wie folgt: Der Stapel wird mit einer festen Speichergröße aufgerufen. Nehmen wir an, unserem 10-Zeichen-Stapel wird 100 zugewiesen. Dies bedeutet, dass die Adressen 91 bis 100 diesem Stapel zugewiesen sind. Außerdem enthält Adresse 90 die Adresse im Hauptprogramm, zu dem die CPU zurückkehren sollte, nachdem das Unterprogramm seine Aufgabe erfüllt hat. Dieser Speicherplatz wird als Rücksendeadresse bezeichnet.
  • 3. Der Stapel wächst nach unten: Wenn wir die tatsächlichen Argumente dem Stapel zuweisen, geht der erste an die höchste Adresse und wird dann durch die folgenden nach unten geschoben. Wenn wir eine 3-Zeichen-Eingabe unserem vereinfachten Stack zuleiten, geht das erste zur Adresse 100. Danach wird das zweite 100 einnehmen und das erste auf 99 schieben. Das dritte wird zu 100 gehen und das zweite auf 99 und das erste auf 98 verschoben. Da die Eingabe hier endet, gibt es keine Argumente mehr. Die Rücksendeadresse wird abgerufen von der CPU, die ihren Anweisungen folgt und zurück zum Hauptprogramm springt.

Bisher haben wir die Stack-Architektur ohne schädlichen Kontext beschrieben. Jetzt werden wir erklären, wie diese Architektur in böswilliger Absicht genutzt werden kann.

Stapel-basierter Pufferüberlauf

Die inhärente Sicherheitslücke in der Stack-Architektur ist, dass sie implizit davon ausgeht, dass die Eingabe der vorgegebenen Größe entsprechen wird. Dies funktioniert gut, wenn die Eingabe entweder kleiner ist oder identisch ist mit dieser Größe. Das Problem entsteht, wenn die Eingabe größer ist als die vorgegebenen Stapelgrenzen.

Gehen wir zurück zu unserem vereinfachten Stack. Nehmen wir an, wir geben dem Unterprogramm eine Eingabe, die größer ist als 10 Zeichen. Zu beachten ist, dass die Adressen 91 bis 100 für die Eingabe zugewiesen sind und die Adresse 90 bereits von der Rücksendeadresse eingenommen wird.

Wenn unsere Eingabe 11 Zeichen umfasst, wird das erste Zeichen zur Adresse 100 gehen und dann nach unten gedrückt werden. Sobald die 90 erreicht ist, wird die Rücksendeadresse überschrieben. Die CPU wird versuchen, den Anweisungen in Adresse 90 zu folgen. Da diese aber nicht mehr existiert, wird der Ausführungsablauf unterbrochen und infolge dessen der Prozess zum Absturz gebracht. Dies wird als Stack-Overflow, also „Stapelüberlauf“ bezeichnet.

An dieser Stelle sei daran erinnert, dass der Shellcode sich in dem Stapel befindet und der Angreifer versucht zu erreichen, dass er ausgeführt wird. Um den Stapelüberlauf für seine Zwecke zu nutzen, wird der Angreifer den Unterprogramm-Input so manipulieren, dass die Rücksendeadresse mit neuen Anweisungen überschrieben wird, die die CPU zum Shellcode-Speicherort umleiten.

In unserem vereinfachten Stapel wird beispielsweise der Angreifer eine Eingabe mit 11 Zeichen erstellen. Der Shellcode befindet sich in den Zeichen 11 und 10. Zeichen 1 enthält Befehle, um auf 11 zu springen und die Eingabe auszuführen. In diesem Fall, wenn das Unterprogramm aufgerufen wird, wird Zeichen 1 nach unten gerückt werden, die Rücksendeadresse überschrieben und die CPU zur Adresse 100 geleitet, wo der Shellcode sitzt. Die CPU wird den Anweisungen blind folgen und den Shellcode ausführen.

Wie sich im oberen Beispiel sehen lässt, sind die Exploitation-Teile nicht miteinander verbunden: Die Einbettung eines Shellcodes ist völlig entkoppelt vom Bearbeiten der Eingabedatei, um eine bestimmte Schwachstelle auszulösen. Die ausgelöste Schwachstelle ermöglicht es, die Rücksendeadresse zu überschreiben.

Die Anweisungen, die die Rücksendeadresse überschreiben, leiten die CPU zum Shellcode, betreffen aber nicht die Funktionalitäten des Shellcodes in irgendeiner Weise. Die Kunst der Exploits ist es, diese unabhängigen Teile zu orchestrieren, damit sie zusammenarbeiten. In ähnlicher Weise ist die Kunst beim Schutz vor Exploits, entweder die unabhängigen Teile direkt oder die Orchestrierung zwischen ihnen zu behindern.

Schlussfolgerung

Wir haben gelernt, wie das grundlegende Exploitation-Framework auf Stapel-basierte Pufferüberlauf- Schwachstellen angewandt wird. Trotz seines Alters (der früheste dokumentierte Angriff war der Morris-Wurm im Jahr 1988), ist diese Vorgehensweise immer noch weit verbreitet in der Bedrohungslandschaft.

Wir begegnen Fällen von Ausnutzung dieser Schwachstellen in verschiedenen Readern, Playern und Microsoft-Office-Dokumenten, aber auch in industriellen Protokolle und Diensten. Im nächsten Teil werden wir die Umsetzung des Exploitation-Frameworks für Heap-basierte Schwachstellen behandeln.

* Thorsten Henning ist Senior Systems Engineering Manager Central & Eastern Europe bei Palo Alto Networks.

(ID:43793086)