
CVE-2024-38063 - Exploitation à distance du noyau via IPv6
Il y a eu exactement un changement dans tout le fichier pilote, qui s'est avéré être le bogue après tout.
Une vue d'ensemble bindiff de tcpip.sys avant et après l'installation du correctif.
Une seule fonction dans tout le pilote a été modifiée. Normalement, je pourrais passer une journée entière à parcourir plus de 20 changements de fonctions différents juste pour trouver celui sur lequel je devrais me concentrer, mais pas cette fois.

Ipv6pProcessOptions() avant le correctif.
Ipv6pProcessOptions() . Après le correctif.
Non seulement une seule fonction a été modifiée, mais une seule ligne de code.
La fonction au nom très long Feature_2660322619__private_IsEnabledDeviceUsage_3() est quelque chose que Microsoft ajoute parfois pour permettre des rollbacks partiels de correctifs. 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 retourne faux, ce qui entraîne l'exécution du code original au lieu de la version corrigé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écorriger une seule vulnérabilité sans désinstaller l'ensemble du rollup mensuel de correctifs et affaiblir considérablement la sécurité du système.
En tenant compte de cela, il est clair que tout ce que fait ce correctif est de remplacer un appel à IppSendErrorList() par IppSendError(), nous donnant un indice que le problème vient d'une sorte de liste. Le diff de correctif le plus simple de tous les temps (du moins je le pensais)
Rétro-ingénieriser le correctif 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énieriser suffisamment de la base de code pour comprendre ce qui se passe, déterminer quel type de vulnérabilité a été corrigé, comment fabriquer une requête pour atteindre le code cible, et quel état résulte en une condition exploitable.
La première partie est assez facile. Le changement se trouve dans Ipv6pProcessOptions(), ce qui nous indique qu'il s'agit d'IPv6 et implique le traitement des options. Donc, un appel rapide au RFC nous dit exactement ce qu'est une option IPv6 et où en trouver une.
Le format de l'en-tête des options de destination d'après Wikipédia.
Ok, cool. Ce que nous cherchons semble être l'en-tête des options de destination, qui se trouve directement après l'en-tête IPv6 principal. Utilisons la bibliothèque Python 'scapy' pour fabriquer un paquet IPv6 de test.
Remarque : pour atténuer les attaques DDoS utilisant des adresses IP usurpées, Windows restreint la capacité de 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])
tcpip!Ipv6pProcessOptions, puis exécuté le script, il était clair que tout ce qu'il fallait 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 (inférieure à 65535 octets).
options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])
Alors, que fait IppSendErrorList() réellement ? Eh bien, le code est assez simple.
La fonction IppSendErrorList dans son intégralité.
Le code parcourt 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 correctif remplace l'appel à IppSendErrorList par IppSendError, alors le problème se produit lorsque IppSendError est appelée sur un élément de la liste autre que le premier.
C'est là que les choses sont passées d'évidentes à anormalement difficiles, même si je pense qu'une grande partie était due à l'une de mes deux cellules cérébrales disponibles qui é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. Tout le 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 l'article de blog d'Axel a été extrêmement utile.
En regardant les fonctions et la structure qu'Axel a rétro-ingéniérié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 parcouru 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.