
Technische Analyse und Proof-of-Concept-Exploit für CVE-2023-28252, eine Windows Common Log File System (CLFS)-Treiber-Privilegienausweitungsschwachstelle, die bei Nokoyawa-Ransomware-Angriffen verwendet wurde.
Seit Februar 2022 wurde eine neue Ransomware gemeldet, die offenbar eine Windows-0-Day-Schwachstelle ausnutzt, so die von Trend Micro durchgeführte Forschung.
Weitere Informationen zu dieser Ransomware finden Sie unter diesem Link.
Laut der Analyse von Kaspersky hat die Nokoyawa-Ransomware-Gruppe seit Juni 2022 andere Exploits eingesetzt, die den Common Log File System (CLFS)-Treiber angreifen, mit ähnlichen, aber unterschiedlichen Merkmalen, die alle mit einem einzigen Exploit-Entwickler verbunden sind.
Im April 2023, als Microsoft den Patch veröffentlichte, wurde die CVE-2023-28252 zugewiesen.
Zuvor, im Jahr 2022, wurde von uns ein ähnlicher Fehler in derselben Komponente untersucht und in diesem Blogbeitrag dokumentiert.
Um die Analyse durchführen zu können, ist es notwendig, das .blf-Dateiformat zu kennen, das von dem anfälligen Common Log File System-Treiber namens CLFS.sys verwaltet wird und sich im Treiberordner innerhalb von system32 befindet.
Weitere Informationen zu diesem Dateityp finden Sie unter den folgenden Links:
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe
Diese Analyse wurde für Windows 11 21H2, clfs.sys Version 10.0.22000.1574 durchgeführt, obwohl sie auch auf Windows 10 21H2, Windows 10 22H2, Windows 11 22H2 und Windows Server 2022 funktioniert.
In früheren Windows-Versionen müssen einige Werte angepasst werden, da sonst ein BSOD auftreten würde.
Microsoft Patch Tuesday April 2023.
Sie können die Treiberversion wie gezeigt überprüfen
Als die Schwachstelle im April 2023 veröffentlicht wurde, begann ich zusammen mit Esteban Kazimirow mit dem Reversing des CLFS.sys-Treibers. In diesem Fall war es jedoch sehr schwierig, allein durch die Analyse des Patches abzuleiten, wo der Fehler lag und wie er ausgelöst werden kann, da die Ausnutzung sehr komplex ist.
Später erschien ein Blogbeitrag, dessen Autor anhand einer Malware-Stichprobe einige Teile des von HexRays dekompilierten Codes sowie einige Informationen zeigte, die uns die Richtung vorgaben, in die die Ausnutzung gehen musste.
Offensichtlich waren die bereitgestellten Informationen nicht vollständig, aber ohne diese Hilfe wäre es unwahrscheinlich gewesen, dass wir den PoC und später einen funktionierenden Exploit hätten erstellen können.
Um das Verständnis zu erleichtern, werden wir zuerst erklären, wie der PoC aufgebaut wird, und dann die Schwachstellenanalyse durchführen.
Dieser Blogbeitrag enthält zwei Abschnitte:
Aufbau des PoC:
1- Beschaffen der benötigten Kernel-Adressen für die Ausnutzung
2- Vorbereiten des Pfads zum Erstellen der .blf-Dateien:
3- Erstellen der „trigger blf“-Datei mit der Funktion CreateLogFile()
4- Erstellen der „trigger blf“-Datei
5- Beschaffen der Kernel-Adresse des BASE BLOCK von trigger blf
6- Aufrufen von AddLogContainer mit dem Handle von trigger blf
7- Vorbereiten der spray blf-Dateien
8- Vorbereiten des Speichers für das Spray
9- Auslösen des Fehlers
Debugging:
1- Überprüfen des Memory Sprays
2- Betrachten des RecordOffset[12] von trigger blf
3- Betrachten des iFlushBlock-Werts in der spray blf-Datei
4- Warum wird von BLOCK 1 SHADOW statt von BLOCK 0 CONTROL gelesen?
5- Warum ist die Prüfsumme in den spray blf-Dateien gleich null?
6- Abschluss der Ausnutzung.
7- Der eigentliche Patch
Ich werde eine Funktion namens InitEnvironment erstellen, um einige notwendige Kernel-Adressen zu erhalten.
Holen der EPROCESS-Adresse meines Prozesses und Speichern in der Variablen g_EProcessAddress, dann die EPROCESS-Adresse des SYSTEM-Prozesses und Speichern in system_EPROCESS, dann die EHTREAD-Adresse des Hauptthreads meines Prozesses und Speichern in g_EThreadAddress und schließlich die Adresse des PREVIOUS MODE, die in dieser Version des PoC nicht verwendet wird.

Diese Methode ist bekannt: Die GetObjectKernelAddress-Funktion ruft NtQuerySystemInformation zweimal auf, mit dem ersten Argument SystemExtendedHandleInformation. Der erste Aufruf erfolgt mit einer falschen Größe und gibt einen Fehler zurück, aber auch die korrekte Größe, die im zweiten Aufruf verwendet wird, um die Informationen aller Handles zu erhalten. Dann wird in einer Schleife die Information jedes Handles durchlaufen und im Feld Object des richtigen handleinfo die gesuchte Adresse im Kernel gefunden.

Ich benötige auch die Kernel-Adressen der folgenden von CLFS.sys exportierten Funktionen:
• ClfsEarlierLsn
• ClfsMgmtDeregisterManagedClient
Und die von NTOSKRNL.exe exportierten Funktionen
• RtlClearBit/PoFxProcessorNotification
• SeSetAccessStateGenericMapping
Um diese Adressen zu erhalten, wird eine ähnliche Methode verwendet, wie um die Kernel-Basis beider Module zu erhalten, indem NtQuerySystemInformation zweimal aufgerufen wird, aber in diesem Fall ist das erste Argument SYSTEM_INFORMATION_CLASS (im PoC verwenden wir die Funktion FindKernelModulesBase zu diesem Zweck).
Dann wird CLFS.sys und NTOSKRNL.exe als normale Module im Benutzermodus durch Aufruf von LoadLibrary geladen, die Adressen im Benutzermodus mit GetProcAddress ermittelt, dann die Imagebase von jedem abgezogen, um den Offset der Funktion zu erhalten, und schließlich jeder Offset zu den entsprechenden Kernel-Basen addiert, um die Kernel-Adressen aller benötigten Funktionen zu erhalten.

Ich erstelle eine Funktion namens createInitialTriggerBlfFile, die eine .blf-Datei generiert und schreibt.
Der Pfad, der als Argument in CreateLogFile verwendet wird, unterscheidet sich von einem normalen Pfad. Zum Öffnen der Datei 1280.blf im Ordner C:\Users\Public müssen wir z.B. den Pfad LOG:C:\Users\Public\1280 angeben. Dieser wird in der Variablen stored_name_CreateLog gespeichert.
Ich mache dies mit wsprintfW(), da stored_env den Pfad C:\Users\Public speichert, der zuvor aus den Umgebungsvariablen abgerufen wurde. Dieser Zeichenfolge füge ich die Zeichenfolge LOG: und einen zufälligen Namen am Ende voran, ohne die Erweiterung .blf.

Dies wird der Pfad zu meiner anfänglichen Datei sein, die ich "trigger blf" nenne. Natürlich muss ich auch den normalen Pfad zu derselben Datei ohne das vorangestellte LOG: und mit der BLF-Erweiterung speichern, um sie mit CreateFile(), WriteFIle() wie jede andere Datei zu öffnen und zu ändern. Dieser Pfad wird z.B. C:\Users\Public\1280.blf sein und in der Variablen stored_name_fopen gespeichert.

Natürlich entsprechen beide Pfade derselben Datei, und ich muss je nach Situation den einen oder anderen verwenden.
Die CreateLogFile-Funktion erfüllt eine Funktion, die CreateFile() recht ähnlich ist (sie erstellt neue Dateien oder öffnet vorhandene Dateien und gibt deren Handle zurück), sogar einige Argumente sind ähnlich, aber CreateLogFile() arbeitet nur mit blf-Dateien.
Außerdem wird beim Öffnen einer vorhandenen Datei überprüft, ob das Format in Ordnung ist, selbst wenn jeder Block eine Prüfsumme hat und wenn diese nicht korrekt ist, wird ein Fehler zurückgegeben.
Ich werde zwei Arten von BLF-Dateien erstellen:
Die Trigger blf
Die Spray blf
Beides sind blf-Dateien, aber auf unterschiedliche Weise modifiziert.
Auf diese Weise erstellt der PoC zuerst die „trigger blf“-Datei mit CreateLogFile mit dem zuvor festgelegten Pfad, z.B. LOG:C:\Users\Public\1280, der in der Variablen stored_name_CreateLog gespeichert ist.
Das fünfte Argument fCreateDisposition kann wie bei CreateFileA() die folgenden Werte annehmen:

In diesem Fall werde ich das Argument OPEN_ALWAYS verwenden, sodass die Datei erstellt wird, wenn sie nicht existiert, und geöffnet wird, wenn sie existiert. Da die Datei noch nicht existiert, wird sie mit einem zufälligen Namen erstellt.
logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);
CreateLogFile() wird unsere „trigger blf“-Datei mit ihren 6 Blöcken und den entsprechenden Prüfsummen erstellen und das Handle zurückgeben, das in der Variablen logFile gespeichert wird.

Jeder Block hat ab dem in der linken Spalte angezeigten Offset einen Header, dessen Größe 0x70 Bytes beträgt.
So erstreckt sich z.B. der Header des CONTROL BLOCK von Offset 0x0 bis 0x70.

Alle Header aller Blöcke haben dieselbe Struktur namens _CLFS_LOG_BLOCK_HEADER.
Dies ist die Header-Struktur:

Bei Offset 0xC des Headers befindet sich die Prüfsumme. Da der CONTROL BLOCK bei Offset 0 beginnt, befindet sich die Prüfsumme bei Offset 0xC der Datei, und jeder Block hat seine Prüfsumme bei 0xC vom Anfang seines Blocks an.

Um die trigger blf-Datei zu ändern, muss ich sie als normale Datei entweder mit CreateFileA oder mit fopen öffnen und sie dann mit WriteFile bzw. fwrite ändern. Dies führe ich am Anfang der Funktion fun_prepare des PoC aus.
Denken Sie daran, dass der normale Pfad in der Variablen stored_name_fopen gespeichert ist, also verwende ich ihn, um die Datei mit wfopen_s (einer Variante von fopen, die Unicode-Zeichenfolgen unterstützt) zu öffnen.
Die Datei wird in der Funktion craftTriggerBlfFile geändert, die von fun_prepare aufgerufen wird.

Dann rufe ich fseek auf, um auf den zu ändernden Offset zu zeigen, und dann wird die Datei mit fwrite geändert.
Die an der „trigger blf“-Datei vorzunehmenden Änderungen sind wie folgt:
Nachdem diese Änderungen vorgenommen wurden, wird FixCRCFile aufgerufen, um die neue Prüfsumme zu berechnen und die Prüfsummen der ersten 4 Blöcke zu korrigieren. Die nächsten beiden Blöcke haben keine Änderungen, daher ist es nicht notwendig, ihre Prüfsummen neu zu berechnen.

Der CLFS.sys-Treiber liest die sechs Blöcke der Datei und führt zur Speicherung ihres Inhalts eine Zuweisung im Kernel-Pool durch.

Es gibt eine sehr wichtige Struktur der Größe 0x90, die im vorherigen Blogbeitrag zu CVE-2022-37969 durch Reversing einige Felder gefunden und als pool_0x90 bezeichnet wurde. Nach viel weiterem Reversing weiß ich jetzt, dass ihr richtiger Name m_rgBlocks ist, und während der Controller Speicher zuweist, um den Inhalt jedes Blocks aus der Datei zu kopieren, speichert er dort die Größe jedes Blocks, den Start-Offset und die Kernel-Adresse, an der er gespeichert wurde.

Es hat sechs CLFS_METADATA_BLOCK, die jeweils einem Block nach seiner Nummer entsprechen.
Jede Struktur CLFS_METADATA_BLOCK ist 0x18 Bytes lang. (0x18*6=0x90)
Bei Offset 0 befindet sich eine Union, aber zumindest in diesem Exploit wird nur das Feld pbImage verwendet, also vereinfacht wäre es:
Die Zuweisung dieser Struktur kann von zwei verschiedenen Stellen des CLFS.sys-Treibers aus erfolgen, je nachdem, ob eine neue Datei erstellt oder eine vorhandene geöffnet wird. Wenn eine neue Datei erstellt wird, weist der Treiber die 0x90 Bytes von CClfsBaseFilePersisted::CreateImage+28A zu, während bei einer vorhandenen Datei von CClfsBaseFilePersisted::ReadImage+6E zugewiesen wird.
Danach werde ich die Startadresse von Block 2 holen, die der trigger blf-Datei entspricht, dem sogenannten BASE BLOCK, der bei Offset 0x800 beginnt und eine Länge von 0x7a00 hat.

Innerhalb der fun_prepare-Funktion wird diese Adresse im Kernel mit diesem Code gefunden.

Zuerst findet die getBigPoolInfo-Funktion alle Zuweisungen im Pool, die das "Clfs"-Tag und eine Größe von 0x7a00 haben, und speichert sie in einem Array.
Danach wird die zuvor geänderte trigger blf-Datei erneut mit CreateLogFile und dem Argument OPEN_EXISTING geöffnet, sodass eine vorhandene Datei geöffnet wird. Dies führt zur Zuweisung ihres BASE BLOCK.
Wenn getBigPoolInfo erneut aufgerufen wird, gibt es einen neuen "Clfs"-Pool der Größe 0x7a00, und seine Adresse wird durch zweimaligen Aufruf von NtQuerySystemInformation abgerufen.
Die Adresse des BASE BLOCK der trigger blf-Datei wird in der Variablen CLFS_kernelAddrArray gespeichert.

Beachten Sie, dass die Funktion CreateLogFile() fehlschlägt, wenn die geänderte trigger blf-Datei nicht die korrekte Prüfsumme hat.
Der letzte Teil der fun_prepare-Funktion ruft die AddLogContainer-API mit dem Handle der trigger blf-Datei auf.

In der letzten Funktion des PoC namens to_trigger wird eine zweite Art von blf-Datei erstellt,
Ich nenne sie spray blf.
Diese Art von Datei wird verwendet, um einen Speicherbereich zu füllen (Spray), es werden 10 gleiche dieser Art benötigt, aber zunächst wird nur eine erstellt. 
Es werden drei Arrays erstellt, um die zufälligen Namen dieser Dateien zu speichern:
stored_log_arrays: speichert zehn neue Zufallsnamen von .blf-Dateien, die mit CreateLogFile verwendet werden.
stored_container_arrays: speichert Zufallsnamen, um zehn neue Container-Dateien zu erstellen.
stored_fopen_arrays: speichert die Log-Dateinamen des ersten Arrays (Variable stored_log_arrays), jedoch mit ihrem normalen Pfad (ohne die Zeichenfolge „LOG:“) und mit der Erweiterung .blf.

In jeder Iteration wird die blf-Datei mit CopyFileW kopiert, und die im Array gespeicherten Namen werden zugewiesen.
Die Funktion fun_trigger ruft craftSprayBlfFile auf, wo Änderungen an jeder Datei vorgenommen werden, und FixCRCFile korrigiert die CRCs.

Zusammenfassend habe ich 10 ähnliche Dateien (spray blf) mit Zufallsnamen erstellt, mit den folgenden Änderungen:

Die letzte Änderung besteht darin, den gesamten Block 0 (CONTROL BLOCK) zu Block 1 (CONTROL BLOCK SHADOW) zu kopieren.

Die Wirkung dieser Änderungen, zusammen mit denen an der trigger blf-Datei, wird später im Debugging-Kapitel erläutert.
Einige dieser Änderungen sind diejenigen, die die Schwachstelle verursachen, während andere nur notwendig sind, um die Treiberüberprüfungen zu umgehen.
An diesem Punkt sind die Dateien bereits erstellt und modifiziert, bereit, um das Spray durchzuführen. Wenn sie dann mit CreateLogFile geöffnet werden, werden sie in dem Speicherbereich platziert, den wir wünschen, wie später gezeigt wird.

In der to_trigger-Funktion wird ein Array mit 12 Elementen erstellt, das die Adresse des BASE BLOCK der trigger blf-Datei plus 0x30 enthält.
Dann wird in der fun_pipeSpray-Funktion der Speicher mit einem Spray von Pipes gefüllt. Darin gibt es eine Schleife, die CreatePipe aufruft und die Anzahl der Pipes erstellt, die als erstes Argument übergeben werden. Das zweite Argument ist ein Array, das die Handles aller erstellten Pipes speichert.

Innerhalb einer Schleife wird CreatePipe aufgerufen, um Lese-Schreib-Pipes zu erstellen.
Auf diese Weise werden zunächst 0x5000 Pipes erstellt und dann erneut aufgerufen, um weitere 0x4000 Pipes zu erstellen.
Dann wird WriteFile verwendet, um in die ersten 5000 Pipes das kürzlich erstellte Array mit den Adressen von BASE BLOCK + 0x30 der trigger blf-Datei zu schreiben.

Jetzt gibt es bereits einen kompakten Block im Speicher. Es werden 0x667 Pipes von Nummer 0x2000 bis 0x2667 freigegeben, da die Pipes im Speicher nicht in derselben Reihenfolge erstellt wurden. Was passieren wird, ist, dass in diesem Speicherblock freie Stellen entstehen.
Beachten Sie, dass die Zuweisungen der Pipes eine Benutzergröße von 0x90 Bytes haben. Wenn sie also freigegeben werden, haben wir
Es gibt die Speicherbereiche der Größe 0x90 zwischen dem mit Pipes gefüllten Speicher frei.
Dann wird in einer Schleife CreateLogFile mit den 10 spray blf-Dateien aufgerufen.
Wenn CreateLogFile zum Öffnen vorhandener Dateien aufgerufen wird, wird die Zuweisung von 0x90 Bytes für die m_rgBlocks für jede spray blf-Datei durchgeführt, sodass diese Zuweisungen die Lücken füllen, die beim Freigeben der Pipes entstanden sind, da sie die gleiche Größe haben.

Dann wird der Vorgang wiederholt: In die letzten 0x4000 Pipes wird das Array mit der Adresse von BASE BLOCK + 0x30 von trigger blf geschrieben.
All diese Manipulationen erzeugen einen kontrollierten Speicherbereich. Ich werde Ihnen zeigen, wie er aussieht, wenn er debuggt wird, aber die Idee ist, dass die m_rgBlocks jeder spray blf-Datei die 0x90-Byte-Lücken besetzen, die freigegeben wurden.
Dann wird im letzten Teil der Fehler innerhalb einer while(1)-Schleife durch einen Aufruf von AddLogContainer auf die spray blf-Dateien ausgelöst.
Innerhalb dieser While-Schleife wird der Bug ausgelöst:

Diese While-Schleife wird beendet, sobald sie das System-Token findet, unter Verwendung der NtFsControlFile-Funktion, die die Attribute der Pipes liest.

Dann überschreibt CreateLogFile erneut das Token unseres Prozesses mit dem gerade gefundenen System-Token, und auf diese Weise erreichen wir die Privilegienausweitung.

Anschließend werden einige Werte wiederhergestellt, die Handles der Pipes und der blf-Dateien geschlossen, und ein Notepad als System ausgeführt, um zu überprüfen, ob wir die Erhöhung korrekt durchgeführt haben.

Beachten Sie die im PUBLIC-Ordner erstellten blf-Dateien. Denken Sie daran, dass Sie für einen weiteren Versuch zuerst die erstellten Dateien löschen müssen. Einige werden gesperrt sein und können nicht gelöscht werden, aber der PoC funktioniert trotzdem.

Bevor ich mit der Wirkung der Änderungen an trigger blf und spray blf-Dateien zur Durchführung der Ausnutzung beginne, muss ich überprüfen, ob sich m_rgBlocks der spray blf-Dateien in Löchern befinden, die in der Speicherverteilung nach dem Pipe-Spray und der anschließenden Freigabe einer festgelegten Anzahl von Pipes entstehen.
Wenn dieser Vorgang beendet ist, sollte sich eine Pipe unter den 0x90 Bytes von m_rgBlocks befinden, sodass bei Verwendung von m_rgBlocks ein OUT OF BOUNDS auftritt und von der darunter liegenden Pipe gelesen wird.
Der PoC bietet eine ideale Stelle zum Setzen eines Breakpoints:

An diesem Punkt ist das Öffnen der spray blf-Dateien abgeschlossen, die Funktion AddLogContainer wurde noch nicht aufgerufen.
Zum Debuggen im Benutzermodus verwende ich x64dbg und für den Kernel-Modus IDA mit dem Windbg-Plugin.

An diesem Punkt sollte der Speicher bereits vorbereitet sein, und ich kann die Verteilung sehen.
Ich pausiere IDA, um eine interessante Stelle zum Setzen eines Breakpoints zu finden.
Ich setze einen Breakpoint bei CClfsBaseFilePersisted::AddContainer, der von AddLogContainer aufgerufen wird, und am Anfang zeigt das RCX-Register auf die CClfsBaseFilePersisted-Struktur und an Offset 0x30 befindet sich ein Zeiger auf m_rgBlocks.

Wenn der Breakpoint erreicht wird, überprüfe ich im Call-Stack, ob AddLogContainer von meinem PoC aus aufgerufen wird.

Das RCX-Register zeigt auf:

Das erste Feld ist der Zeiger auf eine vtable (CLFS! CClfsBaseFilePersisted::'vftable'), und an Offset 0x30 befindet sich der Zeiger auf m_rgBlocks.

Die Blöcke 0, 1, 4 und 5 haben pbImage noch nicht gespeichert, während die Blöcke 2 (BASE BLOCK) und 3 (SHADOW BLOCK) dies getan haben.
Jeder Block in der m_rgBlocks-Tabelle hat sein cbOffset, das der Offset ist, an dem der Block in der Datei beginnt, cbImage ist die Blockgröße und eBlockType ist der Blocktyp.
Wenn das Spray korrekt ist, sollte sich unter m_rgBlocks eine Pipe befinden, und darin die Zeiger auf BASE BLOCK + 0x30 der trigger blf.

Der Befehl "!pool" in Windbg zeigt die Speicherverteilung:

Jeder m_rgBlocks hat ein „Clfs"-Tag und seine Größe ist 0xa0, da es sich um die 0x90 Benutzergröße plus 0x10 Header handelt. Darunter befindet sich eine Pipe mit dem „NpFr“-Tag, die die gleiche 0x90 Benutzergröße + 0x10 Header hat.
Da die Verteilung keine exakte Wissenschaft ist, wurden einige „Clfs“ kontinuierlich platziert, was unerwünscht ist, aber derjenige, mit dem ich arbeite, ist korrekt platziert, gefolgt von einer Pipe.
Eine der ersten Änderungen, die wirkt, ist die in der trigger blf-Datei bei Offset 0x858, wo der Wert 0x369 gespeichert ist.

Der BASE BLOCK beginnt bei Offset 0x800 in der Datei.

Innerhalb des _CLFS_LOG_BLOCK_HEADER bei Offset 0x800+0x58 (0x58 ab Beginn des BASE BLOCK-Headers).

Bei Offset 0x28 beginnt das Array RecordOffsets (DWORD).
Wenn Sie sich 0x30 Bytes vorwärts bewegen, befindet sich bei Offset 0x58 (0x828+0x30=0x858 ab Anfang) Feld 12 der RecordOffsets.

Ich führe den PoC aus, um CreateLogFile wie im folgenden Bild gezeigt aufzurufen:


Bevor ich in CreateLogFile eintrete, werde ich einen Breakpoint an einer Stelle setzen, an der der Wert 0x369 noch nicht verwendet wurde.
In einem Fall, in dem CreateLogFile eine vorhandene Datei öffnet, wird die m_rgBlocks-Struktur hier zugewiesen:
CClfsBaseFilePersisted::ReadImage+6E
Also setze ich in IDA einen Breakpoint genau hier:

Wenn der Breakpoint ausgelöst wird**:**

In m_rgBlocks befindet sich noch Garbage, da es noch nicht initialisiert ist, aber sobald pbImage von Block 2 zugewiesen wird, wird die Adresse an Offset 0x30 vom Anfang gespeichert, da das erste Feld innerhalb jedes CLFS_METADATA_BLOCK pbImage ist.


Jetzt setze ich einen Hardware-Breakpoint auf Schreiben: ba w1 ffffd003'7f5bea30
Nach der Initialisierung auf Null stoppt er, wenn pbImage gespeichert wird.

Die Analyse besagt, dass es Block0 entspricht, da die Konstante r14*8, die danach 0x30 beträgt, nicht berücksichtigt wird. Als Ergebnis wird tatsächlich das pbImage von Block 2 geschrieben.

Beachten Sie, dass CClfsBaseFilePersisted::ReadMetadataBlock verwendet wird, um einen der Blöcke zuzuweisen, unter Verwendung der als Argument übergebenen Größe.

Setzen Sie nun einen Lese-/Schreib-Breakpoint bei 0x58 vom Basisblock, um zu sehen, wann der Wert 0x369 verwendet wird.
ba r1 FFFF978A'16ECF000+0x58

Wenn der Breakpoint erreicht wird, liest er den Wert 0x369, der sich bei RecordOffset[12] befindet, addiert ihn zu einem seltsamen Zeiger auf r14 und erhöht den Inhalt von RAX+r14.
Einige Zeilen weiter oben im Code hat ESI den Wert 0x13 und multipliziert mit 0x18, was der Größe jedes Blocks in m_rgBlocks entspricht.
WINDBG>? 0x18*0x13
Evaluate expression: 456 = 00000000'000001c8
Wenn ich den Wert von r8= 0x1c8, der größer als 0x90 ist, zur Anfangsadresse von m_rgBlocks addiere, wird es OUT OF BOUNDS lesen.


Unterhalb von m_rgBlocks befindet sich die Pipe mit dem Zeiger auf BASE BLOCK + 0x30, er liest diesen Zeiger, der strategisch innerhalb der Pipe platziert wurde.
Die aktuelle Position im Code wurde von der while(1)-Anweisung des Hauptmoduls aus aufgerufen.
In der spray blf-Datei habe ich strategisch den Wert 0x13 bei Offset 0x48a (iFlushBlock) platziert.

Bei Offset 0x8a der spray blf-Datei befindet sich der iFlushBlock von BLOCK 0, dessen Wert 4 ist, während Offset 0x48a zum iFlushBlock von BLOCK 1 gehört und dessen Wert 0x13 ist.

Jetzt muss ich herausfinden, warum iFlushBlock = 0x13 von BLOCK 1 anstatt iFlushBlock = 4 von BLOCK 0 gelesen wird.
Wenn ich zurückblicke, um herauszufinden, woher die 0x13 stammt, sehe ich im Call-Stack, dass WriteMetadataBlock von CClfsBaseFilePersisted::ExtendMetadataBlock+416 aufgerufen wird, dort ist das zweite iFlushBlock-Argument EDX=0x13, das von r9w stammt.


Ein paar Zeilen zuvor wurde CClfsBaseFile::GetControlRecord aufgerufen, um die Adresse von BLOCK 0 abzurufen. Vielleicht liegt das Problem hier, also starte ich neu und setze einen Breakpoint darauf.
GetControlRecord ruft CClfsBaseFile::AcquireMetadataBlock auf, der die m_rgBlocks-Tabelle mit der Adresse von Block 0 füllen sollte. Wenn ich über diese Funktion hinwegsteige, erhalte ich die Adresse von Block 1. Das Problem tritt also innerhalb von CClfsBaseFile::AcquireMetadataBlock auf.
Durch Hinzufügen von 0x8A zur abgerufenen Adresse kann ich bestätigen, dass der Wert 0x13, der zu BLOCK 1 gehört, vorhanden ist.

Ich werde neu starten und dort einen Breakpoint setzen:

CClfsBaseFile::GetControlRecord+27 call CClfsBaseFile::AcquireMetadataBlock

Das zweite an AcquireMetadataBlock übergebene Argument ist Null, es entspricht Block 0, es wird aus der Datei kopiert und seine Adresse in m_rgBlocks gespeichert
.
In der _CLFS_METADATA_BLOCK_TYPE-Blocktyp-Enumeration haben sie andere Namen als ich verwendet habe, aber es sind die gleichen 6 Blöcke.

Nachdem überprüft wurde, dass der Blocktyp kleiner als das Maximum m_cBlocks=6 ist, speichert er einen Referenz-Wert, um zu vermeiden, dass derselbe Block zweimal gelesen wird.

ReadMetadataBlock wird aufgerufen. Das Problem des Lesens von Block 1 anstelle von Block 0 würde innerhalb dieser Funktion liegen.

Wenn alles in Ordnung ist, weist es mit cbImage als Größe zu und speichert die Adresse im Feld Block 0-> pbImage in m_rgBlocks.

Der Befehl !pool zeigt das Tag und die Größe der Zuweisung an.

Ich habe also bereits die Adresse von pbImage von Block 0 in m_rgBlocks gespeichert. Ich muss also sehen, warum dort die Bytes von Block 1 anstelle der Bytes von Block 0 kopiert werden.
Ich gelange zu einem Aufruf von CClfsContainer::ReadSector, bei dem ein Zeiger auf eine Variable, die pbImage enthält, übergeben wird, um die Bytes zu schreiben.

Beachten Sie die Änderungen im Inhalt von pbImage, wenn Sie über ReadSector hinwegsteigen.

Durch Hinzufügen von 0x8a zu pbImage kann ich den Wert 4 finden, der korrekt ist, anstelle von 0x13. Das Problem muss also später auftreten.
Nach dem Aufruf von ClfsDecodeBlock wird ein Fehler 0x0C01A000A zurückgegeben.
CClfsBaseFilePersisted::ReadMetadataBlock+153 ruft ClfsDecodeBlock auf.
Nach diesem Fehler addiert es 1 zum Typ und ruft CClfsBaseFilePersisted::ReadMetadataBlock erneut auf, jedoch mit Typ 1, um Block 1 zu lesen.

In CClfsBaseFilePersisted::ReadMetadataBlock weist es ein neues pbImage zu und speichert es in m_rgBlocks für Block 1.

Blöcke 0 und 1 haben unterschiedliche Adressen. Wenn ich jetzt 0x8a zur Adresse von Block 1 hinzufüge, ist sein Wert 0x13.
Vielleicht verwendet es, da Block 0 einen Fehler zurückgab, Block 1 und gibt ihn an GetControlRecord als Control Block zurück.
Wie zuvor gezeigt, führt die Verwendung des Werts 0x13 anstelle von 4 dazu, dass die Grenzen von m_rgBlocks überschritten werden und die von mir kontrollierten Pipe-Spray-Werte gelesen werden.
Dann wird das pbImage von Block 0 freigegeben und der Zeiger von Block 1 nach Block 0 kopiert.

Es wäre notwendig, den Wert zu finden, der den Fehler 0x0C01A000A innerhalb von ClfsDecodeBlock verursacht.
Innerhalb von ClfsDecodeBlock ist die Checksumme des ersten Blocks Null. Dies ist der Fehler 0xC01A000A.

Vor dem Aufruf von AddLogContainer wurde beim Öffnen einer spray blf-Datei mit einem Hexadezimaleditor die Checksumme auf Null geändert.

Es hätte bereits vorher geändert werden müssen, als es mit CreateLogFile geöffnet wurde.

Aus irgendeinem Grund haben die spray blf-Dateien nach dem Verlassen von CreateLogFile eine Checksumme von Block 0 gleich 0 und geben ein gültiges Handle zurück. Sehen wir uns an, warum das passiert.
Ich halte an bei CreateLogFile, bevor ich eine Spray-BLF-Datei öffne.

Beachten Sie, dass die Spray-Dateien vor dem Aufruf von CreateLogFile die korrekte Checksumme in Block 0 haben, und nach Abschluss der Funktion ändert sich der Checksummenwert auf Null.

Also setze ich einen Breakpoint auf CClfsBaseFile::GetControlRecord, um hineinzuschauen.
Nach dem Durchlaufen von CClfsContainer::ReadSector ist die Checksumme nicht Null.
Vor dem Eintritt in die CRC32-Berechnung setzt es das Checksummen-Feld im Speicher auf Null, um die CRC zu berechnen, und das Ergebnis ist korrekt.


Dann überprüft es den Wert von eExtendState =2 und geht zu WriteMetadataBlock.

Hier ist die Checksumme im Speicher noch Null. Ich muss nur sehen, wann dieser Wert in die Datei geschrieben wird.

Es überprüft einige Werte, die in der blf spray-Datei präpariert sind, um zu CClfsBaseFilePersisted::ExtendMetadataBlock zu gelangen.

Nach einer Schleife zum Lesen von Blöcken, die noch nicht gelesen wurden, hat Block 0 weiterhin Checksumme = 0.

Ankunft bei WriteMetadataBlock.

Da ich laufe, bevor Block 0 durch 1 ersetzt wird, ist der iFlushBlock-Wert der blf spray-Datei immer noch 4, der korrekte Wert.

Jetzt arbeitet es mit Block 4, und es wird Block 4 in die Datei schreiben. Hier ist das Problem noch nicht.
Dann kommt es zu CClfsBaseFilePersisted::FlushControlRecord

Innerhalb erreicht es WriteMetadataBlock, aber mit Argument 0, um Block 0 in die Datei zu schreiben**.**

Dann gibt ClfsEncodeBlock den Fehler 0xC01A000A zurück, obwohl es die Datei mit dem schlechten Block 0 in CClfsContainer::WriteSector, direkt darunter, schreiben wird.

Die Variable var_54 speichert den Fehlerwert 0xC01A000A und wird vor dem Verlassen der Funktion überprüft.

Aber nach dem Aufruf von CClfsContainer::WriteSector, der keinen Fehler zurückgibt, wird der Inhalt von var_54 mit Null überschrieben.
Die Funktion gibt also Null ohne Fehler zurück, und sie arbeitet weiter, da CreateLogFile ein Handle anstelle eines Fehlerwerts zurückgibt.

Der Wert 0x13 in iFlushBlock führt dazu, dass es die Grenzen überschreitet (out of bounds), und es wird der Zeiger gelesen, der sich in den Pipes befindet und auf den Base Block + 30 von trigger blf zeigt.
Dann addiert es 0x28 zu diesem Zeiger ( 0x58 vom Anfang des Basisblocks des trigger blf), der den Wert 0x369 hat.


Die INC-Anweisung erhöht den Wert 0x14 um 1 und wiederholt dies 4 Mal, sodass 0x14 zu 0x18 wird.
WINDBG>db r14+369
ffffcb82'091e7397 14 00 00 00
Danach wird CreateLogFile aufgerufen und liest den Wert 0x1858.

GetSymbol prüft, ob der zuvor in trigger blf erstellte Fake-Block, auf den der Offset 0x1858 zeigt, die korrekten Werte enthält.

Wenn der Zeiger nicht mehrmals inkrementiert worden wäre, hätte er den ursprünglichen Wert 0x1458 und würde auf den richtigen Block zeigen.
Nach dem Verlassen von GetSymbol wird dieser Fake-Block hier verwendet.

Dann wird der Wert des Offsets 0x18 des Fake-Blocks gelesen, wo ich 0x05000000 platziert habe, und es wird zum Inhalt dessen gesprungen, was dort steht.
WINDBG>dps 0x5000000
00000000'05000000 00000000'05001000

Es liest den Inhalt von 0x05000000 und dessen 0x05001000 und dort befindet sich ClfsEarlierLsn.

Diese Funktion wird verwendet, um den Wert 0xFFFFFFFF in RDX zurückzugeben, obwohl dieser Wert beim ersten Mal nicht verwendet wird.
Der zweite Aufruf erfolgt hier, es ruft PoFxProcessorNotification auf, das sich bei 0x501000 +8 befand.

WINDBG>dps 00000000**'05001000**
00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn
00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

In dieser Funktion ist RCX = 0x05000000, es prüft, dass 0x40 Bytes später ungleich Null sein müssen.
WINDBG>dps rcx+40
00000000'05000040 00000000'05000000
Die Adresse, zu der gesprungen wird, ist 0x68 später.
WINDBG>dps rcx+68
00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient
Und das Argument ist 0x48 Bytes später.
WINDBG>dps rcx+48
00000000'05000048 00000000'05000400
Die ClfsMgmtDeregisterManagedClient, es ist eine praktische Funktion, weil ich das Argument kontrollieren kann und ich auch zwei Sprünge zu von mir kontrollierten Funktionen habe.

Der erste Aufruf geht wieder an ClfsEarlierLsn, das RDX=0xFFFFFFFF zurückgab.


Es nimmt die Quelle, um daraus zu schreiben, aus dem Inhalt von RDX=0xFFFFFFFF.
WINDBG>dps rdx
00000000'ffffffff ffff8005'3a4ee000
An der Adresse 0xFFFFFFFF hatte ich den system_EPROCESS & 0xfffffffffffff000 gespeichert.

Das Ziel ist der Zeiger bei 0x5000400 +0x48
*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

Der PipeAttribute-Zeiger im Kernel, der auf einen mit „A“ gefüllten Puffer zeigt, wird mit dem hohen Teil des SYSTEM EPROCESS-Zeigers überschrieben.
Dieser Zeiger wurde erstellt, als ich zuvor _NtFsControlFile mit einem mit „A“ gefüllten Puffer aufgerufen habe.

Der Inhalt dieses Attributs kann mit NtFsControlFile gelesen werden.

Nun zeigt das Pipe-Attribut nicht mehr auf den Puffer mit „A“, sondern auf system_EPROCESS & 0xffffffffffffff000.

Dieser Code wird wiederholt, bis das System-Token abgerufen ist.


Unter Windows 11 befindet sich das System-Token bei Offset 0x4b8 der kürzlich gelesenen EPROCESS-Struktur.

Ich muss nur dieses System-Token in meinen Prozess schreiben, indem ich CreateLogFile aufrufe.

Um diese Aufgabe zu erledigen, wiederhole ich einfach den Schritt, der zum Lesen des System-Tokens verwendet wurde.

Beim doppelten Aufruf wird zuerst ClfsEarlierLsn aufgerufen, um 0xFFFFFFFF in RDX zurückzugeben, und dann wird nt_SeSetAccessStateGenericMapping aufgerufen.

Ich überprüfe, ob der von RDX referenzierte Wert das System-Token ist.

Das Token meines Prozesses ist:

Es wird dorthin schreiben.
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'f601c06c
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'ef841919
Jetzt ist mein Prozess System; ich kann einen Notepad ausführen, um dies zu überprüfen.



BINDIFF zeigt viele geänderte Funktionen.

Die anfällige Funktion befindet sich hier:


Die primäre ist die gepatchte Version, die sekundäre ist die anfällige Version.
Der Patch testet den Rückgabewert von CflsEncodeBlock, der 0xC01A000A ist, speichert ihn in der Variablen var_54, und da er negativ ist, wird er überprüft und vermeidet das WriteSector.
Der Patch schreibt nicht nur die Datei nicht, sondern die Funktion gibt auch korrekt 0xc01a000a zurück, womit CreateLogFile keinen Handle zurückgibt und die Ausnutzung nicht fortgesetzt werden kann.


Nur wenn ClfsDecodeBlock nicht negativ ist, geht es zu WriteSector, lässt aber den negativen Wert 0xC01A000A zurückgeben.
Dies ist der eigentliche Patch, der die Ausnutzung mit dem soeben beigefügten PoC tatsächlich verhindert.
An diesem Punkt haben wir erklärt, wie der Bug ausgenutzt wurde; er führt zur Kontrolle der Funktionen, die es uns ermöglichen, das SYSTEM-Token zu lesen und es in unseren eigenen Prozess zu schreiben, um die lokale Rechteausweitung zu erreichen. Sie finden den funktionalen PoC auf Fortras GitHub.
Wir hoffen, es ist nützlich für Sie; bei Fragen können Sie uns kontaktieren: