Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2024-6769 — Analyse technique et PoC pour CVE-2024-6769, enchaînant le détournement de DLL avec l'empoisonnement du cache d'activation pour élever le niveau d'intégrité de moyen à élevé sur les systèmes Windows. | Kitploit
Outils/GitHubGitHub/fortra/cve-2024-6769
Escalade de PrivilègesAnalyse des VulnérabilitésExploitationArticles et RechercheApprentissage et ÉducationExploitation de Binaires
GitHubfortra/cve-2024-6769

CVE-2024-6769

Analyse technique et PoC pour CVE-2024-6769, enchaînant le détournement de DLL avec l'empoisonnement du cache d'activation pour élever le niveau d'intégrité de moyen à élevé sur les systèmes Windows.

Voir le dépôt
78115il y a 1 anVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

Article de blog : CVE-2024-6769 Empoisonnement du cache d'activation pour passer d'une intégrité moyenne à élevée

Cet article de blog porte sur deux bogues enchaînés : La première étape est un détournement de DLL provoqué par le remappage du lecteur ROOT, et la seconde est un empoisonnement du cache d'activation géré par le serveur CSRSS.

La première étape a été présentée en détail à l'Ekoparty 2023 dans la présentation intitulée "I'm High" par Nicolás Economou de BlueFrost Security. Il a expliqué comment exploiter la vulnérabilité qui, à l'époque, n'avait pas encore été corrigée par Microsoft. Cela permettait à un utilisateur de NIVEAU D'INTÉGRITÉ MOYEN de bénéficier de PRIVILÈGES ÉLEVÉS limités, mais sans l'accès complet nécessaire pour être un Administrateur à part entière.

La seconde étape n'a pas été présentée lors de cette conférence, bien que quelques pistes aient été suggérées pour commencer à la rechercher.

Pour commencer, nous examinerons la première étape afin de fournir un contexte introductif. À partir de là, nous plongerons dans mes recherches sur la seconde étape, en détaillant comment passer d'un niveau d'INTÉGRITÉ ÉLEVÉ limité à un Administrateur complet. Cela inclut une preuve de concept (PoC) complète et fonctionnelle pour les deux étapes sur toutes les versions de Windows, qui a été testée avec succès sur Windows 10, Windows 11, Windows Server 2022 et Windows Server 2019 avec toutes les mises à jour appliquées.

Index :

  • Examen de la première étape
  • Étapes à suivre pour exploiter la seconde étape.
  • Qu'est-ce que le cache d'activation ?
  • Utilisation du vecteur d'attaque ALPC pour empoisonner le cache d'activation
  • Comment le système acceptera-t-il notre contexte d'activation ?
  • Comment empoisonner le cache d'activation ?
  • Comment mon manifeste XML intégré est-il lu ?
  • Comment le manifeste XML intégré est-il analysé ?
  • Comment ma fausse imm32.dll a-t-elle fini par être chargée ?
  • Démonstration vidéo.
  • Preuve de concept fonctionnelle
  • TL;DR Brève description des étapes d'exploitation

Examen de la première étape

Un carré rouge avec du texte blanc et un chiffre dessus Description générée automatiquement

La seule condition pour cette étape est que le processus initial démarre à un NIVEAU D'INTÉGRITÉ MOYEN et que l'utilisateur appartienne au groupe Administrateur.

La première étape de l'exploitation peut être résumée dans les étapes suivantes :

  1. Remappage du lecteur ROOT à l'aide de la fonction NtCreateSymbolicLinkObject.

Par exemple : remappage du disque de

"C:\" à "C:\users\public"

Cela remappera également le dossier "system32" de

"C:\windows\system32" à "C:\users\public\windows\system32"

  1. Après le remappage, certains services sont affectés et tenteront de charger des bibliothèques depuis le nouveau system32 factice contrôlé par l'utilisateur.

L'un de ces programmes affectés est CTFMON, qui s'exécute à un NIVEAU D'INTÉGRITÉ ÉLEVÉ mais sans privilèges d'administrateur.

Normalement, il essaie de charger le module appelé MsCtfMonitor.dll depuis le vrai dossier system32, mais comme le lecteur ROOT a été remappé, il cherche MsCtfMonitor.dll dans notre system32 factice contrôlé, où nous pouvons créer et placer une DLL falsifiée avec le même nom.

  1. Créer MsCtfMonitor.dll

À ce stade, en plaçant notre version de MsCtfMonitor.dll dans le faux dossier system32, sa fonction DoMsCtfMonitor est appelée et exécute notre code à un NIVEAU D'INTÉGRITÉ ÉLEVÉ.

  1. Placer un MessageBoxA sur la fonction DoMsCtfMonitor. Lorsque MsCtfMonitor.dll est chargée, elle affiche le MessageBoxA "TRIGGER".

  1. Vérifier que la DLL a été chargée dans le processus CTFMON qui s'exécute au NIVEAU D'INTÉGRITÉ ÉLEVÉ :

En même temps, nous pouvons corroborer que le processus, bien qu'à un NIVEAU D'INTÉGRITÉ ÉLEVÉ, ne dispose pas de privilèges d'administrateur :

Étapes pour l'exploitation de la seconde étape Un carré rouge avec du texte blanc et un chiffre dessus Description générée automatiquement

Dans sa présentation à l'Ekoparty, Nicolas a suggéré les étapes suivantes pour terminer l'exploitation :

Bien que cela semble simple, cela nécessite beaucoup de temps de rétro-ingénierie et de débogage.

En creusant un peu dans l'histoire de ce vecteur d'attaque, il est devenu clair que l'empoisonnement du cache des contextes d'activation a été utilisé dans certaines exploitations. Par conséquent, il est intéressant d'apprendre comment l'exploitation a été réalisée auparavant pour fournir un contexte et des informations supplémentaires. Des détails sur cette exploitation sont disponibles via l'article du Zero Day Initiative, Activation Context Cache Poisoning: Exploiting CSRSS for Privilege Escalation.

Qu'est-ce que le cache d'activation ?

L'utilisation du cache d'activation se produit lorsqu'un programme va charger une bibliothèque nécessitant une version spécifique.

Par exemple, si une application va charger C:\Windows\System32\comctl32.dll, il n'y a aucune garantie que la comctl32.dll à cet emplacement soit la version dont l'application a besoin. C'est un cas d'utilisation basique du cache des contextes d'activation. Le programme peut envoyer une requête au serveur CSRSS pour traiter une nouvelle entrée de contexte d'activation à ajouter au cache, afin que ce programme puisse charger la version spécifique de la bibliothèque nécessaire.

À cet effet, on utilise le fichier dit manifeste, qui est au format XML. Il est généralement intégré en tant que ressource dans un fichier EXE ou DLL. Sinon, Windows recherchera un fichier manifeste dans le même dossier où se trouve l'exécutable du programme.

L'URL mentionnée ci-dessus contient quelques exemples de fichiers manifeste utilisés par d'anciennes exploitations, comme tromper le système pour charger la bibliothèque advapi32.dll depuis un répertoire contrôlé par l'attaquant grâce à une technique de TRAVERSÉE DE CHEMIN.

Bien sûr, certains vecteurs d'attaque utilisés ont été corrigés, et de nouvelles techniques ont été découvertes. De plus, dans le correctif d'octobre 2022 pour Windows 11 22H2, une nouvelle vérification a été ajoutée.

Après la mise en œuvre de ce correctif, la vérification lorsqu'un contexte d'activation (ACTX) est enregistré ne peut être contournée que si le processus qui ajoute la nouvelle entrée dans le cache a le même RID ou un RID supérieur à celui du processus qui l'utilisera.

Dans winnt.h, nous pouvons voir les valeurs RID :

La proposition pour contourner cette vérification est de créer une requête avec un contexte d'activation à partir du processus CTFMON où la DLL falsifiée s'exécute. Cette DLL falsifiée a RID=0x3000 et après que l'entrée a été ajoutée au cache, TCMSETUP avec RID=0x3000 chargera tapi32.dll.

Lors de ma tentative de suivre les étapes, j'ai fait toutes les combinaisons possibles pour enregistrer l'ACTX en utilisant CreateActCtx. Cela s'est avéré impossible car il y avait toujours une vérification qui l'évitait.

Il est important de noter que cette fonction se trouve dans l'espace utilisateur, exportée par kernel32.dll. Les vérifications peuvent être évitées en patchant la DLL en mémoire, ce qui n'est pas très élégant, mais c'est possible et devrait fonctionner.

La diapositive de présentation de Nicolas suggère d'utiliser un BAS NIVEAU. Cependant, en notant le visage souriant, il était clair que l'utilisation de CreateActCtx n'est pas la meilleure option lors de l'exploitation de ce bogue sans patch.

Utilisation du vecteur d'attaque ALPC pour empoisonner le cache d'activation

Un Appel de procédure locale avancé (ALPC) est un mécanisme de communication inter-processus utilisé pour envoyer des messages à grande vitesse au sein du système d'exploitation Windows. Contrairement à l'API Windows standard, ALPC n'est pas directement disponible pour les applications. C'est plutôt un mécanisme interne accessible uniquement par les composants du système d'exploitation Windows. (Et par nous.)

En approfondissant mes recherches, j'ai remarqué que certaines anciennes exploitations de cache empoisonné utilisaient ALPC pour communiquer directement avec le serveur. Un exemple peut être vu dans l'article de Philip Tsukerman, Activation Contexts—A Love Story.

La fonction CsrClientCallServer implémente l'interface ALPC entre les processus Win32 et le processus CSRSS.

Donc, une tentative d'appel doit être faite au processus CSRSS qui agit comme serveur en utilisant CsrClientCallServer.

En cherchant des exemples dans d'anciennes exploitations, j'ai trouvé une page sur Packet Storm concernant un dépassement de tas pertinent.

Lorsque le serveur CSRSS est appelé avec le bon package, il est reçu dans la fonction BaseSrvSxsCreateActivationContextFromMessage, qui appartient au module sxssrv.dll.

La fonction n'a qu'un seul argument : le pointeur vers le paquet reçu. Pour le rétro-concevoir, j'ai créé une structure TotalMessage personnalisée.

Le paquet de la structure TotalMessage a ses premiers 0x40 octets d'EN-TÊTE, suivis du Message de contexte d'activation intégré, dont la structure est _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG.

La structure TotalMessage peut être vue ci-dessous :

Et voici la structure _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG :

À l'intérieur de cette structure se trouvent six UNICODE_STRINGS correspondant à la langue ou aux CultureFallbacks, AssemblyDirectory, TextualAssemblyIdentity, AssemblyName, et deux structures _BASE_MSG_SXS_STREAM qui contiennent chacune une UNICODE_STRING à l'intérieur.

Voici la structure _BASE_MSG_SXS_STREAM :

Compte tenu de la difficulté à créer un paquet valide accepté par le serveur, il vaut la peine de détailler comment procéder.

La valeur du champ Flags à l'intérieur de _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG est très importante car il existe de nombreuses combinaisons. Sans la valeur d'indicateur correcte, le bogue ne peut pas être exploité.

Par exemple, prenez mon code MsCtfMonitor.dll. Après de nombreuses tentatives, j'ai conclu que la seule valeur d'indicateur correcte pour cette exploitation de bogue est 0x41 :

La combinaison de différentes valeurs pourrait entraîner une valeur d'indicateur de chemin incorrecte :

La même structure TotalMessage aura un en-tête avec taille=0x40 octets. Les 0x1f8 octets restants sont réservés pour la structure _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG :

struct TotalMessage

{

signed __int64 pad[8];

_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG message;

};

La taille d'allocation est 0x40+0x1f8 :

J'ai ensuite assemblé les chaînes et effectué un contexte de cache d'activation pour tapi32.dll. C'est une DLL très rarement utilisée qui est chargée par un processus appelé TCMSETUP. Il a un NIVEAU D'INTÉGRITÉ DE PRIVILÈGES ÉLEVÉ (RID=0x3000) avec les mêmes privilèges qu'un Administrateur.

Dans mon code DLL, la fonction CaptureUnicodestring est appelée. Cela finit par appeler CsrCaptureMessageString :

NTSTATUS CaptureUnicodeString(LPVOID CaptureBuffer, PSTR OutputString,

PCWSTR String, ULONG Length = 0) {

if (Length == 0) {

Length = lstrlenW(String);

}

return CsrCaptureMessageString(CaptureBuffer, (PCSTR)String, Length * 2,

Length * 2 + 2, OutputString);

}

Cette étape est nécessaire pour préparer correctement le package, permettant au système de copier les chaînes de mon paquet vers le processus CSRSS. Cela maintient les chaînes valides et remplace mes pointeurs par des pointeurs valides dans son contexte.

J'ai également ajouté un manifeste XML intégré, avec la langue "Tasks" dedans. C'est une langue inexistante, mais ce sera la clé de l'exploitation (Merci à Nico pour ça) :

Un autre détail important dans mon code est lorsque CaptureBuffer est créé. La fonction CsrAllocateCaptureBuffer a un argument qui définit combien de UNICODE_STRINGS elle doit gérer et copier vers le serveur.

Dans mon cas, j'ai utilisé "4" chaînes :

L'argument avec la valeur "4" est montré ci-dessous :

Pour atteindre le serveur d'activation, la fonction CsrClientCallServer enverra mon paquet depuis mon MsCtfMonitor.dll avec le même ApiNumber 0x1001001E que les anciennes exploitations mentionnées ci-dessus.

Le blog de Geoff Chappell fournit plus de détails sur CsrClientCallServer :

Voici l'appel à CsrClientCallServer :

Et voici le paquet à envoyer, construit dans ma DLL :

La valeur Manifest.Offset pointe vers mon manifeste XML intégré :

Une commande intéressante pour journaliser le processus d'activation est sxstrace, qui est utilisée dans une console administrateur sur la cible.

Cette commande active le traçage et enregistre les résultats du journal dans sxstrace.etl. (Appuyez sur ENTRÉE pour finaliser le traçage.)

sxstrace trace -logfile:sxstrace.etl

Le fichier brut sxstrace.etl peut ensuite être converti dans un format lisible :

sxstrace parse -logfile:sxstrace.etl -outfile:sxstrace.txt

Le système accepte-t-il notre contexte d'activation ?

Si le paquet est correct, il devrait atteindre la fonction BaseSrvSxsCreateActivationContextFromMessage dans le module sxssrv du processus csrss. Ainsi, lors du débogage du noyau à distance, le contexte doit être basculé vers ce processus. Ensuite, les symboles du mode utilisateur doivent être rechargés pour placer un point d'arrêt dessus :

J'ai utilisé IDA PRO pour déboguer le noyau avec le plugin Windbg :

Une fois qu'il s'arrête à BaseSrvSxsCreateActivationContextFromMessage, RCX pointera vers la structure TotalMessage :

Après les 0x40 octets d'EN-TÊTE initiaux (remplis par le système avec certaines valeurs comme le PID du processus client, etc...), mon message d'activation qui appartient à la structure _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG peut être vu :

Notez que les pointeurs vers les chaînes n'ont pas la même valeur que lorsque je les ai envoyées :

Mais ils pointaient correctement vers les chaînes :

Lorsque le paquet a été envoyé du client au serveur, le système a copié les chaînes de mon processus vers le processus CSRSS et a modifié les pointeurs dans mon paquet pour qu'ils soient valides dans son contexte.

Après cela, la fonction BaseSrvSxsCreateActivationContextFromMessage vérifie si les chaînes sont valides.

Dans une boucle, elle vérifie six chaînes, mais elle passe parfaitement la vérification. Dans mon cas, je n'ai passé que quatre chaînes et les deux autres sont nulles.

Après d'autres vérifications mineures, elle appelle BaseSrvSxsCreateActivationContextFromStructEx, qui est la fonction la plus importante dans le processus d'activation :

Comment empoisonner le cache d'activation ?

Une fois arrivé à BaseSrvSxsCreateActivationContextFromStructEx, r8 pointera vers _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG, qui est le message d'activation :

Elle évalue la valeur des indicateurs. Dans mon cas, la valeur était 0x41 contre 0xD :

La fonction test peut être contournée en utilisant l'option d'indicateur qui correspond à la validation de l'architecture du processeur (1).

Après cela, elle obtient le RID du processus appelant et le stocke pour une comparaison ultérieure. Dans ce cas, le RID est 0x3000 car CTFMON a un NIVEAU D'INTÉGRITÉ ÉLEVÉ.

La partie la plus importante de cette fonction est l'appel à BaseSrvActivationContextCacheLookupEntry :

Elle recherche dans le Cache des contextes d'activation pour déterminer s'il existe une entrée pour tapi32.dll.

Elle appelle une fonction nommée BaseSrvActivationContextCacheCompareEntries, qui compare certaines parties de l'entrée du Message d'activation à toutes les entrées existantes dans le cache :

Elle compare la valeur LastWriteTime envoyée dans mon paquet avec la même valeur dans toutes les entrées.

J'avais précédemment calculé cette valeur en utilisant GetFileTime sur tapi32.dll et je l'ai envoyée à l'intérieur de mon paquet d'activation :

Comme il n'y a pas d'entrée pour tapi32.dll, les comparaisons ne correspondent pas. Comme prévu, elle renvoie l'erreur 0xC0000225. Après cela, elle vérifie mon ACTX pour voir s'il est approprié d'être ajouté au cache :

Le serveur a besoin de lire mon manifeste XML intégré, et l'adresse Manifest.Offset qui pointait vers lui. Cependant, dans ce nouveau contexte, ce n'est pas encore un pointeur valide. Il vaut la peine de placer un point d'arrêt sur cette valeur pour voir comment et quand mon manifeste XML intégré est lu en utilisant cette valeur.

Comment mon manifeste XML intégré est-il lu ?

Pour vérifier où CSRSS lit mon manifeste XML intégré qui a été envoyé dans ma demande ACTX, il faut placer des points d'arrêt dans Manifest.Offset. De plus, des points d'arrêt doivent être ajoutés chaque fois qu'il s'arrête, s'il copie vers une autre adresse.

Il s'arrête au point d'arrêt lors de la lecture de l'adresse de la valeur Manifest.Offset.

Il utilisera cette adresse pour lire mon manifeste XML intégré à partir du processus CTFMON en utilisant NtReadVirtualMemory, puisque l'adresse placée dans le champ Manifest.Offset appartient à ce contexte :

Mon manifeste XML intégré est lu et copié vers le tampon de destination :

Basculez vers le contexte du processus CTFMON et vérifiez que mon manifeste XML intégré se trouve à l'adresse Manifest.Offset que j'ai précédemment envoyée. Dans mon cas, c'était 0x7ff93a261470.

La lecture du manifeste XML intégré est appelée depuis SxSGenerateActivationContext. Comme il n'est trouvé dans aucune entrée valide du cache, il essaie de le "générer" en utilisant le manifeste intégré :

À partir de là, il commence à analyser mon manifeste XML intégré.

Comment le manifeste XML intégré est-il analysé ?

En regardant la dernière pile d'appels, j'ai décidé de placer un point d'arrêt dans l'appel à RtlReadOutOfProcessMemoryStream pour m'arrêter lorsque le tampon était complètement rempli.

Maintenant, un point d'arrêt peut être placé sur l'accès à la chaîne "Tasks" pour s'arrêter lorsqu'elle est lue ou traitée par le serveur.

Voici la chaîne tasks à l'intérieur du manifeste XML intégré :

Il s'arrête plusieurs fois en lisant et en copiant :

Il s'arrête dans CharEncoder::wideCharFromUtf8 lorsqu'il convertit la chaîne "tasks" en caractères larges :

Il s'arrête ensuite dans l'analyseur XML :

Il continue à analyser les attributs XML, comme le nom de la fonction parseAttributes l'indique.

Ensuite, il s'arrête dans memcpy appelé depuis ValidateElementAttributes :

Un autre point d'arrêt peut être placé là où il copie :

Il valide l'attribut de langue, comme le nom de la fonction SxspValidateLanguageAttribute l'indique :

Il s'arrête à nouveau dans memcpy mais est appelé depuis SxspCreateAssemblyIdentityfromIdentityElement :

Une fois de plus, il s'arrête dans memcpy, cette fois appelé depuis SxsInsertAssemblyIdentityAttribute+0xc48 :

Ensuite, il s'arrête dans SxsInsertAssemblyIdentityAttribute :

Il appelle memcpy une dernière fois, dans cet exemple depuis BufferedStream::prepairForInput :

Puis il lit la chaîne tasks ici :

Ensuite, il la lit depuis ici :

Il continue la lecture depuis ici :

Les noms de ces fonctions ont attiré mon attention. Dans le nom ProbingCandidate, il contient les mêmes mots (probing manifests) utilisés dans le fichier journal texte SXS.

Il s'arrête à nouveau ici :

Ensuite, il utilise GetFileAttributesExW pour vérifier si le premier fichier mentionné dans le journal texte SXS existe. Comme il n'existe pas, il retourne zéro.

L'ordre de la vérification des fichiers peut être vu dans le fichier journal :

Le deuxième fichier n'existe pas car c'est le chemin dans le dossier tasks vers tapi32.dll :

De là, il semble "prober" le fichier tapi32.manifest dans tasks :

Puis il atteint CProbedAssemblyInformation::ProbeManifestExistence :

Il vérifie si mon fichier manifeste existe dans le dossier tasks. Comme il existe, il retourne sans erreur :

Eh bien, le fichier tapi32.manifest dans le dossier "tasks" a été trouvé.

Le serveur a été forcé de chercher un fichier manifeste dans le sous-dossier "tasks" de system32 par mon manifeste XML intégré avec la valeur de langue "tasks" à l'intérieur :

Si l'on continue à placer des points d'arrêt pour voir où il utilise le chemin, il s'arrête dans EncodingStream::Read où le contenu du fichier tapi32.manifest est lu.

Ensuite, il analysera le contenu du fichier TAPI32.manifest. En cas d'erreur, il l'affichera dans le journal SXS TRACE, ce qui facilite la correction.

Si mon fichier TAPI32.manifest est analysé correctement, il retournera à BaseSrvSxsCreateActivationContextFromStructEx sans erreur. Cela évite d'imprimer un message contenant la chaîne FAILED.

Dans mon cas, la génération du contexte d'activation a réussi, en utilisant mon fichier TAPI32.manifest.

J'ai ensuite atteint l'appel où mon entrée sera insérée dans le cache.

Il passe sans problème, retournant zéro. C'est la valeur correcte et l'entrée avec le fichier TAPI32.manifest falsifié est insérée avec succès.

Mon entrée est incluse dans le cache d'activation et le serveur renvoie une réponse OK à l'appel de la DLL depuis CTFMON.

Le fichier journal texte montre le processus complet.

Il lit le manifeste XML intégré. Comme sa langue est "Tasks", il recherche un nouveau fichier manifeste dans le sous-dossier "Tasks" de system32, exactement comme il chercherait un manifeste dans le sous-dossier de system32 appelé "en-us" si la langue était définie sur "en-us."

Le fichier journal texte SXS affiche le message "Activation Context generation succeeded" !

Comment ma fausse imm32.dll a-t-elle fini par être chargée ?

A pair of boxing gloves Description automatically generated

Après l'ajout de mon entrée ACTX dans le cache, si tcmsetup.exe est exécuté, il chargera tapi32.dll, et il devrait utiliser mon fichier manifeste pour charger imm32.dll.

Cependant, ce n'est pas si simple, car il ne peut pas charger imm32.dll car certaines vérifications peuvent l'en empêcher.

Les vérifications sont effectuées lors d'un appel ultérieur à la même fonction BaseSrvSxsCreateActivationContextFromStructEx, donc supprimez tous les points d'arrêt et laissez-en un seul sur celle-ci.

À partir de là, nous pouvons exécuter TCMSETUP.EXE depuis une console, bien que ma preuve de concept exécute TCMSETUP depuis MsCtfMonitor.dll après que l'empoisonnement du cache d'activation soit terminé :

Il s'arrête au point d'arrêt de nombreuses fois. À chaque arrêt, regardez la structure pointée par r8 pour voir si elle correspond à une demande liée à tapi32.dll.

Après de nombreux arrêts pour d'autres modules, une demande pour TCMSETUP.exe apparaît :

Nous voyons dans la pile d'appels qu'elle provient du moment où le processus est créé. Elle est appelée pour vérifier s'il a une entrée dans le cache d'activation pour TCMSETUP.

Continuez à l'exécuter jusqu'à ce que l'appel arrive pour TAPI32.dll. Avant que cela ne se produise, il y aura plusieurs appels pour TCMSETUP.

Enfin, le paquet qui arrive doit être très similaire à celui effectué précédemment depuis ma DLL lorsque j'ai inséré l'entrée dans le cache. Cependant, maintenant il s'arrête lorsque TCMSETUP essaie de charger TAPI32.dll.

À ce stade, j'ai remarqué quelques valeurs importantes dans ce paquet.

En étendant à partir du début de la structure _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG de 0x40 octets vers le haut, attribuez la structure TotalMessage. Le PID du processus qui fait la demande pour TAPI32.dll est TCMSETUP car il veut charger la DLL.

En changeant le contexte au processus TCMSETUP, la valeur Manifest.Offset peut être vue pointant vers un certain manifeste XML intégré.

Ouvrez tapi32.dll dans le NOTEPAD pour voir que le manifeste XML intégré reçu est le même que celui inclus dans le fichier.

TCMSETUP lit la ressource du fichier avant de lire le manifeste et de le placer dans le paquet en tant que manifeste XML intégré.

Après cela, la comparaison est à nouveau effectuée par la fonction BaseSrvActivationContextCacheCompareEntries, qui est appelée depuis BaseSrvSxsCreateActivationContextFromStructEx. Maintenant, mon entrée pour tapi32.dll est également dans le cache.

BaseSrvActivationContextCacheCompareEntries est appelée dans une boucle pour comparer la demande réelle avec chaque entrée du Cache de contexte d'activation (comme la mienne).

Au début, il compare les deux valeurs LastWriteTime, comme elles sont égales, il continue de comparer d'autres valeurs.

Cette valeur LastWriteTime est cruciale. S'il y a des valeurs différentes, il rejettera mon entrée en cache et ma imm32.dll ne sera pas chargée.

Il continue et s'arrête à la vérification suivante.

Maintenant, il vérifie la valeur ResourceName qui doit être 0x7c dans les deux.

Puis il compare la langue du paquet ACTX réel, qui est "en-us", avec la langue de mon entrée en cache. La langue de mon entrée en cache est également "en-us".

Mon paquet a la même valeur de langue :

Ensuite, il compare l'architecture du processeur, qui dans ce cas sera 9 dans les deux cas :

Ensuite, il compare les deux Manifest.path.

J'ai construit le même chemin sans le coder en dur en utilisant la valeur du répertoire système :

Ensuite, il compare AssemblyDirectory, qui est également le même :

Si toutes les comparaisons sont correctes, il retourne zéro. Cela signifie qu'il a trouvé mon entrée dans le cache d'activation et qu'elle sera utilisée.

Rappelez-vous que lorsque j'ai envoyé ma demande la première fois pour ajouter l'entrée, la comparaison a retourné une erreur car il n'y avait pas d'entrée dans le cache pour TAPI32.dll. Comme mon entrée a été ajoutée précédemment, maintenant elle retourne zéro.

Après cela, il compare les RIDs de TCMSETUP avec CTFMON, comme les deux ont RID = 0x3000, le processus continue.

Une explication complète du correctif RID est disponible dans un blog de la Zero Day Initiative.

Voici le code de ce correctif :

R15 contient le RID de l'appelant TCMSETUP = 0x3000 et le buffer a stocké le RID=0x3000 du processus CTFMON.

Comme indiqué précédemment, Microsoft a ajouté ce correctif de vérification RID en octobre 2022.

Après l'implémentation de ce correctif, si vous voulez essayer d'ajouter l'entrée tapi32.dll au cache en utilisant le même MsCtfMonitor.dll depuis un PROCESSUS DE NIVEAU D'INTÉGRITÉ MOYEN (0x2000), l'entrée sera ajoutée au cache, mais elle échouera. Cela est dû au fait que le RID du processus appelant 0x2000 est stocké et lorsque vous essayez d'exécuter TCMSETUP avec RID=0x3000 pour charger imm32, les RIDs sont comparés et l'entrée est supprimée.

Dans ce cas hypothétique, R15 aura le RID=0x3000 du processus TCMSETUP qui a demandé de charger tapi32.dll, la variable "buffer" aura stocké le RID=0x2000 du processus qui a ajouté l'entrée au cache et qui a un NIVEAU D'INTÉGRITÉ MOYEN.

Sur les versions les plus récentes de Windows, l'empoisonnement du cache ne fonctionnera pas si le processus demandant l'ajout de l'entrée est inférieur à l'exécuteur et l'entrée est supprimée. Les versions précédentes publiées avant ce correctif fonctionneront sans problème avec n'importe quel RID.

Pour en revenir à ce cas, la vérification RID est réussie et les deux processus ont le même RID=0x3000. Par conséquent, l'entrée n'est pas supprimée et le processus continue sans erreur.

Le serveur renvoie la réponse à TCMSETUP. Lorsqu'il charge tapi32.dll, il utilisera mon entrée avec le fichier tapi32.manifest, ce qui chargera imm32.dll depuis le dossier tasks.

C'est le chemin complet de LoadLibrary jusqu'à l'endroit où TCMSETUP fait la demande au cache d'activation lors du chargement de tapi32.dll.

BasepCreateActCtx est celui qui fait sa demande au serveur CSRSS. Une tentative doit être faite pour voir quand il finit par charger le module IMM32.dll.

En regardant kernel32.dll, il appelle CsrBasepCreateActCtxCommon. À l'intérieur, il y a un appel serveur similaire à celui effectué depuis ma DLL pour insérer mon entrée en cache.

Il utilise le même ApiNumber que le mien.

Lors de l'exécution de TCMSETUP, un point d'arrêt peut être placé là lorsqu'il revient du serveur, après que mon fichier tapi32.manifest soit accepté.

Voici l'ensemble de la pile d'appels jusqu'à ce que l'appel au serveur dans CsrBasepCreateActCtxCommon soit produit.

Des points d'arrêt sont placés sur le retour de certaines fonctions de la pile d'appels.

Lorsqu'il s'arrête, on peut observer que imm32.dll a été chargée depuis le dossier "tasks" :

La validation peut être obtenue en utilisant PROCESS MONITOR qui montre que TCMSETUP charge IMM32.dll depuis le dossier "tasks".

Le processus CMD qui vient d'être exécuté a des privilèges HAUT.

De plus, il a les mêmes privilèges que Administrateur.

Avec ces privilèges, nous pouvons maintenant installer n'importe quel programme nécessitant une élévation vers administrateur et écrire dans n'importe quel dossier. Par exemple, écrire dans SYSTEM32 ou tout dossier d'installation de programme, comme on peut le voir dans la VIDÉO DE DÉMONSTRATION ci-dessous.

Voici les privilèges avant l'exploitation (Niveau d'intégrité Moyen, pas Administrateur) :

Et maintenant voici les privilèges après l'exploitation (Niveau d'intégrité Haut, Administrateur COMPLET) :

À ce stade, c'est une bonne occasion de s'élever facilement vers SYSTEM, en déposant une DLL falsifiée dans un dossier système.

Vidéo de démonstration et preuve de concept A cartoon of a tv with boxing gloves Description automatically generatedA gold medal with a red ribbon Description automatically generated

Regardez la vidéo ici et la preuve de concept fonctionnelle est ici

TL; DR : Brève description des étapes d'exploitation

  • J'ai envoyé un message ACTX falsifié au serveur CSRSS.

  • Ce message ACTX contenait un manifeste XML intégré avec un décalage qui pointait vers lui.

  • Lorsque le serveur l'a reçu, il a utilisé ce décalage pour lire le manifeste XML intégré depuis le contexte du processus CTFMON.

  • Le manifeste XML intégré a été analysé. S'il était accepté, il essayait de charger un second manifeste externe depuis un dossier externe.

  • Le dossier à lire dépendait du champ de langue dans le manifeste XML intégré contrôlé par moi.

  • Dans mon cas, le manifeste XML intégré avait "tasks" comme langue. Pour cette raison, il a cherché dans le sous-répertoire "tasks" de system32 un manifeste externe et l'a trouvé.

  • Il a analysé le fichier tapi32.manifest créé par moi, et l'a accepté, permettant de charger la DLL externe IMM32.dll depuis le même dossier "tasks".

Merci à Nicolas Economou car sa présentation a été le point de départ de mes recherches et de la publication de ce billet de blog.

Ricardo Narvaja

Télécharger l’outil