Direct Circular Buffer Injection in mouclass.sys
While investigating input injection methods, I found that writing directly to mouclass’s internal circular buffer produces mouse movement that doesn’t trigger any of the usual detection vectors. The input never passes through MouseClassServiceCallback, never traverses the HID stack, and appears indistinguishable from hardware-generated data when read by the system.
Background
The standard approaches to synthetic mouse input all share a common trait: they use interfaces that anti-cheats actively monitor. User-mode calls like SendInput go through well-documented syscall paths. Filter drivers and virtual HID devices leave traces in the device stack. MouseClassServiceCallback—the function port drivers call to deliver input—is frequently hooked or traced to validate calling contexts.
The mouse class driver aggregates input from all pointing devices before it reaches user-mode. Internally, it maintains a circular buffer of MOUSE_INPUT_DATA structures. Port drivers deliver input via callback, mouclass queues it, and the Raw Input Thread in csrss consumes it through read IRPs. The buffer itself is just a data structure sitting in the device extension—nothing special about it from a memory perspective.
Inside mouclass
The device extension for \Device\PointerClass0 contains the relevant fields at these offsets:
+0x54 InputCount - number of queued entries
+0x68 DataQueueBase - base address of the circular buffer
+0x70 WritePointer - current write position
+0x78 ReadPointer - current read position
+0x88 QueueSize - buffer size in bytes
+0x90 SpinLock - synchronization primitive
+0x98 PendingIrpQueue - list head for waiting read IRPs
When MouseClassServiceCallback is invoked, it acquires the spinlock, writes a MOUSE_INPUT_DATA entry at the write pointer, advances the pointer (wrapping at buffer end), and increments the input count. If read IRPs are pending, it dequeues one and completes it directly with the data instead of buffering.
There’s nothing preventing us from doing the same thing ourselves.
Implementation
The driver obtains a reference to the mouclass device via IoGetDeviceObjectPointer, then accesses the device extension directly. Injection acquires the spinlock, writes to the buffer, and updates the bookkeeping fields:
NTSTATUS injectMouseInput(LONG deltaX, LONG deltaY, USHORT buttonFlags, USHORT buttonData) {
if (!g_ctx.initialized || !g_ctx.mouseDeviceExtension)
return STATUS_DEVICE_NOT_READY;
PUCHAR devExt = (PUCHAR)g_ctx.mouseDeviceExtension;
PKSPIN_LOCK spinLock = (PKSPIN_LOCK)(devExt + MOUCLASS_DEVEXT_SPINLOCK);
KIRQL oldIrql;
MOUSE_INPUT_DATA inputData = { 0 };
inputData.UnitId = 0;
inputData.Flags = MOUSE_MOVE_RELATIVE;
inputData.ButtonFlags = buttonFlags;
inputData.ButtonData = buttonData;
inputData.LastX = deltaX;
inputData.LastY = deltaY;
KeAcquireSpinLock(spinLock, &oldIrql);
PLIST_ENTRY pendingQueue = (PLIST_ENTRY)(devExt + MOUCLASS_DEVEXT_PENDING_IRP);
if (!IsListEmpty(pendingQueue)) {
// pending read IRP exists—complete it directly
PLIST_ENTRY entry = RemoveHeadList(pendingQueue);
PIRP pendingIrp = CONTAINING_RECORD(entry, IRP, Tail.Overlay.ListEntry);
PDRIVER_CANCEL oldCancelRoutine = IoSetCancelRoutine(pendingIrp, NULL);
if (oldCancelRoutine == NULL) {
// IRP is being cancelled, put it back
InsertHeadList(pendingQueue, entry);
} else {
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(pendingIrp);
PVOID irpBuffer = pendingIrp->MdlAddress
? MmGetSystemAddressForMdlSafe(pendingIrp->MdlAddress, NormalPagePriority)
: pendingIrp->AssociatedIrp.SystemBuffer;
if (irpBuffer && irpStack->Parameters.Read.Length >= sizeof(MOUSE_INPUT_DATA)) {
RtlCopyMemory(irpBuffer, &inputData, sizeof(MOUSE_INPUT_DATA));
KeReleaseSpinLock(spinLock, oldIrql);
pendingIrp->IoStatus.Status = STATUS_SUCCESS;
pendingIrp->IoStatus.Information = sizeof(MOUSE_INPUT_DATA);
IoCompleteRequest(pendingIrp, IO_MOUSE_INCREMENT);
return STATUS_SUCCESS;
}
}
}
// no pending IRP or completion failed—queue in circular buffer
PVOID queueBase = *(PVOID*)(devExt + MOUCLASS_DEVEXT_DATA_QUEUE_BASE);
PVOID writePtr = *(PVOID*)(devExt + MOUCLASS_DEVEXT_WRITE_POINTER);
ULONG queueSize = *(PULONG)(devExt + MOUCLASS_DEVEXT_QUEUE_SIZE);
PULONG inputCount = (PULONG)(devExt + MOUCLASS_DEVEXT_INPUT_COUNT);
ULONG maxEntries = queueSize / sizeof(MOUSE_INPUT_DATA);
if (*inputCount >= maxEntries - 1) {
KeReleaseSpinLock(spinLock, oldIrql);
return STATUS_INSUFFICIENT_RESOURCES;
}
*(PMOUSE_INPUT_DATA)writePtr = inputData;
PVOID newWritePtr = (PUCHAR)writePtr + sizeof(MOUSE_INPUT_DATA);
if (newWritePtr >= (PUCHAR)queueBase + queueSize)
newWritePtr = queueBase;
*(PVOID*)(devExt + MOUCLASS_DEVEXT_WRITE_POINTER) = newWritePtr;
(*inputCount)++;
KeReleaseSpinLock(spinLock, oldIrql);
return STATUS_SUCCESS;
}
The pending IRP path is important. When an application reads from the mouse device and no data is queued, mouclass holds the IRP until input arrives. By completing these IRPs ourselves, injected input gets delivered with the same latency characteristics as hardware input—no buffering delay that might look anomalous.
Why it works
The injected data never passes through MouseClassServiceCallback. Anti-cheats that hook this function or walk the call stack to validate the calling driver see nothing. The HID stack is never involved—no mouhid, no hidusb, no filter drivers in the path. When csrss reads from the device, it just finds data in the buffer. The MOUSE_INPUT_DATA structure is identical to what hardware would produce.
Using mouclass’s own spinlock ensures the modification is atomic from the driver’s perspective. There’s no window where it might observe inconsistent state.
Detection surface
A hypervisor could monitor writes to the circular buffer region via EPT. This would require knowing the buffer’s location for each mouclass device, which is feasible but not trivial to maintain across sessions. Periodic integrity checks on device extension fields could detect modifications that don’t correlate with observed hardware activity, though timing this correctly without false positives on legitimate input would be difficult.
Behavioral analysis of input patterns—polling rate consistency, movement distributions, timing jitter—could flag synthetic input regardless of injection method. This is arguably the more robust detection approach since it doesn’t depend on catching the injection mechanism itself.
I haven’t encountered any anti-cheat that currently monitors mouclass’s internal structures specifically.
Offset stability
These offsets have been consistent across Windows 10 and Windows 11 through 23H2:
#define MOUCLASS_DEVEXT_INPUT_COUNT 0x54
#define MOUCLASS_DEVEXT_DATA_QUEUE_BASE 0x68
#define MOUCLASS_DEVEXT_WRITE_POINTER 0x70
#define MOUCLASS_DEVEXT_READ_POINTER 0x78
#define MOUCLASS_DEVEXT_QUEUE_SIZE 0x88
#define MOUCLASS_DEVEXT_SPINLOCK 0x90
#define MOUCLASS_DEVEXT_PENDING_IRP 0x98
mouclass doesn’t change often—it’s stable infrastructure code with no particular reason to be modified. For anything beyond testing, you’d want to resolve these dynamically by pattern scanning the driver.
Notes
The technique requires kernel execution. It’s not a user-mode bypass. The full driver source handles device creation, IOCTL dispatch, and cleanup—the injection function shown here is the interesting part.
It’s possible that some anti-cheats will add mouclass monitoring after this is published. The structure accesses are straightforward to detect if you’re looking for them.