Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2024-38063 — CVE-2024-38063 - Remote-Ausnutzung des Kernels über IPv6 | Kitploit
Tools/GitHubGitHub/faizan-khanx/cve-2024-38063
SpeicherforensikSchwachstellenanalyseExploitationReverse EngineeringNetzwerksicherheitPapers & ForschungLernen & BildungBinary-Exploitation
GitHubfaizan-khanx/cve-2024-38063

CVE-2024-38063

CVE-2024-38063 - Remote-Ausnutzung des Kernels über IPv6

Repository anzeigen
1vor 1 JahrNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

CVE-2024-38063 - Remote-Ausnutzung des Kernels über IPv6

  • Seit dem letzten Windows-Patch vom 13. August bin ich tief in den Untiefen von tcpip.sys (dem Kernel-Treiber, der für die Verarbeitung von TCP/IP-Paketen zuständig ist) unterwegs. Eine Schwachstelle mit einem CVSS-Score von 9,8 im am einfachsten erreichbaren Teil des Windows-Kernels konnte ich einfach nicht ignorieren. Ich habe mich noch nie wirklich mit IPv6 befasst (oder mit den Treibern, die es parsen), daher wusste ich, dass der Versuch, diese Schwachstelle per Reverse Engineering zu finden, extrem herausfordernd sein würde – aber eine gute Lernerfahrung.

Die einfachste Patch-Analyse aller Zeiten

  • Normalerweise kann es Tage oder sogar Wochen dauern, allein durch Reverse Engineering des Patches herauszufinden, welche Codeänderung der Schwachstelle entspricht, aber in diesem Fall ging es sofort. Es war so einfach, dass mir mehrere Leute in den sozialen Medien sagten, ich läge falsch und der Fehler sei woanders. Habe ich tatsächlich auf sie gehört und dann einen ganzen Tag damit verschwendet, den falschen Treiber zu reverse-engineeren? Das werden wir nie erfahren.

Es gab genau eine Änderung in der gesamten Treiberdatei, und wie sich herausstellte, war das tatsächlich der Fehler. image Eine Bindiff-Übersicht von tcpip.sys vor und nach der Installation des Patches.

Nur eine einzige Funktion im gesamten Treiber wurde geändert. Normalerweise könnte ich einen ganzen Tag damit verbringen, über 20 verschiedene Funktionsänderungen durchzugehen, nur um herauszufinden, welche die richtige ist – aber diesmal nicht. image

Ipv6pProcessOptions() vor dem Patch.

image Ipv6pProcessOptions() nach dem Patch.

Es wurde nicht nur eine einzige Funktion geändert, sondern eine einzige Codezeile.

  • Die extrem langnamige Feature_2660322619__private_IsEnabledDeviceUsage_3()-Funktion wird von Microsoft manchmal hinzugefügt, um teilweise Patch-Rollbacks zu ermöglichen. Der Aufruf prüft das Vorhandensein eines globalen Flags oder einer Registry-Einstellung, die, wenn gesetzt, dazu führt, dass die Funktion false zurückgibt, sodass der ursprüngliche Code anstelle der gepatchten Version ausgeführt wird.

  • Der Grund für diese Vorgehensweise von Microsoft ist, dass Sicherheitspatches manchmal unbeabsichtigt etwas kaputt machen. Diese Einstellung ermöglicht es einem Administrator, eine einzelne Schwachstelle zu entpatchen, ohne das gesamte monatliche Patch-Rollup zu deinstallieren und die Systemsicherheit drastisch zu schwächen.

  • Unter Berücksichtigung dessen ist klar, dass dieser Patch lediglich einen Aufruf von IppSendErrorList() durch IppSendError() ersetzt – ein Hinweis darauf, dass das Problem mit einer Art Liste zusammenhängt. Einfachster Patch-Diff aller Zeiten (oder so dachte ich)

Schwachstellen optional, Ausnutzung obligatorisch

  • Den Patch per Reverse Engineering zu analysieren, um den geänderten Code zu finden, ist nur die halbe Miete (oder in diesem Fall weniger als 0,1 %). Der Rest des Prozesses besteht darin, genügend Teile der Codebasis per Reverse Engineering zu verstehen, um zu begreifen, was überhaupt passiert, herauszufinden, welche Art von Schwachstelle gepatcht wurde, wie man eine Anfrage erstellt, um den Zielcode zu erreichen, und welcher Zustand zu einer ausnutzbaren Situation führt.

  • Der erste Teil ist einfach genug. Die Änderung befindet sich in Ipv6pProcessOptions(), was uns sagt, dass es sich um IPv6 handelt und die Verarbeitung von Optionen betrifft. Ein schneller Blick in den RFC verrät uns genau, was eine IPv6-Option ist und wo wir eine finden.

image Das Layout des Destination-Options-Headers von Wikipedia.

Ok, cool. Was wir suchen, scheint der Destination-Options-Header zu sein, der direkt nach dem Haupt-IPv6-Header sitzt. Verwenden wir die Python-Bibliothek 'scapy', um ein Test-IPv6-Paket zu erstellen.

Hinweis: Um DDoS-Angriffe mit gefälschten IP-Adressen zu mildern, schränkt Windows die Fähigkeit ein, rohe IP-Pakete zu erstellen. Aus diesem Grund habe ich mich entschieden, Linux für die Entwicklung meines Proof-of-Concept zu verwenden. Obwohl Linux es Benutzern erlaubt, rohe Layer-2- und Layer-3-Pakete zu erstellen und zu senden, muss das Python-Skript als root ausgeführt werden.

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])
  • Nachdem ich einen Breakpoint auf tcpip!Ipv6pProcessOptions gesetzt und dann das Skript ausgeführt hatte, war klar, dass alles, was nötig war, um die anfällige Funktion zu erreichen, das Senden eines IPv6-Pakets mit einer leeren Optionsstruktur war. Dann habe ich versucht, dem Paket einige ungültige Optionen hinzuzufügen, um zu sehen, ob ich den Aufruf von IppSendErrorList() erreichen kann.

Eine kurze Code-Überprüfung deutete darauf hin, dass fast jede ungültige Optionsformatierung den Aufruf von IppSendErrorList auslösen könnte. Also entschied ich mich für die Jumbo-Packet-Option mit einer ungültigen Länge (weniger als 65535 Bytes).

options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])

  • Also, was macht IppSendErrorList() eigentlich? Nun, der Code ist ziemlich einfach. image Die gesamte IppSendErrorList-Funktion.

  • Der Code iteriert eine verkettete Liste und ruft IppSendError() für jedes Element in der Liste auf. Wieder haben sich die Sterne günstig gefügt und die Dinge waren bisher einfach. Wenn IppSendErrorList nur IppSendError für jedes Element einer Liste aufruft und der Patch den Aufruf von IppSendErrorList durch IppSendError ersetzt, dann tritt das Problem auf, wenn IppSendError für ein Listenelement aufgerufen wird, das nicht das erste ist.

Er macht eine Liste, er prüft sie 52.567 Mal

  • Hier wurde es von offensichtlich zu außergewöhnlich schwierig, obwohl ich denke, dass ein großer Teil davon darauf zurückzuführen war, dass eine meiner beiden verfügbaren Gehirnzellen damit beschäftigt war, eine schlimme Covid-Infektion zu bekämpfen. Ich habe ein paar Tage damit verbracht, Teile des Codes zu verstehen, dann einzuschlafen und dann zu vergessen, was ich herausgefunden hatte. Der gesamte Prozess erforderte über eine Woche Reverse Engineering von Teilen von tcpip.sys, um herauszufinden, was vor sich geht. Aber Axels Blogbeitrag war extrem hilfreich.

  • Wenn man sich die Funktionen und Strukturen ansieht, die Axel per Reverse Engineering ermittelt hat, und an welche anderen Funktionen sie übergeben werden, wird klar, dass das einzige Argument, das an Ipv6pProcessOptions() übergeben wird, dieselbe packet_t-Struktur ist, die im Artikel definiert ist. Im Wesentlichen ist der Zeiger, der an Ipv6pProcessOptions übergeben und von IppSendErrorList iteriert wird, eine verkettete Liste von Paketen.

  • Also setzte ich einen Breakpoint auf Ipv6pProcessOptions() und untersuchte die Liste.

image

Der list->Next-Eintrag ist NULL.

  • Jedes Mal, wenn mein Breakpoint ausgelöst wurde, enthielt die Liste nur ein Paket. Ich habe viel länger als zugegebenermaßen angemessen damit verbracht, herauszufinden, warum und wie ich meine Liste dazu bringen kann, tatsächlich eine Liste zu sein. Mein erster Gedanke war IPv6-Fragmentierung: IPv6 erlaubt es Sendern, große Pakete in separate kleinere Pakete aufzuteilen, die es sinnvoll erscheinen ließe, in einer Liste zusammenzuhalten.

  • Nach umfangreichem Reverse Engineering bestätigte ich meine Annahmen, obwohl die Fragmentierungsliste nichts mit der Liste zu tun hat, mit der wir es hier zu tun haben.

  • Die Antwort fand ich tatsächlich völlig zufällig. Gelegentlich füllte sich die Liste, aber der Grund war unklar. Nach vielem Im-Kreis-Laufen wurde mir klar, dass das Anhalten des gesamten Kernels durch meinen Kernel-Breakpoint dazu führt, dass der Netzwerkadapter Pakete anhäuft. Wenn der Kernel fortgesetzt wird, werden diese Pakete in einer schönen, sauberen Liste an tcpip.sys weitergegeben. Dies geschah nur, wenn die Pakete gesendet wurden, während der Kernel angehalten war, aber nicht verarbeitet wurden, bevor der nächste Breakpoint erreicht wurde.

  • Dieses Verhalten ist wahrscheinlich eine Leistungsoptimierung: Bei niedrigem Durchsatz verarbeitet der Kernel Pakete einzeln, bei höheren Volumina werden Pakete jedoch in Listen organisiert und stapelweise verarbeitet. Höchstwahrscheinlich werden Listen nach Faktoren wie Protokoll und Quelladresse getrennt, um die Verarbeitung zu beschleunigen, sodass unsere Liste nur IPv6-Pakete enthalten sollte, die wir gesendet haben.

  • Nachdem wir nun wissen, dass Pakete bei hohem Durchsatz zu Listen zusammengefasst werden, ist klar, was die einfachste Option wäre. Unser DoS-PoC wird ironischerweise DoS nutzen müssen, um die DoS-Bedingung auszulösen. Wenn wir das System mit Bursts von IPv6-Paketen überfluten, sollten wir in der Lage sein, eine schöne große Liste zu erhalten, die an IppSendErrorList() übergeben wird.

  • Zunächst konnte ich, egal wie viele Pakete ich sendete, die Liste nur dann auf n > 1 bringen, wenn ich den Kernel anhielt. Aber … da wir Python (schmerzhaft langsam) in einer VM (doppelt schmerzhaft langsam) verwenden, müssen wir wahrscheinlich einige Einstellungen optimieren. Um die VM-in-VM-Situation auf meinem Angriffssystem zu kompensieren, habe ich mich entschieden, die Ziel-VM einfach so zu konfigurieren, dass sie nur einen einzelnen CPU-Kern verwendet.

image

Prima! Die Paketliste ist jetzt eine Liste mit vielen Einträgen!

  • Es stellt sich also heraus, dass eine VM in einer VM nicht die beste Wahl für DoS ist – wer hätte das gedacht? Aber wir haben es am Ende geschafft. Jetzt müssen wir nur noch herausfinden, was IppSendError() macht und in welchem Teil das Problem liegt.

Mehr Reverse Engineering, immer wieder, für immer.

  • Nach einigem umfangreichen Reverse Engineering wurde viel klarer, was IppSendError tut. Unter normalen Umständen deaktiviert es lediglich das Paket, indem es net_buffer_list->Status auf 0xC000021B (STATUS_DATA_NOT_ACCEPTED) setzt. Dann sendet es eine ICMP-Fehlermeldung mit Informationen über das fehlerhafte Paket an den Absender zurück.

    image Zwei relevante Teile von IppSendError.

  • Mein erster Ansatz war zu prüfen, ob es in tcpip.sys Funktionen gibt, die den Wert von net_buffer_list->Status ignorieren. Dies würde dazu führen, dass der Treiber Pakete in undefinierten oder unerwarteten Zuständen verarbeitet, was hoffentlich zu einer ausnutzbaren Situation führt.

image

Die Hauptschleife, die für die Paketverarbeitung zuständig ist.

  • Da die Schleife, die für das Aufrufen aller Parsing-Funktionen zuständig ist, in eine Fehlerprüfung eingebettet ist (was bedeutet, dass wir nirgendwo hinkommen, sobald der Fehlercode gesetzt ist), dachte ich, dass dies das falsche Kaninchenloch ist. Stattdessen entschied ich mich, zu IppSendError zurückzukehren und zu prüfen, ob es Codepfade gibt, die den Paketzustand vor dem Setzen des Fehlercodes ändern, was zu einer Race Condition führen könnte.

  • Nach viel mehr Reverse Engineering fand ich den folgenden Code ganz am Ende von IppSendError.

image

Ein Codepfad in IppSendError, der packet_size auf null setzt.

  • Wenn IppSendErrorList (und damit IppSendError) mit dem Argument always_send_icmp auf true aufgerufen wird, scheint es zu versuchen, die ICMP-Fehlermeldung an jedes Paket in der Liste zu senden.

  • Dann erreicht es aus Gründen, die vermutlich nur Gott bekannt sind, einen Codeblock, in dem das Feld packet->packet_size auf null gesetzt wird.

  • Um always_send_icmp auf true zu setzen, müssen wir lediglich einen bestimmten Fehler bei der Verarbeitung des Options-Headers auslösen, indem wir den 'Option Type'-Wert auf eine beliebige Zahl größer als 0x80 setzen.

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)

Aber sollte das Setzen von packet_size auf null den Parser nicht zerstören?

image

Ein Ausschnitt aus der Hauptschleife, die für die Paketverarbeitung zuständig ist.

  • Der Paket-Handler ruft einfach eine VTable-Funktion basierend auf dem Wert von packet->next_header auf, der unverändert bleibt, seit er während des Vorparsens gesetzt wurde. Dies ermöglicht es, die Paketverarbeitung fortzusetzen und gibt uns sogar Kontrolle darüber, welche Verarbeitung stattfindet.

  • Da der Wert von packet->next_header aus dem Feld 'Next Header' des IPv6-Pakets stammt, können wir ihn auf jeden gültigen IPv6-Header-Wert setzen, und die Schleife wird den entsprechenden Parser aufrufen. Dies bietet uns eine Menge potenzielle Angriffsfläche. image

    Das IPv6-Paketformat.

  • Alles, was noch zu tun bleibt, ist, einen erreichbaren Teil des IPv6-Parsers zu finden, der etwas Dummes mit dem packet_size-Feld anstellt.

Zurück zur Fragmentierung

  • Der erste Ort, den ich mir ansah, war der IPv6-Fragment-Parser, denn dort befand sich die alte cve-2021-24086-Schwachstelle, und es schien ein guter Ort zu sein, um mehr verrückten Code zu finden. image

Äh … so nah, aber auch so fern

  • Wir haben hier zwar eine Schwachstelle, aber keine RCE.

  • Im Wesentlichen sind Register auf den meisten CPUs zirkulär. Wenn man ein Register über seinen maximal möglichen Wert erhöht, springt es zurück auf null. Wenn man es unter seinen niedrigsten möglichen Wert verringert, springt es auf den höchstmöglichen Wert. Dies wird als Integer-Overflow bzw. Integer-Underflow bezeichnet. Dieses Verhalten ist bei vorzeichenbehafteten Ganzzahlen etwas anders, aber damit haben wir es hier nicht zu tun.

  • Die erste Zeile, fragment_size = LOWORD(packet->packet_size) - 0x30, besteht aus dem folgenden ASM-Code:

    image

    Der ASM-Code zur Berechnung der Fragmentgröße.

  • AX sind die unteren 16 Bits des EAX-Registers. Obwohl das EAX-Register 32 Bits breit ist, verhält sich AX, als wäre es ein eigenes 16-Bit-Register, daher werden alle Overflows oder Underflows auf AX beschränkt und wirken sich nicht auf den Rest des EAX-Registers aus. Das ist unglaublich praktisch, da ein Underflow auf dem EAX-Register zu einem Wert von 4 Milliarden führen würde, was zu einem Versuch führen würde, 4 GB Arbeitsspeicher zu allozieren, was wahrscheinlich fehlschlagen würde.

  • Da der Wert von packet->packet_size null ist, setzt dieser Code ax auf null und subtrahiert dann 0x30 davon.

  • Unter normalen Bedingungen ist der Paket-Header 0x30 Bytes lang, also ist packet_size - 0x30 die Größe der Fragmentdaten.

  • In unserem Fall ist packet->packet_size 0, sodass das Subtrahieren von sogar 1 dazu führt, dass das Register auf den maximal möglichen 16-Bit-Integer-Wert (0xFFFF) zurückgesetzt wird. Da wir 0x30 subtrahieren, wird der Wert von AX unterlaufen und zu MAX_VALUE - 0x2F oder 0xFFD0, was 65.488 ist.

  • Leider, da dieselbe Berechnung sowohl für die Speicherallokation als auch für das Kopieren von Daten verwendet wird, erhalten wir keinen Buffer-Overflow. Ich glaube, dass auch eine Bereichsprüfung für den Quellpuffer durchführt, sodass wir nicht einmal einen Out-of-Bounds-Read erhalten. Dennoch gehen wir nicht mit leeren Händen aus.

IPv6-Fragmente bleiben im Speicher, bis einer von drei Zuständen eintritt:

  • Wir vermasseln unsere Fragmentierung so schwer, dass das System uns mitteilt, dass Schluss ist.
  • Wir senden ein Fragment mit dem auf 0 gesetzten 'More'-Feld, was anzeigt, dass dies das letzte Fragment ist, und das System beginnt mit der Reassemblierung.
  • Wir senden das letzte Fragment nicht vor Ablauf der Timeout-Periode (60 Sekunden), und das System verwirft die Fragmente.
  • Ipv6pReassemblyTimeout() wird unter Bedingung 3 aufgerufen, also lasst uns untersuchen, wie dies ausgenutzt werden kann.

Ipv6pReassemblyTimeout() wird unter Bedingung 3 aufgerufen, also lasst uns untersuchen, wie dies ausgenutzt werden kann.

image

Das ist genau das, was wir brauchen!

  • Unser Problem war zuvor, dass der Code genau dieselbe Berechnung sowohl für die Speicherallokation als auch für die Kopieroperation verwendete. Dieser Code hier tut das nicht. Werfen wir einen genaueren Blick auf den ASM-Code, um zu sehen, wie er ausnutzbar ist.

image

Der Assembler-Code, der für die Berechnung der Allokationsgröße verantwortlich ist.

  • Wie Sie hier sehen, wird der erste Teil der Berechnung (fragment_list->net_buffer_length + reassembly->packet_length + 8) mit dem 16-Bit-DX-Register durchgeführt.

  • Wenn Sie sich an früher erinnern: Wir haben reassembly->packet_length auf 0xFFD0 underflowen lassen. Das DX-Register beträgt nach dem Hinzufügen der 8 Bytes also 0xFFD8. Wenn fragment_list->net_buffer_length größer als 0x27 (39 Bytes) ist, läuft DX über und setzt auf null zurück.

  • fragment_list->net_buffer_length sollte etwa 0x38 Bytes betragen, was zu einem Überlauf des DX-Registers auf 8 führt. Nachdem die 0x28 Bytes hinzugefügt wurden, erhalten wir eine Speicherallokation von nur 48 Bytes.

  • Da die nachfolgenden memmove()-Aufrufe nur den unveränderten reassembly->packet_length-Wert für die Größe verwenden, führt dies dazu, dass 65.488 Bytes von reassembly->payload in einen 30-Byte-Puffer kopiert werden. -Ein großartiger zusätzlicher Bonus ist, dass ein Großteil der kopierten Daten aus der Fragment-Nutzlast stammt, die wir kontrollieren und die beliebige Daten in jedem Format sein kann, sodass wir einen netten, einigermaßen kontrollierbaren Kernel-Pool-basierten Buffer-Overflow erhalten.

  • Um eine Chance zu haben, die Schwachstelle auszulösen, müssen wir ein oder mehrere Fragmentpakete haben, die sich zum Zeitpunkt des Aufrufs von IppSendErrorList in der verketteten Liste hinter dem fehlerhaften Optionspaket befinden. Aus meinen Tests geht jedoch hervor, dass dies nicht die Ausnutzung garantiert. Ich vermute, aber habe nicht bestätigt, dass der Synchronisierungscode in IppSendError bedeutet, dass wir auch eine Race Condition gewinnen müssen.

Socials

Faizans GitHub-Statistiken

instagram twitter linkedin github

Tool herunterladen
RtlCopyMdlToBuffer()
  • Da ExAllocatePoolWithTagPriority() den allozierten Speicher nicht nullt und RtlCopyMdlToBuffer() nur die tatsächlich verfügbare Datenmenge kopiert, erhalten wir etwa 65 KB nicht initialisierten Kernel-Speicher. Da Speicheradressen nach der Freigabe wiederverwendet werden, ist der Puffer wahrscheinlich mit dem gefüllt, was zuvor an der Adresse vor der Neuzuweisung gespeichert war. Wenn wir mithilfe von Fragmentierung ein Paket konstruieren können, das an uns zurückgesendet wird, wie z. B. eine ICMP-Echo-Anfrage, könnten wir potenziell zufälligen Kernel-Speicher leaken, was zu einer ASLR-Umgehung führt.

  • Darüber hinaus setzt der Code auch reassembly->fragment_size auf den unterlaufenen 16-Bit-Integer (65.488), sodass wir jetzt zwei separate Variablen haben, die wir möglicherweise nutzen könnten, um einen Buffer-Overflow zu verursachen.

  • Die Lösung (oder zumindest eine davon) ist Ipv6pReassemblyTimeout(). Während wir im anfänglichen Fragment-Handling keinen Overflow verursachen können, können wir dies offenbar während der Bereinigung tun.