CVE-2026-62737 is a Windows 11 kernel local privilege escalation zero-day attributed to researcher lb3tyjtIxQ. The vulnerability affects recent Windows 11 builds, including the tested 25H2 environment, and is rooted in an access-control and trust-model failure rather than a conventional memory-corruption bug. The core problem is that a low-privileged local user can reach a protected ExecutionContext device through a proxy path exposed by ndis.sys. Once that path is open, the attacker can place two fully controlled 64-bit values into a kernel task object. One value is later interpreted as a function pointer, the other as its first argument. The kernel worker thread that consumes the task performs only a range check against MmSystemRangeStart, then executes Callback(Argument). That produces a controlled indirect kernel call, which is sufficient for denial of service and, when paired with a kernel base leak, can be developed into local privilege escalation.
The vulnerability is interesting because it does not depend on a heap overflow, a use-after-free, an integer overflow, or a race condition in the classic sense. It is an authorization-design failure. The Windows Object Manager protects the direct device path with an ACL, but the NDIS KLoader proxy creates an alternate route to the same underlying module. The ACL remains technically intact, yet it no longer governs the actual attack path. This is a recurring pattern in kernel security: when a component becomes a dispatcher, multiplexer, or broker, the original security boundary can silently move. If the broker does not re-impose authorization, the protected object becomes reachable from a lower-privilege context than intended.
Vulnerability Identity, Constraints, and Practical Impact
The vulnerability is tracked as CVE-2026-62737. The affected product is the latest Windows 11 environment tested by the original researcher, including Windows 11 25H2. The practical effect is local privilege escalation, but the exploit has an important constraint: the attacker needs a separate kernel address leak to defeat KASLR. Without a kernel base or module base leak, the attacker can still force controlled execution to an unmapped kernel-range address, which causes a system crash. The published PoC behavior confirms this directly: the supplied dump shows a blue-screen termination after the kernel attempts to execute the attacker-controlled callback value.
This distinction matters. The primitive itself is strong: user mode can influence both RIP and the first argument register used by the Windows x64 calling convention. However, modern Windows exploitation usually requires more than a raw indirect call. The attacker must find a useful target that survives kernel mitigations, and that requires knowing where kernel code or data lives. In the tested 24H2 and 25H2 systems, the original researcher notes that a local user can no longer successfully use certain NtQuerySystemInformation behavior to obtain the needed kernel base information. Therefore, CVE-2026-62737 is best understood as a powerful execution primitive that must be chained with an information-disclosure primitive for a complete privilege-escalation attack.
Why NDIS Was the Correct Research Surface
Choosing a kernel attack surface is not about picking the largest driver or the most familiar one. The decisive question is whether an unprivileged local user can actually reach the code path. If the entry point requires Administrator, the vulnerability is still useful in some scenarios, but its value as a local privilege-escalation primitive drops sharply. The most valuable kernel entry points are those that sit on the border between ordinary user-mode activity and kernel-mode service. Device objects, symbolic links, ALPC ports, RPC interfaces, filter-driver callbacks, and network-stack hubs all qualify. NDIS is especially interesting because it is deeply integrated with the system’s networking architecture and has many registration and forwarding relationships.
NDIS has existed for decades and has accumulated a large amount of structural complexity. It supports miniport drivers, protocol drivers, filter drivers, virtual adapters, NDIS clients, and newer framework-style components. That complexity creates many cases where one kernel component receives a request and then forwards it to another component. Such forwarding is exactly where access-control assumptions become fragile. The original target object may have a restrictive ACL, but the forwarding component may have been designed with performance and compatibility in mind, not with a full re-authorization step. The CVE-2026-62737 case proves that this is not a theoretical concern.
The KLoader Proxy and the Meaning of the GUID Path

The first concrete clue was the presence of KLoader-related functions inside ndis.sys. These functions create and service a device named \Device\kloader, together with a DOS-visible symbolic link that allows user-mode access through a path such as \\.\kloader. The important detail is that the path is not limited to a flat device name. It can include a GUID, allowing the caller to address a registered module directly. The syntax becomes something like \\.\kloader\{GUID}. When a create request or IOCTL arrives, ndisKLoaderIrpCreateHandler parses the GUID, resolves the registered module, and forwards the request to the module’s device object.
This design is powerful because it gives a uniform way to reach kernel modules that participate in the KLoader registration model. It is dangerous if the proxy does not enforce the same access policy as the destination device. In this case, the protected ExecutionContext device is registered behind KLoader. The direct device path may be restricted, but the KLoader path is reachable by a low-privileged user. The device’s ACL protects only the direct open path; it does not protect the indirect route created by the proxy. That is the essence of the ACL bypass.
First Decompiled Fragment: KLoader Create Handler and GUID Parsing
The following decompiled fragment shows the beginning of ndisKLoaderIrpCreateHandler. It allocates a proxy object, initializes fields, and calls ParseModuleID to extract the GUID and module identifier from the user-controlled path. The decompiler output is not a complete, compilable function; it is a reverse-engineering artifact that preserves the relevant logic. The key point is that the handler treats the GUID path as a routing token. Once parsing succeeds, the request can be forwarded to the target module.

__int64 __fastcall ndisKLoaderIrpCreateHandler(struct _IRP *a1, __int64 a2){ __int64 v2; // rdi _DWORD *PoolWithTag; // rax _DWORD *v7; // rbx struct _UNICODE_STRING *v8; // r12 unsigned int v9; // edi __int64 *v10; // rdi unsigned int Irp; // esi __int64 v12; // rcx _IO_STACK_LOCATION *CurrentStackLocation; // rax __int64 v14; // [rsp+20h] [rbp-60h] BYREF void (__fastcall *v15)(struct KLOADER_MODULE_REFERENCE__ *); // [rsp+28h] [rbp-58h] BYREF struct _UNICODE_STRING v16; // [rsp+30h] [rbp-50h] BYREF struct _GUID v17; // [rsp+40h] [rbp-40h] BYREF __int64 v18; // [rsp+50h] [rbp-30h] BYREF struct _GUID v19; // [rsp+58h] [rbp-28h] __int64 v20; // [rsp+68h] [rbp-18h] v2 = *(_QWORD *)(a2 + 48); if ( !v2 ) return 3221225659LL; PoolWithTag = ExAllocatePoolWithTag(NonPagedPoolNx, 0x28uLL, 0x62694C4Eu); v7 = PoolWithTag; if ( !PoolWithTag ) return 3221225626LL; PoolWithTag[1] = 0; v8 = (struct _UNICODE_STRING *)(v2 + 88); *((_QWORD *)PoolWithTag + 2) = 0LL; *((_QWORD *)PoolWithTag + 3) = 0LL; *((_QWORD *)PoolWithTag + 4) = 0LL; *PoolWithTag = 0xAEACEFE; *((_QWORD *)PoolWithTag + 1) = 0LL; v17 = 0LL; v16 = 0LL; v9 = ParseModuleID( (const struct _UNICODE_STRING *)(v2 + 88), &v17, &v16 ); // 解析 \\.\kloader\{GUID} if ( v9 ) {LABEL_5: ProxyFileObject::~scalar deleting destructor((__int64 *)v7); return v9; } v20 = 0LL;
The allocation uses ExAllocatePoolWithTag with NonPagedPoolNx and a 0x28-byte size. The tag value 0x62694C4E is useful during pool forensics because it can help identify proxy objects in a crash dump or live kernel memory analysis. The call to ParseModuleID is the semantic center of this fragment. It receives the unicode string associated with the requested path and produces GUID-related output structures. If parsing fails, the proxy object is destroyed and the error is returned. If parsing succeeds, execution continues into the module-resolution and forwarding logic.
📬 Stay Ahead of Cyber Threats
Get the latest cybersecurity news, critical vulnerabilities, threat intelligence, tutorials, and exclusive giveaways delivered straight to your inbox. No spam. Unsubscribe anytime.
Subscribe to the Newsletter →Registry Confirmation and the Hidden ExecutionContext GUID
After identifying the proxy mechanism, the next task was to determine which module sat behind it. The Windows registry provided the answer. Direct enumeration of the parent key was restricted, which is a common ACL pattern: a key may deny enumeration while still allowing access to a child key if the caller knows the full path. This is not a bug by itself, but it is a useful reconnaissance pattern for security researchers. Once the full subkey path was queried directly, the relevant GUID was readable.
The GUID {9C0B898D-6275-48EC-81B4-E5EDBE44B535} mapped to the ExecutionContext module. With that knowledge, the attacker can open \\.\kloader\{9C0B898D-6275-48EC-81B4-E5EDBE44B535} from a low-privileged context. The open succeeds, and subsequent IOCTLs can be delivered. This confirms the bypass: the device is not directly openable under the intended ACL, but it is reachable through the KLoader proxy. The registry evidence is important because it ties the abstract proxy path to a concrete Windows component, making the vulnerability reproducible and auditable.
Extended KLoader Dispatch Fragment and IOCTL Routing
The next decompiler fragment is a longer extraction from the same handler area. It preserves the dispatch behavior that routes specific IOCTLs into ExecutionContext-related functions. Because the fragment was extracted from reverse-engineering output, it includes duplicated opening lines and incomplete surrounding structure. It should be read as a logic map, not as source code that would compile directly. The important portion is the switch statement, which shows how certain IOCTL codes are handled after the proxy has accepted the request.
__int64 __fastcall ndisKLoaderIrpCreateHandler(struct _IRP *a1, __int64 a2){ __int64 v2; // rdi _DWORD *PoolWithTag; // rax _DWORD *v7; // rbx struct _UNICODE_STRING *v8; // r12 unsigned int v9; // edi __int64 *v10; // rdi unsigned int Irp; // esi __int64 v12; // rcx _IO_STACK_LOCATION *CurrentStackLocation; // rax __int64 v14; // [rsp+20h] [rbp-60h] BYREF void (__fastcall *v15)(struct KLOADER_MODULE_REFERENCE__ *); // [rsp+28h] [rbp-58h] BYREF struct _UNICODE_STRING v16; // [rsp+30h] [rbp-50h] BYREF struct _GUID v17; // [rsp+40h] [rbp-40h] BYREF __int64 v18; // [rsp+50h] [rbp-30h] BYREF struct _GUID v19; // [rsp+58h] [rbp-28h] __int64 v20; // [rsp+68h] [rbp-18h] v2 = *(_QWORD *)(a2 + 48); if ( !v2 ) return 3221225659LL; PoolWithTag = ExAllocatePoolWithTag(NonPagedPoolNx, 0x28uLL, 0x62694C4Eu); v7 = PoolWithTag; if ( !PoolWithTag ) return 3221225626LL; PoolWithTag[1] = 0; v8 = (struct _UNICODE_STRING *)(v2 + 88); *((_QWORD *)PoolWithTag + 2) = 0LL; *((_QWORD *)PoolWithTag + 3) = 0LL; *((_QWORD *)PoolWithTag + 4) = 0LL; *PoolWithTag = 0xAEACEFE; *((_QWORD *)PoolWithTag + 1) = 0LL; v17 = 0LL; v16 = 0LL; v9 = ParseModuleID( (const struct _UNICODE_STRING *)(v2 + 88), &v17, &v16 ); // 解析 \\.\kloader\{GUID} if ( v9 ) {LABEL_5: ProxyFileObject::~scalar deleting destructor((__int64 *)v7); return v9; } v20 = 0LL; else { switch ( a5 ) { case 0x22AC44u: v7 = IoctlExecutionContextRegisterPoll((__int64)a2); goto LABEL_24; case 0x124804u: case 0x12480Cu: case 0x128808u: case 0x128810u: v11 = 0LL; *(_QWORD *)v13 = 0LL; v8 = (*(__int64 (__fastcall **)(struct _DEVOBJ_EXTENSION *, struct WDFREQUEST__ *, _QWORD, void **, unsigned int *))(*(_QWORD *)&WPP_MAIN_CB.SectorSize + 2152LL))( WPP_MAIN_CB.DeviceObjectExtension, a2, 0LL, &v11, v13); if ( !v8 ) { v10 = 0LL; *(_QWORD *)v12 = 0LL; v8 = (*(__int64 (__fastcall **)(struct _DEVOBJ_EXTENSION *, struct WDFREQUEST__ *, _QWORD, void **, unsigned int *))(*(_QWORD *)&WPP_MAIN_CB.SectorSize + 2160LL))( WPP_MAIN_CB.DeviceObjectExtension, a2, 0LL, &v10, v12); if ( !v8 ) { a5 = 0; v8 = KnobNamespace::Ioctl( v9, v5, v13[0], v12[0], v11, v10, &a5 ); (*(void (__fastcall **)(struct _DEVOBJ_EXTENSION *, struct WDFREQUEST__ *, _QWORD))(*(_QWORD *)&WPP_MAIN_CB.SectorSize + 2200LL))( WPP_MAIN_CB.DeviceObjectExtension, a2, a5); } } } }LABEL_25: if ( v8 == 259 ) return; goto LABEL_26;case 0x226C5Cu: v7 = IoctlExecutionContextRegisterUserThreadMonitor((__int64)a2);LABEL_24: v8 = v7;LABEL_25: if ( v8 == 259 ) return; goto LABEL_26;}v8 = -1073741637;LABEL_26:(*(void (__fastcall **)(struct _DEVOBJ_EXTENSION *, struct WDFREQUEST__, _QWORD))(*(_QWORD *)&WPP_MAIN_CB.SectorSize + 2104LL))( WPP_MAIN_CB.DeviceObjectExtension, a2, v8);}
This fragment exposes several important IOCTL destinations. The case 0x22AC44 calls IoctlExecutionContextRegisterPoll, indicating that polling registration is part of the exposed surface. The cases around 0x124804, 0x12480C, 0x128808, and 0x128810 move through WDF-style request helpers and eventually reach KnobNamespace::Ioctl, which suggests configuration or tuning knobs exposed through a named namespace. The case 0x226C5C calls IoctlExecutionContextRegisterUserThreadMonitor, which later becomes crucial for waking the worker thread during PoC construction. The final completion path calls through a function pointer associated with the device extension, completing the WDF request with a status value.
The security implication is that the proxy is not merely exposing a benign configuration surface. It is exposing operational IOCTLs that affect execution-context state, thread monitoring, polling, and queue behavior. When a proxy exposes such a surface without re-checking the caller against the target device’s security policy, the result can be a direct path into privileged kernel operations.
ExecutionContext.sys as a WDF Driver and the IOCTL Audit Strategy
ExecutionContext.sys is a WDF-based kernel driver. In practical terms, this means its I/O control path is organized around framework request objects rather than raw IRP dispatch code alone. The entry point of interest is the device-control handler, conceptually equivalent to EvtIoDeviceControl in KMDF terminology. The driver’s dispatch logic does not appear to handle a huge number of IOCTLs, which is helpful for auditing. The researcher can enumerate the IOCTL surface and prioritize handlers that create kernel objects, register callbacks, or enqueue work. Pure configuration reads are lower priority unless they leak kernel state.
The high-priority IOCTLs identified during analysis include Initialize at 0x22EC40, QueueTask at 0x22AC54, RegisterNotification at 0x22AC4C, RegisterPoll at 0x22AC44, and RegisterUserThreadMonitor at 0x226C5C. The Initialize handler establishes the execution context and validates user-supplied buffers and handles. The QueueTask handler creates and queues the vulnerable task object. The notification and thread-monitor handlers provide alternative paths that influence callback execution and thread cleanup. This combination creates a complete attack chain: initialize the context, inject a malicious task, then force the worker thread to consume it.
QueueTask: The Core Injection Primitive

The IoctlExecutionContextQueueTask handler is the primary vulnerability primitive. It takes user input and writes two QWORDs into a newly allocated kernel task object. The decompiled fragment below shows the allocation and the direct copy of user-controlled values into the task object. The first QWORD becomes the argument field at offset 0x00, and the second QWORD becomes the callback field at offset 0x08. There is no validation of the callback’s provenance, no allow-list check, no module-range check beyond what happens later in the consumer thread, and no object-handle abstraction that would prevent arbitrary pointer submission.
Pool2 = (_QWORD *)ExAllocatePool2(64LL, 64LL, 0x6B546345LL);v8 = Pool2;if ( Pool2 ){ v9 = *v12; // 输入缓冲区QWORD[0] v10 = v12[1]; // 输入缓冲区QWORD[1] Pool2[3] = 0LL; Pool2[5] = 0LL; Pool2[6] = 0LL; Pool2[7] = 0LL; Pool2[3] = Pool2 + 2; Pool2[2] = Pool2 + 2; v11 = *(_QWORD *)v3; *v8 = v9; // task + 0x00 v8[1] = v10; // task + 0x08 v8[4] = v8; (*(void (__fastcall **)(volatile signed __int32 *))(v11 + 48))(v3); if ( _InterlockedExchangeAdd(v3 + 88, 0xFFFFFFFF) == 1 ) { KernelModeExecutionContext::~KernelModeExecutionContext( (KernelModeExecutionContext *)v3 ); ExFreePoolWithTag((PVOID)v3, 0x70736944u); } return 0LL;}
The allocation call ExAllocatePool2(64LL, 64LL, 0x6B546345LL) is notable because it uses the modern pool allocation interface. In the decompiler representation, the first numeric argument corresponds to allocation flags, the second is the allocation size, and the third is the pool tag. The 64-byte size gives the task object enough room for the argument, callback, list linkage, and bookkeeping fields. The tag is useful for memory forensics and can help identify task allocations in a kernel dump.
The direct assignments *v8 = v9 and v8[1] = v10 are the heart of the issue. The first assignment stores the attacker-controlled argument at the beginning of the task object. The second stores the attacker-controlled callback immediately after it. The code then calls a function pointer at v11 + 48, which appears to enqueue or submit the task into the execution context. After that, the reference count on the context object is decremented with InterlockedExchangeAdd. If the previous reference count was one, the context object is destroyed and freed. This reference-counting detail is not the vulnerability itself, but it shows that the task is being handed off to a consumer object with its own lifetime management.
KernelThread Consumer: The Fatal MmSystemRangeStart Check
The consumer side is implemented in KernelModeExecutionContext::KernelThread. This is a kernel worker thread created for the execution context. It waits for work, retrieves tasks from the queue, and executes them. The vulnerability is in the validation step immediately before execution. The consumer checks whether the callback pointer is below MmSystemRangeStart. If it is below, the task is treated as invalid or is otherwise handled. If it is equal to or above MmSystemRangeStart, the consumer calls the function pointer with the stored argument.
if ( v6 == 1 ){ while ( 1 ) { NextTask = KernelModeExecutionContext::GetNextTask(this); // 取队列任务 if ( !NextTask ) break; if ( *((_QWORD *)NextTask + 1) < (unsigned __int64)MmSystemRangeStart ) // 判断地址是否再内核段 { if ( *((_QWORD *)NextTask + 4) ) ExFreePoolWithTag(NextTask, 0); } else { (*((void (__fastcall **)(_QWORD))NextTask + 1))( *(_QWORD *)NextTask ); } }}
This first consumer fragment makes the flaw explicit. The expression *((_QWORD *)NextTask + 1) reads the callback pointer from offset 0x08 of the task object. The comparison against MmSystemRangeStart asks only whether the value is in the kernel portion of the address space. If the value passes that comparison, the code performs an indirect call through the task object. The argument passed to the callback is *(_QWORD *)NextTask, which is the value at offset 0x00. That value was supplied by the user-mode caller.
The security mistake is treating an address-range check as a trust check. MmSystemRangeStart tells the kernel where kernel-mode virtual addresses begin. It does not tell the kernel whether the address is mapped, whether it belongs to a legitimate module, whether it is executable, whether it was registered by a trusted component, or whether the current user should be allowed to cause execution at that address. A value can be numerically inside the kernel range and still be completely attacker-chosen. That is enough to hijack control flow.
Second KernelThread Variant and the Same Underlying Flaw
A second decompiled variant of the consumer loop shows the same logic with slightly different error handling. In this version, if the callback pointer is below MmSystemRangeStart, the code records an error state and stores task fields into a context structure before freeing the task. If the pointer is within the kernel range, the indirect call proceeds. The differences are implementation details; the trust failure is identical.
if ( v6 == 1 ){ while ( 1 ) { NextTask = KernelModeExecutionContext::GetNextTask(this); v10 = NextTask; if ( !NextTask ) break; v8 = (void (__fastcall *)(_QWORD))*((_QWORD *)NextTask + 1); if ( (char *)v8 < MmSystemRangeStart ) { **((_DWORD **)this + 31) = 4; *(_QWORD *)(*((_QWORD *)this + 31) + 8LL) = *(_QWORD *)v10; *(_QWORD *)(*((_QWORD *)this + 31) + 16LL) = *((_QWORD *)v10 + 1); if ( *((_QWORD *)v10 + 4) ) ExFreePoolWithTag(v10, 0); return; } v8(*(_QWORD *)v10); } goto LABEL_16;}if ( v6 != 2 ){
In this variant, v8 receives the callback pointer from offset 0x08. The comparison (char *)v8 < MmSystemRangeStart is the same conceptual range check. If the pointer fails the check, the driver writes the value 4 into a structure reached through this + 31, then stores the argument and callback into that structure at offsets +8 and +16. This looks like diagnostic or error-reporting behavior. If the pointer passes the check, the driver calls v8(*(_QWORD *)v10). The first argument is again the task’s attacker-controlled argument field.
The existence of two variants is useful for analysis because it confirms that the flaw is not an artifact of one decompilation view. Whether the consumer frees the invalid task, logs it, or returns early, the critical branch still treats any kernel-range pointer as acceptable for execution. That is the exploit primitive.
Why MmSystemRangeStart Is Not an Authorization Mechanism
The Windows virtual address space is divided into user and kernel regions. On x64 systems, user-mode addresses occupy the lower canonical range, while kernel-mode addresses occupy the upper canonical range. MmSystemRangeStart is a kernel global that reflects this boundary. It is useful for memory-management code, but it is not a security oracle. A pointer can be inside the kernel range for many reasons: it may point to valid kernel code, valid kernel data, freed memory, reserved but unmapped space, or an address that is technically canonical but not currently valid. The range check does not distinguish among these cases.
For a secure callback model, the kernel needs stronger properties. It needs to know that the callback pointer was generated by a trusted component, that it points into an allowed module, that it has not been tampered with, and that the caller is authorized to request that operation. In modern Windows, indirect-call mitigations such as Control Flow Guard can reduce the set of valid targets, but they do not fix the root authorization problem. If an attacker can force the kernel to call a legitimate indirect-call target with attacker-controlled arguments, the exploit may still be viable. The range check alone is therefore insufficient by design.
Controlled RIP and Controlled RCX: The Calling-Convention Consequence
On Windows x64, the first integer or pointer argument is passed in RCX. The second is passed in RDX, the third in R8, and the fourth in R9, with additional arguments on the stack. In this vulnerability, the task object’s first field becomes the first argument to the callback. The task object’s second field becomes the indirect call target. When the consumer executes the call, the processor loads the target address and the argument from the attacker-controlled task object.
This means the attacker controls two crucial values at the moment of execution: the instruction pointer and the first argument. For a denial-of-service PoC, that is enough. The attacker can choose an unmapped kernel-range address and force a crash. For a more advanced exploit, the attacker can choose a valid kernel address that performs a useful operation when entered with a controlled first argument. The exact target depends on the kernel build, loaded modules, mitigation state, and the availability of an address leak. The primitive itself, however, is clear: user mode can cause kernel mode to call an attacker-influenced target with an attacker-influenced argument.
Systemic Pattern: EnterKernel and the Same Design Error
The original analysis notes that the same pattern appears in more than one place inside ExecutionContext.sys. One of those paths is associated with EnterKernel, described as a Fast I/O entry path. The same MmSystemRangeStart comparison and direct indirect call pattern were observed there. This matters because it shows that the vulnerability is not limited to the queue-task consumer. The driver appears to accept user-influenced callback pointers in multiple operational paths.
Fast I/O paths are particularly interesting because they are designed to avoid some of the overhead associated with full IRP processing. They can be reached quickly and may be used in performance-sensitive scenarios. From an attacker’s perspective, a Fast I/O path can be attractive because it may provide a lower-latency way to trigger the vulnerable logic. From a defender’s perspective, the presence of the same flawed check in multiple paths indicates that the issue is architectural. The driver’s interface appears to allow raw kernel function pointers to cross the user-kernel boundary.
SetKmNotifications: A Callback Chain Without Even the Range Check
The notification path provides an even clearer example of the architectural problem. IoctlExecutionContextRegisterNotification, associated with IOCTL 0x22AC4C, allows registration of notification nodes. Later, KernelModeExecutionContext::SetKmNotifications walks a linked list and invokes callbacks stored in those nodes. The decompiled function below shows the callback invocation directly. Unlike the queue consumer, this path does not even show the MmSystemRangeStart comparison in the extracted logic. The callback and argument values originate from user input during registration.
void __fastcall KernelModeExecutionContext::SetKmNotifications( KernelModeExecutionContext *this, __int64 a2){ char *v2; // rdi char v3; // si char *i; // rbx v2 = (char *)this + 88; v3 = a2; for ( i = (char *)*((_QWORD *)this + 11); i != v2; i = *(char **)i ) { LOBYTE(a2) = v3; (*((void (__fastcall **)(_QWORD, __int64))i - 1))( *((_QWORD *)i - 2), a2 ); }}
The loop walks a list beginning at the pointer stored at this + 11. The comparison against v2, which is (char *)this + 88, suggests an intrusive list head embedded in the context object. On each iteration, the pointer i is advanced by reading the next pointer from the current node. The callback is invoked through the expression (*((void (__fastcall **)(_QWORD, __int64))i - 1)). The first argument is read from i - 2, and the second argument is the notification flag passed into SetKmNotifications. The pointer arithmetic indicates that each notification node stores an argument and a callback near the list linkage.
This is a serious design issue because it treats callback registration as a raw pointer exchange. The kernel should not accept arbitrary function pointers from user mode unless the pointer is a handle to a validated object or is constrained by a strong capability model. If the driver’s protocol allows user mode to define what the kernel will later call, then every implementation of that protocol becomes a potential control-flow hijack. The notification path demonstrates that the queue-task bug is not an isolated lapse; it is one expression of a broader interface assumption.
PoC Construction: The Real Difficulty Was Thread Wakeup
Static analysis can prove that dangerous code exists, but it does not automatically produce a working crash. In this case, the difficult part was not injecting the malicious task. The difficult part was making the kernel worker thread consume the task. The worker thread waits on synchronization objects, and simply placing a task into the queue does not necessarily wake it. The researcher therefore had to identify a side effect that would signal the worker thread’s wait condition.
The initialization step is the first practical hurdle. The Initialize IOCTL, 0x22EC40, has strict preconditions. The input buffer is 48 bytes in the tested behavior. Fields at offsets +0x08 and +0x10 are user-buffer pointers that the driver maps through MDLs. These must be valid user-mode addresses because the kernel will describe and lock the pages for the intended operation. The field at +0x18 is a thread handle, and the fields at +0x20 and +0x28 are event handles. These handles are validated through object reference routines such as ObReferenceObjectByHandle, so the PoC must supply real handles to real objects. If this initialization fails, the execution context is not established and the later queue injection cannot be consumed in the intended way.
MDL Mapping and Handle Validation in the Initialize Path
The use of MDLs in the initialization path is technically significant. A Memory Descriptor List describes a set of physical pages corresponding to a virtual address range. When a kernel driver needs to access user buffers safely, it can build an MDL, probe the pages, and lock them so that they remain resident for the duration of the operation. This prevents the buffer from being paged out or remapped in a way that would create a race between user mode and kernel mode. In this vulnerability, the MDL requirement is not the flaw, but it is part of the exploit-development surface because the PoC must pass the initialization checks before reaching the vulnerable queue consumer.
The handle checks are equally important. ObReferenceObjectByHandle is a kernel routine that translates a handle into an object pointer while enforcing access rights and previous mode. If the caller is in user mode, the kernel does not simply trust the handle value; it verifies that the handle is valid for the requested access. The Initialize handler’s use of thread and event handles therefore forces the PoC to create legitimate kernel objects. This is not a mitigation against the final vulnerability, but it does raise the precision required for a reliable PoC.
RegisterUserThreadMonitor and the Thread-Exit Wakeup Route
The wakeup breakthrough came from IoctlExecutionContextRegisterUserThreadMonitor, IOCTL 0x226C5C. This handler registers a user-thread monitor and interacts with NDIS user-thread exit callback registration. The original analysis identifies NdisRegisterUserThreadExitCallback as part of this path. When a registered thread exits, the callback path enters ExecutionContextUserThreadExit and then FreeUserModeResources. The cleanup path signals the context event, which wakes the worker thread. Once awake, the worker thread processes the queued task, including the malicious task injected earlier.
This produces a stable trigger chain. First, the attacker opens the KLoader path to reach the protected ExecutionContext module. Second, the attacker sends the Initialize IOCTL with valid buffers and handles to create the execution context. Third, the attacker sends QueueTask with the malicious argument and callback values. Fourth, the attacker uses a short-lived thread to register the user-thread monitor and then exits that thread. The thread-exit cleanup signals the worker thread. The worker consumes the malicious task and executes the controlled callback. This is the route referred to as route B in the original analysis, and it closes the gap between static vulnerability and observable crash.
PoC Values, Blue Screen, and Dump Evidence
The PoC deliberately chooses marker values that are easy to identify in a crash dump. The callback is set to 0xFFFFF80041414141. This value is within the kernel address range, so it passes the MmSystemRangeStart check, but it is not a valid mapped target. The repeated 41 byte pattern is a conventional debugging marker and is easy to recognize in register state or stack data. The argument is set to 0x1122334455667788, another distinctive value. When the worker thread executes the task, the system crashes.
The resulting dump provides the evidentiary closure that static analysis cannot provide by itself. The instruction pointer shows the controlled callback marker. The first argument register shows the controlled argument marker. The stack and module context show that execution occurred inside the ExecutionContext consumer path after the task was queued from user mode. This proves that the low-privileged IOCTL input reached the dangerous indirect call. It also proves that the MmSystemRangeStart check did not prevent exploitation; it merely filtered out user-range values while accepting any kernel-range value.
Exploitation Beyond Denial of Service: The KASLR Requirement
A controlled crash is a proof of control, but privilege escalation requires controlled execution at a useful target. On modern Windows builds, kernel address space layout randomization makes this difficult without an information leak. The attacker needs to know where the kernel image, loaded modules, or relevant data structures reside. Without that knowledge, choosing a valid callback target is unreliable. The original researcher states that the vulnerability is suitable for combination with a kernel base leak to bypass KASLR and achieve local privilege escalation. This is an accurate description of the exploit architecture.
In the tested 24H2 and 25H2 environments, the researcher notes that local users cannot successfully call certain NtQuerySystemInformation behavior to obtain the needed kernel base information. This changes the exploit landscape. Older techniques that directly queried module information from an unprivileged context are no longer sufficient in that environment. A complete exploit therefore needs another source of kernel address disclosure. The exact leak is not provided in the original write-up, and this article does not invent one. The important technical point is that CVE-2026-62737 provides the execution primitive, while KASLR defeat remains a separate prerequisite.
Modern Mitigations and the Need for a Valid Target
Modern Windows kernels include several mitigations that affect how an attacker can use a controlled indirect call. Kernel Control Flow Guard and related indirect-call validation mechanisms restrict the set of legitimate indirect-call targets. If the attacker chooses an arbitrary address that is not recognized as a valid target, the system may fail before the attacker’s intended payload runs. This does not eliminate the vulnerability, but it changes exploit development. The attacker may need to find a legitimate kernel target that becomes dangerous when called with attacker-controlled arguments.
This is why the argument-control aspect of the bug is important. Even if the callback target must be a valid indirect-call site, the attacker still controls the first argument. If a valid target performs a useful operation based on that argument, the primitive can remain exploitable. The attacker may seek a target that enables an arbitrary write, manipulates a kernel object, or otherwise helps transition to token manipulation. The final privilege-escalation step usually involves modifying the security token associated with the attacker’s process, but reaching that step requires careful handling of kernel structures and build-specific offsets.
Token Manipulation as the Final Privilege-Escalation Step
In Windows local privilege escalation, a common objective is to replace or modify the security token of the current process so that it inherits the privileges of a higher-privileged process. The kernel represents processes and tokens through internal structures, and the token associated with a process is referenced from the process’s kernel object. If an attacker can write to the appropriate field, the process can gain elevated rights. This is conceptually simple but operationally difficult because the attacker must locate the relevant kernel object, understand the build-specific layout, and perform the write without corrupting kernel state.
CVE-2026-62737 does not directly provide a token-write primitive by itself. It provides controlled execution and a controlled first argument. To convert that into a token write, the attacker needs enough kernel knowledge to select a useful path forward. That is why the KASLR leak is essential. Without knowing where kernel objects and code reside, the attacker cannot reliably perform the final token modification. The vulnerability should therefore be evaluated as a high-severity primitive within a larger exploit chain, not as a one-shot magic escalation button.
Why This Vulnerability Is Harder Than a Typical Memory Corruption Bug
Memory corruption bugs often have a local nature: a buffer is too small, a reference count is wrong, or an object is freed too early. Those bugs are serious, but they usually fit into a familiar analysis pattern. CVE-2026-62737 is different because it involves the semantics of a cross-component interface. The vulnerability emerges from the interaction between NDIS, KLoader, device ACLs, registry registration, WDF request handling, and the ExecutionContext task queue. No single line of code looks like a classic overflow, yet the system as a whole permits a low-privileged user to inject a kernel function pointer.
This kind of bug is difficult to find with pure fuzzing because the interface may not crash under random input. The task queue accepts the two QWORDs without memory corruption. The callback check passes for kernel-range values. The worker thread executes the callback exactly as designed. The failure is that the design should never have allowed untrusted user mode to supply that pointer in the first place. Finding this requires a researcher to ask what the interface means, not merely whether it crashes.
The Role of AI in This Research Workflow
The original author frames the research in the context of AI-assisted vulnerability discovery. That framing is useful, but it needs precision. AI can accelerate the tedious parts of kernel research: mapping call graphs, identifying where user buffers flow, locating IOCTL dispatchers, and summarizing decompiled logic. It can help a researcher move through a large binary faster than manual reading alone. However, the decisive choices in this case were human choices. The researcher selected NDIS as a target, recognized the KLoader proxy as an authorization boundary, identified the registry GUID, and understood that the MmSystemRangeStart check was not a trust check.
High-capability models may reduce the need for rigid, step-by-step prompting, but they do not replace the need for a threat model. The researcher still needs to know what a proxy is, what an ACL protects, what an IOCTL surface means, and why a kernel worker thread executing a user-supplied pointer is dangerous. The AI-assisted workflow is best understood as force multiplication. It helps the researcher audit code paths once the right architectural question has been asked. It does not independently decide that the KLoader path is the right place to look.
Defensive Lessons for Kernel Driver Design
The most important defensive lesson is that proxy components must re-enforce authorization. If a broker forwards requests to a protected device, it cannot assume that the original open operation is sufficient. The broker must either perform an access check equivalent to the target device’s ACL or use a model where the caller obtains a handle to the target through the Object Manager and the Object Manager enforces the security descriptor. Path-based routing that bypasses direct object creation is especially dangerous if the target device has restrictive permissions.
A second lesson is that kernel interfaces should not accept raw function pointers from user mode unless there is a very strong reason and a robust validation model. A safer design uses registration identifiers, object handles, or capability tokens that the kernel can translate into internal function pointers. If raw pointers are unavoidable, the driver should validate that they belong to trusted modules, are properly aligned, are within an allowed address range, and are subject to control-flow integrity protections. Even then, pointer provenance remains difficult to prove.
A third lesson is that range checks are not security checks. MmSystemRangeStart answers a memory-management question, not an authorization question. Kernel code should distinguish between “this value is in kernel address space” and “this caller is allowed to make the kernel execute this target.” That distinction is central to CVE-2026-62737. The driver answered the first question but never answered the second.
Detection and Forensic Considerations
Detection for this vulnerability can focus on several artifacts. First, security tooling can monitor access to \\.\kloader paths, especially when unexpected processes open GUID-based subpaths. The specific GUID {9C0B898D-6275-48EC-81B4-E5EDBE44B535} is a high-value indicator in the context of this vulnerability, although defenders should not assume that only this GUID matters. Second, endpoint detection can look for unusual IOCTL sequences against NDIS-related devices, particularly initialization, queue-task, and thread-monitor registration behavior from non-system processes.
Forensic analysis of a crash dump can also identify the issue. The presence of ExecutionContext.sys in the stack, combined with a faulting instruction pointer that matches an artificial kernel-range marker, is highly suspicious. The argument register containing a distinctive user-chosen value further strengthens the conclusion. Pool tags associated with task allocation and proxy objects can help trace the object lifecycle. In a live investigation, kernel object inspection and driver callback registration auditing may reveal abnormal callback entries if a persistent or semi-persistent variant were attempted.
Why the Published PoC Crashes Instead of Escalating
The published PoC intentionally crashes the system because it demonstrates control without requiring a full exploit chain. The callback value is chosen to pass the range check but fail execution because it is unmapped. This creates a clean denial-of-service condition and proves that the attacker-controlled value reached the indirect call. It does not attempt to resolve KASLR, locate a useful target, satisfy control-flow integrity expectations, or manipulate process tokens. Those steps require additional primitives and build-specific engineering.
This distinction is important for accurate severity assessment. A crash is already severe because it affects system availability. The underlying primitive is more severe because it can potentially lead to privilege escalation. However, the complete escalation path depends on the attacker’s ability to combine the vulnerability with other information leaks or exploitation techniques. The vulnerability should therefore be described precisely: it is a local kernel control-flow hijack through an ACL bypass and untrusted callback injection, with privilege escalation possible when paired with KASLR disclosure.
Architectural Context: High-Performance I/O and the Cost of Speed
ExecutionContext.sys exists because modern Windows networking and I/O paths need high performance. The kernel must schedule work efficiently, reduce latency, and support fast data paths. These goals encourage designs where user-mode components can submit work directly and kernel workers can consume it with minimal overhead. The pressure to reduce context switches and avoid heavy validation can create security blind spots, especially when the interface crosses privilege boundaries.
The KLoader proxy likely exists for similar reasons: it provides a convenient registration and routing mechanism for modules that participate in high-performance I/O. However, performance-oriented convenience can become a security problem when it allows a low-privileged caller to reach a module that was intended to be protected. The CVE-2026-62737 case shows that performance features must be reviewed with the same rigor as any other kernel attack surface. If a feature creates a new route to an old device, the old device’s security assumptions may no longer hold.
Comparing This Bug Class to Classic IOCTL Vulnerabilities
Many IOCTL vulnerabilities involve improper buffer length validation, incorrect access-buffer methods, or failure to probe user addresses. Those bugs often result in memory corruption. CVE-2026-62737 is different because the IOCTL handler appears to process the input as intended. The input size is sufficient, the buffer is read, the object is allocated, and the task is queued. The problem is semantic: the handler accepts values that should never be accepted from an untrusted caller.
This semantic nature makes the bug class more subtle. A fuzzer may generate many inputs and still not crash unless the callback happens to be invalid enough to fault. A static analyzer may see the range check and assume validation exists. A manual reviewer must understand that the range check is inadequate. The vulnerability is therefore a good example of why kernel security review requires architectural reasoning in addition to pattern matching.
The Importance of the Registry GUID in Reproducibility
The GUID is not a minor detail. It is the bridge between the abstract KLoader proxy and the concrete ExecutionContext module. Without the GUID, an attacker knows only that a proxy exists. With the GUID, the attacker can construct the exact device path and reproduce the low-privileged open. The registry location and ACL behavior also matter because they explain how the GUID was discovered despite enumeration restrictions.
For defenders, the GUID is a valuable detection and audit anchor. If a system contains KLoader registrations that expose sensitive modules, those registrations should be reviewed. If a GUID corresponds to a driver that should not be reachable by ordinary users, the exposure path should be examined. The CVE-2026-62737 case suggests that registry-based module registration can become part of the attack surface even when the registered device appears protected.
Why the Vulnerability Remains High Severity Even With Mitigations
Modern mitigations complicate exploitation, but they do not remove the core problem. A low-privileged user can still force the kernel to execute a controlled indirect call path. Even if full privilege escalation requires an additional leak, the primitive can be combined with other bugs discovered later. Kernel vulnerabilities often become more dangerous over time as researchers find new information leaks or new gadget targets. The existence of a strong controlled-call primitive is therefore a serious issue regardless of whether a complete public exploit is available.
The vulnerability also demonstrates a design pattern that may exist elsewhere. If other Windows components use proxy devices, registration multiplexers, or high-performance schedulers that accept callback-like values, they may contain similar flaws. The value of CVE-2026-62737 is not only that it affects a specific driver; it also provides a template for auditing similar interfaces. That is why the technical details of the KLoader path, the GUID registration, and the task-queue consumer are so important.
Practical Audit Methodology Derived From This Case
The research process in this case can be turned into a practical audit methodology. Start by identifying user-reachable kernel interfaces. Look for proxies, multiplexers, and registration systems. Ask whether a proxy re-checks authorization before forwarding. Identify registry keys, GUIDs, symbolic links, and device namespaces that map to kernel modules. Enumerate IOCTLs and prioritize those that register callbacks, enqueue work, or create kernel objects. Trace user-controlled data from IOCTL input to any indirect call, function pointer, or callback invocation.
When a validation check is found, ask what it actually proves. A range check proves address-space membership. A handle check proves object-handle validity for a specific access. A module-range check proves that an address is inside a module, but not necessarily that the caller may use it. A control-flow guard check proves that a target is recognized by the CFG bitmap, but not that the call is semantically authorized. The auditor must separate memory-safety validation from authorization validation. CVE-2026-62737 is a case where the former appeared to exist while the latter was absent.
Editorial Assessment
CVE-2026-62737 is a high-quality kernel vulnerability because it exposes a subtle failure in the Windows security boundary. The bug is not dramatic in the way of a heap spray or a complex race condition. It is quiet: a proxy forwards a request, a queue accepts two values, a worker thread performs a range check, and control flow is handed to an attacker-influenced pointer. The result is a clean demonstration that architectural assumptions can be more dangerous than individual coding mistakes.
The research also illustrates the continuing value of human-led kernel security work. AI tools can help map code and accelerate review, but the critical insight came from understanding how device ACLs, proxy routing, and callback trust interact. For security engineers, driver developers, and defenders, the lesson is direct: when a kernel component acts on behalf of another component, it must inherit the security burden of that component. When a kernel interface accepts execution-related values from user mode, it must treat those values as hostile unless proven otherwise. CVE-2026-62737 is a strong reminder that in kernel design, convenience and performance must never silently erase authorization.









