
Depuis février 2022, un nouveau ransomware a été signalé, qui semble utiliser une vulnérabilité zero-day de Windows, selon les recherches menées par Trend Micro.
Plus d'informations sur ce ransomware peuvent être trouvées à ce lien.
Selon l'analyse de Kaspersky, le groupe de ransomware Nokoyawa a utilisé d'autres exploits ciblant le pilote Common Log File System (CLFS) depuis juin 2022, avec des caractéristiques similaires mais distinctes, tous liés à un seul développeur d'exploit.
En avril 2023, lorsque Microsoft a publié le correctif, le CVE-2023-28252 a été attribué.
Auparavant, en 2022, un bogue similaire dans le même composant a été recherché par nous, et documenté dans ce billet de blog.
Pour mener l'analyse, il est nécessaire de connaître le format de fichier .blf, qui est géré par le pilote Common Log File System vulnérable appelé CLFS.sys et qui se trouve dans le dossier des pilotes dans system32.
Plus d'informations sur ce type de fichier peuvent être trouvées dans les liens ci-dessous :
https://github.com/ionescu007/clfs-docs/blob/main/README.md
https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe
Cette analyse est réalisée pour Windows 11 21H2, clfs.sys version 10.0.22000.1574 bien qu'elle fonctionne également sur Windows 10 21H2, Windows 10 22H2, Windows 11 22H2 et Windows server 2022.
Dans les versions précédentes de Windows, il est nécessaire d'ajuster certaines valeurs, sinon nous produirions un BSOD.
Microsoft Patch Tuesday avril 2023.
Vous pouvez vérifier la version du pilote comme indiqué.
Lorsque la vulnérabilité a été publiée, en avril 2023, j'ai commencé avec Esteban Kazimirow à effectuer le reverse engineering du pilote CLFS.sys, bien que dans ce cas, analyser uniquement le correctif était très difficile pour déduire où se trouvait le bogue et comment le déclencher, car l'exploitation est très complexe.
Plus tard, un billet de blog est sorti dont l'auteur, à partir d'un échantillon de malware, a montré certaines parties du code décompilé par HexRays et quelques informations qui ont guidé la direction à prendre pour l'exploitation.
Évidemment, les informations fournies n'étaient pas complètes, mais sans cette aide, il aurait été peu probable d'arriver à construire le PoC puis un exploit fonctionnel.
Pour faciliter la compréhension, nous expliquerons d'abord comment construire le PoC, puis nous ferons l'analyse de la vulnérabilité.
Ce billet de blog contient deux sections :
Construction du PoC:
1- Obtenir les adresses noyau dont nous avons besoin pour l'exploitation.
2- Préparer le chemin pour créer les fichiers .blf :
3- Créer le fichier "trigger blf" en utilisant la fonction CreateLogFile()
4- Façonner le fichier "trigger blf"
5- Obtenir l'adresse noyau du BLOC DE BASE du trigger blf
6- Appeler AddLogContainer avec le handle du trigger blf
7- Préparer les fichiers spray blf
8- Préparer la mémoire pour effectuer le spray
9- Déclencher le bogue
Débogage :
1- Vérifier le spray mémoire
2- Regarder le RecordOffset[12] du trigger blf
3- Regarder la valeur iFlushBlock dans le fichier spray blf
4- Pourquoi lit-il à partir du BLOC 1 SHADOW au lieu du BLOC 0 CONTROL ?
5- Pourquoi la somme de contrôle est-elle égale à zéro dans les fichiers spray blf ?
6- Terminer l'exploitation.
7- Le vrai correctif
Je vais créer une fonction nommée InitEnvironment pour obtenir certaines adresses noyau nécessaires.
Obtenez l'adresse EPROCESS de mon processus et stockez-la dans la variable g_EProcessAddress, puis l'adresse EPROCESS du processus SYSTEM, et stockez-la dans system_EPROCESS, puis l'adresse EHTREAD du thread principal de mon processus, et je la stocke dans g_EThreadAddress et enfin l'adresse du PREVIOUS MODE qui dans cette version du PoC ne sera pas utilisée.

Cette méthode est bien connue, la fonction GetObjectKernelAddress appelle NtQuerySystemInformation deux fois avec le premier argument SystemExtendedHandleInformation, le premier appel est passé avec une taille incorrecte et retourne une erreur, mais retourne également la taille correcte qui est utilisée dans le second appel et obtient les informations de tous les handles, puis en parcourant dans une boucle les informations de chaque handle et dans le champ Object du handleinfo correct, elle obtient l'adresse recherchée dans le noyau.

J'ai aussi besoin des adresses noyau des fonctions suivantes exportées par CLFS.sys :
• ClfsEarlierLsn
• ClfsMgmtDeregisterManagedClient
Et les fonctions exportées de NTOSKRNL.exe
• RtlClearBit/PoFxProcessorNotification
• SeSetAccessStateGenericMapping
Pour obtenir ces adresses, on utilise une méthode similaire à celle utilisée pour obtenir la base noyau des deux modules, en appelant NtQuerySystemInformation deux fois, mais dans ce cas le premier argument sera SYSTEM_INFORMATION_CLASS (dans le PoC nous utilisons la fonction FindKernelModulesBase à cet effet).
Ensuite, il charge CLFS.sys et NTOSKRNL.exe en tant que modules normaux en mode utilisateur en appelant LoadLibrary, obtient les adresses en mode utilisateur avec GetProcAddress, puis soustrait la base d'image de chacun, ce qui donne le décalage de la fonction et enfin ajoute chaque décalage aux bases noyau correspondantes et obtient ainsi les adresses noyau de toutes les fonctions nécessaires.

Je crée une fonction appelée createInitialTriggerBlfFile qui va générer et écrire un fichier .blf.
Le chemin utilisé comme argument dans CreateLogFile est différent d'un chemin normal, par exemple pour ouvrir le fichier 1280.blf situé dans le dossier C:\Users\Public, nous devons définir le chemin LOG:C:\Users\Public\1280. Cela sera enregistré dans la variable stored_name_CreateLog.
Je fais cela en utilisant wsprintfW() car stored_env stocke le chemin C:\Users\Public, précédemment obtenu à partir des variables d'environnement. À cette chaîne, je vais ajouter la chaîne LOG: et un nom aléatoire à la fin, sans l'extension .blf.

Ce sera le chemin de mon fichier initial que j'appellerai "trigger blf". Bien sûr, je dois aussi sauvegarder le chemin normal vers le même fichier sans le LOG: devant et avec l'extension BLF pour l'ouvrir et le modifier avec CreateFile(), WriteFile() comme n'importe quel autre fichier ; ce chemin sera, par exemple : C:\Users\Public\1280.blf, et il sera stocké dans la variable stored_name_fopen.

Bien sûr, les deux chemins correspondent au même fichier, et je dois utiliser l'un ou l'autre selon le cas.
La fonction CreateLogFile remplit une fonction assez similaire à CreateFile() (crée de nouveaux fichiers ou ouvre des fichiers existants et obtient leur handle), certains arguments sont même similaires, mais CreateLogFile() fonctionne uniquement avec les fichiers blf.
De plus, lorsqu'il ouvre un fichier existant, il vérifie que le format est correct, même si chaque bloc a une somme de contrôle et si celle-ci n'est pas correcte, il retourne une erreur.
Je vais créer 2 types de fichiers BLF :
Le Trigger blf
Le Spray blf
Les deux sont des fichiers blf mais modifiés de manière différente.
De cette façon, le PoC crée d'abord le fichier "trigger blf", en utilisant CreateLogFile, avec le chemin par exemple : LOG:C:\Users\Public\1280 que j'ai configuré auparavant, et qui était stocké dans la variable stored_name_CreateLog.
Le cinquième argument fCreateDisposition, comme dans CreateFileA(), peut prendre les valeurs suivantes :

Dans ce cas, j'utiliserai l'argument OPEN_ALWAYS, donc le fichier sera créé s'il n'existe pas et s'il existe, il sera ouvert. Comme le fichier n'existe pas encore, il sera créé avec un nom aléatoire.
logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);
CreateLogFile() créera notre fichier "trigger blf" avec ses 6 blocs et leurs sommes de contrôle correspondantes et retournera le handle qui sera stocké dans la variable logFile.

Chaque bloc aura, à partir du décalage indiqué dans la colonne de gauche, un en-tête dont la taille est de 0x70 octets.
Ainsi, par exemple, l'en-tête du BLOC DE CONTRÔLE va du décalage 0x0 à 0x70.

Tous les en-têtes de tous les blocs ont la même structure appelée _CLFS_LOG_BLOCK_HEADER.
Voici la structure de l'en-tête :

Au décalage 0xC de l'en-tête, je trouve la somme de contrôle, donc comme le BLOC DE CONTRÔLE commence au décalage 0, la somme de contrôle sera au décalage 0xC du fichier, et ainsi chaque bloc aura sa somme de contrôle à 0xC du début de son bloc.

Pour modifier le fichier trigger blf, je dois l'ouvrir comme un fichier normal soit avec CreateFileA soit avec fopen puis le modifier avec WriteFile ou fwrite respectivement, j'effectue cela au début de la fonction fun_prepare du PoC.
Rappelez-vous que le chemin normal est stocké dans la variable stored_name_fopen, donc je l'utilise pour ouvrir le fichier avec wfopen_s (qui est une variante de fopen qui supporte les chaînes Unicode).
Le fichier est modifié dans la fonction craftTriggerBlfFile appelée depuis fun_prepare.

Ensuite, j'appelle fseek pour pointer vers le décalage à modifier, puis avec fwrite le fichier est modifié.
Les modifications à apporter au fichier "trigger blf" sont les suivantes :
Après avoir effectué ces modifications, la fonction FixCRCFile est appelée pour calculer la nouvelle somme de contrôle et corriger les sommes de contrôle des 4 premiers blocs. Les deux blocs suivants n'ont aucune modification, il n'est donc pas nécessaire de recalculer leurs sommes de contrôle.

Le pilote CLFS.sys lit les six blocs du fichier, et pour stocker leur contenu, il effectue une allocation dans le pool noyau.

Il y a une structure très importante de taille 0x90 qui, dans le billet de blog précédent sur CVE-2022-37969, grâce au reverse engineering, j'ai trouvé certains champs et je l'ai appelée pool_0x90. Après beaucoup plus de reverse engineering, je sais maintenant que son vrai nom est m_rgBlocks et que, au fur et à mesure que le contrôleur alloue de la mémoire pour copier le contenu de chaque bloc à partir du fichier, il y enregistre la taille de chaque bloc, le décalage de début et l'adresse noyau où il a été stocké.

Elle contient six CLFS_METADATA_BLOCK qui correspondent à chaque bloc par son numéro.
Chaque structure CLFS_METADATA_BLOCK a une longueur de 0x18 octets. (0x18*6=0x90)
Au décalage 0, il y a une union, mais au moins dans cet exploit seul le champ pbImage est utilisé, donc en simplifiant cela donnerait :
L'allocation de cette structure peut se faire à partir de deux endroits différents du pilote CLFS.sys, selon la création d'un nouveau fichier ou si un fichier existant est ouvert. Dans le cas de la création d'un nouveau fichier, le pilote alloue les 0x90 octets depuis CClfsBaseFilePersisted::CreateImage+28A, tandis que dans le cas d'un fichier existant, il alloue depuis CClfsBaseFilePersisted:ReadImage+6E.
Après cela, je vais obtenir l'adresse de début du bloc 2 qui correspond au fichier trigger blf, appelé BLOC DE BASE qui commence au décalage 0x800 et dont la longueur est 0x7a00.

À l'intérieur de la fonction fun_prepare ci-dessous, cette adresse sera trouvée dans le noyau en utilisant cette partie du code.

Tout d'abord, la fonction getBigPoolInfo trouve toutes les allocations dans le pool qui ont le tag "Clfs" et une taille de 0x7a00, puis les stocke dans un tableau.
Après cela, elle ouvre à nouveau le fichier trigger blf précédemment modifié en utilisant CreateLogFile avec l'argument OPEN_EXISTING, donc elle ouvre un fichier existant, ce qui effectuera l'allocation de son BLOC DE BASE.
Lorsque getBigPoolInfo est appelée à nouveau, il y aura un nouveau pool "Clfs" de taille 0x7a00, et son adresse est récupérée en appelant NtQuerySystemInformation deux fois.
L'adresse du BLOC DE BASE du fichier trigger blf est stockée dans la variable CLFS_kernelAddrArray.

Notez que si le fichier trigger blf modifié n'a pas la somme de contrôle correcte, la fonction CreateLogFile() échouera.
La dernière partie de la fonction fun_prepare appelle l'api AddLogContainer en utilisant le handle du fichier trigger blf.

Dans la dernière fonction du PoC appelée to_trigger, un deuxième type de fichier blf sera créé.
Je le nommerai spray blf.
Ce type de fichier sera utilisé pour remplir un espace mémoire (spray), 10 exemplaires de ce type sont nécessaires, mais initialement un seul est créé. 
Trois tableaux seront créés pour stocker les noms aléatoires de ces fichiers :
stored_log_arrays: stocke dix nouveaux noms aléatoires de fichiers .blf qui seront utilisés avec CreateLogFile.
stored_container_arrays: stocke des noms aléatoires pour créer dix nouveaux fichiers conteneurs.
stored_fopen_arrays: stocke les noms des fichiers journaux du premier tableau (variable stored_log_arrays), mais avec leur chemin normal (sans la chaîne "LOG:") et avec l'extension .blf.

À chaque itération, le fichier blf est copié en utilisant CopyFileW, les noms stockés dans les tableaux sont attribués.
La fonction fun_trigger appelle craftSprayBlfFile où des modifications sont apportées à chaque fichier et FixCRCFile corrigera les CRC.

En résumé, j'ai créé 10 fichiers similaires (spray blf) avec des noms aléatoires avec les modifications suivantes :

La dernière modification consiste à copier l'intégralité du bloc 0 (BLOC DE CONTRÔLE) vers le bloc 1 (BLOC DE CONTRÔLE SHADOW)

L'effet de ces modifications, plus celles apportées au fichier trigger blf, sera expliqué plus tard dans le chapitre de débogage.
Certaines de ces modifications sont celles qui produisent la vulnérabilité, tandis que d'autres ne sont nécessaires que pour contourner les vérifications du pilote.
À ce stade, les fichiers sont déjà créés et modifiés, prêts à effectuer le spray, puis lorsqu'ils sont ouverts avec CreateLogFile, ils seront situés dans la zone mémoire que nous souhaitons, comme cela sera montré plus tard.

Dans la fonction to_trigger, un tableau de 12 éléments est créé, contenant l'adresse du BLOC DE BASE du fichier trigger blf plus 0x30.
Ensuite, dans la fonction fun_pipeSpray, la mémoire est remplie avec un spray de pipes, à l'intérieur il y a une boucle qui appelle CreatePipe et crée le nombre de pipes passé comme premier argument, le deuxième argument est un tableau qui stockera les handles de toutes les pipes créées.

Dans une boucle, elle appelle CreatePipe créant des pipes en lecture-écriture.
De cette façon, d'abord 0x5000 pipes seront créées, puis elle appelle à nouveau pour créer d'autres 0x4000 pipes.
Ensuite, elle utilise WriteFile pour écrire dans les 5000 premières pipes le tableau récemment créé avec les adresses de BASE BLOCK + 0x30 du fichier trigger blf.

Maintenant, elle a déjà un bloc compact créé en mémoire, elle libérera 0x667 pipes à partir du numéro 0x2000 et jusqu'au 0x2667, car en mémoire les pipes ne sont pas dans le même ordre que celui dans lequel elles ont été créées ; ce qui va se passer, c'est qu'il y aura des espaces libres dans ce bloc mémoire.
Notez que les allocations des pipes ont une taille utilisateur de 0x90 octets, donc lorsqu'elles sont libérées, nous aurons
Cela libère les espaces mémoire de taille 0x90 entre la mémoire remplie de pipes.
Ensuite, elle boucle pour appeler CreateLogFile avec les 10 fichiers spray blf.
Lorsque CreateLogFile est appelé pour ouvrir des fichiers existants, l'allocation de 0x90 octets est effectuée pour les m_rgBlocks, un pour chaque fichier spray blf, donc ces allocations occuperont les trous laissés lors de la libération des pipes puisqu'elles ont la même taille.

Ensuite, elle répète le processus d'écriture dans les 0x4000 pipes finales du tableau qui contient l'adresse de BASE BLOCK +0x30 du trigger blf.
Toutes ces manipulations créent un espace mémoire contrôlé, je vais vous montrer comment il est lorsqu'il est en cours de débogage, mais l'idée est que les m_rgBlocks de chaque fichier spray blf occupent les trous de 0x90 octets qui ont été libérés.
Ensuite, déjà dans la partie finale, le bogue est déclenché à l'intérieur d'un while(1) en utilisant un appel à AddLogContainer sur les fichiers spray blf.
Pendant que cela se produit, le bogue est déclenché :

Cette boucle while se termine lorsqu'elle trouve le jeton Système, en utilisant la fonction NtFsControlFile qui lira les attributs des pipes.

Ensuite, en utilisant CreateLogFile, il réécrit à nouveau le jeton de notre processus avec le jeton Système récemment trouvé et de cette manière nous obtenons l'élévation de privilèges.

Ensuite, il restaure certaines valeurs, ferme les handles des pipes et des fichiers blf, et exécute un Notepad en tant que Système pour vérifier que l'élévation a bien eu lieu.

Notez les fichiers blf créés dans le dossier PUBLIC. Souvenez-vous que si vous voulez réessayer, vous devez d'abord supprimer les fichiers créés. Certains seront verrouillés et ne pourront pas être supprimés, mais le PoC fonctionnera quand même.

Avant de commencer avec l'effet des modifications sur trigger blf et spray blf pour réaliser l'exploitation, je dois vérifier que m_rgBlocks des spray blf files se trouvent dans les trous qui apparaissent dans la distribution mémoire, après avoir effectué le spray de pipes et la libération ultérieure d'un nombre fixe de pipes.
Lorsque cette procédure se termine, un pipe devrait se trouver sous les 0x90 octets de m_rgBlocks, donc lorsque m_rgBlocks est utilisé, un OUT OF BOUNDS se produira et il lira à partir de ce pipe qui se trouve en dessous.
Le PoC a un point idéal pour placer un point d'arrêt :

À ce stade, l'ouverture des fichiers spray blf est terminée et la fonction AddLogContainer n'est pas encore appelée.
Pour déboguer en mode utilisateur, j'utiliserai x64dbg et pour le mode noyau, IDA avec le plugin Windbg.

À ce stade, la mémoire devrait déjà être préparée, et je peux voir la distribution.
Je vais mettre IDA en pause pour trouver un point intéressant où placer un point d'arrêt.
Je vais configurer un point d'arrêt à CClfsBaseFilePersisted::AddContainer, qui est appelé depuis AddLogContainer et au début, il a le registre RCX pointant vers la structure CClfsBaseFilePersisted et à l'offset 0x30 il y a un pointeur vers m_rgBlocks.

Lorsque le point d'arrêt est atteint, je vérifie sur la pile d'appels que AddLogContainer est appelé depuis mon PoC.

Le registre RCX pointe vers :

Le premier champ est le pointeur vers une vtable (CLFS! CClfsBaseFilePersisted::'vftable') et à l'offset 0x30 se trouve le pointeur vers m_rgBlocks.

Les blocs 0, 1, 4 et 5 n'ont pas encore enregistré le pbImage, tandis que les blocs 2 (BASE BLOCK) et 3 (SHADOW BLOCK) l'ont fait.
Chaque bloc dans la table m_rgBlocks a son cbOffset qui est le décalage où le bloc commence dans le fichier, cbImage est la taille du bloc, et eBlockType est le type de bloc.
Si le spray est correct, en dessous de m_rgBlocks il devrait y avoir un pipe et à l'intérieur, les pointeurs vers BASE BLOCK + 0x30 du trigger blf.

La commande "!pool" sur windbg affiche la distribution mémoire :

Chaque m_rgBlocks a une étiquette "Clfs" et sa taille est 0xa0 car c'est la taille utilisateur 0x90 plus l'en-tête 0x10 et en dessous il y a un pipe avec l'étiquette "NpFr" qui a la même taille utilisateur 0x90
Comme la distribution n'est pas une science exacte, certains "Clfs" ont été placés continuellement, ce qui n'est pas souhaitable, mais celui avec lequel je travaille est correctement placé, suivi d'un pipe.
L'une des premières modifications qui affecte est celle apportée dans le fichier trigger blf à l'offset 0x858, où la valeur 0x369 est stockée.

Le BASE BLOCK commence à l'offset 0x800 dans le fichier.

À l'intérieur du _CLFS_LOG_BLOCK_HEADER à l'offset 0x800+0x58 (0x58 depuis le début de l'en-tête BASE BLOCK).

À l'offset 0x28 commence le tableau RecordOffsets (DWORD).
En avançant de 0x30 octets, à l'offset 0x58 (0x828+0x30=0x858 depuis le début), se trouve le champ 12 de RecordOffsets.

J'exécute le PoC jusqu'à CreateLogFile comme montré dans l'image ci-dessous :


Avant d'entrer dans CreateLogFile, je vais placer un point d'arrêt à un endroit où la valeur 0x369 n'a pas encore été utilisée.
Dans le cas où CreateLogFile ouvre un fichier existant, la structure m_rgBlocks est allouée ici :
CClfsBaseFilePersisted::ReadImage+6E
Donc, je vais placer un point d'arrêt sur IDA juste ici :

Lorsque le point d'arrêt est déclenché :

Dans m_rgBlocks il y a encore des déchets car il n'est pas encore initialisé, mais dès que pbImage du bloc 2 est alloué, l'adresse sera sauvegardée à l'offset 0x30 depuis le début, car le premier champ à l'intérieur de chaque CLFS_METADATA_BLOCK est pbImage.


Maintenant, je configure un point d'arrêt matériel en écriture : ba w1 ffffd003'7f5bea30
Après l'initialisation à zéro, il s'arrête lorsqu'il sauvegarde pbImage.

L'analyse dit que cela correspond au block0, car il ne prend pas en compte la constante r14*8 qui est 0x30 après, par conséquent il écrit vraiment le pbImage du block 2.

Notez que CClfsBaseFilePersisted::ReadMetadataBlock est utilisé pour allouer n'importe lequel des blocs, en utilisant la taille passée en argument.

Maintenant, placez un point d'arrêt lecture/écriture à 0x58 du bloc de base, pour voir quand il utilise la valeur 0x369.
ba r1 FFFF978A'16ECF000+0x58

Lorsque le point d'arrêt est atteint, il lit la valeur 0x369 située au RecordOffset[12], l'ajoute à un pointeur étrange dans r14 et incrémente le contenu de RAX+r14.
Quelques lignes plus haut dans le code, ESI a la valeur 0x13 et multiplie par 0x18, qui est la taille de chaque bloc dans m_rgBlocks.
WINDBG>? 0x18*0x13
Evaluate expression: 456 = 00000000'000001c8
Si j'ajoute la valeur de r8= 0x1c8 qui est supérieure à 0x90, à l'adresse initiale de m_rgBlocks, il lira OUT OF BOUNDS.


En dessous de m_rgBlocks, se trouve le pipe avec le pointeur vers BASE BLOCK + 0x30, il lit ce pointeur qui a été stratégiquement placé à l'intérieur du pipe.
La position actuelle dans le
code a été appelée depuis l'instruction while(1) du module principal.
À l'intérieur du fichier spray blf j'ai stratégiquement placé la valeur 0x13 à l'offset 0x48a (iFlushBlock).

À l'offset 0x8a du fichier spray blf, iFlushBlock du BLOCK 0 est situé, dont la valeur est 4, tandis que l'offset 0x48a appartient à iFlushBlock du BLOCK 1, et sa valeur est 0x13

Maintenant, je dois découvrir pourquoi il lit iFlushBlock = 0x13 du BLOCK 1 au lieu de iFlushBlock = 4 du BLOCK 0.
Si je regarde en arrière pour trouver d'où vient le 0x13, je vois sur la pile d'appels que WriteMetadataBlock est appelé depuis CClfsBaseFilePersisted::ExtendMetadataBlock+416, là le deuxième argument iFlushBlock est EDX=0x13, qui vient de r9w.


Quelques lignes avant,
CClfsBaseFile::GetControlRecord a été appelé pour récupérer l'adresse
du BLOCK 0, peut-être que le problème est ici, donc je vais redémarrer et placer un point d'arrêt dessus.
GetControlRecord appelle CClfsBaseFile::AcquireMetadataBlock qui devrait remplir la table m_rgBlocks avec l'adresse du block 0, quand je passe outre cette fonction, elle obtient l'adresse du block 1, donc le problème se produit à l'intérieur de CClfsBaseFile::AcquireMetadataBlock.
En ajoutant 0x8A à l'adresse récupérée, je peux confirmer que la valeur 0x13 qui appartient au BLOCK 1 est présente.

Je vais redémarrer et placer un point d'arrêt là :

CClfsBaseFile::GetControlRecord+27 appelle CClfsBaseFile::AcquireMetadataBlock

Le second argument passé à AcquireMetadataBlock est zéro, il
correspond au block 0, il va copier depuis le fichier et stocker son adresse
dans m_rgBlocks
.
Dans l'énumération _CLFS_METADATA_BLOCK_TYPE, les noms des types de bloc sont différents de ceux que j'ai utilisés, mais ce sont les mêmes 6 blocs.

Après avoir vérifié que le type de bloc est inférieur au maximum m_cBlocks=6, il sauvegarde une valeur de référence pour éviter de lire le même bloc deux fois.

ReadMetadataBlock est appelé, le problème de la lecture du block 1 au lieu du block 0 serait à l'intérieur de cette fonction.

Si tout va bien, il alloue en utilisant cbImage comme taille et stocke l'adresse dans le champ block 0-> pbImage dans m_rgBlocks.

La commande !pool affiche l'étiquette et la taille allouée.

Donc, j'ai déjà l'adresse de
pbImage du block 0 stockée dans m_rgBlocks, donc j'ai besoin de voir pourquoi il copie les octets du block 1 là au lieu des octets du block 0.
J'arrive à un appel à CClfsContainer::ReadSector où un pointeur vers une variable contenant pbImage est passé, pour écrire les octets.

Remarquez les modifications dans le contenu de pbimage lorsque je passe outre ReadSector.

En ajoutant 0x8a à pbImage je peux trouver la valeur 4 qui est la valeur correcte, au lieu de 0x13, donc le problème doit se produire plus tard.
Après avoir appelé ClfsDecodeBlock, il retourne une erreur 0x0C01A000A.
CClfsBaseFilePersisted::ReadMetadataBlock+153 appelle ClfsDecodeBlock
Après cette erreur, il ajoute 1 au type et appelle CClfsBaseFilePersisted::ReadMetadataBlock à nouveau mais avec le type 1 pour lire le block 1.

Dans CClfsBaseFilePersisted::ReadMetadataBlock, il alloue et stocke un nouveau pbImage dans m_rgBlocks pour le block 1.

Les blocks 0 et 1 ont des adresses différentes, maintenant si j'ajoute 0x8a à l'adresse du block 1, sa valeur est 0x13.
Peut-être que puisque le block 0 a retourné une erreur, il utilise le block 1 et le retourne à GetControlRecord en tant que Block de Contrôle.
Comme montré précédemment, lorsqu'il utilise la valeur 0x13 au lieu de 4, il va au-delà des limites de m_rgBlocks et lit les valeurs du spray de pipes contrôlées par moi.
Ensuite, il libère le pbImage du block 0
et copie le pointeur du block 1 vers le block 0.

Il serait nécessaire de trouver la valeur qui cause l'erreur 0x0C01A000A à l'intérieur de ClfsDecodeBlock.
À l'intérieur de ClfsDecodeBlock, la checksum du premier bloc est zéro, c'est l'erreur 0xC01A000A.

Avant d'appeler AddLogContainer, en ouvrant n'importe quel fichier spray blf avec un éditeur hexadécimal, la checksum a été changée en zéro.

Elle aurait dû être changée avant lorsqu'il a été ouvert avec CreateLogFile.

Pour une raison quelconque, les fichiers spray blf se retrouvent après la sortie de CreateLogFile avec la checksum du block 0 égale à 0 et retournent un handle valide, voyons pourquoi cela se produit.
Je m'arrête à CreateLogFile avant d'ouvrir un fichier spray blf.

Notez qu'avant d'appeler CreateLogFile, les fichiers spray ont la checksum correcte dans block 0 et après avoir terminé la fonction, la valeur de la checksum devient zéro.

Donc, je place un point d'arrêt sur CClfsBaseFile::GetControlRecord, pour regarder à l'intérieur.
Après avoir passé
CClfsContainer::ReadSector, la checksum n'est pas zéro.
Avant d'entrer dans le calcul du CRC32, il met le champ checksum à zéro en mémoire pour calculer le CRC, et le résultat est correct.


Ensuite, il vérifie la valeur de eExtendState =2 et va à WriteMetadataBlock.

Ici, la checksum est encore zéro en mémoire, j'ai juste besoin de voir quand cette valeur est écrite dans le fichier.

Il vérifie certaines valeurs qui sont conçues dans le fichier blf spray pour atteindre CClfsBaseFilePersisted::ExtendMetadataBlock.

Après une boucle pour lire les blocs qui n'ont pas encore été lus, le block 0 continue avec checksum = 0.

En arrivant à WriteMetadataBlock.

Comme je suis en train de l'exécuter avant qu'il ne remplace le block 0 par 1, la valeur iFlushBlock du fichier blf spray est encore 4, la valeur correcte.

Maintenant, il travaille avec le block 4, et il va écrire le block 4 dans le fichier, ici n'est pas encore le problème.
Ensuite, il arrive à CClfsBaseFilePersisted::FlushControlRecord

À l'intérieur, il atteint WriteMetadataBlock, mais avec l'argument 0, pour écrire le block 0 dans le fichier**.

Ensuite, ClfsEncodeBlock retourne l'erreur 0xC01A000A, bien qu'il va écrire le fichier avec le mauvais block 0 dans CClfsContainer::WriteSector, juste en dessous.

La variable var_54 stocke la valeur d'erreur 0xC01A000A et sera vérifiée avant de quitter la fonction.

Mais après avoir appelé CClfsContainer::WriteSector qui ne retourne pas d'erreur, le contenu de var_54 est écrasé avec zéro.
Donc, la fonction retourne zéro sans erreur et continue de fonctionner car CreateLogFile retournera un handle au lieu d'une valeur d'erreur.

La valeur 0x13 dans iFlushBlock provoque un dépassement hors limites et il lira le pointeur qui se trouve dans les pipes et qui pointe vers le Base Block +30 du trigger blf.
Ensuite, il ajoute 0x28 à ce pointeur, ( 0x58 depuis le début du bloc de base du déclencheur blf) qui a la valeur 0x369.


L'instruction INC va augmenter la valeur 0x14 de 1 et se répète 4 fois, donc 0x14 se termine à 0x18.
WINDBG>db r14+369
ffffcb82'091e7397 14 00 00 00
Après cela, CreateLogFile est appelé, et lit la valeur 0x1858.

GetSymbol vérifie si le bloc factice précédemment créé dans déclencheur blf, pointé par l'offset 0x1858, a les bonnes valeurs.

si le pointeur n'avait pas été incrémenté plusieurs fois, il aurait la valeur originale 0x1458 et pointerait vers le bon bloc.
Après avoir quitté GetSymbol, il va utiliser ce bloc factice ici.

Ensuite, il va lire la valeur de l'offset 0x18 du bloc factice où j'ai placé 0x05000000 et sauter au contenu de ce qui s'y trouve.
WINDBG>dps 0x5000000
00000000'05000000 00000000'05001000

Il lit le contenu de 0x05000000 et son 0x05001000 et là se trouve ClfsEarlierLsn.

Cette fonction est utilisée pour retourner la valeur 0xFFFFFFFF dans RDX bien que cette première fois cette valeur ne soit pas utilisée.
Le deuxième appel a lieu ici, il appelle PoFxProcessorNotification qui se trouvait à 0x501000 +8

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

dans cette fonction RCX = 0x05000000 , il vérifie que 0x40 octets plus loin doit être non nul
WINDBG>dps rcx+40
00000000'05000040 00000000'05000000
L'adresse vers laquelle sauter sera 0x68 plus loin.
WINDBG>dps rcx+68
00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient
Et l'argument sera 0x48 octets plus loin.
WINDBG>dps rcx+48
00000000'05000048 00000000'05000400
Le ClfsMgmtDeregisterManagedClient, c'est une fonction pratique car je peux contrôler l'argument et j'ai également deux sauts vers des fonctions que je contrôle.

Le premier appel est de nouveau à ClfsEarlierLsn qui a retourné RDX=0xFFFFFFFF.


il va prendre la source à écrire à partir du contenu de RDX=0xFFFFFFFF.
WINDBG>dps rdx
00000000'ffffffff ffff8005'3a4ee000
À l'adresse 0xFFFFFFFF j'avais stocké le system_EPROCESS & 0xfffffffffffff000.

La destination est le pointeur situé à 0x5000400 +0x48
*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

Le pointeur PipeAttribute dans le noyau qui pointe vers un tampon rempli de « A » sera écrasé avec la partie haute du pointeur SYSTEM EPROCESS.
Ce pointeur a été créé lorsque j'ai appelé précédemment _NtFsControlFile avec un tampon plein de « A ».

Le contenu de cet attribut peut être lu en utilisant NtFsControlFile.

Maintenant, l'attribut du pipe ne pointe plus vers le tampon avec « A » mais vers system_EPROCESS & 0xffffffffffffff000.

Ce code sera répété jusqu'à ce que le jeton système soit récupéré.


Sur Windows 11, le jeton système se trouve à l'offset 0x4b8 de la structure EPROCESS récemment lue.

J'ai seulement besoin d'écrire ce jeton système dans mon processus en appelant CreateLogFile.

Pour effectuer ce travail, il suffit de répéter l'étape utilisée pour lire le jeton système.

Dans le double appel, il appelle d'abord ClfsEarlierLsn pour retourner 0xFFFFFFFF dans RDX puis appelle nt_SeSetAccessStateGenericMapping.

Je vérifie que la valeur pointée par RDX est le jeton système.

Le jeton de mon processus est :

Il va écrire là.
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'f601c06c
WINDBG>dps rax+8
ffff9b8b'fc446578 ffffc402'ef841919
Maintenant mon processus est System je peux exécuter un Notepad pour vérifier.



BINDIFF montre beaucoup de fonctions modifiées

La fonction vulnérable est ici :


Le primaire est la version patchée, le secondaire est la version vulnérable.
Le correctif teste la valeur de retour de CflsEncodeBlock, qui est 0xC01A000A, la stocke dans la variable var_54, et comme elle est négative, la vérifie et évite le WriteSector.
Le correctif, en plus de ne pas écrire le fichier, la fonction retourne correctement 0xc01a000a, avec lequel CreateLogFile ne retourne aucun handle et l'exploitation ne peut pas continuer.


Seulement si ClfsDecodeBlock n'est pas négatif, il va à WriteSector mais laisse retourner la valeur négative 0xC01A000A.
C'est le correctif réel qui empêche véritablement l'exploitation en utilisant le PoC que je viens de joindre.
À ce stade, nous avons expliqué comment le bug a été exploité, cela conduit à contrôler les fonctions qui nous permettent de lire le jeton SYSTEM et de l'écrire dans notre propre processus pour réaliser l'élévation de privilèges locale. Vous pouvez trouver le PoC fonctionnel sur le GitHub de Fortra.
Nous espérons que cela vous sera utile, si vous avez des questions, vous pouvez nous contacter :