Windows TCP/IP Stack Internals (tcpip.sys)

Last updated: 2026-04-19
Related: Architecture, Primitives, Integer Overflows, Cve 2024 38063, Cve 2022 34718, Cve 2021 24086, Cve 2021 24094, Cve 2020 16898, Cve 2022 21907
Tags: kernel-mode, tcpip, integer-overflow, oob-write, remote, ipsec

Summary

tcpip.sys is the Windows kernel-mode driver implementing the full IPv4/IPv6 TCP/IP network stack. It is a premier attack surface for remote zero-click exploitation: it is always loaded, processes all network packets before any user-mode code runs, and handles complex stateful protocols (fragmentation, reassembly, options parsing, IPsec) in ring-0. This page documents tcpip.sys internals relevant to exploitation, derived from reverse engineering CVE-2020-16898, CVE-2021-24086, CVE-2021-24094, CVE-2022-34718, and CVE-2024-38063.


Core Data Structures

NET_BUFFER / NET_BUFFER_LIST (NDIS)

All network packet data is stored in NDIS structures, documented because NDIS is a public extension interface:

kd> dt NDIS!_NET_BUFFER
   +0x000 Next             : Ptr64 _NET_BUFFER
   +0x008 CurrentMdl       : Ptr64 _MDL         ← current MDL in chain
   +0x010 CurrentMdlOffset : Uint4B              ← byte offset into CurrentMdl
   +0x018 DataLength       : Uint4B              ← total data length (EXPLOITABLE: CVE-2024-38063 zeroes this)
   +0x020 MdlChain         : Ptr64 _MDL          ← full MDL chain
   +0x028 DataOffset       : Uint4B
   +0x030 ChecksumBias     : Uint2B
   ...

kd> dt NDIS!_NET_BUFFER_LIST
   +0x000 Next             : Ptr64 _NET_BUFFER_LIST  ← linked list of NBLs
   +0x008 FirstNetBuffer   : Ptr64 _NET_BUFFER
   +0x08c Status           : Int4B                   ← set to STATUS_DATA_NOT_ACCEPTED (0xC000021B)
                                                        by IppSendError() when packet has error
   ...

WinDbg commands: !ndiskd.nb <addr> and !ndiskd.nbl <addr> to dump NET_BUFFER/NET_BUFFER_LIST with MDL chains.

Packet_t (Undocumented, Reconstructed)

Internal packet wrapper used throughout IPv4/IPv6 parsing:

struct Packet_t {
    Packet_t          *Next;              // linked list (coalesced packet list)
    NET_BUFFER_LIST   *NetBufferList;     // backing NDIS structure
    UINT8              NextHeader;        // current protocol/extension header type
    UINT32             NextHeaderPosition;// byte offset of Next Header field in packet
    ipv6_header_t     *ip_header;        // pointer to parsed IPv6 header (UAF in CVE-2021-24094)
    PVOID              dst_ip;           // destination IP pointer (UAF in CVE-2021-24094)
    UINT32             packet_size;      // (EXPLOITABLE: CVE-2024-38063 zeroes via IppSendError)
    UINT32             flags;            // bit 4 = "created by reassembly" flag
    // ... many more fields
};

Reassembly_t (Undocumented, Reconstructed)

Per-fragment-group reassembly state, keyed by (src_ip, dst_ip, frag_id):

struct Reassembly_t {
    // Hash table linkage
    RTL_DYNAMIC_HASH_TABLE_ENTRY HashEntry;
    // Reassembly data
    ipv6_header_t  Ipv6;                // copy of first fragment's IPv6 header
    UINT32         UnfragmentableLength;// total size of extension headers before Fragment Header
    UINT32         DataLength;          // size of reassembled fragmentable part
    UINT16         packet_length;       // 16-bit! (CVE-2024-38063 exploits 16-bit underflow here)
    UINT16         fragment_size;       // set to underflowed value 0xFFD0 in CVE-2024-38063
    // Fragment list
    struct FragList *fragments;         // linked list of received fragments
    // Offset tracking
    UINT32         offset_of_last_next_hdr; // byte offset of Next Header field to fix up
    UINT8          frag_next_hdr;       // Next Header value from Fragment Header
    // ...
};

Reassembly objects are stored in a global hash table Ipp6ReassemblyHashTable (IPv6) or Ipp4ReassemblyHashTable (IPv4), protected by Ipp6ReassemblyHashTableLock spinlock.

Hash function: IppReassemblyHashKey(iface, identification, packet) = RtlCompute37Hash(src_ip) ⊕ RtlCompute37Hash(dst_ip) ⊕ RtlCompute37Hash(frag_id) | 0x80000000

Protocol_t and Demuxer_t

The IPv6 packet parser uses a dispatch table (Protocol_t.Demuxers[277]) indexed by NextHeader value (0–255 + extra):

struct Demuxer_t {
    void (*Parse)(Packet_t *);   // parsing callback
    BOOL   IsExtensionHeader;    // if true, Parse is called in extension header loop
    // ...
};

Key dispatch entries:

NextHeader = 0  (IPPROTO_HOPOPTS)  → Ipv6pReceiveHopByHopOptions
NextHeader = 43 (IPPROTO_ROUTING)  → Ipv6pReceiveRoutingHeader
NextHeader = 44 (IPPROTO_FRAGMENT) → Ipv6pReceiveFragmentList → Ipv6pReceiveFragment
NextHeader = 58 (IPPROTO_ICMPV6)  → Icmpv6ReceiveDatagrams
NextHeader = 59 (IPPROTO_NONE)    → no further headers
NextHeader = 60 (IPPROTO_DSTOPTS) → Ipv6pReceiveDestinationOptions → Ipv6pProcessOptions

Packet Processing Pipeline

Top-Level: IppReceiveHeaderBatch

Incoming packets (NET_BUFFER_LIST chain)
    ↓
IppReceiveHeaderBatch(packet_list, protocol)
    ├── For each packet: IppReceiveHeadersHelper(packet)
    │       └── Parse IPv6 header → set Packet->NextHeader
    ├── Loop while IsExtensionHeader:
    │       └── Protocol->Demuxers[NextHeader].Parse(packet)
    ├── IppProcessDeliverList()  ← deliver to transport layer
    └── IppFreePacket() + NetioDereferenceNetBufferListChain()

IPv6 Options Processing: Ipv6pProcessOptionsIppSendErrorList

The entry point for Destination Options and Hop-by-Hop Options headers. When an invalid option is encountered:

Ipv6pProcessOptions(coalesced_packet_list)
    ↓
IppDiscardReceivedPackets() == 0 AND var != 0?
    ↓ YES
IsEnabledDeviceUsage_3 = Feature_2660322619__private_IsEnabledDeviceUsage_3()
    ├── IsEnabledDeviceUsage_3 == 0: IppSendErrorList(packet_list)  ← VULNERABLE
    └── IsEnabledDeviceUsage_3 != 0: IppSendError(packet)          ← PATCHED (CVE-2024-38063 fix)

IppSendErrorList behavior (vulnerable):

void IppSendErrorList(Packet_t *list) {
    for (Packet_t *p = list; p; p = p->Next)
        IppSendError(p);  // calls IppSendError on EVERY packet in list, not just erroneous one
}

IppSendError side effect (when always_send_icmp = true, triggered by option type > 0x80):

// Deep inside IppSendError:
packet->packet_size = 0;  // zeroes packet_size field
// ... sends ICMP error back to sender

When IppSendErrorList processes a coalesced list containing both the malformed options packet AND subsequent fragment packets, all packets in the list get packet_size = 0.

IPv6 Fragment Reassembly: Ipv6pReceiveFragment

Ipv6pReceiveFragment(packet)
    ├── IppReassemblyHashKey() → look up or create Reassembly_t
    ├── Record unfragmentable part (first fragment only):
    │       UnfragmentableLength = packet->unfragmentable_size
    │       frag_next_hdr = fragment_header->next_header
    │       offset_of_last_next_hdr = (offset in unfragmentable where NH points to frag header)
    ├── fragment_size = LOWORD(packet->packet_size) - 0x30   ← 16-bit arithmetic!
    │       (CVE-2024-38063: if packet_size=0, LOWORD(0)-0x30 = 0xFFD0 = 65488)
    ├── Store fragment payload
    └── If last fragment received: Ipv6pReassembleDatagram()

Reassembly Completion: Ipv6pReassembleDatagram

Ipv6pReassembleDatagram(packet, reassembly)
    ├── HeaderAndOptionsLength = UnfragmentableLength + sizeof(ipv6_header_t)
    ├── NetioRetreatNetBuffer(FirstNetBuffer, uint16_t(HeaderAndOptionsLength), 0)
    │       ← uint16_t TRUNCATION (CVE-2021-24086: undersized MDL if > 0xFFFF)
    ├── Buffer = NdisGetDataBuffer(FirstNetBuffer, HeaderAndOptionsLength, NULL, ...)
    │       ← if uint16_t(H) != H: NdisGetDataBuffer returns NULL (CVE-2021-24086 crash)
    ├── *Buffer = reassembly->Ipv6  ← NULL deref crash (CVE-2021-24086)
    ├── [PATCH addition]: new_header = alloc(); *new_header = incoming_packet->ip_header
    │       reassembled->ip_header = new_header  ← CVE-2021-24094 fix
    └── If reassembled->flags & BIT4:
            IoQueueWorkItem(IppReassembledReceive, reassembled)  ← deferred (CVE-2021-24094 UAF path)
        Else:
            IppReceiveHeaderBatch(reassembled)

Reassembly Timeout: Ipv6pReassemblyTimeout

Called 60 seconds after first fragment received if reassembly is incomplete. Site of CVE-2024-38063 overflow:

// Allocation size (16-bit DX arithmetic):
DX = fragment_list->net_buffer_length  // ≈ 0x38
DX += reassembly->packet_length         // 0x38 + 0xFFD0 = 0x10008 → overflow → DX = 0x0008
DX += 0x28                              // 0x0008 + 0x28 = 0x30 → ~48 bytes
alloc = ExAllocatePool(DX);             // allocates ~48 bytes

// Copy size (full 32/16-bit reassembly->packet_length = 0xFFD0 = 65488):
memmove(alloc, reassembly->payload, reassembly->packet_length);  // copies 65,488 → OVERFLOW

IPsec ESP Processing (CVE-2022-34718 Context)

Security Association Requirement

IPsec ESP (Encapsulating Security Payload) packets require a Security Association (SA) — a shared set of cryptographic parameters (SPI, algorithm, key) negotiated via IKEv1/IKEv2. Without an SA, tcpip.sys drops ESP packets before parsing. Establishing an SA requires:

  • Domain environment with group policy, OR
  • Pre-shared key (PSK), OR
  • Certificate-based authentication

This is the primary prerequisite for CVE-2022-34718 — it is remote but not fully zero-click (SA must be negotiated first, which requires some initial connectivity setup).

ESP Packet Processing: Ipv6pReassembleDatagram (CVE-2022-34718)

When an IPv6 fragmented packet is carried inside an ESP payload, after decryption tcpip.sys processes the embedded IPv6 packet through the normal reassembly pipeline. In the vulnerable code:

// Offset into NetIoProtocolHeader2 is calculated as:
offset = sizeof(PayloadData) + sizeof(Padding) + sizeof(PaddingLength);
// → Can be > 0x38 (past end of object)

// OOB write: 1 byte, value = Next Header field from ESP tail (= 0x2c for Fragment Header)
*(NetIoProtocolHeader2 + offset) = NextHeader;  // OOB!

The patch adds two guards:

  1. Ipv6pReassembleDatagram: bounds check on the offset calculation
  2. ESP handler: discard ESP packets where embedded extension header type ≤ 0x2c (blocks Fragment=0x2c, Routing=0x2b, Hop-by-Hop=0x00)

NetIoProtocolHeader2

The NetIoProtocolHeader2 is a kernel paged pool object used for protocol header processing in the IPv6 reassembly path. Key facts:

  • Pool tag: investigate with !poolused / dt tcpip!NetIoProtocolHeader2
  • Size: > 0x38 bytes (exact size not public; the bug writes past offset 0x38)
  • Corruption of 1 byte at offset > 0x38 does not reliably crash immediately — depends on what adjacent pool object is present

Attack Surface Summary

Attack VectorComponentBug ClassCVEs
ICMPv6 Router Advertisement with malformed RDNSS optiontcpip!Icmpv6ReceiveDatagramsHeap OOB writeCVE-2020-16898
IPv6 nested fragments with huge ext. headerstcpip!Ipv6pReassembleDatagramuint16_t truncation → NULL derefCVE-2021-24086
IPv6 recursive reassembly state confusiontcpip!Ipv6pReassembleDatagramUAF + type confusionCVE-2021-24094
IPv6 fragment inside IPsec ESP payload (SA required)tcpip!Ipv6pReassembleDatagramOOB write → NetIoProtocolHeader2CVE-2022-34718
IPv6 coalesced malformed opts + fragmenttcpip!Ipv6pProcessOptions + Ipv6pReassemblyTimeout16-bit underflow → heap overflowCVE-2024-38063
HTTP/1.1 requests to IIShttp!UlFastSendHttpResponseUninitialized MDL → invalid unmapCVE-2022-21907

Key tcpip.sys Reversing Techniques

Extracting Patch Diffs

1. Download pre/post-patch tcpip.sys from winbindex.m417z.com
   (index at: https://winbindex.m417z.com/?file=tcpip.sys)
2. Load both in IDA, generate .idb files
3. BinDiff: File → BinDiff → diff the two .idb files
4. Focus on functions with similarity < 0.9 or "changed" status

For CVE-2024-38063: exactly ONE function changed (Ipv6pProcessOptions), one line.
For CVE-2021-24086: 2 functions changed (Ipv6pReassembleDatagram, Ipv6pReceiveFragment).

Key Debugging Commands

// Set breakpoint on IPv6 fragmentation reassembly:
bp tcpip!Ipv6pReassembleDatagram

// Dump NET_BUFFER_LIST chain:
!ndiskd.nbl <addr>

// Dump NET_BUFFER:
!ndiskd.nb <addr>

// Watch packet coalescing (CVE-2024-38063):
bp tcpip!Ipv6pProcessOptions "dt @rcx; g"

// Dump reassembly hash table:
dt tcpip!Ipp6ReassemblyHashTable

Scapy IPv6 Extension Header Construction

from scapy.all import *

# Malformed destination options (CVE-2024-38063 trigger):
# Option type 0x81 > 0x80 → always_send_icmp = true in IppSendError
pkt = Ether(dst=mac) / IPv6(dst=ip, nh=60) / \
      raw(struct.pack('BBBB', 44, 0, 0x81, 0) + b'\x00'*4)

# Fragment packet (Next Header from IPv6 header → 44 = fragment):
frag = Ether(dst=mac) / IPv6(dst=ip, nh=44) / \
       IPv6ExtHdrFragment(m=1, id=0x1337, nh=59) / (b'A'*100)

# Nested fragments for CVE-2021-24086 (0x1ffa routing headers = 0xFFD0 bytes):
routes = raw(IPv6ExtHdrRouting(addresses=[], nh=43)) * (0xffd0//8 - 1)
routes += raw(IPv6ExtHdrRouting(addresses=[], nh=44))

Mitigation Relevance

MitigationEffect on tcpip.sys exploits
Disable IPv6Eliminates all IPv6-based attack surface; breaks IPv6 connectivity
KASLRRequired for reliable RCE; not defeated by DoS-only PoCs
Windows FirewallIneffective against CVE-2020-16898 and CVE-2021-24086 (processed below firewall)
Segment Heap (kernel pool)Raises bar for heap grooming; does not prevent overflow
HVCIDoes not help directly (no shellcode; overflow corrupts data structures)
PatchOnly reliable mitigation

Exploit Relevance

tcpip.sys vulnerabilities represent the most impactful class of Windows kernel bugs: zero-click, pre-authentication, wormable, remote ring-0 code execution. The IPv6 attack surface (extension headers, fragmentation, reassembly, NDP/ICMPv6) has proven repeatedly vulnerable across multiple patch cycles (2020–2024). The complexity of the IPv6 specification — nested fragments, extension header chains, coalescing — provides fertile ground for state confusion and arithmetic errors.

Key lesson for auditors: focus on:

  1. 16-bit arithmetic on size fields (fragment_size, packet_length, HeaderAndOptionsLength)
  2. List processing functions that operate on multiple packets (coalesced lists)
  3. Recursive state machines (nested reassembly) where “first packet” semantics are complex
  4. Delayed execution paths (WorkItems, timers like Ipv6pReassemblyTimeout) that operate on state set by earlier code
  5. Protocol layering interactions — code paths triggered only when one protocol (e.g., ESP) delivers another (e.g., IPv6 fragments) may miss validation steps checked in the normal direct path (CVE-2022-34718 pattern)

References

  • Axel “0vercl0k” Souchet, “Reverse-engineering tcpip.sys: mechanics of a packet of the death”, doar-e.github.io, April 2021
  • Francisco Falcon, “Analysis of a Windows IPv6 Fragmentation Vulnerability: CVE-2021-24086”, blog.quarkslab.com, 2021
  • Marcus Hutchins, “CVE-2024-38063 - Remotely Exploiting The Kernel Via IPv6”, malwaretech.com, August 2024
  • Armis Research Team, “From URGENT/11 to Frag/44”, armis.com, April 2021
  • pi3, “CVE-2020-16898 – Bad Neighbor”, blog.pi3.com.pl, October 2020
  • chompie1337 (IBM X-Force), “Dissecting and Exploiting TCP/IP RCE Vulnerability ‘EvilESP’”, IBM Security Intelligence, 2023 — https://www.ibm.com/think/x-force/dissecting-exploiting-tcp-ip-rce-vulnerability-evilesp
  • Numen Cyber Labs, “TCP/IP Vulnerability CVE-2022-34718 PoC Restoration and Analysis”, 2022 — https://www.numencyber.com/tcp-ip-vulnerability-cve-2022-34718-poc-restoration-and-analysis/
  • Microsoft NDIS Documentation, NET_BUFFER, NET_BUFFER_LIST, WDK
  • winbindex.m417z.com — index of Windows binaries by build