
Detaillierte technische Analyse und Proof-of-Concept-Exploit für CVE-2024-30051, einen Heap-basierten Pufferüberlauf in der Windows DWM Core Library, der eine lokale Privilegienausweitung auf Integrity System level ermöglicht.
In diesem Blogbeitrag erkläre ich eine Sicherheitslücke in der Microsoft Windows DWM Core-Bibliothek, die ich analysiert habe, als der Exploit für Core Impact entwickelt wurde. Sie ermöglicht es einem nicht privilegierten Angreifer, Code als DWM-Benutzer mit Integritätssystemrechten auszuführen (CVE-2024-30051).
Da zum Zeitpunkt der Entwicklung des Exploits nicht genügend öffentliche Informationen vorhanden waren, musste ich viel reverse engineering betreiben. Ich werde hier zeigen, wie das KB5037771-Patch für Windows 23H2 mit IDA PRO zurückentwickelt wird. Ich werde BINDIFF verwenden, um einen binären Vergleich zwischen dwmcore.dll Version 10.0.22621.3447 und Version 10.0.22621.3593 durchzuführen. Ich zeige, wie der Heap-Überlauf entsteht, und werde ihn dann durch Rechteausweitung ausnutzen und schließlich einen funktionsfähigen PoC erstellen.
Index:
[Windows DWM Core Library Sicherheitslücke bezüglich Rechteausweitung (CVE-2024-30051) 1](#windows-dwm-core-library-elevation-of-privilege-vulnerability-cve-2024-30051)
[Details zur Sicherheitslücke: 2](#vulnerability-details)
[Diffing, um den Fehler zu finden: 3](#diffing-to-find-the-bug)
[Analyse des PoC, der CVE-2024-30051 ausnutzt: 8](#analysis-of-the-poc-exploiting-cve-2024-30051)
[1)Initialisierung 8](#initialization)
[2)Hooking 8](#hooking)
[3)Erstellen des Fensters 16](#creating-the-window)
[4)Gerät erstellen 16](#create-device)
[5) Factory erstellen 22](#create-factory)
[6) Gerätekontext erstellen 28](#create-a-device-context)
[7)Kompositionsgerät erstellen 29](#create-a-composition-device)
[8)Aufrufen der hook3-Funktion 31](#calling-dcompositioncreatedevice-function)
[9)Ziel für HWND erstellen 32](#creating-a-target-for-handle-hwnd)
[10)Oberfläche erstellen 33](#creating-surface)
[11)Aufrufen von BeginDraw, EndDraw und CreateVisual. 34](#calling-begindraw-enddraw-and-createvisual)
[11)Aufrufen von Visual SetContent 36](#calling-visual-setcontent)
[12)Objekte freigeben 38](#release-objects)
[13)Kompositionsgerät committen 38](#commit-composition-device)
[14)Aufrufen von hook2 39](#calling-hook2)
[15)Aufrufen von hook 39](#remember-that-the-vulnerable-function-can-be-reached-using-some-methods-of-the-cprimitivegroup-class.-at-this-point-it-creates-a-heap-then-hook2-captures-and-saves-the-corresponding-heaphandle.)
[16)Aufrufen von hook4 41](#calling-the-function-hook4)
[17)Durchführen von Heap Spray 49](#performing-heap-spray)
[18)Ändern des Basis-Chunks vor dem Senden. 51](#modifying-the-base-chunk-before-send)
[19)Debuggen des DWM-Prozesses 52](#debugging-the-dwm-process)
[20)Erhöhen der Privilegien auf Integrity System-Ebene 62](#elevating-privileges-to-integrity-system-level)
Windows DWM Core Library Sicherheitslücke bezüglich Rechteausweitung CVE-2024-30051
Veröffentlicht: 14. Mai 2024
Zuweisende CNA: Microsoft CVE-2024-30051
Auswirkung: Rechteausweitung
Maximaler Schweregrad: Important
Schwachstelle:
CWE-122: Heap-basierter Pufferüberlauf
CVSS: 3.1 7.8 / 7.2
Die Sicherheitslücke besteht aufgrund eines Größenberechnungsfehlers in einer Integer-Division innerhalb der wichtigsten Windows DWM-Bibliothek namens dwmcore.dll. Ein lokaler Benutzer kann einen Pufferüberlauf auf dem Heap in der Methode CCommandBuffer::Initialize in dwmcore.dll verursachen und beliebigen Code mit dem DWM-Benutzer mit Integritätssystemrechten ausführen. Der Exploit führt einen Heap Spray im DWM-Prozess durch, um den Speicher vorzubereiten, und erzeugt schließlich einen Heap-Überlauf in dwmcore.dll, der durch das Freigeben bestimmter Teile des Heap-Sprays ausgelöst wird.
Sobald der Exploit erfolgreich ist, lädt der DWM-Prozess unsere präparierte DLL, die unseren Code oder unsere ausführbare Datei (in unserem Fall eine CMD) als DWM-Benutzer mit Integritätssystemrechten ausführt.

Gehen wir diese Sicherheitslücke durch und sehen, wie sie uns erlaubt, als DWM-Benutzer mit Integrity Level SYSTEM zu laufen. Beachten Sie, dass dieser Benutzer nicht zur Administratorgruppe gehört und daher einige Rechteeinschränkungen hat.
Das Patch für Windows 11 23H2 kann heruntergeladen werden von:
https://www.catalog.update.microsoft.com/Search.aspx?q=KB5037771
windows11.0-kb5037771-x64_19a3f100fb8437d059d7ee2b879fe8e48a1bae42.msu
Die anfällige Version von dwmcore.dll ist: 10.0.22621.3447
Die gepatchte Version von dwmcore.dll ist: 10.0.22621.3593
Bei der Analyse der geänderten Funktionen wird deutlich, dass die gepatchte Version von CCommandBuffer::Initialize viele hinzugefügte Blöcke hat, was sie ganz anders aussehen lässt als die ungepatchte Version.

Nach statischem Reverse Engineering dieser Funktion gibt es zwei Aufrufe an CD2DSharedBuffer::GetBufferSize.
Der erste Aufruf erhält die Größe für die Zuweisung im new und der zweite Aufruf erhält dieselbe Größe für den memcpy.

Zunächst scheint alles korrekt zu sein. Allerdings führt es vor der Zuweisung einige Operationen mit der Größe durch.

Es erhält buffer_size und buffer_size2 durch Aufruf derselben CD2DSharedBuffer::GetBufferSize-Funktion, die beide denselben Wert zurückgibt. Aber im new führt es eine Voroperation durch, eine Integer-Division von buffer_size durch 0x90 und dann Multiplikation mit 0x90, während im memcpy das zurückgegebene buffer_size2 ohne Operation darauf verwendet wird.
Mit diesen Operationen habe ich festgestellt, dass die schließlich im new und im memcpy verwendete Größe unterschiedlich sein kann.
buffer_size = buffer_size2 (zurückgegebene Größen)
size_new= buffer_size/0x90 x 0x90
size_memcpy=buffer_size2
Zum Beispiel, wenn buffer_size 0x91 ist
buffer_size = buffer_size2=0x91
size_new= buffer_size/0x90 x 0x90 =0x90
size_memcpy= buffer_size2= 0x91
Dieses Beispiel beweist, dass es einen Heap-Überlauf gibt. Es werden mehr Bytes kopiert als zugewiesen, und die Größe ist kontrollierbar.
Zum Beispiel, wenn buffer_size 0x23f ist, wie im POC verwendet.
buffer_size = buffer_size2=0x23F
size_new= buffer_size/0x90 x 0x90 =0x1b0
size_memcpy== buffer_size2=0x23f
Nach der Analyse der anfälligen Funktion wollte ich sehen, wie man die anfällige Funktion CCommandBuffer::Initialize erreicht. Hier wird es kompliziert.
Wenn man die Referenzen auf diese Funktion zurückverfolgt, scheint sie von Methoden der CPrimitiveGroup-Klasse erreicht zu werden:

Solche Methoden können von der vftable von CPrimitiveGroup-Objekten
aus zugegriffen werden:
Es hat seinen Konstruktor:

Und es wird auf diese Weise erreicht:

Als ich diesen Prozess anfangs durchlief, nahm ich mir die Zeit, das PDF „The Lost World of DirectComposition: Hunting Windows Desktop Window Manager Bugs“ zu lesen und tauchte in die Welt von Direct Composition ein. Dies half mir, meinen ersten PoC zu erstellen.
Außerdem musste ich win32ksys zurückentwickeln und versuchte, Pakete über die Funktionen zu senden:
NtDCompositionCreateChannel
NtDCompositionProcessChannelBatchBuffer
NtDCompositionCommitChannel

Mein erster PoC erreichte den CPrimitiveGroup-Konstruktor. Allerdings fand ich nach viel Reverse Engineering keinen Weg, die Aufrufe der vftable-Methoden zu handhaben, um zur anfälligen Funktion direkt über ALPC-Aufrufe mit diesen Funktionen zu gelangen.
Ich verbrachte viel Zeit mit kompliziertem Reverse Engineering. Während dieses Prozesses fand ich das Sample der Malware, die die Sicherheitslücke ausnutzte, was immens hilfreich war, weil die Exploitation-Methode viel komplexer ist, als ich ursprünglich dachte. Sie enthält auch mehrere Hooks auf System-APIs und verwendet Methoden, die vielleicht etwas fragwürdig sind. Aber im Krieg und bei Exploits ist alles erlaubt, also begann ich, die Malware zu analysieren, und aus dieser Analyse erstellte ich meinen endgültigen PoC, der die Sicherheitslücke schließlich ausnutzt und den ich im Folgenden erklären werde.
Zuerst möchte ich klarstellen, dass die Malware nicht nur die CVE-2024-30051-Sicherheitslücke ausnutzt, die unseren Prozess auf Integrity System Level anhebt, sondern auch einen zweiten Teil durchführt, der von dort aus zu einem SYSTEM-Benutzer mit allen Rechten führt, was bereits über die erklärte CVE hinausgeht.
Außerdem ist es wichtig zu beachten, dass die Malware viel komplexer ist als mein PoC, der versucht, den Code zu minimieren. Die Malware führt viele weitere Prüfungen durch, um Zuverlässigkeit zu gewährleisten, und funktioniert deshalb beim ersten Versuch. Ich habe all diese Prüfungen verworfen, um zu vereinfachen, und mich der reinen Exploitation gewidmet, auch wenn der PoC vielleicht zwei- oder dreimal ausgeführt werden muss, um die Exploitation zu erreichen.
Der Link zum ausführbaren PoC ist https://github.com/fortra/CVE-2024-30051
Zuerst ruft der PoC GetVersion auf, um die Betriebssystemversion zu erhalten, auf der er läuft, und führt entsprechend verschiedene Initialisierungen einiger globaler Variablen durch. Mein PoC wurde auf Windows 11 23H2 und Windows 11 22H2 getestet. Andere Systeme sind ebenfalls anfällig, und ich habe die Werte hinzugefügt, um sie auszunutzen.
Es hookt vier Systemfunktionen, und ohne diese Hooks kann es die Exploitation nicht erreichen. Diese Systeme sind: RtlAllocateHeap, RtlCreateHeap, NtDCompositionCreateChannel und NtDCompositionCommitChannel.

In diesen Funktionen wird die ersten 5 Bytes gepatcht, um zum eigenen Code zu springen. Natürlich kann der Code nicht sehr weit entfernt sein, da ein 5-Byte-Sprung nicht den gesamten Speicher abdeckt und nahe sein muss.
Um dies zu erreichen, verwendet die Malware einen sehr langen Code, der die Speicherkarte analysiert, um zu entscheiden, wo er die Zuweisung seines eigenen Codes durchführen kann. Da der Code kompliziert ist, habe ich mich darauf konzentriert, zwei einfache Zeilen zu machen:
base_ntdll = GetModuleHandleW(L"ntdll.dll");
global4_ = (char *)VirtualAlloc((LPVOID)(base_ntdll-0x2000), 0x1000uLL, 0x3000u, 0x40u);
Ich habe von der ntdll base 0x2000 abgezogen und diese Adresse an
VirtualAlloc übergeben, um dort zuzuweisen.
64-Bit-DLLs werden im Speicher ziemlich getrennt voneinander mit leeren
Zwischenräumen abgebildet.
Sehen wir, wie die Hooks funktionieren:

Es ruft eine Hooking-Funktion auf, die das Hooking der
RtlAllocateHeap-API durchführt, die drei Argumente hat, das erste ist die
Adresse der zu patchenden API namens sym_RtlAllocateHeap.
Vor dem Patchen zeigt es auf den Start der API:

Hier ist die RtlAllocateHeap-Funktion:

Das zweite Argument ist die Routine namens hook, die ausgeführt wird, wenn die API vollständig gepatcht ist:


Die hook-Funktion ruft my_RtlAllocateHeap auf.
Die Hooking-Funktion patcht die ersten 5 Bytes der API, sodass sie zu hook springt.
Sie ruft den Code im zugewiesenen Bereich auf, wo sie die erste API-Instruktion ausführt, die mit den 5 Bytes übersprungen wurde, und springt dann zu RtlAllocateHeap+5 direkt nach den gepatchten Bytes:

So sieht die API nach dem Hook aus. Die ersten 5 Bytes wurden geändert, sodass sie zu hook springt. Es ruft my_RtlAllocateHeap auf, den Code, der sich direkt darüber befindet, der zum in Lila markierten Bereich zurückkehrt, um die Ausführung der API fortzusetzen:

Wenn die API die Ausführung beendet, kehrt sie zu hook zurück. Von dort vergleicht sie die globale Variable heap_base (die anfangs zero ist) mit dem ersten Argument, das an RtlAllocateHeap übergeben wurde:

Danach wartet der Code auf eine bestimmte spezielle Zuweisung, die einen spezifischen HeapHandle hat. Am Anfang ist diese Variable null, und solange sie null ist, wird sie übersprungen und arbeitet wie ein normales RtlAllocateheap:

Der Parameter HeapHandle wird innerhalb von RtlCreateHeap abgerufen, was zufällig die zweite gehookte API ist.
Wenn man nach Verweisen auf die globale Variable heap_base sucht, ändert sie ihren Wert nur in der hook2-Funktion, die nach dem Hooking von RtlCreateHeap ausgeführt wird:


Die Idee ist also, einen bestimmten HeapHandle zu erfassen und in heap_base zu speichern. Da er nun ungleich null ist, beginnt die Funktion hook, jede Zuweisung zu vergleichen. Der PoC speichert die Speicheradresse, die denselben HeapHandle hat wie der zuvor gespeicherte.
Wenn dies der Fall ist, speichert es die Richtung der Zuweisung in der
Variable namens base:
Diese ersten beiden Hooks sind nun verkettet. Wenn hook2 den erwarteten Wert von HeapHandle speichert, aktiviert es die hook-Funktion, die die Zuweisungsadresse speichert, die denselben HeapHandle verwendet.
Der dritte Hook wird an NtDCompositionCreateChannel übergeben. Beim ersten Aufruf speichert er die MappedAddress, die den Inhalt des dritten Arguments enthält. Von dort ändert er hooked_flag auf 1, sodass er von da an nicht mehr speichert und normal arbeitet.


Die in Variable base gespeicherte Adresse wird später dreimal gelesen. Zwei davon werden im letzten Hook, genannt hook4, vorkommen:

Die Funktion hook4 zu NtDCompositionCommitChannel wird später analysiert, weil sie recht komplex und sehr wichtig ist.
Nachdem die vier Hooks abgeschlossen sind, kehrt es zur Hauptfunktion zurück, um mit der Erstellung eines Fensters zu beginnen. Dies geschieht durch Aufruf von RegisterClassExW. Um jedoch eine Fensterklasse für die spätere Verwendung zu registrieren, sollte sie mit der Funktion CreateWindowExW aufgerufen werden.

Dies initialisiert die COM-Bibliothek durch Aufruf von CoInitializeEx, um vom aufrufenden Thread verwendet zu werden:

Es berechnet die erforderliche Größe des Fensterrechtecks basierend auf der gewünschten Größe:

Die Funktion CreateWindowExW wird aufgerufen, um ein Fenster zu erstellen, das gezeichnet wird:

Von dort ruft es D3D11CreateDevice auf, um ein Gerät oder DirectX-Gerät zu erstellen, das den Bildschirmadapter darstellt:


In meinem PoC ist ppDevice d3dDevice und ppInmediateContext ist d3dContext:

Das Argument flags muss auf 0x20 gesetzt werden:

Dann ruft es AddRef auf:

Dies erhöht den Referenzzähler für einen Schnittstellenzeiger auf ein COM-Objekt:


Der Wert 0x10 wird von THIS subtrahiert:

Im Offset 0xf8 von ID3D11Device-0x10 gibt es einen Zeiger auf TComObject:



Dies wird das neue THIS sein und springt zu TComObject::AddRef:

Und es endet damit, dass zum Objektzähler, der sich im Offset 8 von TComObject befindet, eins hinzugefügt wird:

Dann wird AddRef den Zähler des anderen Objekttyps erhöhen, der in D3D11CreateDevice erstellt wurde, nämlich ID3D11DeviceContext:

In diesem Fall subtrahiert es, um das neue THIS zu finden, 0x108:


Es springt hierher, wo sich im Offset 0x98 das neue THIS befindet:

Dies ist der Zähler. In diesem Beispiel ist es ein QWORD:

Der PoC ruft D2D1CreateFactory auf, um Direct2D zu verwenden, und um die ID2D1Factory-Schnittstelle zu erstellen, die zum Erstellen anderer Direct2D-Ressourcen verwendet wird, die zum Zeichnen oder Beschreiben von Formen verwendet werden können:

Das riid-Argument ist das von der Microsoft-Seite empfohlene:
https://learn.microsoft.com/en-us/windows/win32/api/d2d1/nf-d2d1-d2d1createfactory

Dies sind die von der Malware verwendeten:

Das richtige für ID2D1Factory kann hier gefunden werden**:**
https://github.com/apitrace/dxsdk/blob/master/Include/d2d1_1.h

Da ich kein Experte für Direct Composition bin, habe ich dann die gleichen Schritte wie die Malware verwendet:
Die neue Factory, die zurückgegeben wird, liefert keinen detaillierten Typ. Es heißt void *, was bedeutet, dass es nicht offiziell dokumentiert ist:

Da ich keinen Objekttyp wie in diesem Fall kenne, habe ich eine ausführbare Datei entwickelt, die es verwendet, um es leicht im Speicher zu sehen:

Fügen Sie Haltepunkte in den vier hook-Funktionen hinzu. In diesem Fall zeigt ein Haltepunkt in hook2, wann es den HeapHandle erfasst:

Der hook sollte gestoppt werden, wenn der gewünschte chunk erfasst wird:

Setzen Sie Haltepunkte in die anderen beiden Hooks:

Dann fährt es mit dem Aufruf von QueryInterface fort:

https://help.solidworks.com/2020/english/api/sldworksapi/queryinterface_example_cplusplus_com.htm
https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/shared/dxgi.idl

Es versucht, eine Art dynamisches Casting durchzuführen. Wenn das Objekt vom Typ ID3D11Device die Schnittstelle (die Methoden usw.) von IDXGIDevice akzeptieren kann, erstellt es eine Kopie des ursprünglichen Objekts, das den neuen Typ akzeptiert, und gibt danach den Zeiger darauf zurück. In diesem Fall wird die Variable d3dContext1 vom Typ IDXGIDevice sein:

Beide Objekte erben von CLayeredObject<Cdevice>
Das ursprüngliche ID3D11Device ist**:**

Wie das, das den Zeiger zurückgibt.

Dann erstellt es ein ID2D1Device-Objekt mit der Funktion CreateDevice:

In value2 gibt es ein Objekt vom Typ ID2D1Device zurück.
https://learn.microsoft.com/en-us/windows/win32/api/d2d1_1/nf-d2d1_1-id2d1device-createdevicecontext

Hier ist es im PoC implementiert:


Anschließend wird DCompositionCreateDevice aufgerufen
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatedevice


Die IID gehört zu _IDCompositionDevice


In dem Moment, in dem die Funktion über DCompositionCreateDevice getrace wird, stoppt sie an hook3, wenn sie NtDCompositionCreateChannel aufruft:

Auf diese Weise wird die MappedAddress erfasst, die das System intern verwendet, wenn DCompositionCreateDevice aufgerufen wurde:
Dies ist der Call-Stack bis hierher:

Dies ist der Punkt, an dem das dcomp-Modul die Funktion NtDCompositionCreateChannel aufruft:

Nach der Rückkehr aus dem vorherigen Schritt wird die MappedAddress gespeichert. Mit ALPC wird eine Verbindung zum DWM-Prozess hergestellt und anschließend CreateTargetForHwnd aufgerufen

Es verwendet das Handle HWND des erstellten Fensters. Es bezieht sich auf das soeben erstellte Gerät, welches das THIS dieser Methode ist:

Dann wird CreateSurface aufgerufen
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createsurface

Dann werden BeginDraw, EndDraw aufgerufen und schließlich CreateVisual erreicht.

Es ruft BeginDraw auf
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-begindraw
Dies verwendet die IID _IDXGISurface:


Dann wird EndDraw verwendet:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-enddraw


Schließlich wird CreateVisual aufgerufen:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createvisual

Als nächstes wird IDCompositionVisual::SetContent aufgerufen:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionvisual-setcontent


Und es wird SetRoot aufgerufen:

Das updateObject, das in BeginDraw empfangen wird, spezifiziert in der Dokumentation nicht, um welchen Typ es sich handelt.

Als nächstes werden die zuvor erstellten Objekte freigegeben:

Und nun wird mit demselben dcompDevice-Objekt vom Typ IDCompositionDevice die Commit-Methode aufgerufen:


Der Aufruf dieser Commit-Methode stoppt an hook2, der das gewünschte HeapHandle erfasst:

Dies ist der aktuelle Call-Stack:

Vor der Rückkehr zur Hauptfunktion wird auch ein Chunk mit RtlAllocateHeap erstellt. Es wird abgefangen und in der Variablen base innerhalb der hook-Funktion gespeichert:


Die Aufrufe von Create und Allocate erfolgen nacheinander:

Beide (Allocate und Create) werden von DirectComposition::Cdevice::Commit aufgerufen:

Danach stoppt es, wenn NtDCompositionCommitChannel aufgerufen wird, an hook4:

NtDCompositionCommitChannel wird von hier aus aufgerufen:

Es wird auch von DirectComposition::Cdevice::Commit aufgerufen

Es ist erwähnenswert, dass das System die Befehle bereits gebündelt hat, um sie per ALPC an DWM zu senden. Danach sendet es Befehle mit NtDCompositionCommitChannel.
Die Funktion hook4 fängt die Aufrufe von NtDCompositionCommitChannel ab, und an diesem Punkt werden weitere Befehle zum Batch hinzugefügt.
Schauen wir uns an, was hook4 tut:
Es wird eine Schleife durch den auf base zeigenden Chunk durchgeführt.
Die Schleife wird beendet, wenn der Wert 0x120 innerhalb des Chunks gefunden wird:

Es speichert die Adresse und den Offset, an dem der Wert 0x120 gefunden wurde:

Es überschreibt den Wert 0x120 mit value4, der gleich 0x1b0 + 0x8f = 0x23f ist. Dies ist die Größe, die in memcpy verwendet wird, wenn der Überlauf auftritt:



Es addiert 0xbc + 0x90 zum Adresszeiger, an dem sich 0x120 befand:


Denken Sie daran, dass bei Offset 0x48 von base die Größe 0x120 stand. Diese wurde durch 0x23f überschrieben, daher muss der ursprüngliche Chunk die Größe 0x120 haben:
Die Quelle ist die Zeigeradresse von 0x23f + 0x2c:

Es hat ursprünglich 0x90 addiert, subtrahiert nun aber wieder 0x90.
Das Ziel ist die Adresse des Zeigers auf 0x120 + 0xbc:

Es wird auf dieses geschrieben:

Alle Schreibvorgänge befinden sich innerhalb des Chunks:

Die Schleife wird 3 Mal wiederholt, was dem Ergebnis der gesamten Division von 0x1b0/0x90 entspricht:

Danach – da der ArgChannelHandle-Kanal derselbe ist, der verwendet wurde, als die MappedAddress erfasst wurde – fügt der PoC mit NtDCompositionProcessChannelBatchBuffer Befehle zum Batch hinzu. Diese werden zusammen mit denen verarbeitet, die das System bereits hinzugefügt hatte. Der Batch sammelt sie, und dann werden die Befehle alle zusammen mit NtDCompositionCommitChannel gesendet:


Der gesendete Befehl hat den Wert 8, was SetResourceIntegerProperty für 4 verschiedene Tracker (1,2,3, und 4) entspricht.
Wenn der PoC zur Hauptfunktion zurückkehrt, wird ein anderer Kanal erstellt, um das Heap-Spray durchzuführen.
Es werden 0x10000 Befehle gebündelt, die mit _NtDCompositionCommitChannel gesendet werden:

Dies verwendet den Wert CreateResource=1 und den Typ, der CHolographicInteropTextureMarshaler = 0x50 entspricht:

Die Allokationen werden im folgenden Code durchgeführt. Die Größe der erstellten Objekte für das Spray beträgt 0x1b0:

Dann wird eine Schleife durchgeführt, um die im vorherigen Schritt erstellten Objekte freizugeben, und es werden nun Löcher in der Speicherverteilung erzeugt.
Die Variable counter2 beginnt bei 0x3000 und addiert Schritte von 0x20, solange sie kleiner als 0x7000 ist:

Es werden 0x41er ab der Richtung des Chunks geschrieben, der sich in base + 0x48 + 44 + 0x1b0 befand.
Das heißt, es werden Werte geschrieben, die später verwendet werden – wenn der benachbarte Chunk überläuft:
Dieser pvalue7 befindet sich an der Adresse 0x224 von base:

Dann geht es zur Funktion "escribe":

Es schreibt den pKernelCallbacktable plus 0x388, die LoadLibraryA-Adresse und den Pfad zur DLL, die geladen wird. In diesem Fall habe ich sie s11.dll genannt.

Nun wird ein Kernel-Debugger benötigt, um an der anfälligen Funktion anzuhalten, wenn der Heap-Überlauf auftritt. Dies liegt daran, dass der DWM-Prozess nicht mit einem User-Mode-Debugger debuggt werden kann.
Mit IDA PRO zum Remotedebuggen des Ziels wird ein bedingter Breakpoint gesetzt, so dass er anhält, wenn die Größe gleich 0x1b0 ist:
print ("VALUE1 %x" % ((cpu.rax)))
return cpu.rax==0x1b0.
Da ein User-Mode-Programm vom Kernel aus debuggt wird, muss ein Kontextwechsel zum DWM-Prozess durchgeführt werden, um den Breakpoint zu setzen. Laden Sie die Benutzersymbole mit:
. reload /user
Laden Sie die Kernel-Symbole mit:
. reload /f

Es wird anhalten, wenn ShowWindow überschritten wird:

Es allokiert mit Größe 0x1b0 und kopiert mit Größe 0x23f, was den Heap-Überlauf verursacht:

An diesem Punkt sieht der Call-Stack wie folgt aus:
Um den Überlauf zu erzeugen, empfängt der DWM die Werte im untenstehenden Code:
Die präparierten Werte in base, die von meinem PoC gesendet wurden, werden mit MapViewofFile aus dem DWM-Prozess im Modul dwmcore.dll gelesen:

Die vorherige Funktion wird aufgerufen von:


Wenn es über ALPC von hook4 unter Verwendung von destination_copy (NtDCompositionCommitChannel) gesendet wird, stoppt es:

Denken Sie daran, dass in hook4 Befehle zum Batch hinzugefügt wurden. Allerdings hatte das System auch bereits einige Befehle zum Batch hinzugefügt, einschließlich der base und der präparierten Daten:



In diesem Fall wird ein Speicherbereich gemeinsam genutzt, der bei 000001cd'178d0000 beginnt. Wenn er als Quelle für memcpy verwendet wird, liegt er 0x794 Bytes später in demselben Speicherbereich.
Die Größe des gemeinsam genutzten Speicherbereichs beträgt 0x4000:

Es wird anhalten, wenn die zu allokierende Größe 0x1b0 beträgt, und erreicht das memcpy zum Kopieren von 0x23f Bytes:

Jenseits von 0x1b0 im Speicher befindet sich der Code, der den benachbarten Block überschreibt:
Wenn die Chunks aus dem PoC freigegeben werden, endet es mit einem Sprung zu LoadLibraryA, das die präparierte Bibliothek lädt:

Das kommt von hier:


Der Heap-Spray wurde mit 0x1b0 großen Objekten vom Typ CHolographicInteropTexture durchgeführt.
Da ich Löcher in der Speicherverteilung erzeugt hatte, werden einige Objekte freigegeben. Da der Block, der überlaufen wird, ebenfalls die Größe 0x1b0 hat, besteht eine hohe Wahrscheinlichkeit, dass er sich in den Löchern des Heap-Sprays befindet.
Am Ziel von memcpy befinden sich die Blöcke alle 0x1b0 Bytes:

Der Zeiger auf eine vftable wird durch den Zeiger auf LoadLibrary überschrieben:
Vor dem Überschreiben:

Nach dem Überschreiben:

Denken Sie daran, dass es mit einem Sprung zu [R11+50] endete, dem Zeiger auf LoadLibraryA.
Ausführen des PoC: Kopieren Sie die DLL in denselben Pfad, der im PoC angegeben ist:
Nach dem Ausführen des PoC wird ein CMD-Prozess mit DWM-Benutzer-Integritätsstufe System ausgeführt:

Referenzen:
PoC auf Fortra GitHub: https://github.com/fortra/CVE-2024-30051
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-30051
https://msrc.microsoft.com/update-guide/en-US/advisory/CVE-2024-30051
Damit ist der PoC abgeschlossen. Denken Sie daran, dass der Heap bei häufiger Ausführung in einen instabilen Zustand geraten kann, so dass Sie möglicherweise die Maschine neu starten müssen, damit es wieder funktioniert. Außerdem funktioniert es zwar nicht immer beim ersten Versuch, in der Regel jedoch beim zweiten oder dritten Versuch korrekt. Wie Sie sehen, kann das Reversing schwierig sein. Bei Fragen können Sie mich gerne kontaktieren.
Mail: [email protected]
X: @ricnar456