
Remote-Ausnutzung des Kernels über IPv6
Remote-Ausnutzung des Kernels über IPv6
CVE-2024-38063 - Remote-Ausnutzung des Kernels über IPv6 Marcus Hutchins
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 beschäftigt (oder mit den Treibern, die es parsen), also wusste ich, dass der Versuch, diese Schwachstelle per Reverse Engineering zu analysieren, extrem herausfordernd sein würde, aber eine gute Lernerfahrung.
Zum größten Teil ist tcpip.sys kaum dokumentiert. Ich konnte ein paar Exploit-Beschreibungen für ältere Bugs finden: hier, hier und hier, aber sonst wenig. Wenn das top Suchergebnis meiner englischen Google-Suche auf Chinesisch ist, weiß ich sofort, dass ich völlig überfordert bin und eine schwere Zeit vor mir habe, aber lernen müssen wir. Obwohl Google Translate eine mittelmäßige Arbeit leistete, lieferte der Beitrag unglaublich detaillierte Einblicke in die IPv6-Fragmentierung und gab mir einen guten Vorsprung.
Später, als ich einige Funktionsnamen googelte, stieß ich auf eine weitere Analyse derselben Schwachstelle von 2021, geschrieben von Axel Souchet (alias 0vercl0k), die noch tiefer in die Interna von tcpip.sys eintauchte und mir genug Informationen lieferte, um mehrere undokumentierte Strukturen zu definieren. Die einfachste Patch-Analyse aller Zeiten
Normalerweise kann selbst das Reverse Engineering des Patches, um herauszufinden, welche Codeänderung der Schwachstelle entspricht, Tage oder sogar Wochen dauern, aber in diesem Fall war es sofort erledigt. Es war tatsächlich so einfach, dass mir mehrere Leute in den sozialen Medien sagten, ich läge falsch und der Bug sei woanders. Habe ich tatsächlich auf sie gehört und dann einen ganzen Tag damit verschwendet, den falschen Treiber zu reversieren? Wir werden es vielleicht nie erfahren.
Es gab genau eine Änderung in der gesamten Treiberdatei, die sich tatsächlich als der Bug herausstellte.
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 diejenige ist, die ich mir ansehen sollte, 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 Funktion Feature_2660322619__private_IsEnabledDeviceUsage_3() 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, falls gesetzt, dazu führt, dass die Funktion false zurückgibt, wodurch der ursprüngliche Code anstelle der gepatchten Version ausgeführt wird.
Der Grund, warum Microsoft dies tut, ist, dass Sicherheitspatches manchmal unbeabsichtigt Dinge 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, was uns einen Hinweis darauf gibt, dass das Problem mit einer Art Liste zusammenhängt. Einfachster Patch-Diff aller Zeiten (oder so dachte ich).
Schwachstellen optional, Ausnutzung obligatorisch
Das Reverse Engineering des Patches, um den geänderten Code zu finden, ist nur die Hälfte der Herausforderung (oder in diesem Fall weniger als 0,1 %). Der Rest des Prozesses besteht darin, genügend von der Codebasis zu reverse engineering, um zu verstehen, was überhaupt vor sich geht, herauszufinden, welche Art von Schwachstelle gepatcht wurde, wie man eine Anfrage formuliert, um den Zielcode zu erreichen, und welcher Zustand zu einer ausnutzbaren Bedingung führt.
Der erste Teil ist einfach genug. Die Änderung befindet sich in Ipv6pProcessOptions(), was uns sagt, dass es sich um IPv6 handelt und Optionen verarbeitet werden. Ein schneller Blick in die RFC verrät uns genau, was eine IPv6-Option ist und wo wir eine finden können.
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 basteln.
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. Während Linux Benutzern das Erstellen und Senden von rohen Layer-2- und Layer-3-Paketen erlaubt, erfordert dies, dass das Python-Skript als root ausgeführt wird.
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 das Skript ausgeführt hatte, war klar, dass alles, was nötig war, um die verwundbare Funktion zu erreichen, das Senden eines IPv6-Pakets mit einer leeren Optionsstruktur war. Ich versuchte dann, einige ungültige Optionen zur Struktur hinzuzufügen, um zu sehen, ob ich den Aufruf von IppSendErrorList() erreichen konnte.
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)])
Was macht IppSendErrorList() eigentlich? Nun, der Code ist ziemlich einfach.
Die gesamte IppSendErrorList-Funktion.
Der Code iteriert durch eine verkettete Liste und ruft IppSendError() für jedes Element in der Liste auf. Wieder einmal haben sich die Sterne günstig gefügt, und die Dinge waren bisher einfach. Wenn IppSendErrorList lediglich IppSendError für jedes Element in 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.
Was ist das also für eine Liste, und wie erstellt man eine? Er macht eine Liste, er überprüft sie … 52.567 Mal
Hier wurden die Dinge von offensichtlich zu abnormal 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 verloren, Teile des Codes zu verstehen, 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 los war. Aber Axels Blogbeitrag war extrem hilfreich.
Wenn man sich die Funktionen und Strukturen ansieht, die Axel per Reverse Engineering ermittelt hat, und welche anderen Funktionen sie übergeben werden, wird klar, dass das einzige Argument, das an Ipv6pProcessOptions() übergeben wird, die gleiche packet_t-Struktur aus dem Artikel ist. Im Wesentlichen ist der Zeiger, der an Ipv6pProcessOptions übergeben und von IppSendErrorList iteriert wird, eine verkettete Liste von Paketen.