/CVE-2024-38063

poc for CVE-2024-38063 (RCE in tcpip.sys)

Primary LanguagePythonMIT LicenseMIT

This is a (rather flaky) poc for CVE-2024-38063, a RCE in tcpip.sys patched on August 13th 2024. I didn't find and report this vuln, that would be Wei.

requirements

pip3 install scapy

usage

Modify the fields in the script:

  • iface <- If you have multiple adapters, you need to choose which one to use to send packets. e.g. "eth0" on linux or "Hyper-V Virtual Ethernet Adapter" on windows. If you're going to use your default interface, leave it empty.
  • ip_addr <- IP address of the target system (IPv6)
  • num_tries & num_batches <- How many different packet batches to send. more of them = more heap corruptions caused + higher chance of triggering the vulnerability.
  • mac_addr <- Leave empty, unless scapy complains it can't find the mac address. See below in troubleshooting.

Run the script:

python3 cve-2024-38063.py

The easiest way to reproduce the vuln is by using bcdedit /set debug on on the target system and restarting the machine/VM. This makes the default network adapter driver kdnic.sys, which is very happy to coalesce packets. If you're trying to reproduce the vuln on a different setup, you'll need to get the system in a position where it will coalesce the packets you sent. You can read the troubleshooting section below on more details.

demo

cve-2024-38063.webm

rough rca

You can read this great analysis of the vulnerability by Marcus if you're interested in the technical details. The details I've written below are meant to serve as a summary, rather than serious technical analysis.

  • In certain situations, windows will coalesce multiple IP packets together and batch process them. It processes the extension headers in each packet first, and only then moves on to process the data in each packet.
  • During extension header processing, packet objects of these coalesced packets are linked together in a linked list. Each packet object contains a NET_BUFFER object which contains buffered packet data. At offset 0x30 we also have a current-offset field which indicates how far the packet has been parsed. At this stage, the offset value will generally be 0x28, indicating that the IPv6 header has been parsed but nothing else.
  • When processing the "destination options" extension header in tcpip!Ipv6pReceiveDestinationOptions, a parsing error will result in tcpip!IppSendErrorList being called. This function calls tcpip!IppSendError on each packet object in the linked list (starting from the current one).
  • Under certain conditions (e.g. if the packet is unicast), tcpip!IppSendError has side effects. It "reverts" the buffered packet data back to the start and resets the current-offset field to zero.
  • However, in this whole chain of events, only the first packet is marked as having an error (offset 0x8C). This means that the driver will continue to parse extesion headers of other packets in the linked list, even if they've been "reverted" in IppSendError.
  • The processing of those packets that have been reverted is then done with unexpected data: the buffered packet data is pointing towards the beginning of the packet (i.e. the IPv6 header) rather than to the extension headers, and the offset field value is zero rather than 0x28.

strategy

  • To abuse the vulnerability, we make use of Ipv6pReceiveFragment. The function parses the fragment extension header and assumes that the offset field of the packet will be at least 0x28 when calculating the length of the non-header data in the packet by subtracting 0x30 from the current offset value. This value is then stored in the reassembly object whose purpose is to reassemble the fragmented packet.
  • In our case, the function will be called on a packet that has been reverted by IppSendError. The offset value will be zero and increased to 8 somewhere earlier in Ipv6pReceiveFragment. When calculating the size of non-header data, the value will underflow and be equal to 0xffd8 (the subtraction is done in 16 bits).
  • The length value is used in only two places later:
    • Ipv6pReassembleDatagram, where it's used to calculate the length of an output buffer of the reassembled packet. However, all calculations are done in 32-bits and there's a sanity check that the total length doesn't exceed 0xFFFF, which does happen in this case.
    • Ipv6pReassemblyTimeout, where it's also used in the same manner. However, the calculations here are done in 16 bits and an integer overflow happens. This leads to a buffer overflow when copying data into the buffer later.

To trigger Ipv6pReassemblyTimeout, the sender of the fragment has to be inactive for 1 minute. Our strategy is then:

  • Send malformed destination options to trigger IppSendError, followed by a fragment packet
  • Hope that the two packets are coalesced and that the second packet's object will have its data and offset reset
  • Cause the underflow in Ipv6pReceiveFragment and create a new reassembly object with fragment data length that's a high 16-bit value
  • Wait 1 minute without sending any more packets so that Ipv6pReassemblyTimeout is triggered.
  • Cause an integer overflow in buffer size calculation in Ipv6pReassemblyTimeout and trigger a heap-based buffer overflow.

The packets in the script are spammed so that there's a higher chance of them being coalesced. The main payload is pretty simple:

  • IPv6 packet with a "destination options" extension header with malformed options data that will trigger an error in parsing
  • IPv6 fragment #1, that we hope will be concatenated to the first packet
  • IPv6 fragment #2 (same id), that may also be concatenated to the first two, but its main purpose is to complete the 2nd fragment so that errors aren't thrown out in case normal processing happens

We also set the hop limit and flow label fields in the IPv6 header manually. Recall that the buffered packet data is reset because of the vulnerability. This means that, when processing the fragment packet, the IPv6 header will be interpreted as fragment header data. The hop limit field in the IPv6 header will be interpreted as one of the bits of the id field in the fragment header. By changing it, we ensure that we trigger the vulnerability for multiple different fragments and cause multiple different corruptions, increasing the chance of a crash (since this is a PoC after all). The flow limit field of the ip header will be interpreted as the offset & "more indicator" fields of the fragment header. By setting it to 1, we indicate that there's more headers to come (hence being able to trigger Ipv6pReassemblyTimeout later) and that offset is zero (since this is the first packet with such id that's arriving).

notes

  • The above is just one strategy of exploiting the problem introduced by triggering the vulnerability. I used this strategy as it was quite straightforward and I didn't want to waste time looking into other possibilities. I wouldn't be surprised if other folks come out with much nicer strategies soon.
  • What the vulnerability requires:
    • IPv6 capability on the target system, ability to receive packets (pre-firewall)
    • Ability to get the target system to coalesce the sent packets to some degree. Some adapter + driver pairs are very happy to do this, while others seem to be more hesitant. There could be tricks or special packet chains that one can use to make windows RSC coalesce packets regardless of the adapter or network health, but I don't have any evidence for that.
  • What the vulnerability doesn't require:
    • Spamming packets, the poc only does it so that it increases the chance of coalescing + triggering multiple corruptions as a demonstration.
    • Heavy load situations on the target system, as coalescing could happen in many different situations.
    • Any specific settings on the target system, other than IPv6 being enabled.
    • (Most likely) Waiting a minute to trigger the corruption, I only used this strategy of abusing the vulnerability as it was the simplest. There's a very real chance that the problematic situation caused by the vulnerability could be abused in a more direct manner.
    • (Most likely) Unicast packets, I use them as the code path we're using in Ipv6pReassemblyTimeout requires that the original fragment packet be sent as unicast.

troubleshooting

If it's not working, it could be because:

  • The target system can't be reached via IPv6:
    • Disable windows firewall
    • ping -6 {ipv6_address} from the host pc
    • Make sure you're getting a response
    • Re-enable the firewall
  • The target system is not receiving packets
    • Install wireshark on the target system and check that packets sent by the script are arriving
  • scapy is reporting "Mac address to reach destination not found. Using broadcast."
    • You need to find the mac address of the target machine
    • This can be done by running the ping command from above and checking the reply in wireshark (eth source address field)
    • You could also use scapy: Ether(raw(sr1(IPv6(dst={your_dest_ip})/ICMPv6EchoRequest()))).src, but this doesn't work sometimes
    • Once you have the mac address, put it in the mac_addr field in the script and run the script
  • Packets are not being coalesced on the target system
    • Depending on your adapter network adapter / driver, it may be hard to get windows to coalesce packets without resorting to something like flooding the target akin to a ddos.
    • You can try to modify your adapter settings, e.g. "Packet Coalescing", "Interrupt Moderation", "Interrupt Moderation Mode", "Recv Segment Coalescing", depending on which ones are available. For example, setting "Interrupt Moderation Mode" to "Extreme" on my dedicated server makes the vulnerability reproducible.
  • If all else fails, you can attach a kernel debugger and check a few things:
    • Is tcpip!Ipv6pReceiveDestinationOptions -> tcpip!Ipv6pProcessOptions -> tcpip!IppSendErrorList being hit?
    • Break on tcpip!Ipv6pProcessOptions and check whether [rcx] is zero all of the time. If yes, then packets are not being coalesced for some reason.
    • Break on tcpip!Ipv6pReceiveFragment and check if [rcx+0x30] is equal to zero. If not, then the vulnerability failed to be triggered for some reason.