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-38063 — CVE-2024-38063 - Remotely Exploiting The Kernel Via IPv6 | Kitploit
Outils/GitHubGitHub/faizan-khanx/cve-2024-38063
Memory ForensicsVulnerability AnalysisExploitationReverse EngineeringNetwork SecurityPapers & ResearchLearning & EducationBinary Exploitation
GitHubfaizan-khanx/cve-2024-38063

CVE-2024-38063

CVE-2024-38063 - Remotely Exploiting The Kernel Via IPv6

Voir le dépôt
1il y a 1 anPas encore vérifié

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

CVE-2024-38063 - Exploiter le noyau à distance via IPv6

  • Depuis l'arrivée du dernier correctif Windows le 13 août, je suis plongé dans les entrailles de tcpip.sys (le pilote noyau responsable du traitement des paquets TCP/IP). Une vulnérabilité avec un score CVSS de 9,8 dans la partie la plus 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énieriser cette vulnérabilité serait extrêmement difficile, mais une bonne expérience d'apprentissage.

L'analyse de correctif la plus simple de tous les temps

  • En général, même le simple fait de rétro-ingénieriser le correctif pour trouver quel changement de code correspond à la vulnérabilité peut prendre des jours, voire des semaines, mais dans ce cas, ce fut instantané. C'était tellement facile en fait que plusieurs personnes sur les réseaux sociaux m'ont dit que j'avais tort et que le bogue se trouvait ailleurs. Ai-je vraiment écouté et perdu une journée entière à rétro-ingénieriser le mauvais pilote ? On ne le saura jamais.

Il y a eu exactement un changement dans tout le fichier pilote, qui s'est avéré être le bogue après tout. image 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. image

Ipv6pProcessOptions() avant le correctif.

image 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)

Vulnérabilités optionnelles, exploitation obligatoire

  • 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.

image 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.

root@kitploit:~
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 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. image 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.

Il fait une liste, il la vérifie 52 567 fois

  • 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.

image

L'entrée list->Next est NULL.

  • Chaque fois que mon point d'arrêt était atteint, la liste ne contenait qu'un seul paquet. J'ai passé beaucoup plus de temps que je ne voudrais l'admettre à essayer de comprendre pourquoi et comment faire en sorte que ma liste soit réellement une liste. Ma première pensée a été la fragmentation IPv6 : IPv6 permet aux expéditeurs de diviser les gros paquets en paquets plus petits séparés, qu'il serait logique de garder ensemble dans une liste.

  • Après une rétro-ingénierie approfondie, j'ai confirmé que mes hypothèses étaient correctes, bien que la liste des fragments ne soit pas liée à celle avec laquelle nous traitons ici.

  • J'ai fini par trouver la réponse totalement par hasard. Occasionnellement, la liste se remplissait, mais la raison n'était pas claire. Après beaucoup de tours en rond, j'ai réalisé que lorsque mon point d'arrêt dans le noyau est déclenché, il met en pause tout le noyau, ce qui fait que la carte réseau accumule des paquets. Lorsque le noyau reprend, ces paquets sont transmis à la pile jusqu'à tcpip.sys dans une belle liste bien ordonnée. Cela ne se produisait que si les paquets étaient envoyés pendant que le noyau était en pause, mais pas traités avant le prochain point d'arrêt.

  • Ce comportement est probablement une optimisation de performance, où à faible débit, le noyau traite les paquets individuellement, mais à des volumes plus élevés, les paquets sont organisés en listes et traités par lots. Très probablement, les listes sont séparées en fonction de facteurs tels que le protocole et l'adresse source pour accélérer le traitement, donc notre liste ne devrait contenir que les paquets IPv6 que nous avons envoyés.

  • Maintenant que nous savons que les paquets sont regroupés en listes pendant les hauts débits, il est clair quelle serait l'option la plus facile. Notre PoC de déni de service va ironiquement devoir utiliser un déni de service pour déclencher la condition de déni de service. Si nous inondons le système avec des salves de paquets IPv6, nous devrions pouvoir obtenir une belle grande liste passée à IppSendErrorList().

  • Au début, peu importe combien de paquets j'envoyais, je ne pouvais toujours obtenir une liste de taille n > 1 qu'en mettant le noyau en pause. Mais… comme nous utilisons Python (douloureusement lent), dans une VM (doublement douloureusement lent), nous allons probablement devoir ajuster certains paramètres. Pour contrer la VM-ception qui se produit sur mon système d'attaque, j'ai simplement décidé de reconfigurer la VM cible pour utiliser un seul cœur de CPU.

image

Super ! La liste de paquets est maintenant une liste contenant beaucoup d'entrées !

  • Donc, il s'avère qu'une VM dans une VM n'est pas la meilleure option pour un DoS, qui l'aurait cru ? Mais nous avons fini par y arriver. Maintenant, il suffit de comprendre ce que fait IppSendError() et où se situe le problème.

Encore plus de rétro-ingénierie pour toujours.

  • Après une rétro-ingénierie intensive, il est devenu beaucoup plus clair ce que fait IppSendError. Dans des circonstances normales, il désactive simplement le paquet en définissant net_buffer_list->Status à 0xC000021B (STATUS_DATA_NOT_ACCEPTED). Ensuite, il transmet une erreur ICMP contenant des informations sur le paquet erroné à l'expéditeur.

    image Deux parties pertinentes de IppSendError.

  • Mon premier réflexe a été de voir s'il y avait des fonctions dans tcpip.sys qui ignorent la valeur de net_buffer_list->Status. Cela entraînerait le traitement par le pilote de paquets dans des états indéfinis ou inattendus, menant espérons-le à une condition exploitable.

image

La boucle principale responsable du traitement des paquets.

  • Comme la boucle responsable de l'appel à toutes les fonctions d'analyse est encapsulée dans une vérification d'erreur (ce qui signifie que nous ne pouvons aller nulle part une fois le code d'erreur défini), j'ai pensé que c'était le mauvais terrier de lapin. J'ai donc décidé de revenir à IppSendError et de voir s'il y a des chemins de code qui modifient l'état du paquet avant de définir le code d'erreur, ce qui pourrait conduire à une condition de concurrence.

  • Après encore beaucoup de rétro-ingénierie, j'ai trouvé le code suivant près du bas de IppSendError.

image

Un chemin de code dans IppSendError qui définit packet_size à zéro.

  • Lorsque IppSendErrorList, et donc IppSendError, est appelé avec l'argument always_send_icmp défini à true, il semble tenter d'envoyer l'erreur ICMP à chaque paquet de la liste.

  • Ensuite, pour des raisons probablement connues uniquement de Dieu, il atteint un bloc de code où le champ packet->packet_size est défini à zéro.

  • Pour définir always_send_icmp à true, tout ce que nous avons à faire est de provoquer une erreur spécifique dans le traitement de l'en-tête d'options en définissant la valeur 'Option Type' à un nombre supérieur à 0x80.

root@kitploit:~
def build_malicious_option(next_header, header_length, option_type, option_length):
    dest_options_header = 60
	
    options_header = struct.pack('BBBB', next_header, header_length, option_type, option_length) + b'1337'
    return Ether(dst=mac_addr) / IPv6(dst=ip_addr, nh=dest_options_header) / raw(options_header)

packet = build_malicious_option(next_header=59, header_length=0, option_type=0x81, option_length=0)	
sendp(packet)

Mais, définir packet_size à zéro ne devrait-il pas casser l'analyseur ?

image

Un extrait de la boucle principale responsable du traitement des paquets.

  • Le gestionnaire de paquets appelle simplement une fonction de la VTable basée sur la valeur packet->next_header, qui reste inchangée depuis qu'elle a été définie lors du pré-traitement. cela permet au traitement des paquets de continuer et nous donne même le contrôle sur le traitement qui se produit.

  • Parce que la valeur packet->next_header est obtenue à partir du champ 'Next Header' du paquet IPv6, nous pouvons la définir à n'importe quelle valeur d'en-tête IPv6 valide, et la boucle appellera l'analyseur correspondant. Cela nous donne beaucoup de surface d'attaque potentielle. image

    Le format du paquet IPv6.

  • Il ne reste plus qu'à trouver une partie accessible de l'analyseur IPv6 qui fait quelque chose de stupide avec le champ packet_size.

Retour à la fragmentation

  • Le premier endroit où j'ai décidé de regarder était l'analyseur de fragments IPv6, car c'est là que se trouvait l'ancienne vulnérabilité CVE-2021-24086, cela semblait donc un bon endroit pour trouver plus de code farfelu. image

Eh… c'est si proche, mais aussi si loin

  • Nous avons bien une vulnérabilité ici, mais ce n'est pas une RCE.

  • Essentiellement, sur la plupart des CPU, les registres sont circulaires. Si vous incrémentez un registre au-delà de sa valeur maximale possible, il boucle et revient à zéro. De même, si vous le décrémentez en dessous de sa valeur minimale possible, il boucle et revient à la valeur maximale possible. On parle de débordements d'entiers (integer overflow) et de sous-débordements d'entiers (integer underflow) respectivement. Ce comportement est légèrement différent pour les entiers signés, mais nous n'avons pas affaire à ceux-ci ici.

  • La première ligne, fragment_size = LOWORD(packet->packet_size) - 0x30, se compose du code ASM suivant :

    image

    Le code ASM calculant la taille du fragment.

  • AX est les 16 bits de poids faible du registre EAX. Bien que le registre EAX soit sur 32 bits, AX fonctionne comme s'il s'agissait de son propre registre 16 bits, donc tout débordement ou sous-dépassement est confiné à AX et n'affecte pas le reste du registre EAX. C'est incroyablement pratique car un sous-dépassement sur le registre EAX donnerait une valeur de 4 milliards, ce qui entraînerait une tentative d'allocation de 4 Go de mémoire, ce qui échouerait probablement.

  • Puisque la valeur de packet->packet_size est zéro, ce code met ax à zéro, puis soustrait 0x30 de celui-ci.

  • Dans des conditions normales, l'en-tête du paquet fait 0x30 octets, donc packet_size - 0x30 est la taille des données du fragment.

  • Dans notre cas, packet->packet_size vaut 0, donc même lui soustraire 1 fera que le registre boucle et revienne à la valeur maximale possible d'un entier 16 bits (0xFFFF). Comme nous soustrayons 0x30, la valeur de AX débordera et deviendra MAX_VALUE - 0x2F, soit 0xFFD0, ce qui fait 65 488.

Les fragments IPv6 resteront en mémoire jusqu'à ce que l'une des trois conditions se produise :

  • Nous gâchons suffisamment notre fragmentation pour que le système nous dise qu'il est temps d'arrêter.
  • Nous envoyons un fragment avec le champ 'More' à 0, ce qui indique qu'il s'agit du dernier fragment, et le système commencera le réassemblage.
  • Nous n'envoyons pas le dernier fragment avant l'expiration du délai (60 secondes), et le système abandonne les fragments.
  • Ipv6pReassemblyTimeout() est appelée dans la condition 3, examinons donc comment cela peut être exploité.

Ipv6pReassemblyTimeout() est appelée dans la condition 3, examinons donc comment cela peut être exploité.

image

C'est exactement ce qu'il nous faut !

  • Précédemment, notre problème était que le code utilisait exactement le même calcul pour l'allocation mémoire et l'opération de copie. Ce code, en revanche, ne le fait pas. Regardons de plus près le code ASM pour voir comment il est exploitable.

image

Le code assembleur responsable du calcul de la taille d'allocation.

  • Comme vous pouvez le voir ici, la première partie du calcul (fragment_list->net_buffer_length + reassembly->packet_length + 8) est effectuée en utilisant le registre 16 bits DX.

  • Si vous vous souvenez, nous avons sous-dépassé reassembly->packet_length à 0xFFD0. Donc le registre DX, après avoir ajouté les 8 octets, vaut 0xFFD8. Si fragment_list->net_buffer_length est supérieur à 0x27 (39 octets), DX débordera et se réinitialisera à zéro.

  • fragment_list->net_buffer_length devrait être d'environ 0x38 octets, donc cela provoquera un débordement du registre DX à 8. Après l'ajout des 0x28 octets, nous obtiendrons une allocation mémoire de seulement 48 octets.

  • Comme l'appel memmove() suivant utilise simplement la valeur non modifiée de reassembly->packet_length pour la taille, cela entraînera la copie de 65 488 octets depuis reassembly->payload vers un tampon de 30 octets. -Un excellent bonus est qu'une grande partie des données copiées provient de la charge utile du fragment, que nous contrôlons, et peut être des données arbitraires de n'importe quel format, nous obtenons donc un joli dépassement de tampon du pool noyau assez contrôlable.

  • Pour avoir une chance de déclencher la vulnérabilité, nous devons avoir un ou plusieurs paquets de fragments situés après le paquet d'options malformé dans la liste chaînée au moment où IppSendErrorList est appelée. Cependant, d'après mes tests, cela ne semble pas garantir l'exploitation. Je pense qu'il y a également d'autres conditions qui doivent être remplies. Je soupçonne, sans l'avoir confirmé, que le code de synchronisation dans IppSendError signifie que nous devons également gagner une condition de concurrence.

Réseaux sociaux

Faizan's GitHub stats

instagram twitter linkedin github

Télécharger l’outil
  • Malheureusement, comme le même calcul est utilisé à la fois pour l'allocation mémoire et la copie des données, nous n'obtenons pas de dépassement de tampon. Je crois que RtlCopyMdlToBuffer() effectue également des vérifications de limites sur le tampon source, donc nous n'obtenons même pas de lecture hors limites. Cependant, nous ne repartons pas complètement les mains vides.

  • Parce que ExAllocatePoolWithTagPriority() ne met pas à zéro la mémoire allouée, et que RtlCopyMdlToBuffer() ne copie que la quantité réelle de données disponibles, nous obtenons environ 65 Ko de mémoire noyau non initialisée. Comme les adresses mémoire sont recyclées après désallocation, le tampon est susceptible d'être rempli avec ce qui était précédemment stocké à l'adresse avant la réallocation. Si nous pouvons utiliser la fragmentation pour construire un paquet qui nous est renvoyé, comme une requête Echo ICMP, nous pourrions potentiellement divulguer de la mémoire noyau aléatoire, menant à un contournement d'ASLR.

  • En plus de cela, le code définit également reassembly->fragment_size à l'entier 16 bits sous-dépassé (65 488), donc nous avons maintenant deux variables distinctes que nous pourrions potentiellement utiliser pour provoquer un dépassement de tampon.

  • La solution (ou du moins l'une d'elles) est Ipv6pReassemblyTimeout(). Bien que nous ne puissions pas provoquer de débordement dans le traitement initial des fragments, nous le pouvons apparemment lors du nettoyage.