win32kfull.sys TrySmuggleHidData interlisted back-pointer UAF
Each GUI thread carries a THREADINFO block with per-thread input
state, including the raw-input HID-data queue. An outstanding HIDDATA
request records its owning thread at HIDDATA->OwnerThread /* +0x10 */,
and that owner’s THREADINFO in turn holds a “current node” back-pointer
at pQueuedHidData /* +0x478 */ plus the per-thread SList head at
HidDataSList /* +0x480 */. When a thread exits with an outstanding HID
request, TrySmuggleHidData migrates the node onto a destination
thread’s SList so the request is not lost.
The vulnerable build relocates the node and then clears pQueuedHidData
on the requesting thread (arg1, the caller) instead of on the
owning thread (node->OwnerThread). The owner’s back-pointer is the
one that actually names this node, so it is the one that has to go;
clearing the caller’s field is a no-op against the dangling reference,
and the recorded owner is left pointing at a node that has been pushed
onto another thread’s SList.
/* TrySmuggleHidData — vulnerable build (smuggle branch) */
OwningThread = node->OwnerThread; /* +0x10 */
if (DstThread != OwningThread) {
if ((node->Flags /* +0x30 */ & 1) == 0 && RequestThread == OwningThread)
FlushPostedRawInput(RequestThread);
node = UnlinkHidData(RequestThread, node, NULL);
if (node == NULL || (node->Flags /* +0x30 */ & 1) == 0)
MicrosoftTelemetryAssertTriggeredArgsKM("IXPTellMeIf", 0x20000, 0xbc7);
HMChangeOwnerThread(node, DstThread);
node->Flags = (node->Flags & ~1) | 2; /* +0x30 */
ExpInterlockedPushEntrySList(&DstThread->HidDataSList, /* +0x480 */
&node->SListEntry); /* +0x20 */
if (RequestThread->pQueuedHidData /* +0x478 */ == node)
RequestThread->pQueuedHidData = NULL;
/* OwningThread->pQueuedHidData left dangling — the UAF */
}
HMChangeOwnerThread rewrites the node’s OwnerThread to the
destination and the interlocked push links it onto the destination’s
HidDataSList, but neither action touches the back-pointer that the
previous owner still holds at pQueuedHidData /* +0x478 */. That
previous owner can later walk its pQueuedHidData, follow the pointer
into a node that now lives on another thread’s SList, and either race
the destination’s pop or treat the moved node through the wrong state.
The patch
The fix replaces the caller’s clear with a clear of the owning
thread’s pQueuedHidData — the thread the node actually records as its
owner:
/* TrySmuggleHidData — patched (smuggle branch) */
ExpInterlockedPushEntrySList(&DstThread->HidDataSList, /* +0x480 */
&node->SListEntry); /* +0x20 */
if (OwningThread->pQueuedHidData /* +0x478 */ == node)
OwningThread->pQueuedHidData = NULL;
The relocation now invalidates the back-pointer that genuinely names the
node, so no owner is left following a stale reference into another
thread’s SList. The thread that has to be updated is the one the node
itself names at OwnerThread; clearing a back-pointer on whichever
thread happened to initiate the call leaves the recorded owner pointing
at a node it no longer owns.
Attack path
sequenceDiagram
participant U as OwningThread (user mode)
participant K as TrySmuggleHidData
participant H as HIDDATA node
K->>H: push onto DstThread HidDataSList at plus 0x480, set node OwnerThread to DstThread
K->>H: clear pQueuedHidData on RequestThread only, leave OwningThread stale at plus 0x478
U->>H: OwningThread later dereferences its pQueuedHidData back-pointer
Note over H: pointer names a node now on another thread SList, UAF to EoP
Raw-input HID-data attach across thread exit is reachable from any
standard user session. The broader shape is that an interlocked
operation on a node is not an interlocked operation on its owners:
ExpInterlockedPushEntrySList relocates the node atomically, but every
back-pointer elsewhere that names the node is unchanged by the push, and
each of those pointers is a stale reference until something else clears
it. The owner recorded inside the object is the pointer that has to be
cleared; acting on the caller’s field instead is the assumption that
leaves the real owner following the node into its new location.