
Analyse technique détaillée et preuve de concept pour CVE-2024-30051, un dépassement de tampon basé sur le tas dans la bibliothèque principale DWM de Windows permettant une élévation de privilèges locaux au niveau Integrity System.
Dans ce billet de blog, je vais expliquer une vulnérabilité dans la bibliothèque DWM Core de Microsoft Windows que j'ai analysée lors du développement de l'exploit pour Core Impact. Elle permet à un attaquant non privilégié d'exécuter du code en tant qu'utilisateur DWM avec des privilèges Integrity System (CVE-2024-30051).
Comme il n'y avait pas suffisamment d'informations publiques à l'époque pour développer l'exploit, j'ai dû faire beaucoup de rétro-ingénierie. Je vais donc montrer ici comment rétro-concevoir le correctif KB5037771 pour Windows 23H2 en utilisant IDA PRO, j'utiliserai BINDIFF pour effectuer une comparaison binaire entre dwmcore.dll version 10.0.22621.3447 et version 10.0.22621.3593, montrerai comment le débordement de tas est produit, puis l'exploiterai en élevant les privilèges, et enfin créerai un PoC fonctionnel.
Index :
[Windows DWM Core Library Elevation of Privilege Vulnerability (CVE-2024-30051) 1](#windows-dwm-core-library-elevation-of-privilege-vulnerability-cve-2024-30051)
[Vulnerability details : 2](#vulnerability-details)
[Diffing to find the bug : 3](#diffing-to-find-the-bug)
[Analysis of the PoC exploiting CVE-2024-30051 : 8](#analysis-of-the-poc-exploiting-cve-2024-30051)
[1)Initialization 8](#initialization)
[2)Hooking 8](#hooking)
[3)Creating the window 16](#creating-the-window)
[4)Create Device 16](#create-device)
[5) Create Factory 22](#create-factory)
[6) Create Device Context 28](#create-a-device-context)
[7)Create Composition Device 29](#create-a-composition-device)
[8)Calling hook3 function 31](#calling-dcompositioncreatedevice-function)
[9)Creating target for HWND 32](#creating-a-target-for-handle-hwnd)
[10)Creating Surface 33](#creating-surface)
[11)Calling BeginDraw, EndDraw, and CreateVisual. 34](#calling-begindraw-enddraw-and-createvisual)
[11)Calling Visual SetContent 36](#calling-visual-setcontent)
[12)Release objects 38](#release-objects)
[13)Commit Composition Device 38](#commit-composition-device)
[14)Calling hook2 39](#calling-hook2)
[15)Calling 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)Calling hook4 41](#calling-the-function-hook4)
[17)Performing Heap Spray 49](#performing-heap-spray)
[18)Modifying the base chunk previous to send. 51](#modifying-the-base-chunk-before-send)
[19)Debugging the process DWM 52](#debugging-the-dwm-process)
[20)Elevating Privileges to Integrity System level 62](#elevating-privileges-to-integrity-system-level)
Windows DWM Core Library Elevation of Privilege Vulnerability CVE-2024-30051
Date de publication : 14 mai 2024
CNA d'attribution : Microsoft CVE-2024-30051
Impact : Élévation de privilège
Sévérité maximale : Important
Faiblesse :
CWE-122 : Débordement de tampon basé sur le tas
CVSS : 3.1 7.8 / 7.2
La vulnérabilité existe en raison d'une erreur de calcul de taille dans une division entière au sein de la bibliothèque principale Windows DWM appelée dwmcore.dll. Un utilisateur local peut provoquer un débordement de tampon sur le tas dans la méthode CCommandBuffer::Initialize de dwmcore.dll et peut exécuter du code arbitraire avec l'utilisateur DWM disposant de privilèges Integrity System. L'exploit effectuera un Heap Spray dans le processus DWM pour préparer la mémoire et produit finalement un débordement de tas dans dwmcore.dll qui sera déclenché en libérant certaines parties du Heap Spray.
Une fois l'exploit réussi, le processus DWM chargera notre DLL conçue qui exécute notre code ou notre exécutable (dans notre cas un CMD) en tant qu'utilisateur DWM, lequel possède des privilèges Integrity System.

Parcourons cette vulnérabilité et voyons comment elle nous permet de s'exécuter en tant qu'utilisateur DWM avec le niveau d'intégrité SYSTEM. Notez que, comme il ne s'agit pas d'un utilisateur appartenant au groupe Administrateur, il a certaines restrictions de privilèges.
Le correctif pour Windows 11 23H2 peut être téléchargé depuis :
https://www.catalog.update.microsoft.com/Search.aspx?q=KB5037771
windows11.0-kb5037771-x64_19a3f100fb8437d059d7ee2b879fe8e48a1bae42.msu
La version vulnérable de dwmcore.dll est : 10.0.22621.3447
La version corrigée de dwmcore.dll est : 10.0.22621.3593
En analysant les fonctions modifiées, il est clair que la version corrigée de CCommandBuffer::Initialize a beaucoup de blocs ajoutés, ce qui la rend assez différente de la version non corrigée.

Après une rétro-ingénierie statique de cette fonction, il y a deux appels à CD2DSharedBuffer::GetBufferSize.
Le premier appel obtient la taille à allouer dans le new et le second appel obtient la même taille pour le memcpy.

Tout semble correct au départ. Cependant, avant l'allocation, elle effectue certaines opérations avec la taille.

Elle obtient buffer_size et buffer_size2 en appelant la même fonction CD2DSharedBuffer::GetBufferSize, les deux renvoient la même valeur. Mais dans le new, elle effectue une pré-opération, une division entière de buffer_size par 0x90 puis multiplication par 0x90, tandis que dans le memcpy, elle utilise la valeur retournée buffer_size2 sans opération.
Avec ces opérations, j'ai découvert que la taille finalement utilisée dans le new et dans le memcpy peut être différente.
buffer_size = buffer_size2 (tailles retournées)
size_new = buffer_size / 0x90 * 0x90
size_memcpy = buffer_size2
Par exemple, si buffer_size est 0x91
buffer_size = buffer_size2 = 0x91
size_new = buffer_size / 0x90 * 0x90 = 0x90
size_memcpy = buffer_size2 = 0x91
Cet exemple prouve qu'il y a un débordement de tas. Il copie plus d'octets que ce qui a été alloué, et la taille est contrôlable.
Par exemple, si buffer_size est 0x23f comme utilisé dans le POC.
buffer_size = buffer_size2 = 0x23F
size_new = buffer_size / 0x90 * 0x90 = 0x1b0
size_memcpy = buffer_size2 = 0x23f
Avec la fonction vulnérable analysée, je voulais voir comment atteindre la fonction vulnérable CCommandBuffer::Initialize. C'est là que les choses commencent à se compliquer.
En regardant les références de cette fonction, il semble qu'elle soit atteinte depuis les méthodes de la classe CPrimitiveGroup :

Ces méthodes peuvent être accessibles depuis la vftable des objets
CPrimitiveGroup :
Elle a son constructeur :

Et elle est atteinte de cette manière :

Alors que je parcourais ce processus initialement, j'ai pris le temps de lire le PDF « The Lost World of DirectComposition: Hunting Windows Desktop Window Manager Bugs » et je me suis plongé dans le monde de la Composition Directe. Cela m'a aidé à créer mon premier PoC.
De plus, j'ai dû faire de la rétro-ingénierie de win32ksys et j'ai essayé d'envoyer des paquets via les fonctions :
NtDCompositionCreateChannel
NtDCompositionProcessChannelBatchBuffer
NtDCompositionCommitChannel

Mon premier PoC atteignait le constructeur CPrimitiveGroup. Cependant, après beaucoup de rétro-ingénierie, je n'ai pas trouvé de moyen de gérer les appels aux méthodes de la vftable pour accéder à la fonction vulnérable directement via des appels ALPC utilisant ces fonctions.
J'ai passé beaucoup de temps à faire de la rétro-ingénierie compliquée. Au cours de ce processus, j'ai trouvé l'échantillon du malware qui exploitait la vulnérabilité, ce qui a été extrêmement utile car la méthode d'exploitation est beaucoup plus complexe que ce que je pensais initialement. Elle inclut également plusieurs hookings d'APIs système et utilise des méthodes peut-être un peu discutables. Mais tout est valable en matière de guerre et d'exploits, j'ai donc commencé à analyser le malware et à partir de cette analyse j'ai créé mon PoC final qui exploite enfin la vulnérabilité, que je vais expliquer ci-dessous.
Tout d'abord, je veux clarifier que le malware n'exploite pas seulement la vulnérabilité CVE-2024-30051 qui élève notre processus au niveau Integrity System, mais il effectue également une deuxième partie qui, à partir de là, finit par élever un utilisateur SYSTEM avec tous les privilèges, ce qui dépasse déjà la CVE expliquée.
De plus, il est important de noter que le malware est beaucoup plus complexe que mon PoC qui tente de minimiser le code. Le malware effectue beaucoup plus de vérifications pour assurer la fiabilité et c'est pourquoi il fonctionne du premier coup. J'ai écarté toutes ces vérifications pour simplifier et je me suis consacré à l'exploitation pure, même si cela implique peut-être de devoir exécuter le PoC deux ou trois fois pour réussir l'exploitation.
Le lien vers le PoC exécutable est https://github.com/fortra/CVE-2024-30051
Tout d'abord, le PoC appelle GetVersion pour obtenir la version du système d'exploitation sur lequel il s'exécute et, selon cela, il effectue différentes initialisations de certaines variables globales. Mon PoC a été testé sur Windows 11 23H2 et Windows 11 22H2. D'autres systèmes sont également vulnérables et j'ai ajouté les valeurs pour les exploiter.
Il hooke quatre fonctions système et sans les hooker, il ne peut pas réaliser l'exploitation. Ces systèmes sont : RtlAllocateHeap, RtlCreateHeap, NtDCompositionCreateChannel et NtDCompositionCommitChannel.

Dans ces fonctions, il va patcher les 5 premiers octets pour sauter vers son propre code. Bien sûr, le code ne peut pas être très loin car un saut de 5 octets ne couvre pas toute la mémoire et doit être proche.
Pour ce faire, le malware utilise un code très long, analysant la carte mémoire pour décider où il peut effectuer l'allocation de son propre code. Comme le code est compliqué, je me suis concentré sur deux lignes simples :
base_ntdll = GetModuleHandleW(L"ntdll.dll");
global4_ = (char *)VirtualAlloc((LPVOID)(base_ntdll-0x2000), 0x1000uLL, 0x3000u, 0x40u);
J'ai soustrait de la base de ntdll, 0x2000 et j'ai passé cette adresse à VirtualAlloc pour allouer là-bas. Les DLL 64 bits sont mappées assez séparément dans la mémoire les unes des autres, avec des espaces vides entre elles. Voyons comment fonctionnent les hooks :

Il appelle une fonction hooking, qui est celle qui effectuera le hooking de l'API RtlAllocateHeap, qui a trois arguments, le premier est l'adresse de l'API à patcher, appelée sym_RtlAllocateHeap. Avant le patching, elle pointe vers le début de l'API :

Voici la fonction RtlAllocateHeap :

Le deuxième argument est la routine appelée hook qui sera exécutée lorsque l'API sera complètement patchée :


La fonction hook appelle my_RtlAllocateHeap.
La fonction hooking va patcher les 5 premiers octets de l'api pour qu'elle saute vers hook.
Elle appellera le code dans la zone allouée où elle exécutera la première instruction de l'API qui a été remplacée par les 5 octets, puis sautera vers RtlAllocateHeap+5 juste après les octets patchés :

Voici à quoi ressemblera l'API après le hook. Les 5 premiers octets ont changé pour sauter vers hook. Elle appellera my_RtlAllocateHeap le code qui se trouve juste au-dessus qui retournera dans la zone marquée en violet pour continuer l'exécution de l'API :

Lorsque l'API termine son exécution, elle retourne à hook. De là, elle compare la variable globale heap_base (initialement zéro) avec le premier argument passé à RtlAllocateHeap :

Après cela, le code attend une certaine allocation spéciale, qui a un HeapHandle spécifique. Au début, cette variable est zéro et tant qu'elle est zéro, elle saute et fonctionne comme un RtlAllocateheap normal :

Le paramètre HeapHandle est obtenu à l'intérieur de RtlCreateHeap qui, par coïncidence, est la deuxième API hookée.
En cherchant des références à la variable globale heap_base, elle ne change de valeur que dans la fonction hook2, qui est celle exécutée après le hooking de RtlCreateHeap :


Donc, l'idée est de capturer un certain HeapHandle et de le sauvegarder dans heap_base. Comme il est maintenant différent de zéro, la fonction hook commencera à comparer chaque allocation. Ainsi, le PoC sauvegardera l'adresse mémoire qui a le même HeapHandle que celui précédemment stocké.
Lorsque c'est le cas, il sauvegardera l'adresse de l'allocation dans la
variable nommée base :
Ces deux premiers hooks sont maintenant chaînés. Lorsque hook2 sauvegarde la valeur attendue de HeapHandle, cela active la fonction hook qui sauvegardera l'adresse d'allocation qui utilise le même HeapHandle.
Le troisième hook est soumis à NtDCompositionCreateChannel. La première fois qu'il est appelé, il sauvegardera le MappedAddress, qui est le contenu du troisième argument. De là, il changera hooked_flag à 1 pour qu'à partir de là il ne sauvegarde plus et fonctionne normalement.


L'adresse sauvegardée dans la variable base sera lue plus tard trois fois. Deux d'entre elles se produiront dans le dernier hook, appelé hook4 :

La fonction hook4 pour NtDCompositionCommitChannel sera analysée plus tard car elle est assez complexe et très importante.
Après que les quatre hooks sont terminés, on retourne à la fonction principale pour commencer à créer une fenêtre. Cela se fait en appelant RegisterClassExW. Cependant, pour enregistrer une classe de fenêtre en vue d'une utilisation ultérieure, elle doit être appelée avec la fonction CreateWindowExW.

Cela initialise la bibliothèque COM en appelant CoInitializeEx pour être utilisée par le thread appelant :

Il calcule la taille requise du rectangle de la fenêtre, en fonction de la taille souhaitée :

La fonction CreateWindowExW est appelée pour créer une fenêtre qui sera dessinée :

De là, appelez D3D11CreateDevice pour créer un périphérique ou un périphérique DirectX qui représente l'adaptateur d'affichage :


Dans mon PoC, ppDevice est nommé d3dDevice et ppInmediateContext est nommé d3dContext :

L'argument flags doit être réglé sur 0x20 :

Ensuite, appelez AddRef :

Cela incrémente le compteur de références pour un pointeur d'interface vers un objet COM :


La valeur 0x10 est soustraite à THIS :

À l'offset 0xf8 de ID3D11Device-0x10 se trouve un pointeur vers TComObject :



Ce sera le nouveau THIS et cela finit par sauter vers TComObject::AddRef :

Et cela se termine en ajoutant un au compteur d'objets qui se trouve à l'offset 8 de TComObject :

Ensuite, AddRef augmentera le compteur de l'autre type d'objet créé dans D3D11CreateDevice, qui est de type ID3D11DeviceContext :

Dans ce cas, pour trouver le nouveau THIS, il soustrait 0x108 :


Il saute ici où à l'offset 0x98 se trouve le nouveau THIS :

C'est le compteur. Dans cet exemple, c'est un QWORD :

Le PoC appelle D2D1CreateFactory pour utiliser Direct2D, et pour créer l'interface ID2D1Factory qui est utilisée pour créer d'autres ressources Direct2D pouvant être utilisées pour dessiner ou décrire des formes :

L'argument riid est celui suggéré par la page Microsoft :
https://learn.microsoft.com/en-us/windows/win32/api/d2d1/nf-d2d1-d2d1createfactory

Voici ceux utilisés par le malware :

Le bon pour ID2D1Factory peut être trouvé ici**:**
https://github.com/apitrace/dxsdk/blob/master/Include/d2d1_1.h

Comme je ne suis pas un expert en Composition Directe, j'ai ensuite utilisé les mêmes étapes que le malware :
La nouvelle fabrique retournée ne fournit aucun type détaillé. Elle indique void *, ce qui signifie qu'elle n'est pas officiellement documentée :

Comme je ne connais pas un type d'objet comme dans ce cas, j'ai développé un exécutable qui l'utilise pour le voir facilement en mémoire :

Ajoutez des points d'arrêt dans les quatre fonctions hook. Dans ce cas, un point d'arrêt dans hook2 montrera quand il capture le HeapHandle :

Le hook doit être arrêté lorsque le chunk désiré est capturé :

Placez des points d'arrêt dans les deux autres hooks :

Ensuite, il continue en appelant 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

Il essaie d'effectuer une sorte de casting dynamique. Si l'objet de type ID3D11Device peut accepter l'interface (utiliser les méthodes, etc.) de IDXGIDevice, il crée une copie de l'objet original qui accepte le nouveau type, après quoi il retourne le pointeur vers celui-ci. Dans ce cas, la variable d3dContext1 sera de type IDXGIDevice :

Les deux objets héritent de CLayeredObject<Cdevice>
L'objet ID3D11Device original est**:**

Comme celui qui retourne le pointeur.

Ensuite, il crée un objet ID2D1Device avec la fonction CreateDevice :

Dans value2, il retourne un objet de type ID2D1Device.
https://learn.microsoft.com/en-us/windows/win32/api/d2d1_1/nf-d2d1_1-id2d1device-createdevicecontext

Voici comment cela est implémenté dans le PoC :


Ensuite, appelez DCompositionCreateDevice
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-dcompositioncreatedevice


L'IID appartient à _IDCompositionDevice


Au même moment où la fonction est tracée sur DCompositionCreateDevice, elle s'arrête à hook3, lorsqu'elle appelle NtDCompositionCreateChannel :

De cette façon, elle capture l'adresse MappedAddress que le système utilise en interne lorsque DCompositionCreateDevice a été appelé :
Voici la pile d'appels jusqu'ici :

C'est le point où le module dcomp appelle la fonction NtDCompositionCreateChannel :

Après être revenu de l'étape précédente, sauvegardez l'adresse MappedAddress. En utilisant ALPC, il se connectera au processus DWM puis appellera CreateTargetForHwnd

Il utilise le handle HWND de la fenêtre créée. Il est lié au périphérique que je viens de créer, qui est le THIS de cette méthode :

Ensuite, appelez CreateSurface
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createsurface

Ensuite, appelez BeginDraw, EndDraw, et arrivez à CreateVisual.

Il appelle BeginDraw
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-begindraw
Cela utilise l'IID _IDXGISurface :


Ensuite, il utilise EndDraw :
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurface-enddraw


Enfin, il appelle CreateVisual :
https://learn.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositiondevice-createvisual

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


Et il appelle SetRoot :

L'objet updateObject reçu dans BeginDraw ne spécifie pas de quel type il s'agit dans la documentation.

Ensuite, il libère les objets créés précédemment :

Et maintenant, en utilisant le même objet dcompDevice de type IDCompositionDevice, il appelle la méthode Commit :


L'appel de cette méthode Commit s'arrête sur hook2 qui capture le HeapHandle souhaité :

Voici la pile d'appels maintenant :

Avant de retourner au main, il crée également un chunk en utilisant RtlAllocateHeap. Il est ensuite attrapé et stocké dans la variable base à l'intérieur de la fonction hook :


Les appels à Create et Allocate sont effectués l'un après l'autre :

Les deux (Allocate et Create) sont appelés depuis DirectComposition::Cdevice::Commit :

Après cela, lorsque NtDCompositionCommitChannel est appelé, il s'arrête à hook4 :

NtDCompositionCommitChannel est appelé depuis ici :

Il est également appelé depuis DirectComposition::Cdevice::Commit

Il est à noter que le système a déjà regroupé les commandes à envoyer par ALPC à DWM. Après cela, il envoie les commandes en utilisant NtDCompositionCommitChannel.
La fonction hook4 intercepte les appels à NtDCompositionCommitChannel et à ce stade, d'autres commandes seront ajoutées au lot.
Voyons ce que fait hook4 :
Une boucle est effectuée à travers le chunk pointé par base.
Elle quitte la boucle lorsqu'elle trouve la valeur 0x120 à l'intérieur du chunk :

Elle stocke l'adresse et le décalage où la valeur 0x120 a été trouvée :

Elle écrase la valeur 0x120 avec value4, qui est égale à 0x1b0 + 0x8f = 0x23f. C'est la taille qu'elle utilisera dans memcpy lors du débordement :



Elle ajoute 0xbc + 0x90 au pointeur d'adresse où se trouvait la valeur 0x120 :


Rappelez-vous qu'au décalage 0x48 de base se trouvait la taille 0x120. Celle-ci a été écrasée par 0x23f, donc le chunk d'origine doit avoir une taille de 0x120 :
La source est l'adresse du pointeur de 0x23f + 0x2c :

Il avait initialement ajouté 0x90 mais il soustrait maintenant 0x90 à nouveau. La destination sera l'adresse du pointeur vers 0x120 + 0xbc :

Il va écrire sur ceci :

Toutes les écritures seront à l'intérieur du chunk :

Il va répéter la boucle 3 fois, ce qui est le résultat de la division entière de 0x1b0/0x90 :

Après cela, comme le canal ArgChannelHandle est le même que celui utilisé lorsque l'adresse MappedAddress a été capturée, le PoC ajoutera des commandes au batch en utilisant NtDCompositionProcessChannelBatchBuffer. Celles-ci seront traitées avec celles que le système avait ajoutées. Le batch les collecte et les envoie toutes ensemble en utilisant NtDCompositionCommitChannel :


La commande envoyée a la valeur 8, qui correspond à SetResourceIntegerProperty pour 4 trackers différents (1, 2, 3 et 4).
Lorsque le PoC retourne à la fonction main, il crée un canal différent pour effectuer le HeapSpray.
Il regroupe 0x10000 commandes, qui sont envoyées avec NtDCompositionCommitChannel :

Cela utilise la valeur CreateResource=1 et le type qui correspond à CHolographicInteropTextureMarshaler = 0x50 :

Les allocations sont effectuées dans le code ci-dessous. La taille des objets créés pour réaliser le spray est 0x1b0 :

Il effectue ensuite une boucle pour libérer les objets créés à l'étape précédente et crée maintenant des trous dans la distribution mémoire. La variable counter2 commence à 0x3000 et ajoute des pas de 0x20 tant qu'elle est inférieure à 0x7000 :

Il écrit des 0x41 à partir de la direction du chunk qui était dans base + 0x48 + 44 + 0x1b0
C'est-à-dire qu'il écrit des valeurs qui seront utilisées plus tard, lorsqu'il débordera du chunk adjacent :

Cette pvalue7 se trouve à l'adresse 0x224 de base :

Ensuite, il va à la fonction « escribe » :

Il écrit la pKernelCallbacktable plus 0x388, l'adresse de LoadLibraryA et le chemin de la DLL qui sera chargée. Dans ce cas, je l'ai nommée s11.dll.

Maintenant, un débogueur noyau est nécessaire pour s'arrêter à la fonction vulnérable lorsque le débordement de tas se produit. En effet, le processus DWM ne peut pas être débogué avec un débogueur en mode utilisateur.
En utilisant IDA PRO pour déboguer la cible à distance, définissez un point d'arrêt conditionnel pour qu'il s'arrête lorsque la taille est égale à 0x1b0 :
print ("VALUE1 %x" % ((cpu.rax)))
return cpu.rax==0x1b0.
Puisqu'un programme en mode utilisateur est débogué depuis le noyau, il faut basculer vers le contexte du processus DWM pour placer le point d'arrêt. Rechargez les symboles utilisateur avec :
. reload /user
Rechargez les symboles noyau avec :
. reload /f

Il s'arrêtera lorsque ShowWindow sera exécutée :

Il alloue avec une taille 0x1b0 et copie avec une taille 0x23f, produisant le débordement de tas :

À ce stade, la pile d'appels ressemble à ceci :

Pour créer le débordement, le DWM reçoit des valeurs dans le code ci-dessous :
Les valeurs fabriquées dans base envoyées par mon PoC sont lues en utilisant MapViewofFile à partir du processus DWM dans le module dwmcore.dll :

La fonction précédente est appelée depuis :


Lorsqu'elle est envoyée en utilisant ALPC depuis hook4 en utilisant destination_copy (NtDCompositionCommitChannel), elle s'arrête :

Rappelez-vous que dans les commandes de hook4, des commandes ont été ajoutées au lot. Cependant, le système avait déjà ajouté quelques commandes au lot, y compris le base et les données fabriquées :



Dans ce cas, il partage une zone mémoire qui commence à 000001cd'178d0000. Lorsqu'elle est utilisée comme source pour effectuer le memcpy, elle sera 0x794 octets plus loin dans la même zone mémoire.
La taille de la zone mémoire partagée est 0x4000 :

Il s'arrêtera lorsque la taille à allouer sera 0x1b0, et atteindra le memcpy pour copier 0x23f octets :

Au-delà de 0x1b0, dans la mémoire, se trouve le code qui débordera en écrasant le bloc adjacent :

Lorsque les chunks sont libérés par le PoC, il se termine en sautant à LoadLibraryA, qui charge la bibliothèque fabriquée :

Cela vient d'ici :


Le Heap spray a été fait avec des objets de taille 0x1b0 de type CHolographicInteropTexture.
Comme j'avais créé des trous dans la distribution mémoire, cela libère certains objets. Comme le bloc qui va déborder a également une taille de 0x1b0, il a une forte probabilité de se trouver dans les trous du heap spray.
À la destination du memcpy, les blocs sont situés tous les 0x1b0 octets :

Le pointeur vers une vftable est écrasé par le pointeur vers LoadLibrary :
Avant l'écrasement :

Après l'écrasement :

Rappelez-vous qu'il a fini par sauter à [R11+50], qui est le pointeur vers LoadLibraryA.
En exécutant le PoC, copiez la DLL dans le même chemin que celui indiqué dans le PoC :

Après l'exécution du PoC, un processus CMD est exécuté avec les privilèges de niveau d'intégrité système de l'utilisateur DWM :

Références :
PoC at 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
Ceci complète le PoC. N'oubliez pas que si vous l'exécutez plusieurs fois, le tas restera dans un état instable, donc vous devrez peut-être redémarrer la machine pour le faire fonctionner à nouveau. De plus, bien qu'il ne fonctionne pas toujours du premier coup, il fonctionnera généralement correctement lors d'une deuxième ou troisième tentative. Comme vous pouvez le voir, le reversing peut être difficile, donc si vous avez des questions, vous pouvez me consulter.
Mail: [email protected]
X: @ricnar456