CVE-2026-42980: Reversing and Exploiting the Windows Kernel WMI Underflow¶
Publication context
This research was developed as part of Binary Gecko Academy. Ricardo Narvaja authorized me to publish this material starting today, 2026-07-07.
This article, produced as part of Binary Gecko Academy and published with Ricardo Narvaja's permission starting today, documents the technical root cause and exploitation strategy for
CVE-2026-42980, a Windows NT OS Kernel elevation-of-privilege vulnerability in
the WMI subsystem. The public advisory describes the bug class and impact, but it
does not disclose the vulnerable functions, IOCTLs, or arithmetic. This write-up
fills in those details from patch diffing, static reverse engineering, and a lab
exploit that reaches an interactive NT AUTHORITY\SYSTEM shell.

This material is intended for defensive research and authorized lab testing. The PoC must only be used on systems you own or are explicitly allowed to test.
TL;DR¶
The vulnerable sites are in the WMI serialization paths inside ntoskrnl.exe:
| Path | IOCTL | Vulnerable function | Vulnerable subtraction |
|---|---|---|---|
| query multiple WMI data blocks | 0x22812C |
nt!WmipQueryAllDataMultiple |
OutputBufferLength -= AlignedSize at WmipQueryAllDataMultiple+0x29a |
| execute/query a single instance multiple times | 0x228130 |
nt!WmipQuerySingleMultiple |
OutBufferSize -= AlignedActualSize at WmipQuerySingleMultiple+0x401 |
Both sites maintain a 32-bit remaining-output counter. The vulnerable builds
subtract an aligned provider-reported size without proving that the size is less
than or equal to the remaining counter. If the size is larger, the unsigned
counter wraps to a large value. The next WMI serialization step then treats the
buffer as if it still had plenty of room and writes past the end of the allocated
kernel SystemBuffer.
The exploit uses the 0x228130 path (nt!WmipQuerySingleMultiple) because it
offers a practical in-box trigger: the initial capacity gate is based on a
caller-controlled WNODE item size estimate, while the later subtraction uses the
actual serialized WNODE size returned by the provider.
Patch diff: the exact vulnerable functions¶
Diffing the vulnerable kernel against the patched kernel shows that Microsoft
changed both WMI paths in the same way: the unchecked subtraction was replaced by
a saturating subtraction guarded by Feature_1045423416.
nt!WmipQueryAllDataMultiple (IOCTL 0x22812C)¶
The query path serializes multiple WMI data blocks and maintains a remaining output length. In the vulnerable build, the loop performs this logic:
AlignedSize = (QueryInfo[0] + 7) & 0xFFFFFFF8;
CurrentOutputBuffer = (char *)CurrentOutputBuffer + AlignedSize;
TotalSize += AlignedSize;
OutputBufferLength -= AlignedSize; // vulnerable: unchecked unsigned subtract
At the instruction level, the vulnerable operation is:
The patched build changes this to a saturating form:
v = OutputBufferLength - AlignedSize;
if (Feature_1045423416__private_IsEnabledDeviceUsageNoInline())
v = -(unsigned int)(AlignedSize < OutputBufferLength) & v;
OutputBufferLength = v;
The mask is 0xFFFFFFFF only when the subtraction does not underflow. If
AlignedSize >= OutputBufferLength, the mask becomes zero and the remaining
counter is clamped to 0 instead of wrapping.
nt!WmipQuerySingleMultiple (IOCTL 0x228130)¶
The exploit path is the single-instance/multiple-item path behind:
The vulnerable build contains the same arithmetic bug:
AlignedActualSize = (ReturnedDataSize + 7) & 0xFFFFFFF8;
TotalRequiredSize += AlignedActualSize;
OutBufferSize -= AlignedActualSize; // vulnerable: unchecked unsigned subtract
The diff identifies the vulnerable site as:
and the instruction-level operation as the equivalent of:
The patched build applies the same saturating pattern:
t = OutBufferSize - AlignedActualSize;
if (Feature_1045423416__private_IsEnabledDeviceUsageNoInline())
t = -(unsigned int)(AlignedActualSize < OutBufferSize) & t;
OutBufferSize = t;
This confirms that both vulnerable functions belong to the same arithmetic bug class: unchecked subtraction of an aligned provider-controlled size from a remaining-output counter.
Reachability from user mode¶
The vulnerable paths are reachable from a normal local process through the WMI data device:
[user mode]
NtDeviceIoControlFile(\Device\WMIDataDevice, ioctl, ...)
-> nt!WmipIoControl
IOCTL 0x22812C -> nt!WmipQueryAllDataMultiple
IOCTL 0x228130 -> nt!WmipQuerySingleMultiple
-> nt!WmipQueryAllData / provider execution
-> nt!WmipForwardWmiIrp
-> WMI provider
For the final exploit I use IOCTL 0x228130, because the request format lets me
prepare two WNODE inputs:
- item 0: creates the accounting underflow;
- item 1: materializes the out-of-bounds write with attacker-controlled bytes.
The PoC opens \Device\WMIDataDevice directly and also uses WmiOpenBlock /
WmiQueryAllDataW from advapi32.dll to discover live WMI instances that have a
usable trigger window.
The exact arithmetic in the trigger¶
The exploitable condition in WmipQuerySingleMultiple is a mismatch between the
gate estimate and the value later subtracted.
For each WNODE item, the gate uses a required-size estimate of this form:
For a WMI instance name, the size estimate is effectively tied to the caller-side item data size / name length. However, after the provider path runs, the caller subtracts the actual serialized WNODE size:
The exploitable relationship is:
That means the WNODE item passes the initial capacity check, but the later subtraction wraps.
One concrete window observed during validation was:
nameLen = 0x4c
requiredSize = (0x4c + 0x49) & ~7 = 0x90
R = 0x94
aligned = 0x98
slop = 4
gate: 0x90 <= 0x94 -> accepted
subtract: 0x94 - 0x98 -> 0xFFFFFFFC
On another lab build/profile, the dynamic resolver selected:
guid#156 = {2e2d2463-b537-4da7-8eee-51306f1f482f}
class = WmiMonitorConnectionParams
nameLen = 0x56
requiredSize = 0x98
R = 0x9c
aligned = 0xa0
slop = 4
overwriteOff = 0xf1e
The exact GUID is not the root cause. The important part is the measured window:
ALIGN8(R) > R while the gate still accepts the item.
Why the GUID can change¶
The PoC does not rely on one magic WMI provider. It ships with a WMI GUID dataset and dynamically scans for usable windows at runtime. The chosen provider depends on the Windows build, VM profile, enabled devices, monitor configuration, network state, and other WMI-exposed hardware state.
Examples of successful or candidate providers seen in lab runs included:
| guid index | GUID | WMI class |
|---|---|---|
27 |
{0a214807-e35f-11d0-9692-00c04fc3358c} |
MSNdis_CoTransmitPduErrors |
156 |
{2e2d2463-b537-4da7-8eee-51306f1f482f} |
WmiMonitorConnectionParams |
463 |
{827c0a6f-feb0-11d0-bd26-00aa00b7b32a} |
MSPower_DeviceEnable |
506 |
{8f680850-a584-11d1-bf38-00a0c9062910} |
MSSmBios_RawSMBiosTables |
553 |
{98a2b9d7-94dd-496a-847e-67a5557a59f2} |
MS_SystemInformation |
658 |
{bdd865d1-d7c1-11d0-a501-00a0c9062910} |
MSDiskDriver_Performance |
This is why the exploit computes the overwrite geometry dynamically. If the
window changes from R=0x94/aligned=0x98 to R=0x9c/aligned=0xa0, the overwrite
offset must change too.
The core formula used by the PoC is:
phase = (alignedActualSize + WMI_WNODE_INSTANCE_DATA_OFFSET) & (0x1000 - 1);
overwriteOffset = phase ? (0x1000 - phase) : 0;
with:
For the 0x9c/0xa0 fallback window, this produces overwriteOff=0xf1e.
Turning the wrap into an out-of-bounds write¶
After item 0 wraps outRemaining, item 1 is processed while the kernel believes
the output buffer still has a huge amount of remaining capacity. The item 1 WNODE
is serialized past the end of the kernel SystemBuffer.
The useful part is the WNODE instance-data copy. In the exploit, the attacker controls the bytes copied at:
That is why the code defines:
The second WNODE data buffer is shaped so this instance-data copy lands on the header of a neighboring named-pipe data queue entry.
Pool layout and bucket matching¶
The target object is an npfs named-pipe data queue entry (NP_DATA_QUEUE_ENTRY,
tagged as NpFr in pool usage). The exploit creates many pipes and fills them so
the kernel allocates a stable sequence of same-sized pipe data objects.
One important detail is that the WMI request uses METHOD_BUFFERED. The kernel
allocates the IOCTL SystemBuffer using:
If the WMI SystemBuffer allocation and the sprayed pipe data queue entries are
not in the same pool bucket, the OOB write will not reliably land on the target
object. The exploit therefore pads the WMI input length:
and uses pipe writes that produce effective 0x1000-sized pool objects. The
important geometry is:
In the smaller bucket calibration case, the same idea appears as:
Sprayed NpFr DQE = 0x30 + 0xb0 = 0xe0 -> 0xf0 chunk
SystemBuffer before = max(0x38, 0x94) -> 0xb0 chunk (wrong bucket)
SystemBuffer after = max(0xe0, 0x94) -> 0xf0 chunk (matching bucket)
The public exploit uses the larger 0xff0/0x1000 geometry for the final chain.
Corrupting the pipe queue entry¶
The first WMI trigger corrupts the target pipe entry and inflates its readable
size. A later PeekNamedPipe call can then over-read beyond the original pipe
data and leak adjacent kernel pool metadata.
The exploit uses a stable leak offset:
At that offset it extracts queue/list metadata needed to keep the pipe structure consistent while building stronger primitives.
After the leak stage, the exploit triggers WMI again with a different payload to
shape the corrupted queue entry as an IRP-backed queue entry. That lets
PeekNamedPipe copy data from an attacker-selected kernel address, giving an
arbitrary kernel read primitive.
From arbitrary read to token replacement¶
With arbitrary kernel read, the exploit locates the current process and the SYSTEM process. The offsets are selected by Windows build number. The high-level walk is:
IRP
-> ETHREAD via _IRP.Tail.Overlay.Thread
-> current EPROCESS via KTHREAD/ApcState.Process
-> ActiveProcessLinks walk
-> PID 4 EPROCESS
-> SYSTEM token
Then an IRP completion/write path is used to write the SYSTEM token value into the
current process EPROCESS.Token field. After that, the exploit repairs the
corrupted pipe entry and launches:
The resulting process runs as:
Exploit stages in the public PoC¶
The public PoC prints the chain as phases:
- detect Windows build and select kernel structure offsets;
- resolve
NtFsControlFile,WmiOpenBlock, andWmiQueryAllDataW; - allocate WMI buffers;
- spray named pipes;
- fill the pipes to create the desired pool objects;
- free one pipe object to create a hole;
- open
\Device\WMIDataDevice; - resolve a WMI trigger window and send
IOCTL 0x228130; - find the corrupted pipe;
- leak npfs metadata;
- build arbitrary read;
- locate PID 4 and read the SYSTEM token;
- write the SYSTEM token into the current process;
- repair the pipe object;
- spawn the SYSTEM shell.
Building¶
From the public/ directory:
or:
The output is:
Patch impact¶
The patch prevents the primitive at the arithmetic layer. Once the remaining counter is saturated to zero instead of wrapping, the next WNODE cannot be serialized into the attacker-groomed adjacent pool object. That prevents the pipe queue corruption, which prevents the leak, arbitrary read/write, and token swap.
Binary Gecko Academy and publication permission¶
This work was produced as part of Binary Gecko Academy. I am publishing it with Ricardo Narvaja's permission starting today, 2026-07-07.
Conclusion¶
CVE-2026-42980 is not just a generic WMI bug. The vulnerable functions are
nt!WmipQueryAllDataMultiple and nt!WmipQuerySingleMultiple; the exploited IOCTL
is 0x228130; and the key primitive is an unchecked aligned-size subtraction from
a 32-bit remaining-output counter. The exploitability comes from combining that
arithmetic wrap with attacker-controlled WNODE instance data and precise npfs pool
grooming. The final result is a reliable local privilege escalation to SYSTEM in
the tested vulnerable lab configurations.