
Exploitation à distance du noyau via IPv6
Exploitation à distance du noyau via IPv6
CVE-2024-38063 - Exploitation à distance du noyau via IPv6 Marcus Hutchins
Depuis la dernière mise à jour Windows publiée le 13 août, je me suis plongé dans les tréfonds de tcpip.sys (le pilote du noyau responsable du traitement des paquets TCP/IP). Une vulnérabilité avec un score CVSS de 9,8 dans la partie la plus facilement accessible du noyau Windows était quelque chose que je ne pouvais tout simplement pas laisser passer. Je n'avais jamais vraiment regardé IPv6 auparavant (ni les pilotes responsables de son analyse), donc je savais que tenter de rétro-ingénier cette vulnérabilité serait extrêmement difficile, mais une bonne expérience d'apprentissage.
Pour la plupart, tcpip.sys est largement non documenté. J'ai pu trouver quelques écrits d'exploitation pour des bugs plus anciens : ici, ici, et ici, mais peu d'autres. Lorsque le premier résultat de recherche pour ma requête Google en anglais est écrit en chinois, je sais immédiatement que je suis largement dépassé et que je vais passer un mauvais moment, mais il faut apprendre. Malgré le travail médiocre de Google Traduction, l'article fournissait un aperçu incroyablement détaillé du fonctionnement de la fragmentation IPv6, et m'a donné une bonne longueur d'avance.
Plus tard, en googlant certains noms de fonctions, je suis tombé sur une autre analyse de la même vulnérabilité de 2021, écrite par Axel Souchet (alias 0vercl0k), qui allait encore plus loin dans les entrailles de tcpip.sys et m'a donné suffisamment d'informations pour définir plusieurs structures non documentées. L'analyse de patch la plus facile de tous les temps
Habituellement, même le simple rétro-ingénierie du patch pour déterminer quelle modification de code correspond à la vulnérabilité peut prendre des jours, voire des semaines, mais dans ce cas, ce fut instantané. C'était si facile en fait que plusieurs personnes sur les réseaux sociaux m'ont dit que je me trompais et que le bug était ailleurs. Ai-je vraiment écouté et perdu une journée entière à analyser le mauvais pilote ? On ne le saura jamais.
Il y avait exactement une modification dans l'ensemble du fichier du pilote, qui s'est avérée être effectivement le bug.
Une vue d'ensemble de bindiff de tcpip.sys avant et après l'installation du patch.
Une seule fonction dans tout le pilote a été modifiée. Typiquement, je pourrais passer une journée entière à parcourir plus de 20 modifications de fonctions différentes juste pour déterminer laquelle je devrais examiner, mais pas cette fois.
Ipv6pProcessOptions() avant le patch.
Ipv6pProcessOptions() après le patch.
Non seulement une seule fonction a été modifiée, mais une seule ligne de code.
La fonction au nom extrêmement long Feature_2660322619__private_IsEnabledDeviceUsage_3() est quelque chose que Microsoft ajoute parfois pour permettre des rollbacks partiels de patch. L'appel vérifie la présence d'un indicateur global, ou d'un paramètre de registre, qui, s'il est défini, fait que la fonction renvoie false, entraînant l'exécution du code d'origine au lieu de la version patchée.
La raison pour laquelle Microsoft fait cela est que les correctifs de sécurité cassent parfois involontairement des choses, donc ce paramètre permet à un administrateur de dépatcher une seule vulnérabilité, sans désinstaller l'ensemble du correctif mensuel et affaiblir considérablement la sécurité du système.
En tenant compte de cela, il est clair que tout ce que ce patch fait est de remplacer un appel à IppSendErrorList() par IppSendError(), nous donnant un indice que le problème concerne une sorte de liste. Le diff de patch le plus facile de tous les temps (du moins je le pensais). Vulnérabilités facultatives, exploitation obligatoire
Rétro-ingénierie du patch pour trouver le code modifié n'est que la moitié du défi (ou dans ce cas moins de 0,1%). Le reste du processus consiste à rétro-ingénier suffisamment de la base de code pour comprendre ce qui se passe, déterminer quel type de vulnérabilité a été patché, comment élaborer une requête pour atteindre le code cible, et quel état résulte en une condition exploitable.
La première partie est assez facile. La modification est dans Ipv6pProcessOptions(), ce qui nous indique qu'il s'agit d'IPv6 et implique le traitement des options. Donc, un rapide appel à la RFC nous dit exactement ce qu'est une option IPv6 et où en trouver une.
Le schéma de l'en-tête des options de destination de Wikipédia.
Ok, cool. Ce que nous cherchons semble être l'en-tête des options de destination, qui se trouve juste après l'en-tête IPv6 principal. Utilisons la bibliothèque Python 'scapy' pour construire un paquet IPv6 de test.
Remarque : Pour atténuer les attaques DDoS utilisant des adresses IP usurpées, Windows restreint la capacité à construire des paquets IP bruts. Pour cette raison, j'ai choisi d'utiliser Linux pour développer ma preuve de concept. Bien que Linux permette aux utilisateurs de construire et d'envoyer des paquets bruts de couche 2 et 3, cela nécessite que le script Python soit exécuté en tant que root.
import sys import struct from scapy.all import *
def send_ipv6_option_packet(dest_ip): ethernet_header = Ether() ip_header = IPv6(dst=dest_ip) options_header = IPv6ExtHdrDestOpt() sendp(ethernet_header / ip_header / options_header)
if len(sys.argv) < 2: print('Use: python3 script.py <target_ipv6_address>') exit(-1)
send_ipv6_option_packet(sys.argv[1])
Après avoir placé un point d'arrêt sur tcpip!Ipv6pProcessOptions, puis exécuté le script, il était clair que tout ce qui était nécessaire pour atteindre la fonction vulnérable était d'envoyer un paquet IPv6 avec une structure d'options vide. J'ai ensuite essayé d'ajouter des options invalides à la structure pour voir si je pouvais atteindre l'appel à IppSendErrorList().
Une brève revue de code a indiqué que presque tout formatage d'option invalide pouvait déclencher l'appel à IppSendErrorList. J'ai donc décidé d'utiliser l'option Jumbo Packet avec une longueur invalide (moins de 65535 octets).
options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])
Alors, que fait réellement IppSendErrorList() ? Eh bien, le code est assez simple.
La fonction entière IppSendErrorList.
Le code itère une liste chaînée et appelle IppSendError() sur chaque élément de la liste. Encore une fois, les étoiles se sont alignées et les choses ont été faciles jusqu'à présent. Si IppSendErrorList appelle simplement IppSendError pour chaque élément d'une liste, et que le patch remplace l'appel à IppSendErrorList par IppSendError, alors le problème se produit lorsque IppSendError est appelé sur un élément de liste autre que le premier.
Alors, qu'est-ce que cette liste, et comment en fabriquer une ? Il fait une liste, il la vérifie….52 567 fois
C'est là que les choses sont passées d'évidentes à anormalement difficiles, bien que je pense qu'une grande partie était due au fait qu'une de mes deux cellules cérébrales disponibles était occupée à combattre une mauvaise infection au covid. J'ai perdu quelques jours à comprendre des parties du code, à m'endormir, puis à oublier ce que j'avais compris. L'ensemble du processus a nécessité plus d'une semaine de rétro-ingénierie de parties de tcpip.sys pour comprendre ce qui se passait. Mais le blog d'Axel a été extrêmement utile.
En regardant les fonctions et la structure qu'Axel a rétro-ingéniées, et à quelles autres fonctions elles sont passées, il est clair que le seul et unique argument passé à Ipv6pProcessOptions() est la même structure packet_t définie dans l'article. Essentiellement, le pointeur passé à Ipv6pProcessOptions, et itéré par IppSendErrorList, est une liste chaînée de paquets.
J'ai donc placé un point d'arrêt sur Ipv6pProcessOptions() et inspecté la liste.
L'entrée list->Next est NULL.