
CVE-2024-38063 - Remote-Ausnutzung des Kernels über IPv6
Es gab genau eine Änderung in der gesamten Treiberdatei, und wie sich herausstellte, war das tatsächlich der Fehler.
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.

Ipv6pProcessOptions() vor dem Patch.
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)
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.
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.
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 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.
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.
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.