
Proof-of-Concept-Exploit für CVE-2022-37969, eine lokale Privilegieneskalation im Windows-Treiber für das Common Log File System. Demonstriert Heap-Spray, Token-Diebstahl und beliebiges Schreiben in den Kernel, um SYSTEM-Berechtigungen zu erlangen.
Autoren: Ricardo Narvaja & Daniel Kazimirow (Solid)
Nur zu Demonstrationszwecken. Der vollständige Exploit funktioniert auf anfälligen Windows 11 21H2-Systemen.
Funktionaler PoC basierend auf zuvor veröffentlichten Informationen von Zscaler
Siehe die Aufarbeitung Understanding the CVE-2022-37969 Windows Common Log File System Driver Local Privilege Escalation.
Exploit-Walkthrough:
Das hier verwendete Szenario war Windows 11 21H2 (OS Build 22000.918) clfs.sys v10.0.22000.918
Der erste Schritt besteht darin, eine Datei namens MyLog.blf im öffentlichen Ordner (%public%) zu erstellen, indem die Funktion CreateLogFile() verwendet wird:



Dann werden mehrere Protokolldateien mit zufälligen Namen in einer Schleife erstellt.
Und innerhalb der Schleife wird unsere Funktion getBigPoolInfo() aufgerufen:

Es wird NtQuerySystemInformation() mit 0x42 (66 dezimal) als erstem Argument aufgerufen; dies gibt in v5 die Informationen über die im bigpool vorgenommenen Zuordnungen zurück, deren Struktur vom Typ SYSTEM_BIGPOOL_INFORMATION ist.

Wir müssen diese Funktion zweimal aufrufen. Der erste Aufruf wird einen Fehler zurückgeben, aber uns die korrekte Größe des Puffers für den zweiten Aufruf liefern, um die gewünschten Informationen zu erhalten.

v5 empfängt die Informationen der SYSTEM_BIG_POOL_INFORMATION-Struktur.

Die Anzahl der Zuweisungen im bigpool wird im ersten Feld namens Count gespeichert; im zweiten Feld befindet sich ein Array von SYSTEM_BIGPOOL_ENTRY-Strukturen.

Dann durchsuchen wir alle Strukturen nach dem Tag „Clfs“ und der Größe 0x7a00.

Es speichert in einem Array namens kernelAddrArray die VirtualAddress, das erste Feld jeder Struktur, die das CLFS-Tag und die Größe 0x7a00 hat. Von nun an werden die Pools, die beide Bedingungen erfüllen, als „right pools“ bezeichnet.

Zusätzlich zum Speichern jedes right pool im Array speichert es den letzten gefundenen right pool im Inhalt der Variablen a2, die als Argument der Funktion verwendet wird.

Auf diese Weise zeigt a2 immer auf den zuletzt erstellten right pool mit CLFS-Tag und Größe 0x7a00.
Die Variable v26 speichert immer den zuvor gefundenen right pool, da sie vor dem Aufruf von getBigPoolinfo() gleich v24 ist (v26=v24), aber v24 wird beim Verlassen dieses Aufrufs mit dem letzten gefundenen right pool aktualisiert, und v26 bleibt mit dem zuvor gefundenen right pool.

Dann subtrahiert es beide Adressen, und falls das Ergebnis negativ ist, werden die Operanden umgekehrt, sodass es immer positiv ist.

Auf diese Weise speichert v32 die Differenz zwischen den VirtualAddresses der letzten beiden gefundenen right pools.
Dann macht es etwas Ähnliches; in diesem Fall ist v23 anfänglich null, also setzt es v23 = v32 beim ersten Mal.

Beim nächsten Durchlauf der Schleife hat v23 immer noch denselben Wert und ist nicht null, also bricht es ab und geht hierhin.

V32 hat die letzte Differenz und v23 die vorherige; wenn sie gleich sind, wird ausgegeben und um eins erhöht, aber der Zähler wird auf null zurückgesetzt.
Die Idee ist, 6 aufeinanderfolgende Vergleiche von CLFS-Tags und Größe 0x7a00 zu finden, deren Differenzen gleich sind, und diese Differenz wird 0x11000 sein. Wir werden sehen, dass bei der Ausführung, wenn 6 (da bei null begonnen wird) aufeinanderfolgende mit gleichen Abständen gefunden werden, dieser Wert der Differenz zwischen ihnen ausgegeben wird.


Dort sehen wir, dass er 6 aufeinanderfolgende gefunden hat und die Schleife zum Erstellen der Protokolldateien verlassen hat.
Im Ordner „public“ können wir die erstellten Dateien sehen

Unsere Funktion craftFile() öffnet die ursprüngliche Datei (MyLog.blf) und modifiziert sie, um den Fehler auszulösen.

Nach dem Ändern der Datei ist es notwendig, die CRC32 zu ändern, da sonst ein Fehler wegen einer beschädigten Datei auftritt.
Dieser Wert befindet sich an Offset 0x80C der Datei.

Als Nächstes wird ein Heap-Spray mit der Funktion VirtualAlloc() durchgeführt, um Speicher an den willkürlichen Adressen 0x10000 und 0x5000000 zu reservieren, und in der zweiten Zuweisung (0x10000) wird der Wert 0x5000000 alle 0x10 Bytes gespeichert.

Es verwendet CreatePipe(), um eine anonyme Pipe zu erstellen, und ruft NtFsControlFile() mit dem Argument 0x11003c auf, um ein Attribut hinzuzufügen; später kann dieselbe Funktion mit dem Argument 0x110038 aufgerufen werden, um es zu lesen.
Weitere Details zu dieser Methode finden Sie HIER

Dort sehen wir den Eingabepuffer, der das hinzugefügte Attribut ist; wenn wir NtFsControlFile() erneut mit dem Argument 0x11038 aufrufen, sollte dieser im Ausgang dasselbe Attribut zurückgeben.

Suche im Pool nach dem Tag des erstellten Attributs (NpAt)


Und wenn es gefunden wird, speichert es die VirtualAddress dieses Pools in v30.Pointer.
V30.pointer+24 zeigt auf AttributeValueSize im Kernel-Pool und speichert es in einem der Heap-Sprays, die wir zuvor erstellt haben.

Die Idee ist, auf diese Kernel-Adresse+8 zu schreiben, um den AttributeValue zu überschreiben.


Die PipeAttribute-Struktur hat als erstes Feld eine LIST_ENTRY mit einer Größe von 16 Bytes, dann einen Zeiger auf den Namen des Attributs mit einer Größe von 8 Bytes, und dann kommt bei 0x18 (24 dezimal) das Feld AttributeValueSize, das wir im Heap-Spray speichern.
Danach laden wir CLFS.sys und ntoskrnl im Benutzermodus und finden mit GetProcAddress() die Adressen der Funktionen ClfsEarlierLsn() und SeSetAccessStateGenericMapping().

Dann rufen wir die Funktion FindKernelModulesBase() auf, die die Kernel-Basis beider Module mit NtquerySystemInformation() findet, diesmal mit dem Argument SystemModuleInformation, um die Informationen aller Module zurückzugeben.

Auf diese Weise können wir den Offset jeder Funktion berechnen und sie dann im Kernel erhalten.

Die Funktion pipeArbitraryWrite() wird zweimal aufgerufen; es gibt ein Flag, das beim ersten Aufruf anfänglich null ist und beim zweiten Aufruf den Wert 1 hat; dies ändert die Werte des Heap-Sprays.

Beim ersten Aufruf werden an der Speicheradresse 0x5000000 die folgenden Werte abgelegt.

Denken Sie daran, dass dieser Wert zusätzlich zur Zuordnung in diese Richtung in unserem Heap-Spray gespeichert wird.

So sieht der Speicher nach dem ersten Aufruf aus, wie gesagt bei der Adresse um 0x5000000.

Und im Heap-Spray ab Speicher 0x10000 wird der Zeiger auf AttributeValueSize alle 0x10 Bytes gespeichert, zusätzlich zum Zeiger auf 0x5000000.

Diese Sequenz löst den Fehler aus:

CreateLogFile() wird erneut auf der erstellten Datei und auf einer anderen mit einem zufälligen Namen aufgerufen.
AddLogContainer() wird dann mit den Handles dieser Dateien aufgerufen.

NtSetinformationFile() wird aufgerufen, und die Handles werden geschlossen, wodurch der Zeiger beschädigt wird (wird später erklärt).

Das Heap-Spray verhindert an dieser Stelle einen BSOD:

Wenn wir dort einen Breakpoint setzen, sehen wir, dass der Zeiger beschädigt ist und auf unser Heap-Spray zeigt, mit dem wir die nächsten beiden Funktionsaufrufe der vtable steuern können.


RAX nimmt den Wert 0x5000000 an und springt zuerst zur Funktion an 0x5000000+18 und dann zu 0x5000000+8.


Also springt es zuerst zu fnClfsEarlierLns() und dann zu fnSeSetAccessStateGenericMapping().
Wir verfolgen ab dem Breakpoint und sehen, dass es CLFS!ClfsEarlierLsn() erreicht.

Diese Funktion wird speziell aufgerufen, weil sie bei der Rückkehr EDX auf 0xFFFFFFFF setzt.

An der Adresse 0xFFFFFFFF hatten wir das Ergebnis von SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000 gespeichert.

Wie erwähnt, ist der RDX-Wert bei der Rückkehr von CLFS!ClfsEarlierLsn() 0x00000000FFFFFFFF.

Wir kommen zur zweiten Funktion nt!SeSetAccessStateGenericMapping().

Diese Funktion ist nützlich, da RCX auf unser Heap-Spray zeigt und RDX den Wert 0xFFFFFFFF hat, dessen Inhalt wir kontrollieren.


Der Inhalt von RCX+0x48 enthält den Zeiger auf AttributeValueSize, der in v30.Pointer+24 gespeichert wurde.



Dieser Zeigerwert von AttributeValueSize wird nach RAX verschoben, dann wird der Inhalt der Adresse 0xFFFFFFFF gelesen, wo wir die Adresse von SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000 gespeichert hatten.

Dann überschreibt es in RAX+8 das nächste Feld, das AttributeValue() ist.


Natürlich würde der AttributeValue normalerweise im Kernel auf das von uns hinzugefügte Attribut zeigen.

Und jetzt überschreiben wir es mit einem Zeiger auf das Ergebnis von system _EPROCESS & 0xFFFFFFFFFFFFFFF00.
Das bedeutet, dass bei erneutem Aufruf der Funktion NtFsControlFile() mit dem Argument 0x110038 zum Lesen des Attributs, anstatt des „A“, auf das der AttributeValue-Zeiger zeigte, nun von _EPRROCESS & 0xFFFFFFFFFFFFFFFFF000 die angeforderte Anzahl von Bytes gelesen und im Ausgabepuffer zurückgegeben wird, sodass wir beim ersten Aufruf den Wert des SYSTEM TOKEN erhalten können.

v9b ist die Startadresse des Output-Buffers, in den der Inhalt des Ergebnisses von System EPROCESS & 0xFFFFFFFFFFFFFFF000 kopiert wurde.
Dazu addiert er v14, die letzten 3 Bytes des System- EPROCESS, und dann 0x4b8, den Offset von Token für diese Windows 11-Version; dann findet er den Inhalt dieser Adresse, der den Wert des System-Tokens gespeichert hat.



Denken Sie daran, dass die letzten 4 Bits geändert wurden; das ist nicht signifikant, also stimmt der Wert immer noch überein.
Beim zweiten Aufruf ist der Wert von Flag 1, da er am Ende des ersten Aufrufs erhöht wurde.

Dort sehen wir die Reihenfolge, in der die Werte gespeichert werden.

Die Adresse 0xFFFFFFFF mit dem Wert, den wir gerade vom System Process Token gefunden haben.


Und im Heap-Spray befindet sich der Wert der Token-Adresse meines Prozesses, von dem ich 8 subtrahiere. Dieser Wert plus acht wird als Ziel verwendet. Denken Sie daran, dass auf die Adresse geschrieben wird, auf die RAX+8 zeigt.


An der Speicheradresse ab 0x5000000.

Wir sehen auch, dass der Name eines anderen Containers verwendet wird, da der vorherige vom Systemprozess verwendet wird und nicht erneut geöffnet oder gelöscht werden kann.

Dann wird der Fehler ein zweites Mal auf dieselbe Weise ausgelöst wie beim ersten Versuch.

Es kommt erneut zu CLFS!ClfsEarlierLsn().

Setzt RDX auf 0xFFFFFFFF.

Dann kommt es zu nt!SeSetAccessStateGenericMapping().

Liest die Adresse des Tokens meines Prozesses minus 8, wohin geschrieben wird.

Dann liest es das SYSTEM TOKEN.

Und schreibt an die Adresse des Tokens meines Prozesses (es addiert 8) das System Token.

Und auf diese Weise hat mein Prozess das System-Token.

Sobald das Token geschrieben ist, starten wir einen Prozess, um die Berechtigungen zu überprüfen; in diesem Fall starten wir Notepad.exe.



Denken Sie daran, dass dieser PoC nur unter Windows 11 funktioniert; unter Windows 10 führt er zu einem BSOD, sodass Sie einige Änderungen vornehmen müssen, damit er korrekt funktioniert. Dies wird in diesem Blogpost nicht erklärt.
Analyse der Strukturen
Die Strukturen und der größte Teil der Dokumentation zum CLFS-Dateiformat haben wir der hervorragenden Arbeit von IONESCU über CLFS Internals entnommen.
Wir können sehen, dass in der Funktion ClfsBaseFilePersisted::LoadContainerQ eine Prüfung hinzugefügt wurde.

Die Werte, die eine Addition durchführen, gehören zur Struktur _CLFS_BASE_RECORD_HEADER.

Beachten Sie, dass der Base Block bei Offset 0x800 der Datei beginnt und bei Offset 0x71FF endet, wobei die ersten 0x70 Bytes dem Log Block Header entsprechen.
Als bewährte Methode können wir die _CLF_LOG_BLOCK_HEADER-Struktur in IDA hinzufügen
struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
char ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
};
Dann haben wir den Basis-Datensatz-Header (_CLFS_BASE_RECORD_HEADER), der am Offset 0x870 vom Dateianfang beginnt und 0x1338 Bytes lang ist.

Wenn Sie es in IDA importieren möchten, müssen Sie zuvor die folgenden Typen und fehlenden Strukturen hinzufügen
typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;
struct _CLFS_METADATA_RECORD_HEADER
{
ULONGLONG ullDumpCount;
};
Jetzt kann es hinzugefügt werden:
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[0x0b];
ULONGLONG rgContainerSymTbl[0x0b];
ULONGLONG rgSecuritySymTbl[0x0b];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[0x7c];
ULONG rgContainers[0x400];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

Nach Einbindung der Strukturen stellen wir fest, dass eine Addition zwischen der cbSymbolZone und der Adresse, wo der _CLFS_BASE_RECORD_HEADER endet, durchgeführt wird. (Start + 1338h)
Denken Sie daran, dass cbSymbolZone in der präparierten Logdatei von 0x000000F8 auf 0x0001114B geändert wurde.
(Offset 0x1b98 der Datei)
0x800(offset of the start of the Base Block) + 0x70 (logBlockHeader) + 0x1328 (cbsymbolZone)
0x800+0x70+0x1328 = 0x1b98
Präparierte cbsymbolZone in der Datei MyLog.blf:


Da der Patch in der Funktion CClfsBaseFilePersisted::LoadContainerQ liegt, müssen wir uns das CClfsBaseFilePersisted-Objekt ansehen.
Setzen Sie einen Haltepunkt in CLFS!CClfsBaseFilePersisted::LoadContainerQ; wenn CreateLogFile mit dem Handle der präparierten Datei aufgerufen wird, wird es anhalten.

Rufen Sie die Funktion CClfsBaseFile::GetBaseLogRecord auf, um die Adresse des Basis-Log-Datensatzes (_CLFS_BASE_RECORD_HEADER) zu erhalten.

RAX wird auf die Adresse von _CLFS_BASE_RECORD_HEADER zeigen.

Beachten Sie die _CLFS_BASE_RECORD_HEADER-Struktur im Speicher und das Feld cbsymbolZone 0x1328 Bytes voraus.


r14 speichert die Struktur, die dem „this“ entspricht, also CClfsBaseFilePersisted, da es das this der Funktion CClfsBaseFilePersisted::LoadContainerQ ist.

Die CClfsBaseFilePersisted-Struktur im Speicher:

Also erstellen wir eine Struktur mit der Länge 0x21c0, um seine Felder während des Reverse Engineerings zu vervollständigen (es ist eine undokumentierte Struktur); wir nennen sie struct_CClfsBaseFilePersisted.

Innerhalb der Funktion CClfsBaseFile::GetBaseLogRecord() wird der Zeiger auf _CLFS_BASE_RECORD_HEADER abgerufen. Und wir wissen, dass das „this“ in dieser Funktion die Struktur struct_CClfsBaseFilePersisted ist.

Liest zwei Felder (Offset 0x28 und 0x30)

Feld 0x28 ist ein Word und hat den Wert 6, daher ändern wir den Typ in word in der Struktur.



Vorerst benennen wir es in Konstante 6 (const_6) um.


Laut Dokumentation wäre 6 die Anzahl der Blöcke CLFS_METADATA_BLOCK_COUNT. Das Feld könnte auf diesen Wert verweisen.
Und dieser Zeiger befindet sich am Offset 0x30.

Beachten Sie, dass die dort angezeigte Größe den Header mit der Länge 0x10 einschließt.


Wenn die Funktion ExAllocatePoolWithTag aufgerufen wird, werden einige Bytes angefordert, aber der Header ist nicht enthalten, daher werden 0x90 Bytes (0xa0 – 0x10) im Aufruf angefordert.
Beim Durchsuchen nach dem Text +30h] fanden wir eine lange Liste von Anweisungen, die am Offset 0x30 schreiben. Wenn wir die Liste jedoch nach dem Objekttyp CClfsBaseFilePersisted filtern, bleiben nur wenige Ergebnisse übrig, und wir finden sofort, wo diese Größe mit demselben Tag alloziert wird. (Tipp: Funktionsnamen wie Create und Initialize sind immer die ersten, die man sich ansieht.)


Da wir den Namen noch nicht kennen, nennen wir es pool_0x90, eine weitere undokumentierte Struktur, und wir erstellen eine Struktur dieser Größe.


Das pool_0x90 im Speicher hat einen weiteren Zeiger an seinem eigenen Offset 0x30.

Dieser andere Zeiger zeigt auf den Basisblock in der Datei (Basisblock beginnt bei Offset 0x800).


Bild entnommen aus dem Zscaler-Blogpost:

Die Allozierung ist riesig, da sie den gesamten Basisblock enthält.



Also erstellen wir eine neue Struktur der Größe 0x7a00 und nennen sie BASE_BLOCK.

Die ersten 70 Bytes entsprechen, wie wir bereits wussten, _CLFS_LOG_BLOCK_HEADER und die folgenden 0x1338 _CLFS_BASE_RECORD_HEADER.

Indem wir also den Anfang des Basisblocks mit dem Offset zum nächsten Datensatz (0x70) addieren, erhalten wir den _CLFS_BASE_RECORD_HEADER.

Der _CLFS_BASE_RECORD_HEADER im Speicher.

Betrachtet man andere Methoden desselben CClfsBaseFilePersisted-Objekts, erhält man in CClfsBaseFilePersisted::AddContainer ebenfalls die Adresse von _CLFS_BASE_RECORD_HEADER mittels CClfsBaseFile::GetBaseLogRecord.

Als nächstes wird CClfsBaseFile::OffsetToAddr mit cbOffset aufgerufen, es erhält die Adresse von _CLFS_CONTAINER_CONTEXT und speichert cboffset im Array rgbcontainers, das sich am Offset 0x328 des _CLFS_BASE_RECORD_HEADER befindet.

Die Funktion CClfsBaseFile::OffsetToAddr wird verwendet, um Strukturadressen aus Offsets zu ermitteln.

An diesem Punkt ist der Container-Offset, der bei 0x328 gespeichert wird, noch 0, da wir noch keinen Container hinzugefügt haben.

Der PoC ruft CreateLogFile zweimal auf, das erste Mal mit der fehlerhaften Datei MyLog.blf und das zweite Mal mit der normalen Datei MyLogxxx.blf. Daher müssen wir das Debuggen an allen oben genannten Stellen zweimal anhalten und uns die Adressen der oben genannten Strukturen für beide Dateien im Editor notieren.

Spulen wir ein wenig vor zu CLFS!CClfsLogFcbPhysical::AllocContainer, indem wir dort einen Haltepunkt setzen und dorthin laufen.
Wenn AddLogContainer() im POC erreicht wird, stoppen wir am Haltepunkt.

Setzen wir auch einen Haltepunkt auf CClfsBaseFilePersisted::AddContainer+176, wo wir zuvor gesehen haben, dass der Offset und der Zeiger auf die _CLFS_CONTAINER_CONTEXT-Struktur gefunden werden.


Wenn der Debugger anhält, sehen wir, dass der Offset 0x1468 ist.

In RAX wird die Adresse der _CLFS_CONTAINER_CONTEXT-Struktur zurückgegeben.

Die Struktur ist noch leer, da der Container noch nicht hinzugefügt wurde.

Beachten Sie, dass der Wert SignatureOffset=0x50, den wir bei Offset 0x868 in die fehlerhafte Datei geschrieben haben, nach Abzug der 0x800 vom Beginn des Basisblocks in der _CLFS_LOG_BLOCK_HEADER-Struktur bei Offset 0x68 zu finden ist.


Wenn der PoC die Funktion AddLogContainer() mit der fehlerhaften Datei aufruft, steht an Offset 0x68 von _CLFS_LOG_BLOCK_HEADER anstelle des dort geschriebenen Werts 0x50 aktuell 0xFFFF0050 im Speicher.

Irgendwann wurde dieser Wert vom Programm geändert. Um zu sehen, wann das passiert, setzen wir bei der nächsten Ausführung einen Speicher-Haltepunkt auf Schreiben.
Der Offset wird bei r15 + 0x328 gespeichert (r15 zeigt auf die _CLFS_BASE_RECORD_HEADER-Struktur).


RBX speichert den Offset 0x1468.

Also befindet sich an der Adresse Basisblock + 0x70 + dem ermittelten Offset 0x1468 die Adresse des CLFS_CONTAINER_CONTEXT-Containers.

In der CLFS_CONTAINER_CONTEXT-Struktur am Offset 0x18 wird der pContainer-Zeiger gespeichert. Wir können einen Haltepunkt auf Schreiben setzen und sehen, wann er geschrieben wird.


Dies ist der Zeiger, den wir korrumpieren müssen, da die Funktion, in der sich die Schwachstelle befindet, zuerst den CLFS_CONTAINER_CONTEXT liest, dann in r15 verschiebt und anschließend den Wert von r15+18 liest, also diesen Zeiger, für den wir gerade den Haltepunkt auf Schreiben gesetzt haben.


Es speichert pContainer am Offset 0x1c0 der struct_CClfsBaseFilePersisted-Struktur.

Nach mehreren Unterbrechungen erreichen wir den Moment, in dem es korrumpiert wird. Die oberen Bytes der Zeigeradresse wurden von FFs auf Null geändert.

Dies geschieht, wenn der zweite AddLogContainer() der fehlerhaften Datei aufgerufen wird; der Zeiger des vorherigen MyLogxxx wird korrumpiert.
Das Problem tritt auf, weil der SignaturesOffset, der 0x50 sein sollte, jetzt 0xFFFF0050 ist, was einen Schreibzugriff außerhalb der Grenzen im folgenden memset ermöglicht.


Die Funktion memset() wird die darunterliegende _CLFS_CONTAINER_CONTEXT-Struktur korrumpieren; diese Struktur gehört zur MyLogxxx-Datei, da sie bei der Erstellung 0x11000 Bytes voneinander entfernt platziert wurden.
Auf diese Weise berechnet es genau, wo in die nächste Struktur geschrieben werden muss, und setzt die oberen Bytes des Zeigers auf Null, sodass er auf den User-Heap zeigt, wo der HeapSpray erstellt wurde.
Die Basisblockstruktur der fehlerhaften Datei liegt genau 0x11000 vor der der MyLogxxx-Datei.
Fehlerhaft:

MyLogxxx


RCX ist kleiner als RDX, da 0xFFFF0050 hinzugefügt wurde, anstatt 0x50, wie es sein sollte.

Und wir gelangen zur Funktion memset(), die 0xb0 Bytes mit Nullen füllt, wobei RCX auf die CLFS_CONTAINER_CONTEXT-Struktur der MyLogxxx-Datei zeigt, genauer gesagt auf die oberen fünf Bytes von pContainer.

Dieser Zeiger wird durch Überschreiben der ersten Bytes korrumpiert:

und zeigt so auf eine zuvor von uns durch HeapSpray kontrollierte Speicheradresse


Dann wird das Handle der MyLogxxx-Datei geschlossen, und es erreicht CClfsBaseFilePersisted::RemoveContainer; die Schwachstelle wird schließlich ausgelöst.

Jetzt, da wir mehr Informationen haben, stellen wir fest, dass hier Base_Block.LOG_BLOCK_HEADER.SignaturesOffset und Base_Block..LOG_BLOCK_HEADER.TotalSectorCount gelesen werden.
Im ersten Teil des Patches darf SignaturesOffset nicht größer als 0x7a00 sein; bei uns war es ursprünglich 0x50, wenn es mit einem Wert größer als 0x7a00 ankäme, würde es uns rauswerfen.

Wenn der PoC auf der gepatchten Maschine ausgeführt wird, vergleicht es 0x50 mit 0x7a00, und da es kleiner ist, fährt es fort.

Im folgenden Block wird die fehlerhafte cbSymbolZone zum Wert der Endadresse von _CLFS_BASE_RECORD_HEADER addiert, und diese Summe wird in result_1 gespeichert.

Dann wird die Adresse des Base_Block mit dem SignatureOffset-Wert addiert, der in einer normalen Datei 0x7980 ist.

Die maximale Adresse des base_block ist 0x7a00, jetzt ist die SymbolZone bis zu 0x80 vor der Grenze erlaubt.
Es wird in result_2 gespeichert; das wäre die maximale Grenze für die SymbolZone innerhalb des Basisblocks. Dann vergleicht es beide Ergebnisse: wenn das erste größer als das zweite ist, bedeutet das, dass es außerhalb der Grenzen liegt.


Offensichtlich wird der erste Wert größer sein als der zweite, und es wird nicht fortfahren, da die erste Summe von cbSymbolZone + Endadresse des _CLFS_BASE_RECORD_HEADER die Grenze (also result_2) überschreitet und zu einem „out of bounds“ führt.

Das Letzte, was wir herausfinden müssten, ist, wo der SignatureOffset-Wert von 0x50 zu 0xFFFF0050 wird.Also, fangen wir von vorne an, starten neu und stoppen bei CLFS!CClfsBaseFilePersisted::LoadContainerQ, wo der Wert im Speicher noch nicht geändert wurde und immer noch 0x50 beträgt.
Setzen Sie einen Zugriffs-Breakpoint beim Offset 0x68 in SignatureOffset.

Und nach mehreren Stopps erkennen wir den richtigen Moment, in dem der Wert geändert wird, in der ClfsEncodeBlockPrivate.

Diese Funktion ist nicht gepatcht, also könnte es sich um ein Verhalten handeln, das durch den niedrigen Wert von 0x50 und die Manipulation der restlichen Werte verursacht wird.
Unter den manipulierten Werten sehen wir den ccoffsetArray-Wert, dessen Name in der __CLFS_BASE_RECORD_HEADER**-Struktur rgClients ist und das Array von Offsets darstellt, die auf das Client Context-Objekt zeigen.
Das rgClients-Feld befindet sich beim Offset 0x138 (0x9a8-0x800-0x70) der _CLFS_BASE_RECORD_HEADER**-Struktur.


Im PoC ist dieser Wert fehlerhaft, sodass er auf ein gefälschtes Client-Context-Objekt namens FakeClientContext zeigt.


Dies ist die Client Context-Struktur __CLFS_CLIENT_CONTEXT**
struct _CLFS_CLIENT_CONTEXT
{
CLFS_NODE_ID cidNode;
CLFS_CLIENT_ID cidClient;
USHORT fAttributes;
ULONG cbFlushThreshold;
ULONG cShadowSectors;
ULONGLONG cbUndoCommitment;
LARGE_INTEGER llCreateTime;
LARGE_INTEGER llAccessTime;
LARGE_INTEGER llWriteTime;
CLFS_LSN lsnOwnerPage;
CLFS_LSN lsnArchiveTail;
CLFS_LSN lsnBase;
CLFS_LSN lsnLast;
CLFS_LSN lsnRestart;
CLFS_LSN lsnPhysicalBase;
CLFS_LSN lsnUnused1;
CLFS_LSN lsnUnused2;
CLFS_LOG_STATE eState;
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
};
Der eState-Wert befindet sich beim Offset 0x78 vom Anfang der Struktur, in der manipulierten Datei bei 0x23a0+0x78.


Dieser Wert zeigt den Status des Protokolls.
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
dieser Wert ist auf CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20 gesetzt.
Der andere fehlerhafte Wert ist fAttributes, der dem Satz von FILE_ATTRIBUTE-Flags entspricht, die mit der Basis-Protokolldatei verknüpft sind (wie System und Versteckt).


Da das Feld ein Byte früher bei 0xa beginnt und sich über zwei Bytes erstreckt, ist der Wert von fAttributes 0x100.


Schließlich gibt es den blocknameoffset-Wert, der auf den Offset 0x1bb8 zeigt; ich meine, durch Addition von 0x78 und 0x800 zeigt er auf den Offset 0x2428 der Datei.


Beachten Sie, dass der Offset zum Client Context 0x1b30 beträgt.

Der Client Context befindet sich also beim Offset 0x23a0.


Und nur 0x10 davor befindet sich der Wert, der blocknameoffset entspricht.


Welcher auf den String mit dem Namen zeigen würde.
Der letzte Wert ist der blockattributeoffset, der 0xC vor dem Client Context bei 0x2394 liegt.

Diese letzten beiden Werte gehören zu einer Struktur, die dem Client Context mit einer Länge von 0x30 Bytes vorausgeht, genannt _CLFSHASHSYM.
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;


Sie befinden sich bei 0x20 und 0x24 Bytes vom Anfang der _CLFSHASHSYM-Struktur, also ist in der _CLFSHASHSYM-Struktur der Wert namens blockNameOffset im PoC das cbSymName-Feld und der blockAttributteoffset ist das cbOffset-Feld.


Das sind die manipulierten Werte; jetzt müssen wir sehen, wie sie unseren SignaturesOffset von 0x50 auf 0xFFFF0050 ändern.
Werfen wir einen Blick auf die Funktion CClfsBaseFile::AcquireClientContext(), die den Client-Kontext zurückgeben sollte.

Sie ruft CClfsBaseFile::GetSymbol mit dem vierten Argument auf, das __CLFS_CLIENT_CONTEXT **** sein wird, wo der Zeiger auf den Client Context gespeichert wird.

Innerhalb der Funktion CClfsBaseFile::GetSymbol übergeben wir den manipulierten ccoffsetArray-Offset an CClfsBaseFile::OffsetToAddr und erhalten die Adresse des Client-Kontexts. Setzen wir dort einen Breakpoint, sodass er anhält, wenn die mit CreatelogFile erstellte Datei aufgerufen wird.

Dort wird mit dem manipulierten ccoffsetArray-Argument angehalten.


Die Funktion CClfsBaseFile::OffsetToAddr gibt den falschen Client Context zurück.

Und prüft, dass der Wert von cbOffset nicht Null ist, da sich 0xC vor der _CLFS_CLIENT_CONTEXT-Struktur befindet, die sich in RAX befindet.


Dann vergleicht es cbOffset mit dem ccoffsetArray (der sich in RSI befindet), sie müssen gleich sein, sonst erhalten wir einen Fehler.

Es prüft auch, dass cbSymName gleich cbOffset+0x88 ist, andernfalls erhalten wir ebenfalls einen Fehler.

Und schließlich vergleicht es das cidClient-Byte mit Null.

Wenn all diese Prüfungen erfolgreich sind, wird der Client Context gespeichert.

Die Ausgabe der Funktion r14 zeigt auf den Client Context.

Beim Verlassen von CClfsLogFcbPhysical::Initialize haben wir die Adresse von CLFS_CLIENT_CONTEXT.

Jetzt liest es den Wert von fAttributes (0x100).

Diese Funktion gehört zur Klasse CClfsLogFcbPhysical.


Die hier zugewiesen wurde und deren Größe 0x15d0 und deren Tag „ClfC“ ist.

Lassen Sie uns eine Struktur erstellen, um das zu speichern, was wir reversen; nennen wir sie: struct_CClfsLogFcbPhysical.

Beachten Sie, dass bei 0x2b0 die Adresse der CClfsBaseFilePersisted-Struktur gespeichert wird.

Nachdem viele Werte in der Struktur gespeichert wurden, gelangt es zu einem wichtigen Teil: Es testet eState mit 0x20.


Da der manipulierte Wert 0x20 war, gibt der Test 1 zurück.


Wir sehen, dass im Konstruktor in der vtable ist.

Es wird prüfen, ob die Datei multiplexed ist.

Es geht also den gewünschten Weg und erreicht CClfsLogFcbPhysical::ResetLog.


Mehrere Felder werden auf Null initialisiert, mit Ausnahme eines, das auf 0xFFFFFFFF00000000 initialisiert wird.

Hier holt es den Client Context.

Es speichert den Wert 0xFFFFFFFF00000000.



Es schreibt 0xFFFFFFFF an Offset 0x5c, was der höhere Teil von CLFS_LSN lsnRestart.ullOffset ist.



Jetzt führen wir die Funktion ClfsEncodeBlockPrivate() aus, die, wie wir zuvor gesehen haben, dafür verantwortlich ist, die 0x50 mit 0xFFFF0050 zu überschreiben.
Dort liest es den Wert von SignatureOffset = 0x50, der noch so ist, wie wir ihn in der manipulierten Datei gesetzt haben, und addiert ihn zum Start von CLFS_LOG_BLOCK_HEADER.

Dies ist eine Schleife, die 2 Bytes schreibt. Da der SignatureOffset statt auf einen korrekten Wert, der in einer normalen Datei ein hoher Wert ist, z.B. 0x3f8, wodurch weiter vorne geschrieben wird, hier im selben CLFS_LOG_BLOCK_HEADER schreibt.
Die Idee ist, das Schreibziel zu ändern, um zu versuchen, den SignatureOffset-Wert zu korrumpieren.
Normale Datei

An diesem Punkt beginnt es zu loopen und zwei Bytes zu schreiben.

Der Zähler muss den Wert 0x3d erreichen, um die Schleife zu verlassen.

RCX erhöht sich ab 0x200, wir sind bereits im dritten Zyklus und sein Wert ist 0x600.

In der Iteration 0xe ist RCX 0x1a00.


Das war, wo er die 0xFFFFFFFF000000 geschrieben hatte.


Es liest die letzten zwei Bytes FFFF.

Und es kopiert sie dann in R8.


Wie wir gesehen haben, ist dieser Wert kritisch, da er es ermöglicht, die Prüfung zu umgehen und außerhalb der Grenzen zu schreiben, um den pContainer-Zeiger der Datei zu korrumpieren, die dem memset() folgt, und Nullen oben zu schreiben und ihn auf unseren kontrollierten Speicher (HeapSpray) zeigen zu lassen.
In CClfsBaseFilePersisted::AllocSymbol wird die gleiche Summe, die das Ziel des memset ermittelt, also cbSymbolZone + Endadresse von CLFS_BASE_RECORD_HEADER, mit Base_block + 0xFFFF0050 verglichen, sodass auf beiden Seiten der Gleichung korrumpierte Werte vorliegen.
CbSymbolZone = 0x1114B
Es ist der manipulierte Wert, der zur Endadresse von CLFS_BASE_RECORD_HEADER addiert wird, wodurch er außerhalb der Grenzen schreibt, und das andere Element des Vergleichs, das die Adresse des Base Block + SignatureOffset sein sollte, bleibt SignatureOffset = 0xFFFF0050, was es dieser Prüfung ermöglicht, zu bestehen und außerhalb der Grenzen im memset() zu schreiben und die Spitze des Zeigers zu nullen, der auf unseren HeapSpray zeigen wird.

Da RCX kleiner als RDX ist.

Wie wir zuvor gesehen haben. (Werte können abweichen, da sie zu einer vorherigen Ausführung gehören.)
Es wird den Zeiger korrumpieren, indem es die höchsten Bytes auf 0 setzt.

Dadurch zeigt er auf einen Speicherbereich, den wir durch HeapSpray kontrollieren.


Wenn die Sicherheitslücke ausgelöst wird, gelangen wir zu CClfsBaseFilePersisted::RemoveContainer.

Dort befindet sich der bereits korrumpierte Zeiger und er kann, wie wir zuvor gesehen haben, ausgenutzt werden.

An diesem Punkt haben wir den Bug ausgenutzt; er führt zur Kontrolle der Funktionen, die das Lesen des SYSTEM-Tokens und das Schreiben in unseren eigenen Prozess ermöglichen, um die lokale Privilegienausweitung zu erreichen.
Wir hoffen, dass es nützlich ist. Bei Fragen können Sie uns unter [email protected] und [email protected] kontaktieren.
Viel Spaß!