CVE-2021-24086 — Windows TCP/IP IPv6 Fragmentation NULL Dereference (“Packet of Death”)

Last updated: 2026-04-15
Severity: High
Component: tcpip.sys (IPv6 fragmentation)
Bug Class: Integer Truncation → NULL Dereference
Privilege Escalation: Remote → DoS (BSOD); not exploitable for code execution
Patch: February 9, 2021 (Patch Tuesday)
Related: Tcpip Stack, Cve 2024 38063, Cve 2021 24094, Integer Overflows, Null Deref
Tags: kernel-mode, integer-overflow, null-deref, tcpip, remote


Vulnerability Summary

A remote DoS vulnerability in every version of Windows with IPv6 enabled. A single crafted sequence of IPv6 packets crashes the target kernel (BSOD) via a NULL pointer dereference in tcpip!Ipv6pReassembleDatagram. No authentication or user interaction required.

Root cause: a uint16_t truncation in NetioRetreatNetBuffer causes an undersized MDL allocation, after which NdisGetDataBuffer is called with the full (non-truncated) size, detects a size mismatch, and returns NULL. The caller blindly dereferences this NULL pointer.

Discovered internally by Microsoft’s @piazzt. Public PoC written independently by Axel “0vercl0k” Souchet and Francisco Falcon (Quarkslab).


Root Cause Analysis

The Integer Truncation

In tcpip!Ipv6pReassembleDatagram, the total header length is computed as:

const uint32_t UnfragmentableLength = Reassembly->UnfragmentableLength;
const uint32_t TotalLength = UnfragmentableLength + Reassembly->DataLength;
const uint32_t HeaderAndOptionsLength = UnfragmentableLength + sizeof(ipv6_header_t);  // 0x28

If UnfragmentableLength = 0xFFD0 (crafted via nested fragments, see below), then HeaderAndOptionsLength = 0xFFD0 + 0x28 = 0xFFF8.

The bug: NetioRetreatNetBuffer is called with a truncated uint16_t(HeaderAndOptionsLength):

// Step 1: allocate MDL — uint16_t truncation applied
if (NetioRetreatNetBuffer(FirstNetBuffer, uint16_t(HeaderAndOptionsLength), 0) < 0) ...
// uint16_t(0xFFF8) = 0xFFF8 (ok in this case — both same)
// BUT: if HeaderAndOptionsLength = 0x10028 (from specific configs), uint16_t truncates to 0x0028 — tiny MDL

The critical mismatch: NdisGetDataBuffer is later called with the full 32-bit HeaderAndOptionsLength:

// Step 2: get data pointer — full 32-bit size used
Buffer = (ipv6_header_t*)NdisGetDataBuffer(
    FirstNetBuffer,
    HeaderAndOptionsLength,  // full 32-bit — may differ from what was retreated
    NULL,                    // Storage = NULL
    1, 0
);

When uint16_t(HeaderAndOptionsLength) != HeaderAndOptionsLength, the MDL allocated in step 1 is too small for the access requested in step 2. NdisGetDataBuffer with Storage = NULL returns NULL when the data is non-contiguous and the requested range exceeds the buffer. The function then dereferences this NULL:

*Buffer = Reassembly->Ipv6;  // NULL dereference → BSOD (DRIVER_IRQL_NOT_LESS_OR_EQUAL 0xD1)

Triggering Condition: Crafting Large UnfragmentableLength

The UnfragmentableLength is the total size of IPv6 extension headers in the “unfragmentable part” — i.e., headers before the Fragment Header in the first fragment. Under normal conditions, this is limited by MTU (~1500 bytes).

The trick: nested IPv6 fragments. Windows (unlike FreeBSD and others) accepts fragments-within-fragments and performs recursive reassembly. The attacker crafts:

Outer fragments (ID=0x11111111):
  Fragment 0: [HopByHop] + [Inner Fragment Header (ID=0x22222222, offset=0, M=1)]
                           + 0x1ffa × [Routing Header (8 bytes each)] = 0xFFD0 bytes
                           + [Inner Fragment Header (nh=ICMP, offset=0, M=1)]
  Fragment 1: [HopByHop] + [Inner Fragment Header (ID=0x22222222, offset=last, M=0)]

When outer fragments reassemble, the inner payload is treated as a new fragmented packet. The inner packet contains 0x1ffa empty Routing Headers totaling 0xFFD0 = 65,488 bytes of extension headers. This exceeds the normal MTU constraint because the “first fragment” constraint doesn’t apply to nested reassembly — the inner payload is a single contiguous chunk.

The patch adds a bounds check: if EDX > 0xFFFF (extension headers + fragmentable data exceeds 16-bit limit), bail out.

A second patch in Ipv6pReceiveFragment checks for Jumbograms (size > 0xFFFF) and bails with IppSendError.


Exploitation Technique

Impact: DoS only. The crash is a NULL pointer write:

DRIVER_IRQL_NOT_LESS_OR_EQUAL (0xD1)
Arg1: 0000000000000000 (memory referenced = NULL)
Arg2: 0000000000000002 (IRQL = 2, DISPATCH_LEVEL)
Arg3: 0000000000000001 (write operation)
Arg4: fffff80170b9937b (tcpip!Ipv6pReassembleDatagram+0x14f)

tcpip!Ipv6pReassembleDatagram+0x14f:
  movups xmmword ptr [rax], xmm0   ; rax=0 → BSOD

Stack trace:

tcpip!Ipv6pReassembleDatagram
tcpip!Ipv6pReceiveFragment
tcpip!Ipv6pReceiveFragmentList
tcpip!IppReceiveHeaderBatch
tcpip!IppFlcReceivePacketsCore
tcpip!IpFlcReceivePackets
tcpip!FlpReceiveNonPreValidatedNetBufferListChain
tcpip!FlReceiveNetBufferListChainCalloutRoutine
nt!KeExpandKernelStackAndCalloutInternal

No memory corruption occurs (NULL write → immediate page fault); code execution is not possible from this crash path.


Key Primitives Used

  • Nested IPv6 fragments: bypass MTU-based limit on extension header length
  • uint16_t truncation: undersized MDL relative to requested data size
  • NULL dereference write: BSOD delivery primitive

Proof-of-Concept Notes

Author: Axel “0vercl0k” Souchet + Francisco Falcon (Quarkslab) (independent, near-simultaneous)
Repository: github.com/0vercl0k/CVE-2021-24086 (archived, MIT license)

Technique summary:

  • 0x1ffa empty IPv6ExtHdrRouting headers (each 8 bytes) = 0xFFD0 bytes total extension headers
  • Outer fragments carry this payload; last inner fragment sent non-nested to trigger recursive reassembly
  • Single invocation crashes target:
    sudo python3 cve-2021-24086.py <target_ipv6>
    66 fragments, total size 0xfff8
    Sent 66 packets.
    [TARGET BSOD]
    

Patch Analysis

  • Ipv6pReassembleDatagram: added check — if (ExtHeadersLength + FragmentableLength) > 0xFFFF, delete reassembly set and bail
  • Ipv6pReceiveFragment: added check — if reassembled packet is a Jumbogram (total > 0xFFFF), call IppSendError and bail

Workaround (Microsoft official): netsh int ipv6 set global reassemblylimit=0 (disables IPv6 reassembly — breaks legitimate large IPv6 flows).

Notable: Windows Firewall does NOT protect against this — tested by Quarkslab with firewall on: crash still occurs.


References

  • Axel “0vercl0k” Souchet, “Reverse-engineering tcpip.sys: mechanics of a packet of the death (CVE-2021-24086)”, doar-e.github.io, April 2021
  • Francisco Falcon, “Analysis of a Windows IPv6 Fragmentation Vulnerability: CVE-2021-24086”, blog.quarkslab.com, March 2021
  • McAfee Labs, “Researchers Follow the Breadcrumbs: The Latest Vulnerabilities in Windows Network Stack”, 2021
  • MSRC, “Multiple Security Updates Affecting TCP/IP”, February 2021