
Two kernel vulnerabilities in Razer Lycosa.sys (CWE-125 memory disclosure + CWE-121 stack overflow) chained to local privilege escalation. Coordinated-disclosure materials, verified on Windows 11.
Discovered through https://github.com/416rehman/DeepZero
Vendor: Razer Inc.
Component: Lycosa.sys, the Razer Lycosa keyboard filter driver, x64
SHA-256: a120a6184ab16864e8a5f1dfd0cd178fca541de463b5cef946e18c34b9b6f716
Reporter reference: a120a6184ab16864
Status: not yet reported to the vendor.
This directory reports two distinct defects in the same routine of the same driver. They have separate root causes and separate fixes, so each has its own self-contained folder and can be tracked and assigned its own identifier:
| CVE | Defect | Type | Outcome |
|---|---|---|---|
| CVE-01 | Output length is not checked against the buffer, so the driver returns kernel stack memory | CWE-125 out-of-bounds read | Kernel memory disclosure, defeats address space layout randomisation |
| CVE-02 | Input length is not checked against the buffer, so the driver overwrites its own return address | CWE-121 stack buffer overflow | Arbitrary kernel code execution |
Both are reachable from any account that can log in and run a program. No administrative rights, no elevation, no special privilege. Both were confirmed on Windows 11 25H2 (build 26200.8875) with code integrity enforced and test signing off.
Section 3 describes what the two amount to when used together, which is the reason they are reported at the same time and why the chained proof of concept lives in this root directory.
Both live in the IRP_MJ_DEVICE_CONTROL handler at RVA 0x1270, and both act
on the same 0x400-byte buffer on the kernel stack. The prologue, read from the
shipped binary, fixes the geometry for both:
Lycosa+0x1270 48 89 54 24 10 mov [rsp+10h], rdx ; Irp
Lycosa+0x1275 48 89 4c 24 08 mov [rsp+8], rcx ; DeviceObject
Lycosa+0x127a 48 81 ec 98 04 00 00 sub rsp, 498h ; the frame
Lycosa+0x1291 ba 00 04 00 00 mov edx, 400h ; the buffer size
Lycosa+0x1296 48 8d 8c 24 80 00 00 00 lea rcx, [rsp+80h] ; the buffer
A 0x400-byte buffer at rsp+0x80, inside a 0x498-byte frame, with no non-volatile
registers saved. Counting from the start of the buffer:
0x000 .. 0x3FF the buffer, which both defects are supposed to stay inside
0x418 the return address of the dispatch routine
0x420 the saved DeviceObject argument
0x428 the saved Irp argument
0x418 is 0x498 - 0x80. The disclosure (CVE-01) reads past 0x3FF and hands
back what it finds; the overflow (CVE-02) writes past 0x3FF and replaces it.
DriverEntry creates the device with no security descriptor and publishes a
symbolic link for it, so it is reachable at \\.\Lycosa:
IoCreateDevice(param_1, 0x20, L"\\Device\\Lycosa", 0x22, 0, 0, &device);
IoCreateSymbolicLink(L"\\DosDevices\\Lycosa", L"\\Device\\Lycosa");
Every affected control code decodes as FILE_DEVICE_UNKNOWN, METHOD_BUFFERED,
FILE_ANY_ACCESS. FILE_ANY_ACCESS means no particular access right must be held
on the handle, so the device object's security descriptor is the only gate, and
it grants access to everyone.
All results in these reports were produced from a standard user account whose
only group membership is the built-in Users group. The account had no
administrative rights, was not elevated, and held no privileges beyond the
defaults.
The driver also loads on machines that have never had Razer hardware attached, because the package is validly catalog-signed. That is the pattern used in bring-your-own-vulnerable-driver attacks.
Reported separately because they are separate defects, but a vendor triaging them should know that each makes the other worse.
Modern Windows loads the kernel at a randomised address. An attacker who can overwrite a return address still has to know what to overwrite it with, and that is normally the obstacle. This driver answers both questions on its own:
CVE-01 removes the randomisation. The disclosure returns the dispatch
routine's own return address, a code address inside ntoskrnl.exe. Subtracting
its known offset within the image gives the base at which the kernel is loaded,
and from there every address inside the kernel is known. This costs nothing and
disturbs nothing.
CVE-01 also supplies the value CVE-02 needs in order not to fault. As
described in CVE-02 section 4.4,
the driver reloads the Irp from offset 0x428 on its way out and writes
through it. A naive overflow that reaches the return address also destroys that
pointer and faults before the routine returns. The reliable exploit instead
stops the copy exactly at 0x428, leaving the live Irp of the current
request in place, so the driver completes normally.
CVE-02 then redirects execution, with the kernel base already known.
From an unprivileged account, the chained proof of concept reads the frame, calculates the kernel base and the buffer's own kernel address, and sends a return-oriented chain:
step 1, read what is above the buffer on the kernel stack:
+0x418 return address 0xFFFFF807D565CABB
+0x498 frame pointer 0xFFFFFD042D313750 (read twice, must match)
step 2, turn those into the two addresses the payload needs:
kernel base = 0xFFFFF807D565CABB - 0x25CABB = 0xFFFFF807D5400000
buffer on stack = 0xFFFFFD042D313750 - 0x500 = 0xFFFFFD042D313250
step 4, overflow with a chain that:
pivots the stack onto the buffer, calls nt!ZwCreateFile, and
resumes nt!IopfCallDriver+0x5b
sending 0x428 bytes
call returned: accepted=true error=0
PROOF: C:\Windows\System32\dz_lycosa_kernel_exec.txt now exists.
This is the complete chain, verified end to end. The unprivileged account created
a file under C:\Windows\System32, a directory it is otherwise refused write
access to, by executing nt!ZwCreateFile in kernel mode. accepted=true means
the system call returned normally: the chain resumes the exact address the driver
was going to return to (nt!IopfCallDriver+0x5b, which is add rsp,0x38 ; ret),
so the thread finishes and the machine keeps running. The created file was
confirmed independently from an administrator shell. The full transcript is in
logs/exec_create_file.log, and the method is in
METHODOLOGY.md.
The practical reading is that this one driver provides, to any user of the machine, both halves of what is normally needed to turn a memory-safety defect into kernel code execution: the address disclosure that removes randomisation, and the control-flow hijack that uses it. Together they were shown to yield a concrete privileged action with the machine left running.
The two fixes are independent and both small, stated in full in each report:
OutputBufferLength, and set IoStatus.Information to what was actually
produced.InputBufferLength.Both reports also recommend creating the control device with
IoCreateDeviceSecure and an SDDL string restricting it to administrators and the
system. That alone would not fix either defect, but it would remove the
unprivileged reach that gives both their severity.
Checked before writing up, because a duplicate report wastes a vendor's time:
https://aka.ms/VulnerableDriverBlockList and searched across its 1,713 deny
rules. This file does not appear. The only Razer driver on that list is
Rzpnk.sys, a different component.We believe both defects are previously unreported and would welcome correction.
Several other drivers shipped in the same package directory share this driver's overall shape and are examined separately. Nothing here is a statement about them.
pnputil /add-driver Flter2K.inf /install
sc create lycosa_test type= kernel binPath= C:\path\to\Lycosa.sys start= demand
sc start lycosa_test
Each defect has its own single-purpose proof of concept in its folder, and this root holds the chained one that combines them:
# CVE-01, reads only, safe to run anywhere, quickest confirmation of the report
rustc -O CVE-01-kernel-memory-disclosure/poc/lycosa_disclosure.rs -o disc.exe
disc.exe
# CVE-02, stops the machine by design
rustc -O CVE-02-kernel-stack-overflow/poc/lycosa_overflow.rs -o ovf.exe
ovf.exe --yes-crash-this-machine
# the chain: CVE-01 + CVE-02 into a file created in System32, machine left running
rustc -O poc/lycosa_chain.rs -o chain.exe
chain.exe --exec # default target under System32
chain.exe --exec C:\Users\Public\proof.txt # or any path you choose
Run everything from a standard user account. The chained PoC states the two
driver constants it depends on (FRAME and BUF_AT) at the top of its source,
so it can be pointed at a different driver build by changing those two numbers.
The kernel offsets its --exec mode uses are specific to one Windows build; the
program checks the derived kernel base at run time and its --calibrate mode
reports the one value that changes between builds.
README.md this overview and the chaining analysis
METHODOLOGY.md how both were found and confirmed, in order
poc/lycosa_chain.rs the CHAINED proof of concept (both defects)
evidence/ the binary, its package, decompiled sources, dumps
logs/exec_create_file.log transcript of the chained run in section 3
CVE-01-kernel-memory-disclosure/ standalone disclosure for the out-of-bounds read
README.md, poc/lycosa_disclosure.rs, evidence/, logs/
CVE-02-kernel-stack-overflow/ standalone disclosure for the stack overflow
README.md, poc/lycosa_overflow.rs, evidence/, logs/