CVE-2021-24094 — Windows TCP/IP IPv6 Recursive Reassembly UAF + Firewall Bypass

Last updated: 2026-04-15
Severity: Critical
Component: tcpip.sys (IPv6 fragmentation, recursive reassembly)
Bug Class: Use-After-Free + State Confusion (Type Confusion)
Privilege Escalation: Remote → Potential RCE; demonstrated firewall bypass primitive
Patch: February 9, 2021 (Patch Tuesday)
Related: Tcpip Stack, Cve 2021 24086, Use After Free, Race Conditions
Tags: kernel-mode, uaf, type-confusion, race-condition, tcpip, remote


Vulnerability Summary

A state confusion in the Windows IPv6 recursive reassembly path leads to two attack primitives: a Use-After-Free (when metadata pointers in the reassembled outer packet refer to freed inner fragment memory) and a firewall bypass (where a vulnerable Windows stack reassembles a packet differently than perimeter security appliances, enabling traffic obfuscation).

The UAF arises because ip_header and dst_ip fields in the outer reassembly object are not updated after inner reassembly completes — they are left pointing to the last processed inner fragment. In the recursive path, IppReceiveHeaderBatch is queued to a WorkItem (separate thread), so by the time it executes, the inner fragments have already been freed.

Discovered and analyzed by the Armis research team. Patched same day as CVE-2021-24086 (February 9, 2021).


Root Cause Analysis

Background: Normal IPv6 Reassembly

IppReceiveHeaderBatch processes packets via a dispatch table indexed by NextHeader. When a fragment header (Next Header = 44) is encountered, Ipv6pReceiveFragmentListIpv6pReceiveFragment is called.

Ipv6pReceiveFragment maintains a Reassembly_t structure keyed by (src_ip, dst_ip, frag_id). When the last fragment arrives, Ipv6pReassembleDatagram is called and the reassembled packet is re-fed to IppReceiveHeaderBatch.

The reassembled packet’s ip_header field is set from the first fragment’s ip_header (correct behavior). Key metadata offset_of_last_next_hdr and frag_next_hdr are also taken from the first fragment to properly strip the Fragment Header and fix the Next Header chain.

The Bug: State Confusion in Recursive Path

When recursive fragmentation is used:

  • Inner fragments reassemble → inner Ipv6pReassembleDatagram fires
  • Inner reassembled packet (=outer Fragment #1) is re-processed through Ipv6pReceiveFragment
  • Ipv6pReceiveFragment treats this as a first fragment and stores its ip_header into the outer reassembly object
  • But this ip_header actually points to the last inner fragment (not the outer first fragment)

Critical: the patch adds lines to Ipv6pReassembleDatagram that copy ip_header and dst_ip from the incoming_packet to a freshly allocated header object (new_header). Without this, these fields contain stale pointers to the last-processed inner fragment.

// Patch addition in Ipv6pReassembleDatagram (simplified):
new_header = ExAllocate(...);
*new_header = incoming_packet->ip_header;  // copy ip header content
reassembled_packet->ip_header = new_header; // point to fresh allocation
// ... similar for dst_ip ...

Use-After-Free Condition

In the recursive path:

  1. Outer Fragment #1 is the result of inner reassembly (4th bit of incoming_packet->flags is set).
  2. Ipv6pReassembleDatagram detects this flag and queues IppReceiveHeaderBatch via IoQueueWorkItem (separate thread = IppReassembledReceive).
  3. This deferred call happens after IppFreePacket and NetioDereferenceNetBufferListChain are called on the outer reassembly fragments.
  4. The outer reassembled packet still holds ip_header and dst_ip pointing to freed fragment memory.
  5. When the WorkItem fires and IppReceiveHeaderBatch runs, it accesses these freed pointers.

Timeline:

[Thread 1] Inner reassembly completes
[Thread 1] ip_header (buggy) = last inner fragment address
[Thread 1] IoQueueWorkItem(IppReassembledReceive, outer_reassembled_packet)
[Thread 1] IppFreePacket(inner fragments) ← frees ip_header target
[Thread 2] IppReassembledReceive → IppReceiveHeaderBatch(outer_reassembled_packet)
           ↑ accesses freed ip_header and dst_ip → UAF

Firewall Bypass Primitive

The same state confusion causes the outer reassembled packet’s basic IPv6 header to retain NextHeader = IPPROTO_FRAGMENT (from the inner last fragment), while the offset_of_last_next_hdr points to a Routing Header’s Next Header field (from the outer first fragment).

Result: the outer reassembled packet is re-processed by Ipv6pReceiveFragment, which interprets the first extension header of the payload as if it were a Fragment Header. This is a type confusion: Routing Header bytes parsed as Fragment Header fields.

Exploitation of the type confusion:

  • Fragment Header offset+M fields (bytes 2-3) align with Routing Header’s Routing Type + Segments Left fields.
  • Setting these to zero satisfies both interpretations (Fragment: offset=0, M=0 = single-fragment reassembly; Routing: Routing Type=0, Segments Left=0 = valid).
  • An attacker places arbitrary extension headers in the 8-byte “payload” of the routing header (not validated).
  • By carefully constructing the packet, the same byte stream looks like ICMP Echo Request to firewalls/other stacks, but a TCP SYN to port 445 to the vulnerable Windows target.

Demonstrated result: vulnerable Windows Server responds with TCP SYN-ACK from port 445 to a packet that perimeter security sees as an ICMPv6 Echo Request. The firewall passes it without inspection.


Exploitation Technique

UAF Exploitation Challenges

The freed memory contains packet metadata fields (ip_header = IPv6 header content, dst_ip = destination IP). These are data values, not function pointers. The practical path to RCE from this UAF is unclear:

  • The freed memory contains IPv6 header bytes (not kernel object with vtable)
  • Heap grooming would need to place a useful object at the freed address
  • Microsoft classifies this as potential RCE; Armis has not ruled it out

Firewall Bypass Primitive (Demonstrated)

Crafting an encapsulated packet:

  1. Outer packet looks like IPv6 + Routing Header + ICMPv6 Echo Request (to firewalls)
  2. Vulnerable Windows stack interprets same bytes as: IPv6 → Routing Header → [parsed as Fragment Header: offset=0, M=0] → TCP header (+ ICMP header embedded in TCP options)
  3. Windows sends TCP SYN-ACK from port 445 in response

This bypasses firewalls that use Deep Packet Inspection keyed on ICMPv6, since the firewall never sees the TCP traffic. Useful in OT/ICS environments where patching is blocked by operational concerns.


Key Primitives Used

  • State confusion: ip_header / dst_ip refer to wrong fragment after recursive reassembly
  • Use-After-Free: WorkItem execution after fragment free
  • Type confusion: Routing Header bytes parsed as Fragment Header fields
  • Traffic obfuscation/firewall bypass: different packet interpretation between target and firewall

Proof-of-Concept Notes

No public RCE PoC exists. Armis demonstrated the firewall bypass technique in their blog with Wireshark captures showing TCP SYN-ACK from port 445 in response to what appears to be ICMPv6 Echo.

Related PoC infrastructure: github.com/0vercl0k/CVE-2021-24086 covers tcpip.sys fragmentation mechanics applicable to understanding the same code paths.


Patch Analysis

  • Ipv6pReassembleDatagram: Added allocation of new_header and copy of incoming_packet->ip_header into it; ip_header and dst_ip in reassembled packet now point to fresh allocation instead of last fragment.
  • Identical pattern to the IPv4 Ipv4pReassembleDatagram fix (CVE-2021-24074).
  • An additional non-functional line updating offset_of_routing_option_in_packet was added (mirrors IPv4 fix, logically unused for IPv6).

References

  • Armis Research Team, “From URGENT/11 to Frag/44: Analysis of Critical Vulnerabilities in the Windows TCP/IP Stack”, armis.com, April 2021
  • MSRC, “Multiple Security Updates Affecting TCP/IP”, February 2021
  • RFC 8200, “Internet Protocol Version 6 (IPv6) Specification”, section 4.5 (fragmentation)
  • Antonios Atlasis, “IPv6 Extension Headers: New Features and New Attack Vectors”, TROOPERS 2013