
CVE-2024-38063 - Remotely Exploiting The Kernel Via IPv6
There was exactly one change made in the entire driver file, which it turns out, actually was the bug after all.
A bindiff overview of tcpip.sys before and after installing the patch.
Only a single function in the whole driver has been modified. Typically, I could spend a whole day going through 20+ different function changes just to figure which is the one I should be looking at, but not this time.

Ipv6pProcessOptions() before the patch.
Ipv6pProcessOptions() . After the patch.
Not only was it just a single function that was changed, but a single line of code.
The extremely long-named Feature_2660322619__private_IsEnabledDeviceUsage_3() function is something Microsoft sometimes adds to enable partial patch rollbacks. The call checks for the presence of a global flag, or registry setting, which if set, will cause the function to return false, resulting in the original code being executed instead of the patched version.
The reason Microsoft does this is because security patches sometimes unintentionally break things, so this setting enables an administrator to unpatch a single vulnerability, without uninstalling the entire monthly patch rollup and drastically weakening their system security.
Taking this into account, it’s clear that all this patch does is replace a call to IppSendErrorList() with IppSendError(), giving us a clue that the issue is with some kind of list. Easiest patch diff ever (or so I thought)
Reverse engineering the patch to find the altered code is only half of the challenge (or in this case less than 0.1%). The rest of the process consists of reverse engineering enough of the codebase to understand what is even going on, figuring out what kind of vulnerability was patched, how to craft a request to reach the target code, and what state results in an exploitable condition.
The first part is easy enough. The change is in Ipv6pProcessOptions(), which tells us it’s IPv6 and involves processing options. So, a quick call to the RFC tells us exactly what an IPv6 option is and where we can find one.
The destination options header layout from Wikipedia.
Ok, cool. What we’re looking for appears to be the destination options header, which sits directly after the main IPv6 header. Let’s use the Python library ‘scapy’ to craft a test IPv6 packet.
Note: To mitigate DDoS attacks using spoofed IP addresses, Windows restricts the ability to construct raw IP packets. For this reason, I opted to use Linux to develop my proof-of-concept. Whilst Linux does allow users to construct and send raw layer 2 and layer 3 packets, it requires the Python script to be run as 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, then running the script, it was clear that all that was needed to reach the vulnerable function was sending an IPv6 packet with an empty options structure. I then tried adding some invalid options to the structure to see if I could reach the call to IppSendErrorList().A brief code review indicated that almost any invalid option formatting could trigger the call to IppSendErrorList. So, I decided on using the Jumbo Packet option with an invalid length (less than 65535 bytes).
options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])
So, what does IppSendErrorList() actually do? Well, the code is pretty simple.
The entire IppSendErrorList function.
The code iterates a linked list and calls IppSendError() on every item in the list. Again, the stars have aligned and things have been easy thus far. If IppSendErrorList just calls IppSendError for each item in a list, and the patch replaces the call to IppSendErrorList with IppSendError, then the problem occurs when IppSendError is called on a list item other than the first.
This is where things went from obvious to abnormally difficult, though I think a large part of this was due to one of my two available brain cells being preoccupied with fighting a bad covid infection. I lost a couple of days to understanding parts of the code, falling asleep, then forgetting what it was I had figured out. The whole process required over a week of reverse engineering parts of tcpip.sys to figure out what was going on. But Axel’s blog post was extremely helpful.
By looking at the functions and structure Axel reverse engineered, and which other functions they’re passed to, It’s clear that the one and only argument passed to Ipv6pProcessOptions() is the same packet_t structure defined in the article. Essentially, the pointer passed to Ipv6pProcessOptions, and iterated by IppSendErrorList, is a linked-list of packets.
So, I set a breakpoint on Ipv6pProcessOptions() and inspected the list.

The list->Next entry is NULL.
Every time my breakpoint was hit, the list contained only one packet. I spent way longer than I’d like to admit trying to figure out why and how to get my list to actually be a list. My first thought was IPv6 fragmentation: IPv6 allows senders to split large packets into separate smaller packets, which it would make sense to keep together in a list.
After extensive reverse engineering, I confirmed my assumptions were correct, though the fragment list is not related to the one we’re dealing with here.