kovaplusfltr.sys: an unprivileged kernel stack overflow in the roccat kova[+] hid filter driver
on this page
part of the glaurung windows driver findings catalog. method narrative: ctrl-F-ing around: how glaurung autonomously discovered a heap overflow in notepad.exe.
KovaPlusFltr.sys is a HID filter driver for the ROCCAT Kova[+] mouse. it copies an attacker-controlled ioctl length into a fixed 3000-byte kernel stack buffer with only a lower bound and no stack cookie, which lets a standard user overwrite the saved kernel return address. it is the strongest primitive in this catalog so far, an unprivileged controlled kernel-mode write rather than a read or an admin-gated crash. the exposure is narrow: the bug exists only on a machine where the driver is loaded, which on a normal install means the Kova[+] hardware is present.
summary
| driver | KovaPlusFltr.sys, ROCCAT Kova[+] mouse HID filter driver (vendor now Turtle Beach) |
| build | 1.0.0.0, compiled 2010-01-25, catalog-signed; SHA256 eec9bd07…f15e0b |
| class | CWE-121 (stack buffer overflow), CWE-787 (out-of-bounds write of the saved return address) |
| bug | attacker-controlled ioctl length copied into a fixed 3000-byte kernel stack buffer with only a lower bound; no /GS cookie |
| reach | unprivileged local (\\.\KovaPlusFltr is openable by any standard user), but only when the driver is loaded (Kova[+] hardware present) |
| primitive | controllable ring-0 saved return address: a reliable bugcheck, and kernel RIP control where HVCI/kCFG are off |
| cvss 3.1 | AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 7.8, with C:H/I:H gated by HVCI |
| proof | real-byte qemu bugcheck (0x50) on the shipping driver, plus a live debugger capture of the saved-RIP overwrite |
| disclosure | reported to MSRC; closed as a third-party driver (out of scope, not a severity decision). the realistic remediation is the vulnerable-driver blocklist and a LOLDrivers entry |
the bug
KovaPlusFltr.sys is a pure WDM driver with NTOSKRNL imports only and no KMDF framework. it creates a control device \Device\KovaPlusFltr with a \DosDevices\KovaPlusFltr symlink (user path \\.\KovaPlusFltr). the device-control handler zeroes a 3000-byte stack buffer and then fans out on the ioctl code:
0x115c0 sub rsp, 0xc68 ; handler frame
0x115e1 mov edx, 0xbb8 ; 3000
0x115e6 lea rcx, [rsp+0x90] ; the stack buffer
0x115ee call [RtlZeroMemory] ; zero 3000 bytes
0x11745 ... ; ioctl fan-out on [rsp+0xc5c] ioctl 0x222478 decodes to DeviceType 0x22, method 0 (METHOD_BUFFERED), access 0 (FILE_ANY_ACCESS), function 0x91e. its handler does this:
0x11926 ; case 0x222478
0x1193c mov eax, [IoStack+0x10] ; Parameters.DeviceIoControl.InputBufferLength
0x11943 cmp [rsp+0x48], 0 ; je … ; SystemBuffer != NULL
0x1194b cmp [rsp+0x40], 4 ; ja … ; the only length check: len > 4
0x1197b lea rcx, [rsp+0x90] ; dst = the 3000-byte stack buffer
0x11976 mov rdx, [rsp+0x48] ; src = SystemBuffer (attacker bytes)
0x11973 mov r8, rax ; len = InputBufferLength (attacker)
0x11983 call [RtlCopyMemory] ; copy attacker length of attacker bytes onto the stack the only constraint on the copy length is len > 4. there is no upper bound. InputBufferLength is attacker-controlled, and because the ioctl is METHOD_BUFFERED, SystemBuffer is a kernel copy of attacker-supplied bytes. so RtlCopyMemory writes an attacker-chosen number of attacker-chosen bytes into a fixed 3000-byte stack buffer.
the geometry makes the overflow reach the return address. the buffer sits at rsp+0x90 and the frame is sub rsp, 0xc68, so the saved return address lives at rsp+0xc68, which is 0xc68 - 0x90 = 0xbd8 = 3032 bytes from the start of the buffer. an InputBufferLength of 3032 or more writes past the 3000-byte buffer, across the 32 bytes of saved registers and slack, and onto the saved return address. there is no guard on that write: the binary does not import __security_check_cookie, so the frame has no /GS stack cookie.
the sibling ioctl 0x22247c (handler at 0x11a9b) reaches the same buffer a second way, copying OutputBufferLength bytes of SystemBuffer into rsp+0x90 under the same lower-only bound.
who can reach it
the device is created with plain IoCreateDevice, not IoCreateDeviceSecure. there is no FILE_DEVICE_SECURE_OPEN characteristic and no SDDL string anywhere in the binary, so the device falls back to the default device-object DACL, which standard non-administrator users can open. IRP_MJ_CREATE performs no SeAccessCheck of its own. the ioctls are FILE_ANY_ACCESS and METHOD_BUFFERED. so any local process can open \\.\KovaPlusFltr and call DeviceIoControl with an arbitrarily large input length. no administrator rights are required.
reachability is gated on the driver being loaded. this is a per-device HID filter for one specific mouse, so the control device exists only when the driver is loaded, and on a normal install the driver loads because the Kova[+] hardware is present and the ROCCAT software bound it into the mouse’s HID stack. the Kova[+] is a discontinued mouse from ROCCAT, a vendor since absorbed into Turtle Beach, so the affected population is small. once the driver is loaded, the trigger needs no hardware interaction; it is a software ioctl to the control device.
what it actually buys you
an attacker controls the copy length and the copied bytes into a frame with no stack cookie, and therefore controls the saved return address. two outcomes follow, at different confidence levels:
- denial of service, unconditional. overrunning the frame makes the function return through a corrupted or unmapped pointer and the system bugchecks. this is the
A:Hcomponent and it reproduces every time. - kernel code execution, exploitation-gated. the saved return address is overwritten with attacker bytes and no
/GScookie detects it. on a system without HVCI and kernel CFG this is the standard stack-smash-to-code-execution path; with those mitigations on, the realistic outcome falls back to the bugcheck. the CVSS scoresC:H/I:Hon the worst-case-defensible configuration and notes the gating.
the primitive is stronger than the rest of the catalog, where ndfltr is an out-of-bounds read and ndkping is admin-gated. severity in class is high. the limiting factor is the install base, not the primitive.
how glaurung found it
discovery followed the calibrated driver pipeline, not an open-ended prompt. pointing an llm at a .sys and asking it to find bugs is a sub-one-percent true-positive regime, so the cheap deterministic steps run first.
- symbolic pre-pass. glaurung runs a symbolic-execution pass over the driver’s ioctl handlers, a reimplementation of the ioctlance ioctl-vulnerability analysis built to parity inside the toolkit. its precomputed corpus for this binary flagged two rows, “Source Controllable - memcpy” and “Buffer Overflow - Controllable PC”, keyed to the SHA256. this is a lead, not a finding: the symbolic ioctl and function addresses it reports are angr address-space artifacts, not real VAs.
- ground-truth disassembly. glaurung re-grounded the lead on capstone disassembly of the real bytes. that resolved the actual handler (
0x115c0), the real ioctl (0x222478), the unboundedRtlCopyMemoryat0x11983, the 3000-byte buffer, the 3032-byte distance to the saved return address, and the absence of a__security_check_cookieimport. that last detail decides whether the overflow is exploitable, and it is not reliable from lifted pseudo-c. the two ioctlance rows collapsed to one defect, where the controllable PC is the effect of the unbounded copy. - independent re-confirmation. the chain was walked a second time by a separate disassembly pass that decoded the ioctl bitfields by hand, resolved both copy targets to NTOSKRNL
RtlCopyMemory, and confirmed the plainIoCreateDevicewith no SDDL. the sibling at0x22247cwas found by the same shape. - scoop check. the full LOLDrivers database, an NVD keyword search, and a targeted web search returned no prior CVE or advisory for KovaPlusFltr or ROCCAT.
simulating the hardware
the harder problem here was reproducing the bug without the mouse. the Kova[+] is discontinued, the lab has no unit, and the driver does nothing useful until Windows believes its device exists and has started. we reproduced it in two stages, each simulating more of the hardware.
stage one, force-load, for the device and the write primitive. the control device is created in DriverEntry (IoCreateDevice at 0x131da, IoCreateSymbolicLink at 0x13210), which runs at load time regardless of hardware. force-loading the real signed bytes as a plain legacy kernel service (sc create … type= kernel, test-signing on) with no ROCCAT hardware present, \\.\KovaPlusFltr opened successfully from a standard user. attaching a kernel debugger over the qemu gdbstub and firing the ioctl with a 3040-byte (0xbe0) buffer showed the primitive directly:
ATTACK (ioctl 0x222478, InputBufferLength = 0xbe0 = 3040):
saved-RIP [rsp+0xc68] 0xfffff802c988697e -> 0x4141414141414141
(the next slot is intact: surgical control of exactly the return address)
CONTROL (same path, InputBufferLength = 0x40 = 64):
saved-RIP [rsp+0xc68] 0xfffff8019fd0697e -> unchanged the 64-byte control matters. it drives the same handler down the same copy with an in-bounds length and leaves the return address untouched, which shows the trigger reaches the vulnerable copy rather than missing it, and that the overwrite is a function of the length rather than of calling the ioctl at all. this is the live qemu and gdbstub workflow the catalog uses for kernel-state capture.
stage two, simulate the device, for a real bugcheck. force-loading produces the device and the controlled overwrite but not a clean on-target crash. with no started device underneath, the handler’s post-copy code forwards to an absent lower stack and parks instead of returning through the smashed frame. the driver gates that path on a “device started” flag, a .data byte at 0x120c0 set when PnP starts the device, and it expects a real lower device stack to forward to. a phantom software devnode (SwDevice) failed: the stack never started (CM_PROB_FAILED_START), so the gate stayed closed.
the working approach synthesized the hardware one layer down. we gave qemu an emulated USB HID device (usb-tablet) to stand in for the mouse, then bound the real signed KovaPlusFltr.sys as a {HIDClass} lower filter so PnP attached and started it on top of that emulated device. that set the started flag and gave the handler a real lower stack to forward through, so the full path runs. an unprivileged user-mode PoC that opened \\.\KovaPlusFltr and sent ioctl 0x222478 with InputBufferLength = 0x10000 produced a real Windows 11 kernel crash:
BugCheck 0x50 PAGE_FAULT_IN_NONPAGED_AREA (WRITE)
fault in nt!memcpy <- KovaPlusFltr+0x1989
FAILURE_BUCKET_ID AV_VRF_KovaPlusFltr
kernel stack flooded with 0x41 the grade is honest about each stage. this is a level-2 reproduction: the shipping driver’s real bytes, executing in a real Windows kernel, crashing under an attacker ioctl, with the environment synthesized (an emulated HID device standing in for the absent mouse) and the buggy code path unchanged. the benign control passes on the same image. that is the standard a synthesized-environment reproduction has to meet: drive the real function, keep the bug untouched, and include a control that exercises the same path without triggering, so the synthesis is not what produced the crash.
disclosure
reported to the Microsoft Security Response Center. MSRC closed the case as a third-party driver, because KovaPlusFltr.sys is not a Microsoft component and MSRC does not service third-party drivers directly. this is the expected outcome, and it is a question of ownership rather than severity. the other two findings in the catalog, ndkping and ndfltr, were declined on the merits; this one Microsoft does not own.
the disclosure goes where a vulnerable signed driver is actually neutralized:
- the Microsoft Vulnerable Driver Blocklist, via the WDSI driver portal the MSRC closure pointed to. a blocklisted hash is the durable defense for a driver the vendor will not patch.
- a LOLDrivers entry with the SHA256 and the ioctl detail, for detection coverage.
the vendor track, a Turtle Beach or ROCCAT fix, is not worth pursuing here. the Kova[+] is discontinued, and a fix for a dead product helps no one that a blocklist entry does not. there is no public CVE for this driver. a signed driver outlives the hardware it shipped with, and when one carries an unprivileged kernel-write primitive and the vendor is gone, blocklisting the hash is the remediation that is left.