Breaking down Windows critical vulnerability CVE‑2024‑38063
On the second Tuesday of each month, Microsoft releases Patch Tuesday, a Windows update that addresses critical vulnerabilities. On August 13, the vendor fixed a critical vulnerability in the network stack that allows privileged remote access over TCP/IP if IPv6 is enabled.
The vulnerability identifier is CVE‑2024‑38063. In essence, it is a zero‑click Windows TCP/IP RCE. According to Microsoft Security Bulletin, the flaw is due to an integer underflow weakness in one of the functions of the tcpip.sys
driver responsible for handling IPv6 packets. Due to the high severity of this flaw, our vulnerability research team started playing Patch Tuesday — Exploit Wednesday (and it took them two weeks to win).
To understand what caused the vulnerability, we examined the driver files before and after the patch. Essentially, there are two ways to do this:
- Save the old file, then update Windows, and get the new file. This is the most reliable method, but it is desirable that the old file is not too old. Otherwise, instead of a nice and convenient comparison in BinDiff, you are likely to get a huge number of changes, most of which are not related to security.
- Use Winbindex, an excellent website for doing all kinds of Windows‑related research. Updated files appear there with a slight delay, but it is not that critical.
To be clear, we decided to use the second method: we went to the Winbindex website and downloaded the two latest versions of the Windows 10 22H2 driver.
Here, 10.0.19041.4780 is the only file version where the vulnerability has been addressed. You can see the publication date for any of these files by clicking Show in the Extra column, or just by mousing over the update number.
Once the files are uploaded, you can perform the comparison using the open‑source utility BinDiff; for example as a plug‑in for IDA Pro. When comparing the two files, we came across a rather unusual situation in the Patch Tuesday analysis: the only function affected by the change was Ipv6pProcessOptions
, which is used for handling such IPv6 packet options as Jumbo Payload or Hop‑By‑Hop.
If you look inside the function in the decompiled code, you will find the only modified section at the end.
In the July 23 version:
In the August 13 version:
The IppSendError
function is now used instead of IppSendErrorList
when errors occur in the processing of IPv6 options. The difference between them is that IppSendErrorList
sends errors to every packet in the chain, while IppSendError
sends them to only one packet. You can see this if you look at the decompiled code of the IppSendErrorList
function:
Further analysis of the code around the modified section reveals the logic: Ipv6pProcessOptions
is designed to process only one packet (or fragment) while IppSendErrorList
goes through all packets in the chain. This is an obvious logical mistake that may lead to something bad. However, we did not know what kind of bad exactly, so we continued our research. But first we decided to develop a checker to see if we understood the logic correctly.
Indeed, there were six responses to three packets with errors in IPv6 options:
- three errors per Packet1 (Packet1 → Packet2 → Packet3)
- two errors per Packet2 (Packet2 → Packet3)
- one error per Packet3 (Packet3)
It should be noted that packets with errors will reach the sender only if Windows Firewall is disabled on the system. However, having the firewall enabled does not affect whether the packets sent reach the vulnerable system, as they are processed at the OS kernel level even before being processed by the firewall.
When viewing the decompiled code of the IppSendError
function, you may notice the following fragment:
This behavior is correct as calling this function terminates the processing of the current packet and sends an error message. However, due to the fact that in the vulnerable driver version this code snippet is executed for each packet from the chain, including unprocessed ones, there is a probability that the IPv6_HeaderSize
field will be used for processing subsequent packets. In fact, this field is equal to the IPv6 header size, including all nested option headers.
We realized that this should not be like that and that the size field should not be equal to zero in a working packet. We could not figure out where exactly to apply this primitive for quite some time, hence we turned to previous studies of vulnerabilities in the Windows TCP/IP network stack (1, 2).
We came across an article exploring a similar vulnerability stemming from IPv6 fragmentation, CVE-2022-34718. The vulnerability was in the Ipv6pReassembleDatagram
function that uses the undocumented Packet_t
and Reassembly_t
objects.
Although this function does not use the field whose value we changed, it affects the fields of the Reassembly_t
object in the parent Ipv6pReceiveFragment
:
Here we can see that 0×30
is subtracted from the value of the IPv6_HeaderSize
field that was incorrectly changed earlier, because the function assumes that the size cannot be less than 48 bytes at this stage. This code snippet is a great opportunity to apply the previously acquired primitive, but we still need to get here. To do so, we need to pass two checks, the first of which can be seen in the image above: it checks whether the fragment is the first in the chain. Although this condition reduces the exploitation possibilities, it does not reduce them enough: one packet may well suffice.
The second condition is to check that the fragment offset is not zero, but because of the processing in IppSendError
(within which NetioRetreatNetBufferList
is called to return the caret to the beginning of the packet), this condition actually checks that FlowLabel
in the IPv6 header structure is not zero.
tcpip.sys
code
This structure field is used in the above mentioned Ipv6pReassembleDatagram
function when copying data from the buffer.
At this point, we were already confident that we had fully understood the vulnerability, but the kernel did not get to the point of executing the necessary code because of the next check at the beginning of the function:
Here, the TotalLength
variable is equal to the sum of the size of the newly initialized Reassembly_t
with our size in underflow (it always equals 0xffd8
after exploitation):
You might think that there is an overflow of UnfragmentableLength
here, but the addition is performed within the UInt32
type, so the overflow will not occur and TotalLength
will be greater than 0xfff
.
While researching other functions that use the Reassembly
structure, we found a handler that is called when the timeout for the next fragment expires, Ipv6pReassemblyTimeout
. This function also copies data from our buffer using negative (large positive) size.
To get to this code snippet, we need to set FlowLimit
equal to one (due to IppSendError
, the driver interprets this field as FragOffset
) and wait for one minute. After that, the pool buffer in the Windows kernel overflows with values almost completely controlled by us, because it is quite easy to spray the necessary chunks nearby. It would seem that everything is fine, right?
There is always a big problem in the remote exploitation of such vulnerabilities: we do not know the kernel addresses after KASLR. If we were executing on the attacked host itself (which is quite possible and allows privileges to be elevated to SYSTEM), we could exploit the full variety of KASLR leaks in Windows (1, 2). However, this is not our case, although the above method of using the primitive provides excellent opportunities for local privilege escalation and remote BSOD. Strict limitations on the size of the allocated memory section do not allow us to learn the kernel address without letting the kernel crash.
There are two caveats here:
- Any additional kernel vulnerability that could compromise KASLR addresses—in a chain with the presented vulnerability—leads to RCE.
- The Windows networking stack is one of the most complex parts of the OS. The exploitation primitive presented in this article can affect sections of code in subsequent packet processing, so it is likely that there are other (highly sophisticated) attack methods that will lead to KASLR leakage and kernel compromise.
Despite the complexity associated with the exploitation of this zero‑click TCP/IP RCE flaw, we recommend you to disable IPv6 at the hosts where it is not used. As part of safety recommendations, BI.ZONE EDR detects enabled IPv6 components in the host’s network interfaces and notifies the monitoring team of the identified misconfiguration. This rule is available both in the on‑prem BI.ZONE EDR version and in BI.ZONE TDR.