
Exploit de preuve de concept pour CVE-2022-37969, une élévation de privilèges locale du pilote Windows Common Log File System. Démontre le heap spray, le vol de jeton et l'écriture arbitraire dans le noyau pour obtenir les privilèges SYSTEM.
auteurs : Ricardo Narvaja & Daniel Kazimirow (Solid)
À des fins de démonstration uniquement. L'exploit complet fonctionne sur les systèmes vulnérables Windows 11 21H2.
PoC fonctionnel basé sur les informations précédemment publiées par Zscaler
Consultez l'article Understanding the CVE-2022-37969 Windows Common Log File System Driver Local Privilege Escalation.
Présentation de l'exploitation :
Le scénario utilisé ici était Windows 11 21H2 (OS Build 22000.918) clfs.sys v10.0.22000.918
La première étape consiste à créer un fichier nommé MyLog.blf dans le dossier public (%public%), en utilisant la fonction CreateLogFile() :



Ensuite, il crée plusieurs fichiers journaux avec des noms aléatoires en utilisant une boucle.
Et dans la boucle, il appelle notre fonction getBigPoolInfo() :

Elle appelle NtQuerySystemInformation(), avec 0x42 (66 décimal) comme premier argument, cela retournera dans v5 les informations sur les allocations faites dans le bigpool, dont la structure est de type SYSTEM_BIGPOOL_INFORMATION.

Nous devons appeler cette fonction deux fois. La première retournera une erreur, mais nous donnera la taille correcte du tampon pour appeler la seconde fois et obtenir les informations souhaitées.

v5 recevra les informations de la structure SYSTEM_BIG_POOL_INFORMATION.

Le nombre d'allocations dans le bigpool est stocké dans le premier champ appelé Count, dans le second champ se trouve un tableau de structures SYSTEM_BIGPOOL_ENTRY.

Ensuite, nous recherchons dans toutes les structures la balise "Clfs" et la taille 0x7a00.

Il stocke dans un tableau appelé kernelAddrArray l'adresse virtuelle qui est le premier champ de chaque structure ayant la balise CLFS et la taille 0x7a00. Désormais, les pools qui remplissent ces deux conditions seront appelés : "pools corrects".

En plus de stocker chaque pool correct dans le tableau, il stocke le dernier pool correct trouvé dans le contenu de la variable a2, qui est utilisée comme argument de la fonction.

Ainsi, a2 pointe toujours vers le dernier pool correct créé avec la balise CLFS et la taille 0x7a00.
La variable v26 stocke toujours le pool correct précédent trouvé car elle est égale à v24 (v26=v24), avant d'appeler getBigPoolinfo(), mais v24 est mise à jour en sortant de cet appel avec le dernier pool correct trouvé, et v26 reste avec le pool correct précédent trouvé.

Ensuite, il soustrait les deux directions, et si le résultat est négatif, il inverse les opérandes pour qu'il soit toujours positif.

Ainsi, dans v32 sera stockée la différence entre l'adresse virtuelle des deux derniers pools corrects trouvés.
Ensuite, il fait quelque chose de similaire. Dans ce cas, v23 est initialement nul, donc il fait v23 = v32 la première fois.

La prochaine fois dans la boucle, v23 a toujours la même valeur et n'est pas nul, donc il s'arrête et va ici.

V32 a la dernière différence et v23 la précédente, si elles sont égales, il sort et incrémente de un, mais remet le compteur à zéro.
L'idée est de trouver 6 comparaisons consécutives de balises CLFS et de taille 0x7a00 dont les différences sont égales, et cette différence sera 0x11000. Nous verrons lors de l'exécution que lorsqu'il trouve 6 (car il repart de zéro) consécutives avec des distances égales, il donnera cette valeur de différence entre elles.


Là, nous voyons qu'il a trouvé 6 consécutives et a quitté la boucle de création de fichiers journaux.
Dans le dossier "public", nous pouvons voir les fichiers créés

Notre fonction craftFile() ouvre le fichier original (MyLog.blf) et le modifie pour déclencher le bogue.

Après avoir modifié le fichier, il est nécessaire de changer le CRC32, sinon nous obtiendrons une erreur de fichier corrompu.
Cette valeur se trouve à l'offset 0x80C du fichier.

Ensuite, il effectue un Heap Spray, en utilisant la fonction VirtualAlloc() pour allouer de la mémoire, à des adresses arbitraires 0x10000 et 0x5000000 respectivement, et en sauvegardant dans la seconde allocation (0x10000), la valeur 0x5000000, tous les 0x10 octets.

Il utilise CreatePipe() pour créer un tube anonyme et appelle NtFsControlFile() avec 0x11003c comme argument pour ajouter un attribut, plus tard on peut appeler cette même fonction avec l'argument 0x110038 pour le lire.
Plus de détails sur cette méthode peuvent être trouvés ICI

Là, nous voyons le tampon d'entrée qui est l'attribut que nous ajoutons. Si nous appelons à nouveau NtFsControlFile() avec l'argument 0x11038, en sortie il devrait retourner ce même attribut.

Recherche dans le pool la balise de l'attribut créé (NpAt)


Et quand il la trouve, il sauvegarde dans v30.Pointer l'adresse virtuelle de ce pool.
V30.pointer+24 pointe vers AttributeValueSize dans le pool noyau et le sauvegarde dans l'un des HeapSprays que nous avons effectués précédemment.

L'idée est d'écrire à cette adresse noyau+8, pour écraser le AttributeValue.


La structure PipeAttribute a comme premier champ un LIST_ENTRY d'une taille de 16 octets, puis un pointeur vers le nom de l'attribut de taille 8 octets, puis vient à 0x18 (24 décimal) le champ AttributeValueSize qui est celui que nous stockons dans le HeapSpray.
Après cela, nous chargeons CLFS.sys et ntoskrnl en mode utilisateur, et en utilisant GetProcAddress() nous trouvons les adresses des fonctions ClfsEarlierLsn() et SeSetAccessStateGenericMapping().

Ensuite, nous appelons la fonction FindKernelModulesBase() qui trouvera la base noyau des deux mêmes modules en utilisant NtquerySystemInformation() cette fois avec l'argument SystemModuleInformation pour retourner les informations sur tous les modules.

De cette façon, nous pouvons calculer l'offset de chaque fonction, puis les obtenir dans le noyau

La fonction pipeArbitraryWrite() est appelée deux fois. Il y a un drapeau qui est initialement nul pour le premier appel et lorsque dans le second appel il vaut 1, il changera les valeurs du HeapSpray.

Dans le premier appel à l'adresse mémoire 0x5000000, les valeurs suivantes sont situées

Rappelez-vous que cette valeur, en plus d'être allouée à cette adresse, est stockée dans notre HeapSpray.

Voici à quoi ressemble la mémoire après le premier appel, comme nous l'avons dit à l'adresse autour de 0x5000000

Et dans le HeapSpray à partir de la mémoire 0x10000, il stockera le pointeur vers AttributeValueSize tous les 0x10 octets, en plus du pointeur vers 0x5000000.

Cette séquence déclenchera le bogue :

CreateLogFile() est appelée à nouveau sur le fichier modifié et sur un autre avec un nom aléatoire.
AddLogContainer() est ensuite appelée en utilisant les handles de ces fichiers.

NtSetinformationFile() est appelée, et les handles sont fermés avec lesquels le pointeur est corrompu (cela sera expliqué plus tard)

Le HeapSpray empêche un BSOD de se produire à ce stade :

En plaçant un point d'arrêt ici, nous pouvons voir que le pointeur est corrompu et pointe vers notre HeapSpray, avec lequel nous pouvons contrôler les deux prochains appels de fonction de la vtable.


RAX prend la valeur 0x5000000 et saute d'abord vers la fonction située à 0x5000000+18 puis vers 0x5000000+8.


Donc, il saute d'abord vers fnClfsEarlierLsn() puis vers fnSeSetAccessStateGenericMapping().
Nous traçons à partir du point d'arrêt et voyons qu'il atteint CLFS!ClfsEarlierLsn().

Cette fonction est appelée exclusivement car lorsqu'elle retourne, elle met EDX à 0xFFFFFFFF

À l'adresse 0xFFFFFFFF, nous avions stocké le résultat de SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000

Comme mentionné, lors du retour de CLFS!ClfsEarlierLsn(), la valeur de RDX est 0x00000000FFFFFFFF

Nous arrivons à la seconde fonction nt!SeSetAccessStateGenericMapping()

Cette fonction est utile, car RCX pointe vers notre HeapSpray, et RDX vaut 0xFFFFFFFF, dont nous contrôlons le contenu


Le contenu de RCX+0x48 contient le pointeur vers AttributeValueSize qui a été stocké dans v30.Pointer+24



Cette valeur de pointeur de AttributeValueSize est déplacée dans RAX, puis lit le contenu de l'adresse 0xFFFFFFFF où nous avions stocké l'adresse de SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000.

Ensuite, il écrase dans RAX+8 le champ suivant qui est AttributeValue()


Bien sûr, le AttributeValue pointerait normalement dans le noyau vers l'attribut que nous avons ajouté.

Et maintenant, nous l'écrasons avec un pointeur du résultat du système _EPROCESS & 0xFFFFFFFFFFFFFFF00.
Cela signifiera que lorsque nous appellerons à nouveau la fonction NtFsControlFile(), cette fois avec l'argument 0x110038 pour lire l'attribut, au lieu de retourner le "A" pointé par le pointeur AttributeValue, il lira maintenant à partir de _EPRROCESS & 0xFFFFFFFFFFFFFFFFF000 le nombre d'octets demandé et le retournera dans le tampon de sortie, ce qui nous permettra d'obtenir dans le premier appel la valeur du TOKEN SYSTEME.

v9b est l'adresse de début du Tampon de sortie où le contenu du résultat de System EPROCESS & 0xFFFFFFFFFFFFFFF000 a été copié.
À cela, il ajoute v14 qui sont les 3 derniers octets du EPROCESS système, puis ajoute 0x4b8 qui est l'offset du Token pour cette version de Windows 11, puis trouve le contenu de cette adresse qui contiendra la valeur du Token Système.



Rappelez-vous que les 4 derniers bits ont été modifiés, ce n'est pas significatif, donc la valeur correspond toujours.
Dans le second appel, la valeur du drapeau est 1 car elle a été incrémentée à la fin du premier appel.

Là, nous voyons l'ordre dans lequel les valeurs sont stockées

L'adresse 0xFFFFFFFF avec la valeur que nous venons de trouver du Jeton du processus Système.


Et dans le HeapSpray se trouve la valeur de l'adresse du jeton de mon processus à laquelle je soustrais 8. Cette valeur plus huit sera utilisée comme cible. Rappelez-vous que nous écrivions à l'adresse pointée par RAX+8.


Dans l'adresse mémoire commençant à 0x5000000

Nous voyons aussi qu'il utilise le nom d'un autre conteneur, car le précédent est utilisé par le processus système et ne peut pas être rouvert ou supprimé.

Ensuite, le bogue est déclenché une seconde fois de la même manière que la première.

Il arrive à nouveau à CLFS!ClfsEarlierLsn().

mettant RDX à 0xFFFFFFFF

Ensuite, il arrive à nt!SeSetAccessStateGenericMapping()

Lit l'adresse du jeton de mon processus moins 8 où il va écrire

Ensuite, il lit le TOKEN SYSTEME

Et écrit à l'adresse du jeton de mon processus (il ajoute 8), le Token Système

Et ainsi mon processus a le jeton système

Une fois le jeton écrit, nous démarrons un processus pour vérifier les privilèges, dans ce cas, nous lançons Notepad.exe



Rappelez-vous que ce PoC ne fonctionne que sous Windows 11, sous Windows 10 il produira un BSOD, donc vous devez apporter quelques modifications pour qu'il fonctionne correctement, ce qui n'est pas expliqué dans cet article de blog.
Analyse des structures
Les structures et la plupart de la documentation sur le format de fichier CLFS ont été tirées de l'excellent travail d'IONESCU sur CLFS Internals.
Nous pouvons voir qu'une vérification a été ajoutée dans la fonction ClfsBaseFilePersisted::LoadContainerQ

Les valeurs qui effectuent une addition appartiennent à la structure _CLFS_BASE_RECORD_HEADER.

Notez que le Bloc de base commence à l'offset 0x800 du fichier et se termine à l'offset 0x71FF, les 0x70 premiers octets correspondant à l'En-tête du bloc journal
En règle générale, nous pouvons ajouter la structure _CLF_LOG_BLOCK_HEADER dans IDA
struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
char ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
};
Ensuite, nous avons l'en-tête d'enregistrement de base (_CLFS_BASE_RECORD_HEADER) qui commence au décalage 0x870 à partir du début du fichier et fait 0x1338 octets de long.

Si vous souhaitez l'importer dans IDA, vous devez d'abord ajouter les types et structures manquants suivants
typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;
struct _CLFS_METADATA_RECORD_HEADER
{
ULONGLONG ullDumpCount;
};
Il est maintenant prêt à être ajouté :
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[0x0b];
ULONGLONG rgContainerSymTbl[0x0b];
ULONGLONG rgSecuritySymTbl[0x0b];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[0x7c];
ULONG rgContainers[0x400];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

Après avoir inclus les structures, nous remarquons qu'il effectue une addition entre le cbSymbolZone et l'adresse où se termine le _CLFS_BASE_RECORD_HEADER. (début + 1338h)
Rappelez-vous que le cbSymbolZone a été modifié dans le fichier journal fabriqué de 0x000000F8 à 0x0001114B.
(décalage 0x1b98 du fichier)
0x800(décalage du début du bloc de base) + 0x70 (logBlockHeader) + 0x1328 (cbsymbolZone)
0x800+0x70+0x1328 = 0x1b98
cbsymbolZone fabriqué sur le fichier MyLog.blf :


Comme le correctif se trouve dans la fonction CClfsBaseFilePersisted::LoadContainerQ, nous devons jeter un œil à l'objet CClfsBaseFilePersisted.
En plaçant un point d'arrêt dans CLFS!CClfsBaseFilePersisted::LoadContainerQ et lorsque CreateLogFile est appelée avec le handle du fichier fabriqué, il s'arrêtera.

Appelez la fonction CClfsBaseFile::GetBaseLogRecord pour obtenir l'adresse de l'enregistrement de base du journal (_CLFS_BASE_RECORD_HEADER)

RAX pointera vers l'adresse de _CLFS_BASE_RECORD_HEADER

Notez la structure _CLFS_BASE_RECORD_HEADER en mémoire et le champ cbsymbolZone 0x1328
octets en avant


r14 stocke la structure correspondant au « this », qui est CClfsBaseFilePersisted puisqu'il s'agit du this de la fonction CClfsBaseFilePersisted::LoadContainerQ.

La structure CClfsBaseFilePersisted en mémoire :

Créons donc une structure de longueur 0x21c0 pour compléter ses champs pendant que nous la rétro-ingénierons (c'est une structure non documentée), nous l'appellerons struct_CClfsBaseFilePersisted

À l'intérieur de la fonction CClfsBaseFile::GetBaseLogRecord(), on obtient le pointeur vers _CLFS_BASE_RECORD_HEADER. et nous savons que le « this » dans cette fonction est la structure : struct_CClfsBaseFilePersisted.

Lire deux champs (décalage 0x28 et 0x30)

Le champ 0x28 est un mot et a la valeur 6, donc nous changeons le type en mot dans la structure.



Pour l'instant, nous le renommons en constante 6 (const_6)


Selon la documentation, 6 serait le nombre de blocs CLFS_METADATA_BLOCK_COUNT. Le champ pourrait faire référence à cette valeur.
Et ce pointeur est au décalage 0x30.

Notez que la taille affichée inclut l'en-tête d'une longueur de 0x10


Lorsque la fonction ExAllocatePoolWithTag est appelée, quelques octets sont demandés, mais l'en-tête n'est pas inclus, donc 0x90 octets (0xa0 – 0x10) seront demandés dans l'appel.
En recherchant par texte +30h], les instructions qui écrivent au décalage 0x30, nous avons trouvé une longue liste, mais en filtrant la liste par le type d'objet CClfsBaseFilePersisted, il ne nous reste que quelques résultats et nous trouvons immédiatement où cette taille est allouée, et la même balise. (Astuce : les noms de fonctions Create et Initialize sont toujours les premiers à regarder)


Comme nous ne connaissons pas encore le nom, nous le mettrons pool_0x90, qui est une autre structure non documentée, et nous créerons une structure de cette taille.


Le pool_0x90 en mémoire a un autre pointeur à son propre décalage 0x30.

Cet autre pointeur pointe vers le bloc de base dans le fichier (le bloc de base commence au décalage 0x800)


Image tirée du blogpost de Zscaler:

L'allocation est énorme, car elle contient tout le bloc de base.



Nous allons donc créer une nouvelle structure de taille 0x7a00 et l'appeler BASE_BLOCK

Les 70 premiers octets correspondent déjà à _CLFS_LOG_BLOCK_HEADER et les 0x1338 suivants à _CLFS_BASE_RECORD_HEADER.

Ainsi, en ajoutant le début du bloc de base avec le décalage vers l'enregistrement suivant (qui est 0x70), nous obtenons le _CLFS_BASE_RECORD_HEADER

Le _CLFS_BASE_RECORD_HEADER en mémoire.

En regardant d'autres méthodes du même objet CClfsBaseFilePersisted, dans CClfsBaseFilePersisted::AddContainer, on obtient également l'adresse de _CLFS_BASE_RECORD_HEADER avec CClfsBaseFile::GetBaseLogRecord.

Ensuite, appelez CClfsBaseFile::OffsetToAddr en utilisant cbOffset, il obtient l'adresse de _CLFS_CONTAINER_CONTEXT, et stocke cboffset dans le tableau rgbcontainers qui se trouve au décalage 0x328 de _CLFS_BASE_RECORD_HEADER.

La fonction CClfsBaseFile::OffsetToAddr est utilisée pour trouver les adresses des structures à partir du décalage

À ce stade, le décalage du conteneur qui sera stocké à 0x328 est toujours 0, car nous n'avons pas encore ajouté de conteneur.

Le PoC appelle CreateLogFile deux fois, la première fois avec le fichier malformé MyLog.blf et la seconde fois avec le fichier normal MyLogxxx.blf, nous devons donc arrêter le débogage deux fois à tous les endroits ci-dessus et noter dans le bloc-notes les adresses des structures ci-dessus pour les deux fichiers.

Avançons un peu jusqu'à CLFS!CClfsLogFcbPhysical::AllocContainer en y plaçant un point d'arrêt et en y allant.
Lorsque AddLogContainer() est atteint dans le POC, nous nous arrêtons au point d'arrêt.

Définissons également un point d'arrêt sur CClfsBaseFilePersisted::AddContainer+176 où nous avons vu plus tôt qu'il trouvera le décalage et le pointeur vers la structure _CLFS_CONTAINER_CONTEXT.


Lorsque le débogueur s'arrête, nous pouvons voir que le décalage est 0x1468.

Dans RAX sera retournée l'adresse de la structure _CLFS_CONTAINER_CONTEXT.

la structure est encore vide car le conteneur n'a pas encore été ajouté.

Notez que la valeur SignatureOffset=0x50 que nous avons écrite au décalage 0x868 du fichier malformé, en soustrayant 0x800 du début du bloc de base, se trouvera dans la structure _CLFS_LOG_BLOCK_HEADER au décalage 0x68.


Lorsque le PoC appelle la fonction AddLogContainer() en utilisant le fichier malformé, au décalage 0x68 de _CLFS_LOG_BLOCK_HEADER, au lieu de la valeur 0x50 que nous y avons écrite, on trouve actuellement un 0xFFFF0050 en mémoire.

À un moment donné, cette valeur a été modifiée par le programme ; pour voir quand cela s'est produit, lors de la prochaine exécution, nous définirons un point d'arrêt mémoire en écriture.
Le décalage est stocké à r15 + 0x328 (r15 pointe vers la structure _CLFS_BASE_RECORD_HEADER)


RBX stocke le décalage 0x1468.

Donc, dans l'adresse du bloc de base + 0x70 + le décalage 0x1468 que nous avons trouvé, se trouvera l'adresse du conteneur CLFS_CONTAINER_CONTEXT.

Dans la structure CLFS_CONTAINER_CONTEXT au décalage 0x18 se trouvera le pointeur pContainer qui y sera stocké, nous pouvons définir un point d'arrêt en écriture et voir quand il est écrit.


C'est le pointeur que nous devons corrompre car dans la fonction où se trouve la vulnérabilité, elle lit d'abord le CLFS_CONTAINER_CONTEXT, puis le déplace dans r15 et lit ensuite la valeur de r15+18, qui est ce pointeur sur lequel nous venons de définir le point d'arrêt en écriture.


il stocke le pContainer au décalage 0x1c0 de la structure struct_CClfsBaseFilePersisted.

Après plusieurs arrêts, nous atteignons le moment où il est corrompu. Le haut de l'adresse du pointeur est passé de FFs à zéro.

Cela se produit lorsque le second AddLogContainer() du fichier malformé est appelé, le pointeur du MyLogxxx précédent est corrompu.
Le problème vient du fait que SignaturesOffset, qui devrait être 0x50, est maintenant 0xFFFF0050, ce qui permet d'écrire hors limites dans le memset qui suit.


La fonction memset() va corrompre la structure _CLFS_CONTAINER_CONTEXT qui se trouve en dessous, cette structure correspond au fichier MyLogxxx, car lors de la création, ils étaient situés à 0x11000 octets l'un de l'autre.
De cette façon, il calcule exactement où écrire sur la structure suivante et met à zéro le haut du pointeur, afin qu'il pointe vers le tas utilisateur où le HeapSspray a été créé.
la structure du bloc de base du fichier malformé est juste 0x11000 avant celle du fichier MyLogxxx.
Malformé :

MyLogxxx


RCX est plus petit que RDX car 0xFFFF0050 a été ajouté, au lieu de 0x50 comme il se doit.

et nous sommes arrivés à la fonction memset(), pour mettre la quantité de 0xb0 octets à zéro, avec RCX pointant vers la structure CLFS_CONTAINER_CONTEXT du fichier MyLogxxx, plus précisément vers les cinq octets de poids fort de pContainer.

Ce pointeur sera corrompu en écrasant les premiers octets :

restant pointant vers une adresse mémoire préalablement contrôlée par nous via HeapSpray


Ensuite, le handle du fichier MyLogxxx sera fermé, et on atteint CClfsBaseFilePersisted::RemoveContainer, la vulnérabilité est finalement déclenchée.

Maintenant que nous avons plus d'informations, nous remarquons qu'il lit ici le Base_Block.LOG_BLOCK_HEADER.SignaturesOffset et le Base_Block. .LOG_BLOCK_HEADER.TotalSectorCount
Dans la première partie du correctif, SignaturesOffset ne doit pas être supérieur à 0x7a00, dans le nôtre il était initialement à 0x50, s'il arrivait avec une valeur supérieure à 0x7a00, cela nous éjecterait.

En exécutant le PoC sur la machine corrigée, il compare 0x50 avec 0x7a00 et comme il est plus petit, il continue.

Dans le bloc suivant, le cbSymbolZone malformé est ajouté à la valeur de l'adresse finale de _CLFS_BASE_RECORD_HEADER et cette somme est stockée dans result_1.

Ensuite, l'adresse du bloc de base est ajoutée à la valeur SignatureOffset, qui dans un fichier normal est 0x7980.

L'adresse maximale du base_block est 0x7a00, maintenant la SymbolZone est autorisée jusqu'à 0x80 avant la limite.
Il la stockera dans result_2, c'est-à-dire que ce serait la limite maximale pour la SymbolZone à l'intérieur du bloc de base, puis il compare les deux résultats si le premier est supérieur au second, cela signifie qu'il est sorti des limites.


Évidemment, le premier membre sera plus grand que le second et il ne continuera pas, puisque la première somme de cbSymbolZone + adresse finale de _CLFS_BASE_RECORD_HEADER dépasse la limite (qui est result_2) et conduit à un « out of bounds ».

La dernière chose que nous devrions déterminer est l'endroit où la valeur SignatureOffset de 0x50 devient 0xFFFF0050.Alors, recommençons, redémarrons et arrêtons-nous à CLFS!CClfsBaseFilePersisted::LoadContainerQ là où la valeur n'a pas encore été modifiée en mémoire et est toujours 0x50.
Définissez un point d'arrêt d'accès à l'offset 0x68 dans SignatureOffset.

Et après plusieurs arrêts, nous détectons le moment précis où il modifie la valeur, dans ClfsEncodeBlockPrivate.

Cette fonction n'est pas patchée, donc il pourrait s'agir d'un comportement causé par la faible valeur de 0x50 et le reste des valeurs étant manipulées.
Parmi les valeurs fabriquées, nous pouvons voir la valeur ccoffsetArray dont le nom dans la structure _CLFS_BASE_RECORD_HEADER est rgClients et représente le tableau d'offsets qui pointent vers l'objet Client Context.
Le champ rgClients est situé à l'offset 0x138 (0x9a8-0x800-0x70) de la structure _CLFS_BASE_RECORD_HEADER.


Dans le PoC, cette valeur est malformée pour pointer vers un faux objet client context, appelé FakeClientContext

Ceci est la structure Client Context _CLFS_CLIENT_CONTEXT
struct _CLFS_CLIENT_CONTEXT
{
CLFS_NODE_ID cidNode;
CLFS_CLIENT_ID cidClient;
USHORT fAttributes;
ULONG cbFlushThreshold;
ULONG cShadowSectors;
ULONGLONG cbUndoCommitment;
LARGE_INTEGER llCreateTime;
LARGE_INTEGER llAccessTime;
LARGE_INTEGER llWriteTime;
CLFS_LSN lsnOwnerPage;
CLFS_LSN lsnArchiveTail;
CLFS_LSN lsnBase;
CLFS_LSN lsnLast;
CLFS_LSN lsnRestart;
CLFS_LSN lsnPhysicalBase;
CLFS_LSN lsnUnused1;
CLFS_LSN lsnUnused2;
CLFS_LOG_STATE eState;
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
};
La valeur eState se trouve à l'offset 0x78 du début de la structure, dans le fichier fabriqué 0x23a0+0x78.


Cette valeur montre l'état du journal.
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
cette valeur est définie à CLFS_LOG_STATE CLFS_LOG_SHUTDOWN =0x20
L'autre valeur malformée est fAttributes qui correspond à l'ensemble des indicateurs FILE_ATTRIBUTE associés au fichier journal de base (tels que System et Hidden).


Puisque le champ commence un octet plus tôt à 0xa et s'étend sur deux octets, la valeur de fAttributes est 0x100.


Enfin, il y a la valeur blocknameoffset qui pointe vers l'offset 0x1bb8, je veux dire, en ajoutant 0x78 et 0x800 on pointe vers l'offset 0x2428 du fichier.


Notez que l'offset vers le Client Context est 0x1b30

Donc, le Client Context est à l'offset 0x23a0.


Et juste 0x10 avant, se trouve la valeur correspondant à blocknameoffset.


Ce qui pointerait vers la chaîne avec le nom
le dernier est blockattributeoffset qui se trouve 0xC avant le Client Context à 0x2394.

Ces deux dernières valeurs appartiennent à une structure précédant le Client Context de 0x30 octets, appelée**_CLFSHASHSYM**
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;


ils sont à 0x20 et 0x24 octets du début de la structure _CLFSHASHSYM, donc dans la structure _CLFSHASHSYM la valeur appelée blockNameOffset dans le POC est le champ cbSymName et blockAttributteoffset est le champ cbOffset.


Ce sont les valeurs malformées, nous devons maintenant voir comment elles affectent le changement de notre SignaturesOffset de la valeur 0x50 à 0xFFFF0050.
Jetons un coup d'œil à la fonction CClfsBaseFile::AcquireClientContext(), qui devrait retourner le client context.

elle appelle CClfsBaseFile::GetSymbol avec le quatrième argument qui sera _CLFS_CLIENT_CONTEXT ** où il stockera le pointeur vers Client Context.

À l'intérieur de la fonction CClfsBaseFile::GetSymbol, nous passons l'offset malformé ccoffsetArray à CClfsBaseFile::OffsetToAddr et obtenons l'adresse du client context, définissons un point d'arrêt là pour qu'il s'arrête lors de l'appel du fichier créé avec CreatelogFile.

Là, il s'arrête avec l'argument fabriqué ccoffsetArray.


La fonction CClfsBaseFile::OffsetToAddr retourne le faux Client Context

Et vérifie que la valeur de cbOffset n'est pas nulle puisque 0xC se trouve avant la structure _CLFS_CLIENT_CONTEXT qui est dans RAX.


Ensuite, il compare cbOffset avec ccoffsetArray (qui est dans RSI), ils doivent être égaux, sinon nous obtiendrons une erreur.

Il vérifie également que cbSymName soit égal à cbOffset+0x88, sinon nous obtiendrons aussi une erreur.

Et enfin, il compare l'octet cidClient avec zéro

Si toutes ces vérifications réussissent, le client context sera sauvegardé.

La sortie de la fonction r14 pointe vers le Client Context

En sortant de CClfsLogFcbPhysical::Initialize, nous aurons l'adresse de CLFS_CLIENT_CONTEXT.

Maintenant, il lit la valeur de fAttributes (0x100)

cette fonction appartient à la classe CClfsLogFcbPhysical


Qui a été allouée ici, et sa taille est 0x15d0 et son tag est “ClfC”

Créons une structure pour stocker ce que nous reversons, nous l'appellerons : struct_CClfsLogFcbPhysical.

Notez qu'à 0x2b0, il sauvegarde l'adresse de la structure CClfsBaseFilePersisted.

Après avoir sauvegardé de nombreuses valeurs dans la structure, il passe à une partie importante, il teste eState avec 0x20.


Puisque la valeur fabriquée était 0x20, le test retournera 1.


Nous voyons que dans le constructeur dans la vtable se trouve

Il vérifiera si le fichier est multiplexé.

Ainsi, il emprunte le chemin souhaité, atteignant CClfsLogFcbPhysical::ResetLog.


Plusieurs champs sont initialisés à zéro sauf un qui est initialisé à 0xFFFFFFFF00000000.

Ici, il récupère le Client Context

il stocke la valeur 0xFFFFFFFF00000000.



Il écrit 0xFFFFFFFF à l'offset 0x5c qui est la partie haute de CLFS_LSN lsnRestart.ullOffset



Maintenant, nous exécutons la fonction ClfsEncodeBlockPrivate(), qui est responsable de l'écrasement du 0x50 par 0xFFFF0050 comme nous l'avons vu précédemment.
Là, il lit la valeur de SignatureOffset = 0x50 qui est toujours telle que nous l'avons mise dans le fichier malformé et l'ajoute au début de CLFS_LOG_BLOCK_HEADER.

c'est une boucle qui écrit 2 octets, comme le SignatureOffset au lieu de pointer vers une valeur correcte qui dans un fichier normal est une valeur élevée, par exemple 0x3f8 qui lui fait écrire plus loin, ici il écrira dans le même CLFS_LOG_BLOCK_HEADER
L'idée est de changer la destination d'écriture pour tenter de corrompre la valeur SignatureOffset.
Fichier normal

À ce stade, il commencera à boucler et à écrire deux octets.

Le compteur doit atteindre la valeur 0x3d pour sortir de la boucle.

RCX augmente à partir de 0x200, nous sommes déjà dans le troisième cycle, et sa valeur est 0x600

dans l'itération 0xe, RCX est 0x1a00


C'était là où il avait écrit le 0xFFFFFFFF000000.


Il lit les deux derniers octets FFFF

Et il les copiera ensuite dans R8


Comme nous l'avons vu, cette valeur est critique car elle permet de contourner la vérification et d'écrire hors limites pour corrompre le pointeur pContainer du fichier qui suit le memset() et d'écrire des zéros en haut et de le laisser pointer vers notre mémoire contrôlée (HeapSpray).
Dans le CClfsBaseFilePersisted::AllocSymbol, la même somme qui va obtenir la destination du memset qui est cbSymbolZone + adresse finale de CLFS_BASE_RECORD_HEADER la compare avant contre Base_block + 0xFFFF0050, donc elle a des valeurs corrompues des deux côtés de l'équation.
CbSymbolZone= 0x1114B
C'est la valeur malformée qui, ajoutée à l'adresse finale de CLFS_BASE_RECORD_HEADER, lui fera écrire hors limites et l'autre membre de la comparaison qui devrait être l'adresse du Base Block + SignatureOffset reste SignatureOffset =0xFFFF0050 ce qui permet à cette vérification de passer et d'écrire hors limites dans le memset() et de mettre à zéro le haut du pointeur qui restera pointant vers notre HeapSpray.

Puisque RCX est plus petit que RDX.

Comme nous l'avons vu précédemment. (Les valeurs peuvent différer car elles appartiennent à une exécution précédente)
Il corrompra le pointeur, en mettant les octets les plus élevés à 0

Le laissant pointer vers une zone mémoire que nous contrôlons via HeapSpray


Ainsi, lorsque la vulnérabilité est déclenchée, nous arrivons à CClfsBaseFilePersisted::RemoveContainer

Là, se trouvera le pointeur déjà corrompu et il peut être exploité comme nous l'avons vu précédemment.

À ce stade, nous avons exploité le bogue, il mène au contrôle des fonctions qui permettent de lire le jeton SYSTEM et d'écrire dans notre propre processus pour réaliser l'élévation de privilèges locale.
Nous espérons que cela vous sera utile, si vous avez des doutes, vous pouvez nous contacter à [email protected] et [email protected]
Profitez-en !