Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2022-37969PoC — Tutorial zu CVE-2022-37969 mit Fokus auf die Methodik der Kernel-Exploitation, nicht die internen Ursachen der CVE | Kitploit
Tools/GitHubGitHub/emilc3978/cve-2022-37969poc
Privilege EscalationSpeicherforensikSchwachstellenanalyseExploitationReverse EngineeringLernen & BildungBinary-Exploitation
GitHubemilc3978/cve-2022-37969poc

CVE-2022-37969PoC

Tutorial zu CVE-2022-37969 mit Fokus auf die Methodik der Kernel-Exploitation, nicht die internen Ursachen der CVE

Repository anzeigen
2vor 8 MonatenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

Inhalt

Allgemeine Einleitung

Dies wurde erstellt, um allgemeine Aspekte der Windows-Ausnutzung zu klären. Es erklärt grundlegende Konzepte, die auf CVE-2022-37969 angewendet werden. Das Endergebnis ist ein funktionierender PoC. Es klärt nicht jeden Aspekt der CVE, liefert aber wiederverwendbare Codefragmente und erklärt Mechanismen, die in vielen allgemeinen Exploits zu finden sind.

Der Zielbenutzer wäre ein Anfänger im Reverse Engineering, ein Exploit-Entwickler, der einen funktionierenden Proof-of-Concept-Quellcode sucht, um grundlegende Windows-Interna zu testen und zu verstehen. Es bietet einen Referenzpunkt für weiterführendes Lernen.

Anforderungen: Grundlegende Kernel-Debugging-Fähigkeiten, grundlegendes Reverse Engineering, grundlegende Windows-Interna, C/C++-Programmierkenntnisse

Kontext

Ein Programm ist ein Stück Code, das auf einer Maschine läuft. Im Allgemeinen empfängt ein Programm Daten (Eingabe), führt Berechnungen mit den Eingaben durch und erzeugt Daten (Ausgabe). Die meisten Programme werden von Menschen geschrieben und haben daher Fehler. Ein Fehler wird durch Quellcode erzeugt, der nicht korrekt geschrieben wurde (der Programmierer wollte etwas mit der Eingabe tun, der resultierende Code unterschied sich vom beabsichtigten Ergebnis). Die meisten Fehler werden vor der Veröffentlichung des Produkts behoben, aber einige bleiben bestehen. Dies liegt daran, dass es verschiedene Arten von Fehlern gibt, einige schwieriger zu erkennen als andere.

Windows ist ein Computerprogramm, wurde von Menschen geschrieben und hat daher Fehler. Warum ist das wichtig? Weil Windows-Systeme Programme ausführen können, die sensible Daten wie Bankkonten, Gesundheitsdatenbanken und andere verarbeiten. Einige Fehler können verwendet werden, um illegal auf geschützte Daten zuzugreifen (dies ist ein guter Anwendungsfall für einen Exploit).

Es gibt viele Arten von Fehlern, einige nützlich, einige nicht. Im Allgemeinen werden Fehler durch Eingaben in das Programm erzeugt, die in Verbindung mit den falsch geschriebenen Codezeilen eine fehlerhafte Ausgabe oder ein fehlerhaftes Verhalten des Programms erzeugen. Das Auffinden dieser Eingabe ist die Aufgabe des Sicherheitsspezialisten (oder Hackers). Der nächste Schritt besteht darin, die resultierende fehlerhafte Ausgabe/das fehlerhafte Verhalten zu bewerten und die Frage zu beantworten: "Kann es auf nützliche Weise verwendet werden?" Hier werden Fehler in verschiedene Kategorien eingeteilt. Zum Beispiel kann ein Fehler ein Verhalten erzeugen, das einige Datenstrukturen beschädigt und den Zielcomputer zum Neustarten zwingt. Sein Nutzen ist begrenzt. Ein Fehler kann dazu führen, dass die Eingabe in einen Speicherbereich geschrieben wird, der die Zugriffsberechtigungen für geschützte Dateien steuert. Diese Art von Fehler ist nützlicher.

Aus der Menge aller möglichen Fehler sucht der Hacker also die Teilmenge, die für seinen Zweck am nützlichsten ist. Allgemein gesprochen lautet das Problem: "Kann ich dem Zielprogramm eine speziell gestaltete Eingabe geben, sodass ich das System nicht beschädige, aber meine Zugriffsebene erhöhen und profitieren kann?"

Nach dieser nicht-technischen Einführung kann der Umfang des Tutoriums formuliert werden: Können wir ein Windows-Programm finden, das fehlerhafte Eingaben akzeptiert und infolge falschen Entwicklercodes unsere Berechtigungen illegal von einem normalen Benutzer auf Administrator erhöhen kann?

Zielprogramm: Windows CLFS (Common Log File System Driver)

Exploit-Name: CVE-2022-37969

Typ: Lokale Privilegieneskalation

VERWUNDBARE ISO ZUM HERUNTERLADEN: Hier herunterladen

Privilegieneskalation unter Windows – allgemeine Theorie

Der Windows-Adressraum ist grob unterteilt in Benutzermodus (Ausführung allgemeiner Programme) und Kernel-Modus (Betriebssystem selbst und Hardware-Komponenten-Software → Treiber). Ein normaler Benutzer darf nicht auf den Kernel-Modus zugreifen, aber es gibt Mechanismen, durch die Programme von normalen Benutzern auf Teile des Kernel-Codes zugreifen können (Systemaufrufe, Treiberprozeduren). Warum brauchen wir Zugriff? Um mit dem Betriebssystem auf sichere und kontrollierte Weise zu interagieren, die von den OS-Designern bereitgestellt wird.

Einige Treiber verwenden vom Benutzer bereitgestellte Dateneingaben, um auf Kernel-Datenstrukturen zu operieren. Wenn die Eingabe einen Fehler erzeugt, könnte der Kernel beschädigt werden. Ein Fall ist der Common Log File System Driver. Durch die Verwendung spezieller Eingaben können wir den Treiber zwingen, Kernel-Datenstrukturen zu verändern, die die Berechtigungsstufe für den Benutzer halten, und normaler Benutzer durch Administrator zu überschreiben.

Was muss geändert werden, um die Berechtigung auf Admin zu erhöhen?

Wir beginnen mit dem Endziel im Kopf. Windows speichert in einer Kernel-Datenstruktur namens _EPROCESS Informationen für jeden auf dem System laufenden Prozess. Beispiel für _Eprocess

Ein wichtiges Feld ist struct _EX_FAST_REF Token. Dies ist eine weitere Datenstruktur, die weiter auf Daten verweist, die die Berechtigungsstufe des jeweiligen Prozesses referenzieren. Im folgenden Bild hat der Systemprozess ein System-Token und der Explorer-Prozess ein normales Benutzer-Token.

Tokens

Um die Berechtigung von Explorer.exe zu erhöhen, müssten wir also den Wert aus dem _EPROCESS-->Token von System in das _EPROCESS-->Token von Explorer kopieren. Wir werden etwas Ähnliches erreichen, indem wir das System-Token in das Token unseres eigenen Programms kopieren und eine Eingabeaufforderung aus dem erhöhten Prozess starten (Kindprozesse erben das Token des Vaterprozesses).

Um diese Aktionen durchzuführen, benötigen wir Mechanismen für:

  1. Das Abrufen der Adresse der _EPROCESS-Datenstruktur im Kernel
  2. Das Lesen des Werts des Token-Feldes für den Systemprozess
  3. Das Abrufen der Adresse der _EPROCESS-Datenstruktur für Explorer
  4. Das Schreiben des Werts des System-Tokens an den Token-Offset von Explorer in seiner _EPROCESS-Struktur

Lokalisieren der _EPROCESS-Datenstruktur für einen Zielprozess anhand der PID

Einführung: Die Natur von Windows im Laufe der Jahre: Mit der Entdeckung neuer Schwachstellen musste Windows gepatcht werden, um sie zu entschärfen. Auch mit dem Aufkommen neuer Technologien musste Windows aktualisiert werden, um wettbewerbsfähig zu bleiben. Eine entscheidende Anforderung war die Abwärtskompatibilität mit früheren Versionen. Und manchmal wurde Sicherheit durch Unklarheit erreicht. Datenstrukturen, Funktionsdefinitionen wurden aus Handbüchern entfernt, aber die Funktionalität blieb erhalten. Durch Reverse Engineering konnten Forscher diese Funktionalitäten für verschiedene Zwecke nutzen.

Zum Auffinden der Kernel-Adresse von _EPROCESS verwenden wir eine undokumentierte Funktion: NtQuerySystemInformation (siehe Link für Parameter). Durch die Verwendung des Parameters SystemInformationClass können wir angeben, welche Art von Informationen wir abrufen möchten. Wir werden allgemeine Prozessinformationen abrufen, indem wir den Wert SystemExtendedHandleInformation angeben (#define SystemExtendedHandleInformation 0x40).

Ein Nachteil der Verwendung von NtQuerySystemInformation ist, dass wir nicht im Voraus wissen, welche Länge die zurückgegebenen Daten haben werden, aber NtQuerySystemInformation hat einen Mechanismus, der hilft. Wenn es mit einem falsch dimensionierten Array für die erforderlichen Daten aufgerufen wird, gibt es einen Fehler zurück und die korrekte Datengröße, die hätte angefordert werden sollen. Dies kann verwendet werden, um Prozessinformationen korrekt zu lesen, wie folgt:

  1. Rufen Sie NtQuerySystemInformation mit einem Dummy-Parameter SystemInformationLength auf.
  2. Lesen Sie den zurückgegebenen Parameterwert ReturnLength.
  3. Rufen Sie NtQuerySystemInformation erneut mit dem korrekten, zuvor zurückgegebenen SystemInformationLength-Wert auf.

Die zurückgegebene Datenstruktur ist vom Typ PSYSTEM_HANDLE_INFORMATION_EX. Dies ist eine undokumentierte Datenstruktur. (Siehe Link), die zu einer Datenstruktur SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX führt, die im Feld Object die Kernel-Adresse der _Eprocess-Datenstruktur für den entsprechenden Prozess enthält.

Die Logik wäre also: Iterieren über alle PSYSTEM_HANDLE_INFORMATION_EX-Elemente, das Feld SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX UniqueProcessId mit der gewünschten Prozess-PID vergleichen und das entsprechende Object-Feld auswählen, um seine _Eprocess-Adresse im Kernel zu finden.

Ein Code-Ausschnitt:

proc_1 proc_2

NtQuerySystemInformation wird als Zeiger auf eine Funktion deklariert und seine Adresse wird zur Laufzeit dynamisch über loadlibrary und getprocaddress bezogen.

  1. typedef NTSTATUS(WINAPI* ptr_NtQuerySystemInformation)(int, PVOID, ULONG, PULONG);
  2. ntdll=LoadLibrary(L"ntdll.dll");
  3. MyNtQuerySystemInformation = (ptr_NtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");

Zum Lesen und Schreiben des Tokens müssen wir uns auf Schwachstellen in clfsw32.sys und NamedPipes stützen.

Lesen und Schreiben von Daten im Kernel mithilfe von Pipes

Dies ist eine Art Blackbox, die in anderen Artikeln detailliert beschrieben wird, aber das Mindestwissen für diese Funktionalität wird erklärt, um ein grundlegendes Verständnis des Exploit-Prozesses zu erlangen. Pipes sind Interprozesskommunikationsmechanismen. Prozesse können mithilfe von Pipes Informationen aneinander weitergeben. Pipes werden als Kernel-Datenstrukturen dargestellt, die einige Felder haben, die aus dem Benutzermodus gefüllt werden können. Ein Beispiel wären Pipe-Attribute.

(Später werden wir dies mit der CLFS-Schwachstelle verknüpfen, um beliebiges Lesen/Schreiben vom Benutzermodus in den Kernel-Modus zu erreichen)

Datenallokation im Kernel

Für weiterführende Lektüre konsultieren Sie bitte fengshui-spraying-big-kids-pool. Eine vereinfachte Erklärung des Mechanismus wäre:

Der Kernel hat zwei Arten, Speicher in Bezug auf die Größe, die wir allozieren möchten, zu verwalten: Small-Pool für Objekte <4 KB + Header und Big-Pool für Objekte >4 KB + Header. Big-Pool-Seiten sind wichtig, weil sie aus dem Benutzermodus aufgezählt werden können. Dies bedeutet, dass ein normaler Benutzer alle Start-Kernel-Adressen finden kann, die eine Big-Pool-Seite enthalten.

Wie? Jede Big-Pool-Seite hat ein Feld namens Tag (das verwendet werden kann, um Informationen über die Art der dort gespeicherten Daten zu erhalten). Alle Big-Pool-Seiten im System können mit NtQuerySystemInformation mit SystemBigPoolInformation als Wert für den Parameter SystemInformationClass aufgezählt werden. Aus allen Seiten können wir dann nach Tag filtern und die Adresse der Big-Pool-Seiten erhalten, an denen wir interessiert sind. Zum Beispiel verwendet CLFS Big-Pool-Seiten mit dem Tag 'Clfs'. Wir können die Adresse aller Seiten im Kernel erhalten, auf denen CLFS-Objekte alloziert sind.

proc_1

Zurück zu Pipes: Wir können auf die gleiche Weise vorgehen, eine Pipe groß genug allozieren, um den Big-Pool-Mechanismus zu nutzen, die Big-Pool-Seiten aufzählen, nach dem spezifischen Tag der Pipe suchen und diese Seiten filtern.

Somit können wir im Benutzermodus den Ort preisgeben, an dem der Kernel unsere Pipes alloziert hat. Was ist mit der Kontrolle über Daten, die in den Kernel gehen, und dem Lesen davon?

Dazu verlassen wir uns auf Pipe-Attribute. Ähnlich wie das Big-Pool-Tag ist das Pipe-Attribut ein Array, das Informationen enthalten kann, die die Pipe beschreiben (vom Benutzer gefüllt). Mit der undokumentierten Funktion NtFsControlFile können wir beliebig in die Pipe-Attribut-Datenstruktur lesen und schreiben. Da sie undokumentiert ist, wird nur ein Proof-of-Concept bereitgestellt, der einen PipeAttribute-Vektor setzt und dann liest. Die einzige Modifikation, die den Primitiven zum Lesen/Schreiben erlaubt ist, die Kontrolle über den Inhalt des Eingabe-/Ausgabepuffers und deren Größe.

Schreiben und Lesen aus Pipe-Attributen im Kernel z.B.:

Hier allozieren wir eine Pipe groß genug, um die Big-Pool-Seiten (0x2000) zu nutzen, und setzen einen Eingabe- und Ausgabepuffer auf einen kontrollierten Wert. Achtung: Die ersten 2 Bytes des Eingabewerts MÜSSEN 0x5a 0x00 sein, damit es funktioniert.

pipewr

Hier lesen wir zurück, was wir zuvor in den Kernel geschrieben haben, ändern wieder nur den Ausgabepuffer.

peprd

Und das Ergebnis:

piperes

Wie sieht eine Pipe-Big-Pool-Seite im Speicher aus und warum ist die vorherige Operation für uns nützlich?

Hier sind die Inhalte der Pipe-Big-Pool-Seite im Kernel: Durch Abfragen der Big-Pool-Seiten fanden wir den Beginn der Pipe-Datenstruktur im Kernel. An der Adresse pipe_begin+0x20 finden wir einen Zeiger auf unseren Eingabepuffer+0x2.

pipe_kern

Wenn wir die Funktion zum Lesen von Pipe-Attributen aufrufen, wird das Betriebssystem Folgendes tun:

  1. Ort der Pipe im Kernel identifizieren
  2. 0x20 addieren, den Zeiger dereferenzieren und den Inhalt aus dem Kernel-Speicher in unseren Benutzerpuffer auslesen.

Warum ist das nützlich? Stellen Sie sich vor, wir könnten den Zeiger bei pipe_begin+0x20 durch die Adresse des Sicherheitstokens des Systemprozesses ersetzen und das Pipe-Attribut lesen aufrufen. Wir würden seinen Wert im Benutzermodus preisgeben. Der Ersatz wird über die CLFS.SYS-Schwachstelle erreicht.

Memory Spraying

Das Verständnis dieser Technik ist entscheidend für das Verständnis des Exploits.

Die meisten Exploits sind nicht deterministischer, sondern probabilistischer Natur. Selbst wenn der Code, der die Schwachstelle ausnutzt, korrekt ist, könnte der Exploit nicht funktionieren. Indem das Zielprogramm in einen instabilen Zustand versetzt wird, muss der Exploit-Entwickler sicherstellen, dass das System nach der Ausführung des Exploits nicht abstürzt. Stellen Sie sich ein Programm vor, das bei Ausnutzung die Möglichkeit bietet, von einer Adresse zu lesen, die von einem Variablenwert innerhalb eines Programms abhängt.

z.B.:

Angenommen, target=0x1000000+ var_1&0xff+ var_2&0xff00.

Der Hacker kann target oder var_1 oder var_2 nicht kontrollieren.

Aber der Exploit bietet die Möglichkeit, von der Adresse target zu lesen.

Wir könnten von überall zwischen 0x1000000 und 0x100FFFF lesen, was in einigen Fällen nützlich sein könnte oder nicht.

z.B.2:

Angenommen, ein Exploit bietet die Möglichkeit, ein QWORD von einer Adresse zu lesen, die einem bestimmten Muster entspricht, und den Inhalt in den Sicherheitstokenwert zu setzen. Angenommen, wir haben zuvor den Sicherheitstokenwert für System.exe erhalten. Wie können wir den Exploit nutzen, um Privilegien zu erhöhen?

read_addr=0x1000000+alfa&0xFFFF00

Wir können den alfa-Parameter nicht kontrollieren.

OFF TOPIC, ABER SEHR WICHTIG: Der Kernel kann auf den Benutzerspace desjenigen Prozesses zugreifen, der zu diesem Zeitpunkt Kernel-Code ausführt.

Von welcher Adresse können wir lesen? Nun, 0x1000000, 0x1000100 (alfa=1), 0x1000200 (alfa=2), ..., 0x1FFFF00 (alfa=FFFF00).

Um sicherzustellen, dass der Exploit erfolgreich ist, sollte der Programmierer Folgendes tun:

  1. Im Benutzerspace einen Speicherblock der Größe 0x1000000 (ungefähr) allozieren: memory=virtualalloc(dest=0x1000000, size=0x1000000, ....)
  2. for (i=0x1000000; i<0x2000000; i+=0x100) ((QWORD*)i)[0]=system_token_value;
  3. Der Code bei 2. füllt den Speicher an jedem möglichen alfa-Wert mit dem System-Token-Wert. Es ist also garantiert, dass unabhängig vom Wert des alfa-Parameters das System-Token an der Adresse vorhanden ist.

Dies ist tatsächlich Memory Spraying. Speichermanipulation, die einem bestimmten Muster folgt, das alle möglichen Werte eines probabilistischen Ausdrucks abdeckt, der das Ergebnis eines Exploits ist.

Eine Voraussetzung dafür, dass dies in unserem Fall funktioniert, ist, dass Speicher an Adresse 0x1000000 alloziert werden kann. Dies ist eine Einschränkung, mit der wir auch im Fall des echten Exploits arbeiten müssen.

Common Log File System

Nachdem einige technische Aspekte geklärt sind, wird nun ein grundlegendes Verständnis von CLFS benötigt. Microsoft-Link. CLFS wird für Anwendungsprotokolle, Datenbankprotokolle, Transaktionen usw. verwendet.

Exploits erfordern im Allgemeinen ein bestimmtes Speicherlayout, um zu funktionieren. Die Form des Layouts wird durch Variablenwerte innerhalb des Programms zum genauen Zeitpunkt der Ausnutzung bestimmt.

Dieses Tutorial wird nicht im Detail den anfälligen Code oder das Format des Log-Dateisystems erklären. Es wird ein grundlegendes Verständnis der Prozesse bieten, die den Exploit erzeugen.

Log-Dateien sind eine spezielle Art von Dateien, die ein bestimmtes Format haben und mit CLFS-Treiber- und DLL-APIs interagiert werden können. Innerhalb dieses Systems gibt es auch das Konzept von Log-Containern. Die Log-Container sind ebenfalls Log-Dateien, werden aber im Speicher mit einer Haupt-Log-Datei verknüpft (der Log-Datei, zu der sie hinzugefügt werden).

Diese Konstruktion funktioniert wie folgt:

  1. Erstellen oder Öffnen einer Haupt-Log-Datei
  2. Erstellen oder Öffnen sekundärer Log-Dateien
  3. Verwenden der AddLogContainer-API, um die sekundären Log-Dateien als Container zur Haupt-Log-Datei hinzuzufügen

Diese Operation erzwingt die Allokation von neuem Speicherplatz innerhalb der Haupt-Log-Datei und wird verschiedene Objekte im Speicherlayout der Haupt-Log-Datei modifizieren. Die Modifikation des Speicherlayouts würde konzeptionell so aussehen:

container_concept

Hochrangiger Überblick über den Exploit-Mechanismus

Wie bei jeder wichtigen Datenstruktur/Datei führt der CLFS-Treiber vor der Verwendung Integritätsprüfungen gegen das Format der Log-Datei durch:

  1. Manipulationsschutz (Integrität): Der Hash der Datei wird an einem konstanten Offset in der Datei gespeichert. Um die Datei erfolgreich zu öffnen, berechnet der Treiber den Datei-Hash und prüft ihn gegen den gespeicherten Wert. Wenn die Werte nicht übereinstimmen, bedeutet dies, dass die Datei nach der Erstellung manipuliert wurde, und ein Fehler wird ausgelöst.
  2. Bereichs- und Dateistrukturprüfungen: Verschiedene Header und Längen werden überprüft, um sicherzustellen, dass das Format der Datei eingehalten wird.

Der zweite Punkt ist der Ausgangspunkt der Schwachstelle. Der Angreifer kann eine zuvor erstellte Log-Datei modifizieren, ihren Hash neu berechnen und das Hash-Feld bearbeiten, damit der Treiber den Integritätstest besteht. Die Modifikationen beziehen sich auf Datei-Header-Längen. Ein sorgfältig gestalteter Satz von Werten bestimmt, dass eine Bereichs-Längen-Prüfung bestanden wird, die sonst fehlschlagen und einen Fehler auslösen würde. Der Treiber akzeptiert somit die gefälschte Länge als gültig und setzt die Codeausführung normal fort. Die gefälschte Länge wird verwendet, um einen Offset innerhalb der Datei zu berechnen, an den eine fest codierte Adresse geschrieben wird. Dies gibt dem Angreifer die Fähigkeit, die Adresse zu kontrollieren, an der die vorherige Schreiboperation stattfindet.

Diese Schwachstelle muss mit Pipe-Kernel-Lese-Schreib-Operationen verkettet werden, um den Exploit abzuschließen. Visueller Beweis:

range-check

In der vorherigen Abbildung sehen wir die Funktion AllocSymbol in CLFS.sys, die für die fehlerhafte Bereichsprüfung verantwortlich ist. Die anfällige Längenprüfung ist diejenige, die den Fehlercode 0xC0000023 zurückgeben würde (BaseLogRecord + v9 + Size_1 + 0x1338 > v8 + *(0x68)). Ihre Eingabe in die Bedingung wird teilweise vom Angreifer kontrolliert. Es ist möglich, einen Wert in die Formel zu injizieren, der dazu führt, dass die Prüfung bestanden wird, obwohl sie normalerweise fehlschlagen würde. Durch Kontrolle von v8 und v9 können wir den Wahrheitswert der Bedingung auf FALSE manipulieren, die Fehlerrückgabe verhindern und zur willkürlichen Berechnung des Werts der Variablen v10 unter Verwendung des vom Angreifer kontrollierten Werts v9 führen.

V10 wird weiter für ein memset verwendet, wobei 0 als zu setzender Wert verwendet wird. Der Angreifer erhält also die Fähigkeit, willkürlich Speicher auf 0 zu setzen. Das ist noch nicht alles. Vor der endgültigen Rückkehr wird *a3=v10. A3 ist ein Parameter, der als Adresse an die Funktion AllocSymbol übergeben wird. Der Aufrufer von AllocSymbol erhält also nach der Rückkehr aus AllocSymbol den Wert von V10.

Der Aufrufer von AllocSymbol ist FindSymbol.

find-symbol

Innerhalb von FindSymbol ist V33 der Parameter, der als a3 in AllocSymbol bezeichnet wird. Also erhält v33 den Wert von v10. Dann wird der Wert an der Adresse von v33 auf eine Konstante gesetzt. (0xc1fdf006) und weiter mit dem Wert 0x30 präfixiert.

constant-fix

Der Angreifer erhält also die Fähigkeit, eine beliebige Speicherstelle mit einer Konstanten zu überschreiben.

Warum wäre dies wichtig? Wenn wir den Standardwert eines Funktionszeigers mit einer konstanten Adresse überschreiben können, die sowohl aus dem Benutzermodus als auch aus dem Kernel-Modus zugänglich ist, und eine Garantie haben, dass der jeweilige Zeiger aufgerufen wird, erhalten wir die Kontrolle über die Codeausführung. Obwohl dies die allgemeine Idee ist, gibt es technische Hürden, die weiter erklärt werden.

CLFS-Zeigerkorruption, oder welche Stelle überschrieben werden muss, um den Ausführungsfluss zu kapernFrüher haben wir über CLFS-Container gesprochen. Wenn ein Container zu einer Datei hinzugefügt wird, wird in der übergeordneten Datei eine Struktur gefüllt. Die Struktur hat einen Zeiger namens CClfsContainer* pContainer;. Wenn der entsprechende Container freigegeben wird, finden in der übergeordneten Datei Bereinigungsvorgänge statt, die pContainer dereferenzieren und die Werte und als Funktionszeiger verwenden.

Daher garantiert die Möglichkeit, pContainer zu steuern und die containerspezifischen APIs zum Hinzufügen und Entfernen auszuführen, eine Kontrollflussumleitung.

Wo befindet sich pContainer?

Die CLFS-Dateistruktur ist undokumentiert, aber es gibt einige individuelle Versuche, die Dateistruktur zurückzuentwickeln.

Ein Überblick über eine Logdateistruktur:

bird_eye_file_Structure

Und eine detaillierte Ansicht des Basisblocks:

detailed_base_blok

Unterschiede zwischen der Struktur und der Speicheransicht:

CLFS-Dateien werden in Big-Pool-Seiten mit einem Tag-Wert von Clfs zugewiesen. Wenn die Log-Adresse eines CLFS im Userspace abgerufen wird (dieselbe Methode, die zum Abrufen der Kernel-Space-Adresse eines Pipe-Objekts verwendet wird), gibt der Kernel DIE ADRESSE ZURÜCK, AN DER DER BASISBLOCK BEGINNT. (bei Offset 0x800 im vorherigen Bild). Bei Offset 0xb98 sehen wir eine Datenstruktur namens regContainers. Dies ist ein Array von 32-Bit-Werten. Jeder Wert bezieht sich auf einen Container und stellt den Offset (gezählt ab 0x870) dar, an dem sich die internen Datenstrukturen eines Containers in der Basisblockdatei befinden.

Das Speicherlayout einer Containerdatei ist wie folgt:

  1. CLFS_CONTAINER_CONTEXT-Struktur

container

Bei Offset 0x18 in die CLFS_CONTAINER_CONTEXT-Struktur befindet sich unser Ziel pContainer.

Offenbar ist der Offset zu pContainer konstant, nämlich das erste Element des regContainers-Arrays. Und sein Wert ist 0x1468.

Nachweis, dass pContainer dereferenziert und als Funktionszeiger verwendet wird:

Das Dereferenzieren und Zugreifen auf pContainer als Funktionszeiger ist der Kern des Exploits. Dies geschieht innerhalb der Remove container-Funktion im CLFS.SYS-Treiber. Der Exploit wird also ausgelöst, wenn der Container entfernt wird.

Nachweis:

container_removal

Lassen Sie uns die Schritte zum Entfernen eines Containers durchgehen, um zu zeigen, dass der Zeiger pContainer tatsächlich als Zeiger auf eine Funktion zugegriffen wird.

  1. In Zeile 22 gibt es einen Aufruf der GetBaseLogRecord-API, die die Kernel-Adresse des Basisblocks + 0x70 zurückgibt (sie verschiebt den Dateizeiger am Header vorbei).
  2. In Zeile 23 wird eine Kopie des Ergebnisses (BaseRecord_1) initialisiert.
  3. In Zeile 41 wird eine weitere Kopie einer Variablen a4 als v10 initialisiert.
  4. In Zeile 50: Es gibt eine interessante Vektorindizierungskonstruktion: LODWORD(a4) = *( (_DWORD*)BaseLogRecord_1 + StartingIndex_1 + 0xCA); Wenn nur ein einzelner Container zur Datei hinzugefügt wurde, dann ist StartingIndex_1 null. Der von GetBaseLogRecord zurückgegebene Wert wird in einen DWORD(32bit)-Vektor umgewandelt und um 0xCA Elemente indiziert. Beachten Sie, dass (_DWORD*)BaseLogRecord_1 + 0 + 0xCA nicht einfach den Wert 0xCA zu BaseLogRecord_1 addiert. Aufgrund der Umwandlung entspricht dies BaseLogRecord_1[0xCA]. Dies spiegelt sich im entsprechenden disassemblierten Code wider: mov eax, [rdi+r12*4+328h] wobei rdi die Basis ist, r12 ist StartingIndex_1 (beachten Sie, dass es mit 4 multipliziert wird, was der Länge eines 32-Bit-Elements (DWORD) entspricht) und mit 0x328 addiert wird, was 0xCA*0x4 ist.

Der Wert von a4 ist letztendlich BaseBlock+0x70+0+328=(BaseBlock+0x398), was uns zum ersten Wert von RgConrainers führen würde (0x398+0x800=0xB98).

  1. In Zeile 76 wird v13 aus v10 (Kopie von a4) durch Umwandlung in einen QWORD-Vektor (Elementgröße 8) und Indizierung mit 3 Positionen gewonnen. Beachten Sie in Zeile 16 der Dekompilierung, dass a4 vom Typ _CLFS_CONTAINER_CONTEXT ist. v10 ist also das dritte Feld der Struktur, das pContainer entspricht.
  2. In den Zeilen 82 & 83 wird pContainer dereferenziert, mit 0x18 und 0x8 addiert, als Zeiger auf eine Funktion interpretiert und ausgeführt. (call cs:__guard_dispatch_icall_fptr ist tatsächlich ein Aufruf eines jmp eax-Befehls)

Dies beweist, dass wir durch Überschreiben des pConainter-Werts den Kontrollfluss des Treibers ändern und auf vom Angreifer kontrollierte Adressen lenken können.

Kernel-Spraying mit Objekten

Früher wurde erklärt, dass man manchmal einen Speicherbereich mit einem vorbestimmten Muster von Werten füllen muss, um die erfolgreiche Ausführung eines Exploits zu gewährleisten. Dies liegt daran, dass der Hacker nur eine Teilmenge der Variablen kontrollieren kann, die den Zustand des Programms zum Zeitpunkt der Ausnutzung ausmachen. Im einfachen Beispiel des Memory-Sprayings früher haben wir nur Werte verwendet, die in einem Vektor geschrieben wurden, um eine bestimmte Bedingung zu erfüllen.

Im realen Fall sind die Bedingungen komplizierter. Wir müssen Logdateien im Kernel-Speicher in einer bestimmten Reihenfolge mit einem bekannten Offset zwischen ihnen anordnen.

Lassen Sie uns zunächst viele Logdateien in einer Schleife erstellen und untersuchen, wie das Betriebssystem Speicher für sie zuweist. Das Experiment verwendet das folgende Muster:

  1. Identifizieren Sie standardmäßig auf dem System vorhandene CLFS-Dateien.
  2. Weisen Sie eine neue CLFS-Datei zu.
  3. Identifizieren Sie die Kernel-Adresse des Basisblocks der neuen Datei.
  4. Wiederholen Sie dies für ~50 Dateien.
  5. Untersuchen Sie die Adressen, an denen Dateien zugewiesen wurden.

Das folgende Codefragment erreicht dies:

page_study

Und das Ergebnis:

result_addresses

Ordnen wir die Adressen:

ordered_addresses

Durch die Untersuchung des Offsets zwischen den Adressen ergibt sich ein pseudomuster bezüglich der Zuweisungen: Es gibt kontinuierliche Zuweisungen, die einen konstanten Offset zwischen sich haben. Zum Beispiel von ffffd80faf444000 bis ffffd80faf4ee000 beträgt der Offset zwischen zwei aufeinanderfolgenden Zuweisungen 0x11000.

Diese Annahme ist entscheidend für die Funktionsweise des Exploits. Sie wird verwendet.

Eine weitere Annahme: Nehmen wir einige Seiten, die 0x11000 voneinander entfernt sind. Zum Beispiel: ffffd80faf444000,ffffd80faf455000,ffffd80faf466000,ffffd80faf477000,ffffd80faf488000,ffffd80faf499000. Alle Seiten entsprechen geöffneten CLFS-Dateien. Wenn wir eine Datei schließen, wird die Seite freigegeben und der Speicher an der jeweiligen Adresse wird frei. Dies würde im Speicher so aussehen (nehmen wir an, wir schließen die Datei, die der Seite bei ffffd80faf466000 entspricht): ffffd80faf444000,ffffd80faf455000,xxxxxxxxxxxxxxxx,ffffd80faf477000,ffffd80faf488000,

WENN wir die Datei erneut öffnen, wird das Betriebssystem mit hoher Wahrscheinlichkeit eine Seite an derselben Adresse (ffffd80faf466000) zuweisen, um das Loch zu füllen und den Speicher kontinuierlich zu machen. --> Dies ist ebenfalls entscheidend für die Ausführung des Exploits.

Schema des Exploits bisher

Nun führen wir ein Schema der Strategie ein, die wir anwenden werden, um sicherzustellen, dass wir den *pConainter-Zeiger mit einer Adresse überschreiben, die wir im Userspace kontrollieren.

Exploit-Schema

  1. Weisen Sie eine Reihe von CLFS-Dateien zu, was zu einem Speicherlayout wie diesem führt:

first_allocation

  1. Identifizieren Sie eine Sequenz (idealerweise maximal) von Dateien, die 0x11000 voneinander entfernt sind.

second_Allocation

Im vorherigen Bild start(aux1)+0x11000=start(A),start(A)+0x11000=start(B),start(B)+0x11000=start(aux2)

  1. Wir verwenden Logfile A, um den *pcontainer-Zeiger von Logfile B zu überschreiben, und lösen den Exploit durch Schließen von Logfile B mit einem speziellen Delete-after-Close-Code aus.

  2. Fügen Sie einen Log-Container zu Logfile B hinzu, um die Felder zuzuweisen und zu aktualisieren, die signalisieren, dass B eine korrekte Containerdatei hat. Wir können aux2 als Container zu B hinzufügen.

  3. Schließen Sie LogfileA, damit wir es auf der Festplatte bearbeiten können. Es ist wichtig, A und B in der Mitte einer maximalen Sequenz von 0x11000 voneinander entfernten Dateien zu wählen, um ein Loch im Speicher zu erzeugen, dessen Füllung das Betriebssystem priorisiert. Wenn es dies nicht tut, schlägt der Exploit fehl.

  4. Berechnen Sie den Hash von A neu, bearbeiten Sie sein Hash-Feld, um die Integrität zu wahren, und öffnen Sie A erneut und hoffen Sie, dass der Kernel es an derselben Adresse platziert, sonst schlägt der Exploit fehl.

  5. Rufen Sie AddLogContainer auf A mit einer normalen Logdatei auf, um das Überschreiben von B's *pContainer-Zeiger auszulösen.

  6. Löschen Sie die Datei B, sodass RemoveConainter aufgerufen wird und die Ausführung an B's *pContainer übergeben wird, der nun korrupt ist und auf vom Benutzer zugewiesenen Code zeigt.

Das folgende Bild zeigt die zuvor genannten Schritte.

steps_first

CODE - Erhalten eines korrekten Speicherlayouts

Wir beginnen damit, 50 CLFS-Dateien zuzuweisen. Nach jeder Zuweisung fragen wir alle CLFS-Seiten im Kernel-Speicher ab und erstellen eine Liste mit allen Adressen, die für jede Datei zugewiesen wurden. Als nächstes identifizieren wir 2 Dateien, die 0x11000 voneinander entfernt sind (A und B im Schema). (und speichern sie in den Variablen first, second)

allocate_clfs

Nachdem wir die Adresse identifiziert haben, suchen wir nach zwei Dateien, die 0x11000 voneinander entfernt sind: A->first, B->second.

find_tw_files

Als nächstes schließen wir A (first) und bearbeiten es auf der Festplatte (manipulieren seine Header) und öffnen es erneut.

malform_first_file_o

Bearbeiten der Header von A und Neuberechnung seines Hashs

Dieser Schritt erfordert eine weitere Erklärung, da wir einige genaue Werte berechnen müssen, die beim Auslösen des ausnutzbaren Codes in AllocSymbol zum Überschreiben von B's *pConainter-Zeiger führen.

Aus der früheren Analyse der Funktionen AllocSymbol und FindSymbol wissen wir, dass wir die Datei A so modifizieren müssen, dass sie den logischen Wert der IF-Bedingung auf FALSE setzt und einen Wert in die Variable v9 injiziert, der zu einem Schreibvorgang an der Position von B's *pContainer-Zeiger führt.

Zunächst: Auf welchen Wert sollte v9 gesetzt werden?

AllocSymbol berechnet v10 (Zieladresse des Schreibens) als: v10=BaseLogRecord + v9 + 0x1338. Dies wird im Kontext des Adressraums von A berechnet. BaseLogRecord ist also: A's Kernel-Seitenadresse + 0x70. v9 wird vom Angreifer kontrolliert, und 0x1338 ist konstant.

Wo befindet sich B's *pContainer relativ zu seiner Kernel-Seitenadresse?

Dies wurde bereits früher detailliert beschrieben. Von B's Kernel-Seitenadresse müssen wir 0x398 hinzufügen, um zu B's regContainter-Vektor zu gelangen, und dann mit dem ersten Element indizieren, um den Offset zur ersten CONTEINER_CONTEXT-Struktur zu erhalten. Wie bereits erwähnt, wurde für den ersten Container der Index als 0x1468 (gezählt von B's BaseBlock +0x70) bestimmt.

Der Ort von B's *pContainer ist also B's Kernel_Address + 0x70 + 0x1468 + 0x18 (drittes Element in der CONTEINER_CONTEXT-Struktur)

B's Kernel_Address = A's Kernel_Address + 0x11000 (da wir den Speicher auf diese Weise konstruiert haben)

A's BaseRecordAddress = A's Kernel_Address + 0x70

V10 = A's Kernel_Address + 0x70 + v9 + 0x1338

V10 muss B's *pContainer überschreiben.

V10 muss also sein: v10 = B's Kernel_Address + 0x70 + 0x1468 + 0x18 (drittes Element in der CONTEINER_CONTEXT-Struktur)

Ersetzen von B's Kernel_Address: v10 = A's Kernel_Address + 0x110000 + 0x70 + 0x1468 + 0x18

Vereinfachen von v10: A's Kernel_Address + 0x70 + v9 + 0x1338 = A's Kernel_Address + 0x11000 + 0x70 + 0x1468 + 0x18

Auflösen nach v9 = 0x11000 + 0x70 + 0x1468 + 0x18 - 0x1338 - 0x70 = 0x11148

Welche Felder in A müssen nun überschrieben werden? --> Es gibt 2 Kategorien von Feldern:

  1. Felder, um die IF-Bedingung in AllocSymbol dazu zu zwingen, FALSE zu ergeben.
  2. Feld, das v9 entspricht, um den genauen Abstand zu B's *pContainer zu berechnen.

Felder der Kategorie 1 werden nicht detailliert beschrieben. V9 entspricht dem Feld bei Offset 0x1b98 auf der Festplatte innerhalb der Datei A. Es werden keine weiteren Details dazu gegeben, warum diese Felder den Exploit auslösen; es liegt am Leser, sich darüber zu informieren, falls interessiert.

Hier sind die Werte, die geändert werden müssen:

disk_a_modification

Beachten Sie (Little Endian), dass der Wert 0x11149 anstelle von 0x11148 verwendet wird. Dies liegt daran, dass wir die höherwertigen Oktetts in *pContainer kontrollieren müssen. Das niederwertigste Oktett hat die Form X0 (10 oder 20 oder 30...). Wir können einen Memory-Spray an jedem dieser Werte durchführen, um diese Bedingung zu berücksichtigen.

Beachten Sie, dass v10 auch verwendet wird, um einen Speicherbereich der Länge 0xa0 mit 00 zu füllen (memset).

first_memset

Dann wird an derselben Adresse mit einer Konstanten überschrieben.

overwrite_constant

Beachten Sie den konstanten Wert der Form 0x30c1fdf006X0.

Dies geschieht, wenn wir einen Log-Container zu Datei A hinzufügen, aber wir haben den Prozess der Hash-Neuberechnung nach der Änderung der Felder von A noch nicht behandelt.

Die Ansicht der Logdatei auf der Festplatte unterscheidet sich etwas von der Ansicht im Speicher. Wir ändern nur den Inhalt des Basisblocks, der eine Länge von 0x7a00 hat und auf der Festplatte bei Dateioffset 0x800 beginnt. Der verwendete Hash-Algorithmus zum Hashen des Basisblocks ist CRC32. Das Feld, das den Wert des Hashs enthält, ist ebenfalls im Basisblock bei Offset 0x80c gespeichert.

Vorgehen zur Neuberechnung des Hashs:

  1. Setzen Sie den alten CRC32-Wert bei 0x80c auf Null.
  2. Bearbeiten Sie die Felder gemäß den fest codierten Werten und Offsets.
  3. Berechnen Sie CRC32 für den Basisblock ab 0x800 mit einer Länge von 0x7a00.
  4. Ersetzen Sie den genullten Wert durch den neuen CRC32-Wert.

Der vollständige Code, der für das Auslösen des Exploits verantwortlich ist:

Um die Codeausführung zum Userspace zu leiten, müssen wir:

  1. Das Speicherlayout der CLFS-Dateien A und B erhalten (bereits erklärt).
  2. A schließen, es auf der Festplatte mit manipulierten Headern bearbeiten und erneut öffnen (erklärt).
  3. Einen Log-Container zu B (im Code als second bezeichnet) hinzufügen, um Containerstrukturen zu initialisieren. Der Container muss eine normale Logdatei sein (eine der 50 zuvor zugewiesenen).
  4. Den Benutzerspeicher vorbereiten, der den geänderten Ausführungspfad darstellt (WIRD ALS NÄCHSTES ERKLÄRT).
  5. Einen Log-Container zu A (im Code als first bezeichnet) mit einer normalen Logdatei als Container hinzufügen. Das Hinzufügen eines Containers zur manipulierten Version von A löst ein Überschreiben von B's (second) *pConainter-Wert mit einem konstanten Wert der Form 0x30c1fdf006X0 aus.
  6. Setzen Sie B (second) so, dass es nach dem Schließen automatisch gelöscht wird, mit NtSetInformationFile. Es ist entscheidend, diese API zu verwenden, da das einfache Schließen des Handles nicht die RemoveContainer-API auslöst.

ntsetinfofile

  1. Schließen Sie B und lösen Sie die Umleitung aus.

Benutzerspeicher-Spray und Layout

Es gibt einige Bedingungen, die dem Benutzerspeicher auferlegt werden, der das Ziel für die Umleitung des Ausführungsflusses unter Verwendung von *pContainer ist. Wir können nicht einfach zufällig mit der Ausführung von Anweisungen aus dem Userspace beginnen, während sich das Programm im Kernel-Modus befindet.

Der Zweck dieses Abschnitts ist es, den Wert des System-Tokens im Userspace preiszugeben, ohne einen Systemabsturz (BSOD) zu verursachen.

Wie kann man von einer Adresse im Kernel lesen und das Ergebnis im Userspace speichern? Eine kurze Erinnerung an den Pipe-Abschnitt des Tutorials:

second_pipes

Hier haben wir:

  1. Eine Pipe (Kernel-Objekt) mit einer Größe zugewiesen, die groß genug ist, um innerhalb eines BigPools zugewiesen zu werden.
  2. Einige Informationen in den Pipe-Attributabschnitt geschrieben (die Alphabetzeichenkette BCDEFBBBB).
  3. Die Adresse des Pipe-Objekts im Userspace mit dem Tag-Feld der BigPool-Seiten erhalten.
  4. Die Pipe-Struktur im Kernel mit Windbg untersucht.
  5. Daten aus dem Kernel im Userspace mit NtFsControlFile PipeReadAttribute gelesen.

Die Idee, das Pipe-Objekt mit unserem Ziel, den System-Token-Wert zu erhalten, zu verknüpfen, besteht darin, NtFsControlFile PipeReadAttribute zu verwenden, um von der Adresse des System-Tokens zu lesen, anstatt vom Beginn des Puffers, der die Informationen enthält, die wir dem Kernel mit PipeWriteAttribute übergeben haben.

Dies bedeutet, dass wir den Wert des Zeigers an der Adresse PIPE_BEGIN+=0x20 modifizieren müssen, um die Adresse zu enthalten, an der sich der System-Token befindet.

Die Adresse ist bekannt, wurde in den früheren Phasen des Tutorials ermittelt, als wir die EPROCESS-Struktur lokalisiert und analysiert haben.

Hier müssen wir einen Mechanismus finden, der es uns ermöglicht, an einer festen Position in der Pipe-Struktur zu schreiben.

Hierfür verwenden wir die Code-Umleitung, die durch die Ausnutzung der CLFS-AddLogConainer-Funktion erreicht wird. Man könnte meinen, es reiche aus, einen Shellcode zu schreiben, der den rohen Austausch durchführt, aber (obwohl nicht getestet) würde das sicherlich nicht funktionieren. Dies liegt daran, dass der Treiber im Kernel-Kontext läuft und Code aus einem Benutzerprozessbereich ausführt.

Um diese Einschränkung zu umgehen, müssen wir Kernel-ROPs finden, die einen Schreibvorgang eines beliebigen Werts bewirken. Das heißt, wir müssen einige Kernel-Code-Stücke finden, die am Ende einer Funktion stehen und mit einer ret-Anweisung enden, und ihre Adressen als Ziele für die Umleitung bereitstellen. Auf diese Weise wird der Code immer noch vom Kernel ausgeführt.

Dies ist die Hauptidee, aber es ergeben sich Einschränkungen aus der Art und Weise, wie der CLFS-Treiber den Code in *pConainer aufruft:

mem_spray_user

Um ein brauchbares Speichermuster zu erstellen, müssen wir die Art und Weise untersuchen, wie die RemoveContiner-Funktion auf den Code von *pContainer zugreift.

Im vorherigen Bild heben wir die Codeabschnitte hervor, die *pContainer dereferenzieren und als Funktionszeiger verwenden.

RDI ist die Konstante, die durch die Ausnutzung von AllocSymbol in *pcontainer geschrieben wurde. Wie Sie sehen, ist die Konstante nicht vollständig fest; sie variiert im ersten Byte (die Form ist 6X0, X steht für alles).

mov rax, [rdi] dereferenziert den konstanten Wert. Um keinen ungültigen Lesevorgang (und BSOD) zu verursachen, müssen wir sicherstellen, dass rdi einen Zeiger auf eine gültige Adresse enthält. Dazu müssen wir den Benutzerspeicher von 0x30C1FDF00000 bis mindestens 0x30C1FDF006FF sprayen. Dies geschieht durch Zuweisen eines Speicherblocks mit VirtualAlloc. Wenn das System aus irgendeinem Grund keinen Speicher zuweisen kann, schlägt der Exploit fehl.

LPVOID dirtySpray = VirtualAlloc((LPVOID)0x000030c1fdf006d0, 0x1000000, 0x3000, 0x4);

Dann speichern wir für jeden möglichen Wert von X bei 0x30C1FDF000X0 bis 0x30C1FDF00FX0 einen anderen Wert, der einem gültigen Benutzerspeicher entspricht, und wählen einen beliebigen Wert, z.B. 0x5000000. Auf diese Weise stellen wir sicher, dass rax für jedes rdi der Form 0x30C1FDF006X0 gleich 0x5000000 ist.

Nach diesem Schritt sehen wir zwei weitere Dereferenzierungsvorgänge: mov rax, [rax+0x18] und mov rax ,[rax+0x8]

Um sicherzustellen, dass der Speicher an diesen Adressen noch konsistent ist, müssen wir bei 0x5000008 und 0x5000018 Zeiger auf Kernel-Funktionen (ROP1 und ROP2, werden später berechnet) speichern.

Dies ist der Algorithmus zur Erzeugung des Spray-Musters.

algo_spray_first_stage

Hinweis: Das Muster wird sich weiterentwickeln, wenn wir die ROPs einführen, da sie auch Parameter haben, die berücksichtigt werden müssen, aber dies ist das Minimum, um eine ungültige Speicheradressierung zu verhindern.

So sieht der gesprayte Speicher der ersten Stufe im Debugger aus (beobachten Sie, wo der Wert 0x5000000 platziert ist --> X0-ausgerichtet):

sprayed_mem_constant

So sieht der Speicher bei 0x5000000 aus (die ROPs, die noch im Detail beschrieben werden):

sprayed_mem_ROP

Und der Prozess der Code-Umleitung im Kernel-Debugger:

debugger_redir

Beachten Sie den Wert des RDI-Registers und den Befehlszeiger im Debugger. RDI wird einmal dereferenziert und ergibt 0x5000000, das in RAX gespeichert ist. [RAX+0x18] ist der zweite ROP, und weiter unten wird [RAX+0x8] die Adresse des ersten ROP sein.

Kernel-ROPs: Was sind sie, warum nützlich?

An diesem Punkt haben wir die folgenden Teile des Exploits, die wir miteinander verknüpfen müssen:

  1. Die Adresse des System-Tokens.
  2. Umleitung des Codes mittels CLFS-Exploit.
  3. Eine Pipe mit Attributobjekt.

Das Ziel an diesem Punkt ist es, die Code-Umleitung mit einem Code-Stück zu verknüpfen, das den Zeiger der Pipe auf den Attributpuffer mit der Adresse des System-Tokens überschreibt und dann mit PipeReadAttribute die Informationen im Userspace zurückzulesen.Wie zuvor erwähnt, muss der umgeleitete Code ebenfalls im Kernel sein. Außerdem haben wir nur zwei Funktionen zur Verfügung. Das Tutorial wird nicht behandeln, wie genau diese beiden Funktionen gefunden wurden, aber es gibt vermutlich eine Liste häufig verwendeter ROP-Kandidaten.

Wir werden zwei Funktionen untersuchen:

  1. SeSetAccessStateGenericMapping in ntoskrnl.exe, als zweites aufgerufen ([rax+0x8])
  2. ClfsEarlierLsn in CLFS.SYS, als erstes aufgerufen ([rax+0x18])

Analyse von ClfsEarlierLSn:

ealier

Die einzige Aufgabe dieser Funktion bei Aufruf mit falschen Parametern ist es, EDX auf 0xFFFFFFFF zu setzen und zurückzukehren. Warum dies benötigt wird, wird bei der Analyse des zweiten ROP klar.

Analyse von SeSetAccessStateGenericMapping:

sesetAccess

Dies muss Zeile für Zeile im Detail besprochen werden.

Eingaben: Beim Eintritt in diese Funktion:

  1. Der RAX-Wert spielt keine Rolle, da er in der ersten Zeile der Funktion überschrieben wird.
  2. RCX ist der Wert von RDI, also die Konstante 0x30C1FDF006X0.
  3. [RCX+0x48] hat die Form 0x30C1FDF00YX8, ist immer 0x8-ausgerichtet und kollidiert nicht mit 0x30C1FDF006X0, das 0-ausgerichtet ist und 0x5000000 enthält.

Ablauf des Funktionscodes:

  1. mov rax, [rcx+48h] dereferenziert 0x30C1FDF00YX8 und verschiebt den Wert in RAX.
  2. movups xmm0, xmmword ptr [rdx] verschiebt 16 Bytes von RDX in das XMM0-Register. Denken Sie daran, dass RDX von ClfsEarlierLSn hier den Wert 0xFFFFFFFF hat.
  3. movdqu xmmword ptr [rax+8], xmm0 verschiebt den Wert von XMM0 nach [RAX+0x8].

Im Wesentlichen wird hier aus einer Adresse gelesen und innerhalb des Kernels gespeichert. Es kann wie folgt interpretiert werden:

mem_move

Um also den Attributpuffer-Zeiger der Pipe mit einem Zeiger auf das System-Token zu überschreiben, müssen wir 16 Oktetts bei 0xFFFFFFFF allozieren und dort die Adresse des System-Tokens speichern. Dann müssen wir den Speicher bei 0x30C1FDF00YX8 wiederholt mit dem Wert der Zieladresse minus 0x8 besprühen. Das ist der Wert des Offsets zum Pipe-Attributpuffer minus 0x8. Also PIPE_KERNEL_PAGE_ADDRESS+0x20-0x8.

So wird dies im eigentlichen Code erreicht:

mem_pattern

Ein weiterer Aspekt, der nicht erklärt wurde: Wie erhält man Kernel-Adressen der ROPs im Userspace?

Es ist ein Trick. Zuerst kann man die Basisadresse jedes Kernel-Moduls mit NtQuerySystemInformation und den folgenden Parametern abrufen: NtQuerySystemInformation(SystemModuleInformation, (HANDLE)ModuleInfo, ModuleInfoSize, &retlen)

Dann filtert man nach dem ModuleInfo->Modules[i].Name-Wert, um die gewünschte Bibliothek zu finden. Dies liefert die Basisadresse im Kernel.

Anschließend nutzen wir die Tatsache aus, dass der Offset zwischen der Basisadresse und der Exportadresse konstant ist, unabhängig vom Adressraum, in dem die ausführbare Datei geladen ist.

Wir laden das Modul im Userspace (Sie können das tun) mit LoadLibrary und ermitteln die Adresse des Exports mit GetProcAddress. Berechnen Sie die Differenz zwischen Export und User-Basis und addieren Sie sie zur Kernel-Basis (ermittelt mit NtQuerySystemInformation). So erhalten wir im Userspace die Adresse des Exports, das im KernelSpace geladen ist.

Auslesen des System-Token-Werts

Sobald der Zeiger auf das Attribut korrumpiert und so gesetzt wurde, dass er auf die Position des System-Tokens zeigt, sollte ein Aufruf von MyNtFsControlFile mit den richtigen Parametern die Adresse auslesen und den System-Token-Wert im Userspace preisgeben.

read_token

Wie geht es weiter?

Mit dem korrekten Wert des System-Tokens müssen wir ihn, um den Exploit abzuschließen, anstelle unseres eigenen Prozesses schreiben. Das heißt, wir überschreiben das Token des Prozesses, der den Exploit ausführt, mit dem Wert des System-Tokens.

Die Schritte, die für diese Änderung erforderlich sind, beinhalten keine neuen Techniken oder Methoden, sondern basieren auf der Wiederverwendung von vorhandenem Code. Der Algorithmus zum Überschreiben unseres eigenen Token-Werts beinhaltet eine Schreiboperation im Kernel an einer bestimmten Adresse. Dies wird durch die ROP-Funktionen SeSetAccessStateGenericMapping und ClfsEarlierLSn erreicht. Dies bedeutet, dass WIR DEN CLFS-EXPLOIT EIN ZWEITES MAL AUSLÖSEN MÜSSEN. Das ist richtig, wir müssen die Container-Allokation und das Memory-Spraying ein zweites Mal durchführen und hoffen, dass das Betriebssystem nicht abstürzt.

Schritte zum Überschreiben unseres eigenen Token-Werts:

  1. Identifizieren, wo im Kernel unser eigenes Token gespeichert ist (ähnlich wie beim ersten Mal, als wir die Adresse für das System-Token identifiziert haben).
  2. Führen Sie das systemweite Memory-Spraying und das Objekt-Spraying mit CLFS-Dateien erneut durch.
  3. Setzen Sie im userspace-gesprayten Speicher die Pufferinhalte so, dass die ZIELADRESSE die Adresse unseres eigenen System-Tokens ist und die Quelladresse (bei Adresse 0xFFFFFFFF) der Wert des System-Tokens ist, der beim ersten Ausführen des Exploits ermittelt wurde.
  4. Lösen Sie den CLFS-Exploit ein zweites Mal aus.
  5. Starten Sie eine cmd.exe und überprüfen Sie die Berechtigungen.

Da wir beim zweiten Mal kein Auslesen aus dem Kernel benötigen, haben wir diesmal keine Verwendung für die Pipe.

Da die Schritte kein zusätzliches Wissen erfordern, kann das Tutorial hier mit einem finalen Beweis einer cmd.exe mit nt authority\system enden.

final_proof

Zusammenfassung

Der Schwerpunkt dieses Tutorials lag nicht auf den Interna der CVE selbst, sondern eher auf einer benutzerfreundlichen Beschreibung des Prozesses zur Erstellung eines Windows-Privileg-Escalation-Beispiels. Viele Exploits teilen sich dieselben Methoden und Bausteine wie Memory-Spraying oder das Arbeiten mit bestimmten Windows-Datenstrukturen. Und meistens klafft eine große Lücke zwischen theoretischem Windows-Interna-Wissen und dem effektiven Schreiben eines Algorithmus, der eine Schwachstelle ausnutzt.

Tool herunterladen
[[pContainer]+0x18]
[[pContainer]+0x8]
ffffd80faf499000