From Integer Overflow to Arbitrary Kernel VA Read/Write: CVE-2026-62735 in HTTP.sys

By Juan Sacco from Exploit Pack <jsacco@exploitpack.com>

While researching the latest Pwn2Own disclosures, I came across CVE-2026-62735, an integer-overflow vulnerability in HTTP.sys, the Windows kernel HTTP protocol stack.

The vulnerability was originally discovered by Siyeon Wi and demonstrated at Pwn2Own Berlin 2026. ZDI tracks it as ZDI-26-536. My research focused on studying the exploit in depth and understanding its underlying mechanics. From there, my objective was to obtain a controlled kernel VA read/write primitive and turn it into a reusable DOG backend for Kernel Pack. See the ZDI advisory

The following sections describe my adventure reproducing and studying the vulnerability, the pool layout used to control the overflow, the side quests encountered along the way, and how this ultimately led to a corrupted named-pipe data entry that could be transformed into a reusable arbitrary kernel VA read/write primitive.

Overview

CVE-2026-62735 is a local privilege-escalation vulnerability in HTTP.sys caused by insufficient validation during an HTTP response-header size calculation.

The complete chain can be summarized as follows, as per my research:

  1. A local HTTP API server constructs a response containing a very large collection of multiple known headers.
  2. The accumulated header length exceeds the maximum 32-bit value, causing HTTP.sys to calculate a much smaller total than the actual header data requires.
  3. HTTP.sys then allocates a nonpaged-pool buffer using this incorrectly reduced size.
  4. The real header data exceeds that allocation during response generation.
  5. The kernel heap overflow corrupts an adjacent named-pipe data-queue entry.
  6. The kernel heap overflow reaches the groomed adjacent nonpaged-pool allocation, corrupting a named-pipe data-queue entry and exposing the pipe queue-head pointer.
  7. Using the leaked queue-head address, the exploit redirects the corrupted named-pipe data-queue entry to a controlled fake entry backed by a real IRP. Reading from the pipe then causes NPFS to copy data from an attacker-selected kernel virtual address into a user-mode buffer, producing the arbitrary kernel-read primitive.
  8. The exploit captures a legitimate NPFS IRP as a template, submits a pending NtFsControlFile request, and locates its nested IRP. It then points the crafted data-queue entry at that IRP and triggers NPFS with a one-byte pipe read, causing an eight-byte transfer from a controlled user-mode staging buffer to an attacker-selected kernel VA.
  9. The primitive remains active and is exposed to DOG for Kernel Pack through its normal kernel read/write interface.

ZDI describes the vulnerability as allowing a local attacker with low-privileged code execution to elevate privileges and execute code as SYSTEM. Microsoft rates it Important with a CVSS score of 7.8, while ZDI assigns a score of 8.8. See the Microsoft advisory and ZDI-26-536.

Affected Component

The vulnerable component is:

C:\Windows\System32\drivers\HTTP.sys (Build 26200.8875 25H2)

What is HTTP.sys? HTTP.sys is a Windows kernel-mode device driver and web server component that intercepts, queues, and processes HTTP and HTTPS network requests before they reach user-mode applications. (MSDN Reference)

The vulnerable path is associated with processing an HTTP response through functions including:

UlpCreateInternalResponseOld
UlGenerateMultipleKnownHeadersOld
UlpPrepareHttpResponseOld
UlCaptureHttpResponseOld
UlSendHttpResponseIoctlOld
UlSendHttpResponseIoctl
UxDeviceControl

The Old suffix is important because the exploit reaches the older internal response-generation path.

Where is the pudding?

The starting point is the calculation performed while HTTP.sys determines how much memory is required for the internal response and its headers.

A simplified representation is:

TotalHeaderBytes = MultipleHeaderBytesLocal + FixedHeaderBytes + H3ExtraHeaderBytes + VariableHeaderBytes

The relevant values are accumulated using 32-bit arithmetic. A sufficiently large MultipleHeaderBytesLocal causes an integer overflow, yielding an undersized allocation for the data that follows.

The public analysis illustrates the condition with values similar to:

0xffffff32 + 0x2e + 0 + 0xa0 = 0x00000000 (32-bit)

The response still contains the full, oversized header set, while the overflowed 32-bit calculation supplies a much smaller allocation size.

The screenshot shows the Integer overflow in UlpCreateInternalResponseOld. The four header-size components are accumulated into the 32-bit TotalHeaderBytes32. The wrapped subtotal is subsequently promoted and incorporated into the wider AllocationSize calculation. Because the 0x7fffffff validation occurs after the 32-bit overflow, it cannot detect the original oversized total.

HTTP.sys then enters UlGenerateMultipleKnownHeadersOld, which copies the header data into the previously allocated buffer. Because the 32-bit length calculation produced an undersized allocation while the copy processes the full header collection, the operation writes beyond the buffer into the following nonpaged-pool allocation.

The integer overflow and the pool overflow are therefore two stages of the same vulnerability:

Incorrect arithmetic
    -> undersized allocation
    -> oversized header generation
    -> adjacent pool corruption

Reaching the Vulnerable Response Path

The exploit creates a local HTTP API server and registers the following URL:

http://127.0.0.1:8088/dog-http-rw/

A WinHTTP client thread connects to the loopback server and sends a normal GET request. The server receives that request and constructs an HTTP_RESPONSE_V2.

The response contains a collection of HTTP_RESPONSE_INFO structures using HttpResponseInfoTypeMultipleKnownHeaders. It also contains a trailer represented through a raw data chunk.

As shown in the screenshot UxDeviceControl extracts the IOCTL from the current IRP stack location, derives its function index, validates the corresponding UxIoctlTable entry and invokes its registered handler. For IOCTL 0x12403F, index 0xF selects UlSendHttpResponseIoctl.

The response is passed to the HTTP.sys request queue through control code: 0x12403F

Each known-header value has a maximum length represented by a USHORT, so the required 32-bit total is assembled from many individual header values.

The exploit first creates one header containing the controlled overflow bytes. It then distributes the remaining padding across additional known-header groups until the 32-bit multiple-header calculation produces the required undersized allocation.

Before triggering HTTP.sys, the code verifies that its own calculated total matches the intended 32-bit target. This prevents an incorrect header from producing an unexpected allocation or overwrite distance.

Controlling the Overwrite

The selected HTTP.sys allocation size is: 0x2000 bytes

The controlled overflow size is: 0x30 bytes

Those 0x30 bytes are used to replace the metadata of a named-pipe data-queue entry located immediately after the vulnerable HTTP.sys allocation.

The first malicious header value is padded with controlled data. A fabricated DATA_QUEUE_ENTRY is placed at the end of that value so that it begins precisely where the target 0x2000-byte allocation ends.

The overwritten entry contains controlled values for:

Flink
Blink
Irp
SecurityContext
EntryType
QuotaInEntry
DataSize
Reserved

Its forward and backward links point to a fake data entry mapped at a fixed user-mode address. Its data size is extended by eight bytes, which creates the initial information disclosure.

This is much more useful than an uncontrolled pool crash: the overflow replaces one complete queue entry with a structure that NPFS will continue to process.

Shaping the Nonpaged Pool with Named Pipes for Kernel Grooming

Kernel pool grooming is the deliberate creation and release of similarly sized kernel allocations to influence where a subsequent allocation is placed. Here, pipe-backed entries populate the relevant pool size class, selected entries are released, and the vulnerable HTTP.sys allocation is encouraged to occupy one of those freed regions next to a remaining candidate entry. This positioning allows the out-of-bounds write to modify the adjacent allocation.

Let's begin by placing the target named-pipe object after the vulnerable HTTP.sys allocation.

The exploit creates 2,000 named-pipe instances using:

\\.\pipe\DOGHttpRw

They are separated into two collections:

1,000 candidate pipes
1,000 pipes selectively freed to make specific pool regions available for reallocation

The payload size for each named-pipe data allocation is:

0x2000 - 0x30

After the named-pipe allocation header is included, each entry occupies the desired 0x2000-byte pool size.

The candidate pipes are first padded with two entries. A third entry is then written to each candidate pipe while matching entries are written to the second pipe collection.

This creates an alternating layout conceptually similar to:

allocated candidate | freed region | allocated candidate | freed region | allocated candidate

The entries selected for deallocation are consumed with ReadFile, releasing their kernel allocations. The code begins releasing entries near the middle of the collection to prevent adjacent freed allocations from being merged into larger regions.

The malicious HTTP response is then submitted. The vulnerable HTTP.sys allocation can occupy one of the newly freed regions and overflow into the adjacent candidate entry.

Finding the Corrupted Pipe

After triggering HTTP.sys, the exploit examines the candidate pipes with PeekNamedPipe.

A normal pipe contains the expected amount of queued data. The corrupted entry advertises eight additional bytes, so the affected pipe returns:

expected pipe data + leaked kernel pointer

The exploit identifies the pipe whose available length matches this condition and reports:

[+] Overflown data entry found

The additional eight bytes contain the adjacent queue entry’s list-head pointer:

[+] Leaked adjacent queue-entry list head: 0xffff...

That pointer is the first kernel address disclosed by the exploit.

Using the new read primitive, the code follows the adjacent entry’s backward link to calculate the address of the overwritten data entry. It then searches for an NPFS client context with the expected node type and identifies the outbound queue containing that entry.

The current backend reports these values as follows:

[+] Adjacent data entry: 0xffff...
[+] Overwritten data entry: 0xffff... (Flink 0x...)
[+] Outbound queue head: 0xffff...

These pointers describe the NPFS queue state created by the pipe spray used for kernel pool grooming.

From the Information Leak to Arbitrary Kernel Read

Before exploitation, the backend maps and locks a user-mode region at:

0x00001fd000000000

The corrupted queue entry's Flink and Blink fields point to this address.

The mapped area holds a fake named-pipe data entry followed by a fake IRP. For a read operation, the backend configures the fake entry as a buffered queue entry, assigns it a large data size and points its IRP field at the controlled IRP.

The fake IRP's associated pointer is set to the requested kernel address.

When PeekNamedPipe processes the corrupted queue, NPFS treats the controlled objects as part of the pipe's legitimate data queue. Data is returned from the selected kernel address after the existing pipe prefix.

The backend copies the bytes located after that prefix into the caller's output buffer.

Requested kernel address
    -> fake IRP associated pointer
    -> corrupted NPFS queue
    -> PeekNamedPipe
    -> kernel bytes returned after known prefix

DOG’s backend divides larger read requests into chunks of at most 0x4000 bytes.

At this stage, the exploit has progressed from an eight-byte pointer disclosure to an arbitrary kernel virtual-address read primitive.

Creating a Write Primitive

The write primitive requires a valid IRP state, so the exploit creates a separate pipe for this purpose.

A known marker is written into that pipe: 0xdeadbeef

The code walks the NPFS outbound queues until it finds an entry whose data begins with that marker. It then starts another overlapped write on the helper pipe and intentionally leaves it pending.

This produces a real NPFS IRP in kernel memory.

The backend locates the pending queue entry, follows its IRP pointer and copies 0x1000 bytes into a local template:

[*] Capturing a real IRP template on the write pipe
[+] Template entry: 0xffff... (IRP 0xffff...)

Important values from the legitimate IRP are preserved, including its cancellation routine, thread pointer and stack count. Other fields are changed to describe the controlled transfer.

A new pending file-system request is then created through NtFsControlFile. The read primitive locates the resulting outer queue entry and its nested IRP.

The fake queue entry is reconfigured for an eight-byte operation and points at that nested IRP. Reading one byte from the corrupted pipe causes NPFS to process the fabricated entry and complete the transfer.

The native write size is therefore:

8 bytes

For writes smaller than eight bytes, DOG first reads the existing quadword, changes only the requested portion and writes the complete value back.

The write contexts remain allocated and locked while their kernel requests are pending. They are released only when the backend is closed.

Side quest: Recover the Kernel Base

The read/write primitive can operate on known kernel addresses, but we also need the runtime base of ntoskrnl.exe (kernel base).

This exploit cannot use an MSR read to obtain IA32_LSTAR as shown in the previous article, for an infoleak. But instead, attempts to derive nt_base from information already present in the captured IRP.

The IRP contains a live kernel CancelRoutine pointer.

The CancelRoutine pointer in a Windows I/O Request Packet (IRP) indicates whether an I/O request can be canceled (MSDN Reference)

We first scan backward from the captured CancelRoutine pointer in page-sized steps until we identify the AMD64 PE image containing it, recovering the base of npfs.sys. We then inspect its imports to derive the base of ntoskrnl.exe.

The complete sequence is:

  1. Scan backward from CancelRoutine to recover the base of npfs.sys.
  2. Parse the npfs.sys import table.
  3. Find a live function imported from ntoskrnl.exe.
  4. Use NTKernelWalkerLib’s symbol RVA to calculate the ntoskrnl.exe base.

What we are looking is to parses that image's in-memory import directory and look for an import descriptor named: ntoskrnl.exe / ntkrnlmp.exe

For each named import, the original thunk supplies the function name while the live import-address-table entry supplies its KASLR-adjusted kernel address.

NTKernelWalkerLib resolves the corresponding symbol RVA from the local ntoskrnl image and symbols.

The candidate kernel base is calculated as:

nt_base = live imported function address - imported function RVA

Before accepting it, the exploit verifies that the result is a canonical, page-aligned kernel address with valid DOS, PE32+ and AMD64 headers and a plausible image size.

If successful, we print, later to be shown as screenshot:

[+] Kernel base from captured IRP: 0xffff... (ntoskrnl.exe!FunctionName)

Side quest: Reuse the R/W primitive

The exploit needed to survive repeated read and write operations without rebuilding the exploit state each time. After initialization, the backend keeps the exploit session alive so the same corrupted NPFS queue can service multiple kernel reads and writes.

It preserves the pipe handles, fake user-mode page, captured IRP template, leaked queue addresses, pending write requests, and their associated events and buffers. The resolved kernel base is cached for later operations. 

Each request temporarily reconfigures this shared state for a new source or destination address. A mutex ensures that only one operation can modify and use the fabricated queue at a time. Shutdown must unwind the live session carefully. The backend first uses its write primitive to place the queue metadata into a state suitable for cleanup. It then cancels the outstanding requests in reverse creation order, stops the helper writer, closes the handles and events, unlocks the mapped memory, and releases the remaining buffers.

The current cleanup path reports:

[*] Closing the HTTP.sys queue

This was a major part of the design. A successful read or write does not mean the surrounding kernel objects can simply be abandoned. Pending IRPs and corrupted linked-list state must be handled carefully to avoid a bugcheck and believe me we had many.

Exploit as part of DOG (Kernel Pack)

The following screenshot shows the final exploit running from DOG as part of the backends, starting from standard user.

And this last screenshot shows the NT/System shell achieved to proof LPE, but of course this RW primitive can now be utilized as a VA capable DOG backend for any other actions.

Where is the bug?

The root cause of the bug is the failure to validate arithmetic used for an HTTP response allocation.


HTTP.sys accepts a collection of header lengths whose accumulated size can approach the 32-bit limit. The total wraps before the allocation is made, but the later response-generation code continues processing the complete header collection.

This creates a mismatch between:

Memory allocated / Memory actually written

Affected Versions and Patching

The public reproduction was developed for Windows 11 25H2 build 26200.8875. Its documentation identifies build 26200.9168 as the corrected 25H2 boundary. Microsoft published the security update for CVE-2026-62735 on August 11, 2026. See the public reproduction and Microsoft advisory.

During laboratory testing, the operating-system build displayed by winver should not be treated as the only evidence of the active HTTP.sys version. Cumulative packages may be installed, staged or partially rolled back.

Disclosure Timeline

According to ZDI, and Exploit Pack:

  • May 21, 2026: The vulnerability was reported to Microsoft.
  • August 11, 2026: Coordinated public disclosure.
  • August 11, 2026: The advisory was published and updated.
  • September 14: 2026: The exploit has been added to DOG (Kernel Pack)

The vulnerability is credited to Siyeon Wi. The public complete exploit was independently reproduced by nhh9905. This article covers my analysis of that public exploit and its transformation into a DOG kernel read/write backend. See ZDI-26-536.

Conclusion and Takeaways

CVE-2026-62735 demonstrates how a small arithmetic error in a complex kernel protocol stack can become a complete local privilege-escalation primitive.

The initial bug is a 32-bit integer overflow in the calculation of HTTP response-header storage. That wrapped value produces an undersized nonpaged-pool allocation. The later header-generation path writes the real data and crosses the allocation boundary.

References

01001001 01100110 00100000 01111001 01101111 01110101 00100000 01100011 01100001 01101110 00100000 01110010 01100101 01100001 01100100 00100000 01110100 01101000 01101001 01110011 00100000 01110011 01100001 01111001 00100000 01001000 01101001 00100001 00100000 00111100 01101010 01110011 01100001 01100011 01100011 01101111 01000000 01100101 01111000 01110000 01101100 01101111 01101001 01110100 01110000 01100001 01100011 01101011

Back to blog