
Analisi tecnica dettagliata e exploit proof-of-concept per CVE-2024-30051, un heap-based buffer overflow nella libreria Windows DWM Core Library che consente l'escalation dei privilegi locali al livello Integrity System.
In questo post del blog, spiegherò una vulnerabilità nella libreria DWM Core di Microsoft Windows che ho analizzato quando lo exploit per Core Impact veniva sviluppato. Permette a un utente malintenzionato non privilegiato di eseguire codice come utente DWM con privilegi di Integrità di Sistema (CVE-2024-30051).
Poiché all'epoca non c'erano abbastanza informazioni pubbliche per sviluppare l'exploit, ho dovuto fare molto reverse engineering, quindi qui mostrerò come fare il reverse della patch KB5037771 per Windows 23H2 usando IDA PRO, userò BINDIFF per eseguire un binary diffing tra dwmcore.dll versione 10.0.22621.3447 e versione 10.0.22621.3593, mostrerò come viene prodotto l'heap overflow e poi lo sfrutterò elevando i privilegi, infine creerò un PoC funzionante.
Indice:
[Windows DWM Core Library Elevation of Privilege Vulnerability (CVE-2024-30051) 1](#windows-dwm-core-library-elevation-of-privilege-vulnerability-cve-2024-30051)
[Dettagli della vulnerabilità: 2](#dettagli-della-vulnerabilità)
[Diffing per trovare il bug: 3](#diffing-per-trovare-il-bug)
[Analisi del PoC che sfrutta CVE-2024-30051: 8](#analisi-del-poc-che-sfrutta-cve-2024-30051)
[1) Inizializzazione 8](#inizializzazione)
[2) Hooking 8](#hooking)
[3) Creazione della finestra 16](#creazione-della-finestra)
[4) Creazione del Device 16](#creazione-del-device)
[5) Creazione della Factory 22](#creazione-della-factory)
[6) Creazione di un Device Context 28](#creazione-di-un-device-context)
[7) Creazione di un Composition Device 29](#creazione-di-un-composition-device)
[8) Chiamata della funzione hook3 31](#chiamata-della-funzione-hook3)
[9) Creazione del target per HWND 32](#creazione-del-target-per-hwnd)
[10) Creazione della Surface 33](#creazione-della-surface)
[11) Chiamata a BeginDraw, EndDraw e CreateVisual 34](#chiamata-a-begindraw-enddraw-e-createvisual)
[12) Chiamata a Visual SetContent 36](#chiamata-a-visual-setcontent)
[13) Rilascio degli oggetti 38](#rilascio-degli-oggetti)
[14) Commit del Composition Device 38](#commit-del-composition-device)
[15) Chiamata a hook2 39](#chiamata-a-hook2)
[16) Chiamata a hook 39](#ricorda-che-la-funzione-vulnerabile-può-essere-raggiunta-usando-alcuni-metodi-della-classe-cprimitivegroup.-a-questo-punto-crea-un-heap-poi-hook2-cattura-e-salva-il-corrispondente-heaphandle.)
[17) Chiamata a hook4 41](#chiamata-alla-funzione-hook4)
[18) Esecuzione dell'Heap Spray 49](#esecuzione-dellheappray)
[19) Modifica del chunk base prima dell'invio 51](#modifica-del-chunk-base-prima-dellinvio)
[20) Debug del processo DWM 52](#debug-del-processo-dwm)
[21) Elevazione dei privilegi al livello Integrity System 62](#elevazione-dei-privilegi-al-livello-integrity-system)
Vulnerabilità di Elevazione dei Privilegi nella Libreria DWM Core di Windows CVE-2024-30051
Rilasciata: 14 maggio 2024
CNA assegnante: Microsoft CVE-2024-30051
Impatto: Elevazione dei Privilegi
Gravità massima: Importante
Debolezza:
CWE-122: Buffer Overflow basato su Heap
CVSS: 3.1 7.8 / 7.2
La vulnerabilità esiste a causa di un errore di calcolo della dimensione in una divisione intera all'interno della principale libreria DWM di Windows chiamata dwmcore.dll. Un utente locale può causare un buffer overflow nell'heap nel metodo CCommandBuffer::Initialize in dwmcore.dll e può eseguire codice arbitrario con l'utente DWM con privilegi di Integrità di Sistema. L'exploit eseguirà un Heap Spray nel processo DWM per preparare la memoria e infine produce un Heap Overflow in dwmcore.dll che verrà attivato rilasciando determinate parti dell'heap spray.
Una volta che l'exploit ha successo, il processo DWM caricherà la nostra DLL appositamente creata che esegue il nostro codice o il nostro eseguibile (nel nostro caso un CMD) come utente DWM che ha privilegi di Integrità di Sistema.

Esaminiamo questa vulnerabilità e vediamo come ci permette di eseguire come utente DWM con Livello di Integrità SYSTEM. Si noti che poiché non si tratta di un utente appartenente al gruppo Amministratori, ha alcune restrizioni di privilegi.
La patch per Windows 11 23H2 può essere scaricata da:
https://www.catalog.update.microsoft.com/Search.aspx?q=KB5037771
windows11.0-kb5037771-x64_19a3f100fb8437d059d7ee2b879fe8e48a1bae42.msu
La versione vulnerabile di dwmcore.dll è: 10.0.22621.3447
La versione patchata di dwmcore.dll è: 10.0.22621.3593
Analizzando le funzioni modificate, è chiaro che la versione patchata di CCommandBuffer::Initialize ha molti blocchi aggiunti, rendendola molto diversa dalla versione non patchata.

Dopo aver fatto reverse engineering statico di quella funzione, ci sono due chiamate a CD2DSharedBuffer::GetBufferSize.
La prima chiamata ottiene la dimensione da allocare nel new e la seconda chiamata ottiene la stessa dimensione per il memcpy.

Tutto sembra inizialmente corretto. Tuttavia, prima dell'allocazione, esegue alcune operazioni con la dimensione.

Ottiene buffer_size e buffer_size2 chiamando la stessa funzione CD2DSharedBuffer::GetBufferSize, che restituisce entrambi lo stesso valore. Ma nel new esegue una pre-operazione, una divisione intera di buffer_size per 0x90 e poi moltiplicazione per 0x90, mentre nel memcpy usa il buffer_size2 restituito senza operare su di esso.
Con queste operazioni, ho scoperto che la dimensione finalmente usata nel new e nel memcpy può essere diversa.
buffer_size = buffer_size2 (dimensioni restituite)
size_new = buffer_size / 0x90 x 0x90
size_memcpy = buffer_size2
Ad esempio, se buffer_size è 0x91
buffer_size = buffer_size2 = 0x91
size_new = buffer_size / 0x90 x 0x90 = 0x90
size_memcpy = buffer_size2 = 0x91
Questo esempio dimostra che c'è un heap overflow. Copia più byte di quanti allocati, e la dimensione è controllabile.
Ad esempio, se buffer_size è 0x23f come usato nel POC.
buffer_size = buffer_size2 = 0x23F
size_new = buffer_size / 0x90 x 0x90 = 0x1b0
size_memcpy = buffer_size2 = 0x23f
Analizzata la funzione vulnerabile, volevo vedere come raggiungere la funzione vulnerabile CCommandBuffer::Initialize. È qui che le cose iniziano a complicarsi.
Rivedendo i riferimenti a questa funzione, sembra essere raggiunta dai metodi della classe CPrimitiveGroup:

Tali metodi possono essere accessibili dalla vftable degli oggetti CPrimitiveGroup:
Ha il suo costruttore:

E viene raggiunto in questo modo:

Mentre inizialmente attraversavo questo processo, mi sono preso il tempo di leggere il PDF “The Lost World of DirectComposition: Hunting Windows Desktop Window Manager Bugs” e mi sono immerso nel mondo di Direct Composition. Questo mi ha aiutato a creare il mio primo PoC.
Inoltre, avevo bisogno di fare reverse di win32ksys e ho provato a inviare pacchetti attraverso le funzioni:
NtDCompositionCreateChannel
NtDCompositionProcessChannelBatchBuffer
NtDCompositionCommitChannel

Il mio primo PoC ha raggiunto il costruttore di CPrimitiveGroup. Tuttavia, dopo molto reverse engineering, non ho trovato un modo per gestire le chiamate ai metodi della vftable per arrivare alla funzione vulnerabile direttamente attraverso chiamate ALPC usando queste funzioni.
Ho passato molto tempo a fare reverse engineering complicato. Durante questo processo, ho trovato il campione del malware che sfruttava la vulnerabilità, il che è stato immensamente utile perché il metodo di sfruttamento è molto più complesso di quanto pensassi inizialmente. Include anche diversi hook di API di sistema e usa metodi che forse sono un po' discutibili. Ma tutto è valido in guerra e negli exploit, quindi ho iniziato ad analizzare il malware e da quell'analisi ho creato il mio PoC finale che finalmente sfrutta la vulnerabilità, che spiegherò di seguito.
Prima di tutto, voglio chiarire che il malware non solo sfrutta la vulnerabilità CVE-2024-30051 che eleva il nostro processo a Livello di Integrità di Sistema, ma esegue anche una seconda parte che da lì finisce per elevare un utente SYSTEM con tutti i privilegi, il che va già oltre il CVE spiegato.
Inoltre, è importante notare che il malware è molto più complesso del mio PoC, che cerca di minimizzare il codice. Il malware esegue molti più controlli per garantire l'affidabilità e per questo funziona al primo tentativo. Ho scartato tutti quei controlli per semplificare e mi sono dedicato allo sfruttamento puro, anche se forse è necessario eseguire il PoC due o tre volte per ottenere lo sfruttamento.
Il link al PoC eseguibile è https://github.com/fortra/CVE-2024-30051
Prima di tutto, il PoC chiama GetVersion per ottenere la versione del sistema operativo su cui è in esecuzione e secondo quella esegue diverse inizializzazioni di alcune variabili globali. Il mio PoC è stato testato su Windows 11 23H2 e Windows 11 22h2. Anche altri sistemi sono vulnerabili e ho aggiunto i valori per sfruttarli.
Esegue l'hook di quattro funzioni di sistema e senza hookarle non può ottenere lo sfruttamento. Queste funzioni sono: RtlAllocateHeap, RtlCreateHeap, NtDCompositionCreateChannel e NtDCompositionCommitChannel.

In queste funzioni modificherà i primi 5 byte per farle saltare al proprio codice. Naturalmente, il codice non può essere molto lontano poiché un salto di 5 byte non copre tutta la memoria e deve essere vicino.
Per farlo, il malware usa un codice molto lungo, analizzando la mappa di memoria per decidere dove può eseguire l'allocazione del proprio codice. Poiché il codice è complicato, mi sono concentrato su due semplici righe:
base_ntdll = GetModuleHandleW(L"ntdll.dll");
global4_ = (char *)VirtualAlloc((LPVOID)(base_ntdll-0x2000), 0x1000uLL, 0x3000u, 0x40u);
Ho sottratto dalla base di ntdll, 0x2000 e ho passato quell'indirizzo a VirtualAlloc per allocare lì.
Le DLL a 64 bit sono mappate abbastanza separatamente nella memoria l'una dall'altra con spazi vuoti tra di loro.
Vediamo come funzionano gli hook:

Chiama una funzione di hooking, che è quella che eseguirà l'hooking dell'API RtlAllocateHeap, che ha tre argomenti, il primo è l'indirizzo dell'API da patchare, chiamato sym_RtlAllocateHeap.
Prima della patch, punta all'inizio dell'API:

Ecco la funzione RtlAllocateHeap:

Il secondo argomento è la routine chiamata hook che verrà eseguita quando l'API è completamente patchata:


La funzione hook chiama my_RtlAllocateHeap.
La funzione hooking modificherà i primi 5 byte dell'API in modo che salti a hook.
Chiamerà il codice nell'area allocata dove eseguirà la prima istruzione dell'API che è stata sostituita con i 5 byte e poi salterà a RtlAllocateHeap+5 subito dopo i byte patchati:

Ecco come apparirà l'API dopo l'hook. I primi 5 byte sono cambiati in modo che salti a hook. Chiamerà my_RtlAllocateHeap il codice che è appena sopra che tornerà all'area evidenziata in viola per continuare l'esecuzione dell'API:

Quando l'API termina l'esecuzione, tornerà a hook. Da lì confronterà la variabile globale heap_base (che inizialmente è zero) con il primo argomento passato a RtlAllocateHeap:

Dopodiché il codice attende una certa allocazione speciale, che ha uno specifico HeapHandle. All'inizio questa variabile è zero e finché è zero salterà e funzionerà come un normale RtlAllocateheap:

Il parametro HeapHandle viene ottenuto all'interno di RtlCreateHeap che è casualmente la seconda API hookata.
Cercando riferimenti alla variabile globale heap_base, cambia il suo valore solo nella funzione hook2, che è quella eseguita dopo l'hooking di RtlCreateHeap:


Quindi, l'idea è catturare un certo HeapHandle e salvarlo in heap_base. Poiché ora è diverso da zero, la funzione hook inizierà a confrontare ogni allocazione. Quindi, il PoC salverà l'indirizzo di memoria che ha lo stesso HeapHandle di quello precedentemente memorizzato.
Quando è questo il caso, salverà l'indirizzo dell'allocazione nella variabile chiamata base:
Questi primi due hook sono ora concatenati. Quando hook2 salva il valore atteso di HeapHandle, attiva la funzione hook che salverà l'indirizzo dell'allocazione che usa lo stesso HeapHandle.
Il terzo hook è inviato a NtDCompositionCreateChannel. La prima volta che viene chiamato salverà il MappedAddress, che è il contenuto del terzo argomento. Da lì cambierà hooked_flag a 1 così da allora in poi non salverà più e funzionerà normalmente.


L'indirizzo salvato nella variabile base verrà letto successivamente tre volte. Due di queste avverranno nell'ultimo hook, chiamato hook4:

La funzione hook4 per NtDCompositionCommitChannel sarà analizzata in seguito perché è piuttosto complessa e molto importante.
Dopo che i quattro hook sono completati, torna alla funzione principale per iniziare a creare una finestra. Questo viene fatto chiamando RegisterClassExW. Tuttavia, per registrare una classe di finestra per un uso successivo, dovrebbe essere chiamata con CreateWindowExW function.

Questo inizializza la libreria COM chiamando CoInitializeEx per essere usata dal thread chiamante:

Calcola la dimensione richiesta del rettangolo della finestra, basata sulla dimensione desiderata:

La funzione CreateWindowExW viene chiamata per creare una finestra che verrà disegnata:

Da lì, chiama D3D11CreateDevice per creare un device o dispositivo DirectX che rappresenta l'adattatore video:


Nel mio PoC ppDevice è chiamato d3dDevice e ppImmediateContext è chiamato d3dContext:

L'argomento flags deve essere impostato a 0x20:

Poi chiama AddRef:

Questo incrementa il contatore di riferimento per un puntatore a interfaccia di un oggetto COM:


Il valore 0x10 viene sottratto a THIS:

All'offset 0xf8 da ID3D11Device-0x10 c'è un puntatore a TComObject:



Questo sarà il nuovo THIS e finisce per saltare a TComObject::AddRef:

E termina aggiungendo uno al contatore oggetto che si trova nell'offset 8 di TComObject:

Poi, AddRef incrementerà il contatore dell'altro tipo di oggetto creato in D3D11CreateDevice, che è di tipo ID3D11DeviceContext:

In questo caso, per trovare il nuovo THIS, sottrae 0x108:


Salta qui dove all'offset 0x98 c'è il nuovo THIS:

Questo è il contatore. In questo esempio, è un QWORD:

Il PoC chiama D2D1CreateFactory per usare Direct2D, e per creare l'interfaccia ID2D1Factory che viene usata per creare altre risorse Direct2D che possono essere utilizzate per disegnare o descrivere forme:

L'argomento riid è quello suggerito dalla pagina Microsoft:
https://learn.microsoft.com/en-us/windows/win32/api/d2d1/nf-d2d1-d2d1createfactory

Questi sono quelli usati dal malware:

Quello giusto per ID2D1Factory si può trovare qui**:**
https://github.com/apitrace/dxsdk/blob/master/Include/d2d1_1.h

Poiché non sono un esperto di Direct Composition, ho quindi usato gli stessi passaggi del malware:
La nuova factory restituita non fornisce alcun tipo dettagliato. Dice void *, il che significa che non è ufficialmente documentata:

Poiché non conosco un tipo di oggetto come in questo caso, ho sviluppato un eseguibile che lo usa per vederlo facilmente in memoria:

Aggiungi breakpoint nelle quattro funzioni hook. In questo caso, un breakpoint in hook2 mostrerà quando cattura il HeapHandle:

L'hook dovrebbe fermarsi quando il chunk desiderato viene catturato:

Metti breakpoint negli altri due hook:

Poi continua chiamando QueryInterface:

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

Tenta di eseguire una sorta di casting dinamico. Se l'oggetto di tipo ID3D11Device può accettare l'interfaccia (usare i metodi, ecc.) di IDXGIDevice, crea una copia dell'oggetto originale che accetta il nuovo tipo, dopodiché restituisce il puntatore a esso. In questo caso la variabile d3dContext1 sarà di tipo IDXGIDevice:

Entrambi gli oggetti ereditano da CLayeredObject<Cdevice>
L'originale ID3D11Device è**:**

Come quello che restituisce il puntatore.

Poi crea un oggetto ID2D1Device con la funzione CreateDevice:

In value2 restituisce un oggetto di tipo ID2D1Device.
https://learn.microsoft.com/en-us/windows/win32/api/d2d1_1/nf-d2d1_1-id2d1device-createdevicecontext

Ecco come è implementato nella PoC:


Poi chiama DCompositionCreateDevice
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatedevice


L'IID appartiene a _IDCompositionDevice


Nello stesso momento in cui la funzione viene tracciata su DCompositionCreateDevice, si ferma a hook3, quando chiama NtDCompositionCreateChannel:

In questo modo viene catturato il MappedAddress che il sistema utilizza internamente quando DCompositionCreateDevice è stata chiamata:
Questo è lo stack di chiamate fino a qui:

Questo è il punto in cui il modulo dcomp chiama la funzione NtDCompositionCreateChannel:

Dopo essere tornato dal passo precedente, salva il MappedAddress. Utilizzando ALPC, si connetterà al processo DWM e poi chiamerà CreateTargetForHwnd

Utilizza l'handle HWND della finestra creata. È correlato al dispositivo che ho appena creato, che è il THIS di questo metodo:

Poi chiama CreateSurface
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createsurface

Poi chiama BeginDraw, EndDraw, e arriva a CreateVisual.

Chiama BeginDraw
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-begindraw
Questo utilizza l'IID _IDXGISurface:


Poi utilizza EndDraw:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-enddraw


Infine, chiama CreateVisual:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createvisual

Successivamente, chiama IDCompositionVisual::SetContent:
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionvisual-setcontent


E chiama SetRoot:

L'updateObject ricevuto in BeginDraw non specifica nella documentazione di che tipo sia.

Successivamente, rilascia gli oggetti creati in precedenza:

E ora utilizzando lo stesso oggetto dcompDevice di tipo IDCompositionDevice, chiama il metodo Commit:


Chiamare quel metodo Commit si ferma su hook2 che cattura il desired HeapHandle:

Questo è lo stack di chiamate ora:

Prima di tornare al main, crea anche un chunk utilizzando RtlAllocateHeap. Viene quindi intercettato e memorizzato nella variabile base all'interno della funzione hook:


Le chiamate a Create e Allocate vengono eseguite una dopo l'altra:

Entrambe (Allocate e Create) vengono chiamate da DirectComposition::Cdevice::Commit:

Dopo di che, quando viene chiamato NtDCompositionCommitChannel, si ferma su hook4:

NtDCompositionCommitChannel viene chiamato da qui:

Viene anche chiamato da DirectComposition::Cdevice::Commit

Vale la pena menzionare che il sistema ha già messo in coda i comandi da inviare tramite ALPC a DWM. Dopo di che, invia comandi utilizzando NtDCompositionCommitChannel.
La funzione hook4 intercetta le chiamate a NtDCompositionCommitChannel e a questo punto verranno aggiunti ulteriori comandi alla coda.
Vediamo cosa fa hook4:
Viene eseguito un ciclo attraverso il chunk puntato da base.
Esce dal ciclo quando trova il valore 0x120 all'interno del chunk:

Memorizza l'indirizzo e l'offset dove è stato localizzato il valore 0x120:

Sovrascrive il valore 0x120 con value4, che è uguale a 0x1b0 + 0x8f = 0x23f. Questa è la dimensione che utilizzerà in memcpy quando si verifica l'overflow:



Aggiunge 0xbc + 0x90 al puntatore all'indirizzo dove si trovava 0x120:


Ricorda che all'offset 0x48 da base c'era la dimensione 0x120. Quella è stata sovrascritta con 0x23f, quindi il chunk originale deve avere dimensione 0x120:
La sorgente è il puntatore all'indirizzo di 0x23f + 0x2c:

Inizialmente aveva aggiunto 0x90 ma ora sta sottraendo di nuovo 0x90.
La destinazione sarà l'indirizzo del puntatore a 0x120 + 0xbc:

Scriverà su questo:

Tutte le scritture saranno all'interno del chunk:

Ripeterà il ciclo 3 volte, che è il risultato della divisione intera di 0x1b0/0x90:

Dopo di ciò, poiché il canale ArgChannelHandle è lo stesso che è stato utilizzato quando MappedAddress è stato catturato. La PoC aggiungerà comandi al batch utilizzando NtDCompositionProcessChannelBatchBuffer. Questi verranno elaborati insieme a quelli che il sistema aveva già aggiunto. Il batch li raccoglie e poi i comandi vengono inviati tutti insieme utilizzando NtDCompositionCommitChannel:


Il comando inviato ha il valore 8, che corrisponde a SetResourceIntegerProperty per 4 diversi tracker (1,2,3 e 4).
Quando la PoC ritorna alla funzione main, crea un canale diverso per eseguire l'HeapSpray.
Mette in coda 0x10000 comandi, che vengono inviati con NtDCompositionCommitChannel:

Questo utilizza il valore CreateResource=1 e il tipo che corrisponde a CHolographicInteropTextureMarshaler = 0x50:

Le allocazioni vengono eseguite nel codice sottostante. La dimensione degli oggetti creati per effettuare lo spray è 0x1b0:

Esegue quindi un ciclo per rilasciare gli oggetti creati nel passo precedente e ora crea buchi nella distribuzione di memoria.
La variabile counter2 inizia a 0x3000 e aggiunge passi di 0x20 fino a quando è minore di 0x7000:

Scrive 0x41 dall'indirizzo del chunk che era in base + 0x48 + 44 + 0x1b0
Cioè, sta scrivendo valori che verranno utilizzati in seguito—quando si verifica l'overflow del chunk adiacente:
Quel pvalue7 si trova all'indirizzo 0x224 da base:

Poi va alla funzione “escribe”:

Scrive la pKernelCallbacktable più 0x388, l'indirizzo di LoadLibraryA e il percorso della DLL che verrà caricata. In questo caso, l'ho chiamata s11.dll.

Ora è necessario un debugger kernel per fermarsi alla funzione vulnerabile quando si verifica l'heap overflow. Questo perché il processo DWM non può essere debugato con un debugger user mode.
Utilizzando IDA PRO per eseguire il debug remoto del target, imposta un breakpoint condizionale in modo che si fermi quando la dimensione è uguale a 0x1b0:
print ("VALUE1 %x" % ((cpu.rax)))
return cpu.rax==0x1b0.
Poiché un programma in user mode viene debugato dal kernel, è necessario passare al contesto del processo DWM per inserire il breakpoint. Ricarica i simboli utente con:
. reload /user
Ricarica quelli del kernel con:
. reload /f

Si fermerà quando ShowWindow viene eseguito passo passo:

Alloca con dimensione 0x1b0 e copia con dimensione 0x23f, producendo l'heap overflow:

A questo punto lo stack di chiamate appare così:
Per creare l'overflow, il DWM riceve valori nel codice sottostante:
I valori manipolati in base inviati dalla mia PoC vengono letti utilizzando MapViewofFile dal processo DWM nel modulo dwmcore.dll:

La funzione precedente viene chiamata da:


Quando viene inviato tramite ALPC da hook4 utilizzando destination_copy (NtDCompositionCommitChannel) si ferma:

Ricorda che in hook4 sono stati aggiunti comandi al batch. Tuttavia, il sistema aveva già aggiunto alcuni comandi al batch, inclusi base e i dati manipolati:



In questo caso condivide un'area di memoria che inizia a 000001cd'178d0000. Quando viene utilizzata come sorgente per eseguire il memcpy, sarà a 0x794 byte più avanti nella stessa area di memoria.
La dimensione dell'area di memoria condivisa è 0x4000:

Si fermerà quando la dimensione da allocare è 0x1b0, e raggiunge il memcpy per copiare 0x23f byte:

Oltre 0x1b0 nella memoria si trova il codice che overflowerà sovrascrivendo il blocco adiacente:
Quando i chunk vengono rilasciati dalla PoC, termina saltando a LoadLibraryA, che carica la libreria manipolata:

Ciò proviene da qui:


L'Heap spray è stato effettuato con oggetti di dimensione 0x1b0 di tipo CHolographicInteropTexture.
Poiché avevo creato buchi nella distribuzione di memoria, questo rilascia alcuni oggetti. Poiché il blocco che andrà in overflow ha anch'esso dimensione 0x1b0, ha un'alta probabilità di essere posizionato nei buchi dell'heap spray.
Alla destinazione del memcpy, i blocchi si trovano ogni 0x1b0 byte:

Il puntatore a una vftable viene sovrascritto dal puntatore a LoadLibrary:
Prima della sovrascrittura:

Dopo la sovrascrittura:

Ricorda che è terminato saltando a [R11+50], che è il puntatore a LoadLibraryA.
Eseguendo la PoC, copia la DLL nello stesso percorso indicato nella PoC:
Dopo aver eseguito la PoC, viene eseguito un processo CMD con privilegi di Integrity System Level dell'utente DWM:

Riferimenti:
PoC su GitHub di Fortra: 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
Questo completa la PoC. Ricorda che se la esegui molte volte, l'heap rimarrà in uno stato instabile, quindi potrebbe essere necessario riavviare la macchina per farla funzionare di nuovo. Inoltre, anche se potrebbe non funzionare sempre al primo tentativo, di solito funziona correttamente al secondo o terzo tentativo. Come puoi vedere, il reverse engineering può essere difficile, quindi se hai domande, puoi consultarmi.
Mail: [email protected]
X: @ricnar456