Très proche de la faille d’exploitation zero-day : exploitation du service de streaming du noyau Microsoft.

Image d'un homme barbu travaillant tard au bureau pour respecter son délai, avec deux collègues assis derrière lui

Le mois dernier, Microsoft a corrigé une vulnérabilité du Microsoft Kernel Streaming Server, un composant du noyau Windows utilisé pour la virtualisation et le partage des périphériques des appareils photo. La vulnérabilité, CVE-2023-36802, permet à un pirate local d'élever ses privilèges au niveau de SYSTEM.

Cet article de blog détaille mon processus de découverte d'une nouvelle surface d’attaque dans le noyau Windows, la découverte d'une vulnérabilité zero-day, la découverte d'une classe de bogues intéressante et le développement d'un exploit stable. Il n'est pas nécessaire d'avoir des connaissances spécialisées sur le noyau Windows pour suivre cet article, mais une compréhension de base de la corruption de la mémoire et des concepts du système d'exploitation est utile. Je présenterai également les principes fondamentaux de l'analyse initiale d'un pilote de noyau inconnu et simplifierai le processus d'examen d'une nouvelle cible.

La surface d’attaque

Le serveur de streaming Microsoft Kernel (mskssrv.sys) est un composant d'un service de Windows Multimedia Framework, Frame Server. Le service virtualise l’appareil photo et permet de le partager entre plusieurs applications.

J’ai commencé à découvrir cette surface d’attaque après avoir pris connaissance de la vulnérabilité CVE-2023-29360, qui était initialement listée comme une vulnérabilité du pilote TPM. Le bogue provient en fait du serveur de streaming Microsoft Kernel. Bien qu'à l'époque je ne connaissais pas le MS KS Server, le nom de ce pilote a suffi à éveiller mon intérêt. Bien que nous ne sachions rien de l’objectif ou de la fonctionnalité, je me suis dit qu’un serveur de streaming dans le noyau pouvait être un endroit utile pour rechercher des vulnérabilités. J’y suis allée à l’aveugle, j’ai cherché à répondre aux questions suivantes :

  • Dans quelle mesure une application non privilégiée peut-elle interagir avec ce module du noyau ?
  • Quel type de données provenant de l'application est traité directement par le module ?

Pour répondre à la première question, j'ai commencé par analyser le binaire dans un désassembleur. J’ai rapidement identifié la vulnérabilité susmentionnée, un bug logique simple et élégant. Le problème semblait simple à déclencher et à exploiter pleinement, j'ai donc cherché à développer rapidement une preuve de concept afin de mieux comprendre le fonctionnement interne du pilote mskssrv.sys.

Les dernières actualités technologiques, étayées par des avis d’experts

Restez au fait des tendances les plus étonnantes du secteur dans le domaine de l’IA, de l’automatisation, des données et bien d’autres avec la newsletter Think. Consultez la Déclaration de confidentialité d’IBM.

Merci ! Vous êtes abonné(e).

Vous recevrez votre abonnement en anglais. Vous trouverez un lien de désabonnement dans chaque newsletter. Vous pouvez gérer vos abonnements ou vous désabonner ici. Consultez la Déclaration de confidentialité d’IBM pour plus d’informations.

Analyse initiale

Déclencher l'exécution sur MS KS Server

Tout d'abord, nous devons être en mesure d'accéder au pilote à partir d'une application de l'espace utilisateur. La fonction vulnérable est accessible depuis la routine DispatchDeviceControl du pilote, ce qui signifie qu'elle peut être atteinte en envoyant une commande IOCTL au pilote. Pour ce faire, il est nécessaire d'obtenir un descripteur du périphérique du pilote via un appel à CreateFile en utilisant le chemin d'accès du périphérique. En règle générale, il est facile de trouver le nom du périphérique/chemin d'accès : recherchez un appel à IoCreateDevice dans le pilote et examinez le troisième paramètre qui contient le nom du périphérique.

Fonction dans mskssrv.sys qui appelle IoCreateDevice avec un pointeur NULL pour le nom du périphérique.

Fonction dans mskssrv.sys qui appelle IoCreateDevice avec un pointeur NULL pour le nom du périphérique.

Dans ce cas, le paramètre du nom de l'appareil est NULL. Le nom de la fonction appelante suggère que mskssrv est un pilote PnP, et l'appel à IoAttachDeviceToDeviceStack indique que l'objet périphérique créé fait partie d'une pile de périphériques. En réalité, cela signifie que plusieurs pilotes sont appelés lorsqu'une requête d'E/S est envoyée à un périphérique. Pour les dispositifs PnP, le chemin d'interface du dispositif est nécessaire pour accéder au dispositif.

À l'aide du débogueur du noyau WinDbg, nous pouvons voir quels appareils appartiennent au pilote mskssrv et à la pile de périphériques :

Résultat des commandes !drvobj et !devobj affichant les périphériques supérieurs et inférieurs

Résultat des commandes !drvobj et !devobj affichant les périphériques supérieurs et inférieurs

Ci-dessus, nous voyons que l'appareil de mskssrv est connecté à l'objet inférieur appartenant au pilote swenum.sys et dispose d'un dispositif supérieur associé à ksthunk.sys.

Dans le gestionnaire de périphériques, nous pouvons trouver l'ID de l'instance du périphérique cible :

Le gestionnaire de périphériques affiche l'identifiant d'instance du périphérique et le GUID de l'interface

Le gestionnaire de périphériques affiche l'identifiant d'instance du périphérique et le GUID de l'interface

Nous disposons maintenant de suffisamment d'informations pour obtenir le chemin d'accès à l'interface de l'appareil à l'aide du gestionnaire de configuration ou des fonctions SetupApi. À l'aide du chemin d'accès à l'interface du périphérique récupéré, nous pouvons ouvrir un accès au périphérique.

Enfin, nous sommes désormais en mesure de déclencher l'exécution de code dans mskssrv.sys. Lorsque le périphérique est créé, la fonction PnP dispatch create du pilote est appelée. Pour déclencher l'exécution d'un code supplémentaire, nous pouvons envoyer des IOCTL pour communiquer avec le périphérique, qui s'exécutera dans la fonction dispatch device control du pilote.

Débogage d’un pilote fantôme

Lors de l’exécution d’une analyse binaire, il est une bonne pratique d’utiliser une combinaison d’outils statiques (désassembleur, décompilateur) et dynamiques (débogueur). WinDbg peut être utilisé pour déboguer le pilote cible. En plaçant des points d'arrêt aux endroits où l'exécution du code est censée se produire (dispatch create, dispatch device control).

Au début, j'ai rencontré quelques difficultés : aucun des points d'arrêt que j'avais définis dans le pilote n'était atteint. Je me suis demandé si j'ouvrais le bon périphérique ou si je commettais une autre erreur. J'ai réalisé plus tard que mes points d'arrêt étaient désactivés parce que le pilote était déchargé. J'ai cherché des réponses sur Internet, mais la recherche sur mskssrv n'a pas donné beaucoup de résultats, bien qu'il soit chargé et accessible par défaut sur Windows. Parmi les quelques résultats que j'ai trouvés, il y avait un fil de discussion sur OSR, où quelqu'un d'autre avait rencontré un problème similaire.

Commentaire sur un forum

Il s'avère que les pilotes de filtre PnP peuvent être déchargés s'ils n'ont pas été utilisés pendant un certain temps, et rechargés à la demande en cas de besoin.

J'ai résolu les problèmes que je rencontrais en fixant des points d'arrêt après l'ouverture d'un accès vers le périphérique, mais avant d'appeler DeviceIoControl, afin de m'assurer que le pilote a été récemment chargé.

Un examen rapide des fonctionnalités des pilotes

Le pilote mskssrv n’a qu’une taille binaire de 72 Ko et prend en charge les codes de contrôle Device IO qui appellent les fonctions suivantes :

  • FSRendezvousServer::InitializeContext
  • FSRendezvousServer::InitializeStream
  • FSRendezvousServer::RegisterContext
  • FSRendezvousServer::RegisterStream
  • FSRendezvousServer::DrainTx
  • FSRendezvousServer::NotifyContext
  • FSRendezvousServer::PublishTx
  • FSRendezvousServer::PublishRx
  • FSRendezvousServer ::ConsumeTx
  • FSRendezvousServer::ConsumeRx

L'examen de ces noms de symboles permet de déduire certaines fonctionnalités du pilote, notamment en ce qui concerne la transmission et la réception de flux. A ce stade, je me suis penché sur les fonctionnalités prévues du pilote. J'ai découvert dans cette présentation de Michael Maltsev sur le cadre des exigences multimédia de Windows où j'ai découvert que le pilote fait partie d'un mécanisme inter-processus pour Partager les flux de caméra.

Comme le pilote n’est pas très grand et qu’il n’y a pas beaucoup d’IOCTL, je peux examiner chaque fonction pour avoir une idée des composants internes du pilote. Chaque fonction IOCTL opère soit sur un objet d'enregistrement de contexte, soit sur un objet d'enregistrement de flux, qui est alloué et initialisé via leurs IOCTL « Initialize » correspondants. Le pointeur vers l'objet est stocké dans Irp->CurrentStackLocation->FileObject->FsContext2. FileObject pointe vers l'objet fichier de périphérique créé pour chaque fichier ouvert, et FsContext2 est un champ destiné à stocker les métadonnées de chaque objet fichier.

La vulnérabilité

J'ai identifié ce bug en essayant de comprendre comment communiquer directement avec le pilote, en renonçant dans un premier temps à l'analyse des composants en mode utilisateur, fsclient.dll et frameserver.dll. J'ai failli passer à côté du bug, car j'ai supposé que les développeurs avaient instancié une simple vérification qui n'a pas été prise en compte. Examinons la fonction PublishRx IOCTL :

Extrait de décompilation de FSRendezvousServer::PublishRx

Extrait de décompilation de FSRendezvousServer::PublishRx

Une fois l'objet flux récupéré à partir de FsContext2, la fonction FSRendezvousServer::FindObject est appelée afin de vérifier que le pointeur correspond à un objet présent dans deux listes stockées par le FSRendezvousServer global. Au départ, j'ai supposé que cette fonction disposerait d'un moyen de vérifier le type d'objet demandé. Cependant, la fonction renvoie TRUE si le pointeur est présent dans l'une ou l'autre des listes d'objets de contexte ou d'objets flux. Veuillez noter qu'aucune information sur le type supposé de l'objet n'est transmise à FindObject. Cela signifie qu'un objet de contexte peut être transmis comme un objet de flux. Il s'agit d'une vulnérabilité liée à une confusion de type d'objet. Elle se produit dans toutes les fonctions IOCTL qui opèrent sur des objets de flux. Pour corriger cette vulnérabilité, Microsoft a remplacé FSRendezvousServer::FindObject par FSRendezvousServer::FindStreamObject, qui vérifie d'abord que l'objet est un objet de flux en contrôlant un champ de type.

Exploitation

Primitive

Étant donné que les objets d'enregistrement de contexte sont plus petits (0x78 octets) que les objets d'enregistrement de flux (0x1D8 octets), les opérations sur les objets de flux peuvent être effectuées sur une mémoire hors limites :

Illustration de la vulnérabilité de la confusion des types d'objets

Illustration de la vulnérabilité de la confusion des types d'objets

Jet d'eau pour piscine

Pour tirer parti de la vulnérabilité primitive, nous devons être en mesure de contrôler la mémoire hors limite à laquelle nous accédons. Cela peut se faire en déclenchant l'allocation de nombreux objets dans la même zone de mémoire que l'objet vulnérable. Cette technique est appelée pulvérisation (de tas ou de pool). L'objet vulnérable est alloué dans un pool de mémoire non paginé à faible fragmentation. Nous pouvons utiliser la technique classique d’Alex Ionescu pour pulvériser des tampons qui permettent un contrôle total du contenu de la mémoire sous un en-tête DATA_QUEUE_ENTRY de 0x30 octets. En utilisant cette technique, nous pouvons obtenir la mise en page de mémoire montrée dans le diagramme :

Illustration de pulvérisation dans un pool non paginé

En utilisant la méthode choisie de pulvérisation en pool, les champs dans les décalages d'objets dans les plages 0xC0-0x10F et 0x150-0x19F peuvent être contrôlés. J'ai réexaminé les fonctions IOCTL pour les objets de flux afin de rechercher des primitives d'exploitation. J'ai recherché les endroits où les champs d'objets contrôlables sont accessibles et manipulables.

Écriture constante

J'ai identifié une primitive d'écriture constante efficace dans PublishRx IOCTL. Cette primitive peut être utilisée pour écrire une valeur constante à une adresse mémoire arbitraire. Examinons un extrait de la fonction FSStreamReg::PublishRx :

Extrait de décompilation de FSStreamReg::PublishRx

Extrait de décompilation de FSStreamReg::PublishRx

L'objet stream contient une tête de liste à l'offset 0x188 qui décrit une liste d'objets FSFrameMdl. Dans l'extrait de décompilation ci-dessus, cette liste est parcourue et si la valeur de la balise dans l'objet FSFrameMdl correspond à la balise dans le tampon système transmis par l'application, la fonction FSFrameMdl::UnmapPages est appelée.

En utilisant la primitive d'exploitation susmentionnée, le FSFrameMdlList et donc l'objet FsFrameMdl pointé par pFrameMdl peuvent être entièrement exploités. Examinons maintenant UnmapPages :

Décompilation de FSFrameMdl:UnmapPages

Décompilation de FSFrameMdl:UnmapPages

Sur la dernière ligne de la fonction décompilée ci-dessus, la valeur constante 2 est écrite dans une valeur de décalage de cet objet (FSFrameMdl ) qui est contrôlable. Cette écriture constante peut être utilisée conjointement avec la technique I/O Ring pour obtenir une lecture/écriture arbitraire du noyau et une élévation de privilèges. Vous pouvez en savoir plus sur le fonctionnement de cette technique ici et ici.

Bien que j'aie choisi d'utiliser la primitive d'écriture constante, une autre primitive d'exploitation utile apparaît également dans cette fonction. Les arguments BaseAddress et MemoryDescriptorList pour l’appel à MmUnmapLockedPages sont contrôlables. Ceci pourrait être utilisé pour démapper un mapping à une adresse virtuelle arbitraire et construire une primitive de type use-after-free.

Le problème de charge

À ce stade, plusieurs primitives d'exploitation appropriées permettant une lecture-écriture arbitraire du noyau ont été identifiées. Vous avez peut-être remarqué que le contenu de l'objet stream doit être vérifié à plusieurs reprises pour obtenir le chemin de code souhaité. Dans la plupart des cas, l'état adéquat de l'objet peut être obtenu par pulvérisation en pool. Cependant, j’ai rencontré un problème qui a causé quelques difficultés. Vous trouverez ci-dessous un extrait de code de FSStreamReg::PublishRx après qu'il ait parcouru la liste FSFrameMdlList :

Extrait de décompilation de FSStreamReg::PublishRx

Extrait de décompilation de FSStreamReg::PublishRx

Dans la décompilation ci-dessus, bPagesUnmapped est une variable booléenne qui est définie si FSFrameMdl::UnmapPages est appelé. Si c’est le cas, le décalage 0x1a8 de l’objet flux est récupéré et, s’il n’est pas nul, KeSetEvent est appelé dessus.

Ce décalage correspond à la mémoire hors limites et se situe dans un POOL_HEADER, la structure de données qui sépare les allocations de mémoire tampon dans le pool. Il pointe notamment vers le champ ProcessBilled, qui est utilisé pour stocker un pointeur vers l'objet _EPROCESS pour le processus « chargé » de l'allocation. Cela permet de comptabiliser le nombre d'allocations de pool qu'un processus particulier peut avoir. Toutes les allocations de pool ne sont pas « facturées » à un processus, et celles qui ne le sont pas ont le champ ProcessBilled défini sur NULL dans le POOL_HEADER. De plus, le pointeur EPROCESS stocké dans ProcessBilled est en fait XOR avec un cookie aléatoire, de sorte que ProcessBilled ne contient pas de pointeur valide.

Cela pose une difficulté, car les tampons NpFr sont chargés vers le processus appelant, et donc ProcessBilled est défini. Lors du déclenchement de la primitive d'exploit nécessaire, bPagesUnmapped sera défini sur TRUE. Si un pointeur invalide est transmis à KeSetEvent, le système se bloque. Il est donc nécessaire de s’assurer que le POOL_HEADER concerne une allocation non chargée. À ce stade, j'ai remarqué que l'objet d'enregistrement de contexte(Creg) lui-même n'était pas chargé. Toutefois, cet objet ne permet pas de contrôler le contenu de la mémoire au niveau du décalage FSFrameMdl. Les objets NpFr et Creg doivent donc être pulvérisés et séquencés correctement.

Fuite de pool — Évitez de pulvériser et espérer que cela fonctionne !

Contrairement aux grandes allocations de pool, vous ne pouvez pas divulguer les adresses des allocations de pool LFH via NtQuerySystemInformation. De plus, l'ordre d'allocation est aléatoire. Par conséquent, il n’y a aucun moyen de savoir si les tampons adjacents à l’objet vulnérable sont dans le bon ordre pour exploiter la primitive et éviter de faire planter le système. Heureusement, cette vulnérabilité peut être utilisée pour déclencher une fuite du pool des tampons adjacents. Examinons la fonction IOCTL pour ConsumeTx :

Extrait de décompilation de FSRendezvousServer::ConsumeTx

Extrait de décompilation de FSRendezvousServer::ConsumeTx

Ci-dessus, la fonction FSStreamReg : :GetStats est appelée :

Décompilation de FSStreamReg::GetStats

Décompilation de FSStreamReg::GetStats

Ici, le contenu de la mémoire hors limites de l'objet stream vulnérable est copié dans le SystemBuffer qui est renvoyé à l'application de l'espace utilisateur appelante. Cette primitive de fuite d'information peut être utilisée pour effectuer un contrôle de signature sur les tampons adjacents à l'objet vulnérable. Un scan de nombreux objets vulnérables peut être effectué jusqu'à ce que l'objet se trouve dans la configuration de mémoire souhaitée. Une fois que l'objet souhaité est localisé, la disposition de la mémoire est la suivante :

CVE-2023-36802 : disposition du pool de mémoire dynamique à faible fragmentation

CVE-2023-36802 : disposition du pool de mémoire dynamique à faible fragmentation

Maintenant que l'objet vulnérable cible a été localisé à la bonne position dans la mémoire, l'exploit primitif susmentionné sur l'objet cible peut être déclenché sans provoquer de plantage du système.

Dans l'exploitation illégale

Après avoir signalé le problème au CSEM, une exploitation sauvage de la vulnérabilité a été découverte.

Les méthodes d’exploitation présentées dans cet article de blog font partie des nombreuses approches qui pourraient être adoptées. À l'heure actuelle, il n'existe aucune information publique sur la manière dont les pirates ont exploité cette vulnérabilité. Vous pouvez trouver le code pour exploiter ici.

Conclusion

Une analyse rétroactive des correctifs a révélé qu’une grande partie du nouveau code avait été ajoutée à msksrv.sys dans la version 1809 de Windows 10. La surveillance des nouveaux ajouts de code est souvent utile pour détecter les vulnérabilités.

Une autre leçon classique, bien que souvent répétée, à retenir de cette analyse : ne faites pas de suppositions sur les vérifications effectuées. Un ami et collègue a suggéré que la confusion de types lors de l'utilisation de FsContext2 pourrait être une « catégorie de bugs courante mais sous-étudiée ». Je pense qu'une analyse plus poussée des variantes est justifiée pour cette classe de bugs, en particulier pour les pilotes qui traitent de la communication inter-processus.

Cette vulnérabilité a été découverte lors d'une simple tentative d'interaction avec une surface d’attaque inconnue. Avoir une « connaissance quasi nulle » d'un système peut également signifier avoir l'esprit suffisamment ouvert pour le réinventer.

Mixture of Experts | 12 décembre, épisode 85

Décryptage de l’IA : Tour d’horizon hebdomadaire

Rejoignez notre panel d’ingénieurs, de chercheurs, de chefs de produits et autres spécialistes de premier plan pour connaître l’essentiel de l’actualité et des dernières tendances dans le domaine de l’IA.