~/posts/binary/cve-2025-8061-lnvmsrio-ioctl-resource-boundaries.md

LnvMSRIO: IOCTLs and privileged resource boundaries

Trace CVE-2025-8061 through device access, buffered IOCTLs, copy directions, packed MSR fields, and CPU state. Separate historical SYSTEM execution from offline checks and vendor remediation scope.

date[31:24]
read[23:16]
10 min
cat[15:8]
Binary
Contents
  1. 0x00Advisory scope and sample conditions
  2. 0x01Follow the device handle into IOCTL dispatch
  3. 0x02One SystemBuffer serves two roles
  4. 0x03Mapping length and copy length differ
  5. 0x04Encode MSR fields at exact byte offsets
  6. 0x05Bind address results to one kernel build
  7. 0x06Entry and return state must be paired
  8. 0x07Results, limits, and remediation checks
  9. 0x08References

LnvMSRIO.sys 3.1.0.36 carries addresses and register numbers from user requests into physical-memory mappings and MSR operations. CVE-2025-8061 starts with excessive access to privileged resources, not a complicated memory-corruption primitive. A signed, loadable driver can still expose an unsafe interface.

The analysis separates historical decompiler views, Windows 11 24H2 runtime records, and offline layout models. Device reachability, IOCTL buffers, copy direction, and processor state need different evidence. No driver was loaded, MSR changed, or kernel payload executed on the current machine.

Advisory scope and sample conditions

Scope and evidence6 rows
Item Finding
Vulnerability CVE-2025-8061, CWE-782
Vendor product scope Dispatcher 3.0 and 3.1 Driver
Version boundary Affected versions below 3.1.0.41
Explicit exclusion Dispatcher 3.2 Driver
Examined sample LnvMSRIO.sys 3.1.0.36, x64
Historical terminal version 10.0.26100.4351, Windows 11 24H2

The Lenovo advisory and vendor-submitted CVE record describe an access-control issue reachable by an authenticated local user. They explicitly exclude systems with Core Isolation Memory Integrity enabled. Select the update from the remediation table for the affected model rather than matching a filename alone. The vendor credits YiShun Zeng and Luis Casvella with independently reporting the issue.

The historical test disabled HVCI-related protections. That condition belongs next to the privilege-change result. The signature dialog reports a valid signature from Lenovo, but this establishes provenance and integrity, not appropriate constraints on every IOCTL.

Follow the device handle into IOCTL dispatch

flowchart LR
  accTitle: From initialization to resource operations
  accDescr: Driver initialization creates a device and symbolic link, registers the device-control dispatch, and routes accepted requests to physical-memory or MSR handlers.
  A["DriverEntry"] --> B["Initialization"]
  B --> C["Device and WinMsrDev link"]
  B --> D["MajorFunction 0x0e"]
  C --> E["Existing device handle"]
  E --> D
  D --> F["IOCTL dispatch"]
  F --> G["Physical-memory handlers"]
  F --> H["MSR handlers"]

The initialization view creates \DosDevices\WinMsrDev and assigns the device-control handler to DriverObject->MajorFunction[0x0e]. Opening an existing device and installing a driver are separate prerequisites; installation also depends on loading privileges and system policy.

The four constants decode as follows using the CTL_CODE layout. Access specifies handle access requirements, not the application's entire authorization policy.

Four dispatch paths4 rows
IOCTL Branch Function Access Method
0x9c406104 Physical read 0x841 1: FILE_READ_DATA 0: METHOD_BUFFERED
0x9c40a108 Physical-write entry 0x842 2: FILE_WRITE_DATA 0: METHOD_BUFFERED
0x9c402084 MSR read 0x821 0: FILE_ANY_ACCESS 0: METHOD_BUFFERED
0x9c402088 MSR write 0x822 0: FILE_ANY_ACCESS 0: METHOD_BUFFERED

All four have DeviceType = 0x9c40. FILE_ANY_ACCESS on the MSR requests still presupposes an existing device handle. An audit therefore needs the actual device security descriptor, requested access, and open result. The MSR-write code is 0x9c402088, not the physical-read constant.

One SystemBuffer serves two roles

All four requests use METHOD_BUFFERED. The I/O manager supplies Irp->AssociatedIrp.SystemBuffer for both input and output and allocates the larger of the two requested lengths. This is consistent with user-mode DeviceIoControl accepting separate input and output pointers.

Three distinct lengths3 rows
Length Meaning
InputBufferLength Bytes available under the input contract
OutputBufferLength Caller-provided output capacity
IoStatus.Information Valid returned bytes reported by the driver

A larger allocation does not make every byte valid input or initialized output. See Microsoft's buffer documentation. Some argument types and names in the historical pseudocode are inconsistent. The reconstruction below uses field offsets and data direction, not those prototypes as a compilable ABI.

Physical-memory request headerx86-64 · LE
OffsetNameTypeSize
0x00physical_addressuint64_t8
0x08operation_typeuint32_t4
0x0ccountuint32_t4
sizeof(struct PhysicalRequestHeader) = 0x10 (16 bytes)

The physical-read path requires a 0x10-byte input. Its first eight bytes supply the physical address; the remaining fields control the mapping length and copy branch. MmMapIoSpace returns a kernel virtual mapping. Its address argument is a physical-address value, not a user pointer; supported usage and failure handling still follow the API contract.

Mapping length and copy length differ

The read view computes operation_type * count for the mapping, but the three wrappers do not all copy that product. Their displayed argument order is src, dst, count; internally they call the conventional memcpy(dst, src, length).

Lengths in the read path3 rows
operation_type Mapping-length expression Bytes copied by the wrapper
1 1 × count count
2 2 × count count << 1
8 8 × count count << 2

The 8 branch therefore does not imply eight copied bytes per item. The displayed product is a 32-bit value, whereas the wrapper zero-extends the count before shifting. Boundary inputs in the offline model separate these lengths further. This is an audit lead requiring instruction-level and runtime confirmation, not a newly validated out-of-bounds finding.

flowchart LR
  accTitle: Mapping and copying are separate operations
  accDescr: MmMapIoSpace maps a physical region; a read copies from the mapping to the system buffer. A write copies payload bytes in the opposite direction for the confirmed write branches.
  P["Physical region"] -->|"MmMapIoSpace"| M["Kernel mapping"]
  M -->|"Read"| O["SystemBuffer output"]
  I["SystemBuffer payload +16"] -->|"Write branches"| M

The physical-write entry uses a 16-byte control header followed by the actual payload. A naturally aligned C structure containing Data[1] may include tail padding. Its sizeof is not a general formula for arbitrary payload lengths; calculate the request size explicitly and check addition and multiplication boundaries.

The write view has another significant exception: branch 8 passes the mapping as the source of memcpy_wrapper3 and the input buffer at +16 as its destination, reversing the usual write diagram. This has decompiler evidence but no fresh instruction or runtime confirmation. The diagram's write arrow therefore applies only to matching branches, not a claim that every mode is an established arbitrary write.

Physical-memory access also does not automatically provide arbitrary kernel-virtual-address access. Mappings, page types, and the physical location of the object need further evidence. No physical-address scan was used to fill those gaps.

Encode MSR fields at exact byte offsets

The read branch takes a 32-bit MSR number, executes rdmsr, and combines EDX:EAX into a 64-bit result. The write record places the four-byte number at +0 and the eight-byte value at +4: a packed 12-byte layout.

MSR write record · illustrative value
00000000820000C08877665544332211
  1. register0x00–0x03
  2. value0x04–0x0B
msr_layout.pypython
import struct

register = 0xc0000082
sample_value = 0x1122334455667788
request = struct.pack("<IQ", register, sample_value)
assert len(request) == 12
assert struct.unpack_from("<I", request, 0)[0] == register
assert struct.unpack_from("<Q", request, 4)[0] == sample_value

sample_value is illustrative byte data, not a register value to write. The local Win64 layout check placed the value at +8 in the default structure, making it 16 bytes long. One-byte packing or explicit serialization produced offset +4 and size 12.

Likewise, sizeof(pointer) measures the pointer, not the register-number field. A permissive handler accepting excess bytes does not validate the wrapper. Register allowlists, value constraints, input/output lengths, and exception paths require separate checks. A field name or valid structure size does not authorize a resource.

Bind address results to one kernel build

The historical module-query results are summarized below. The second needs an explanation involving token privileges, rather than a claim that 24H2 removed module-address queries.

EnumDeviceDrivers observations and documentation3 rows
Record Result or condition
Earlier-environment capture 0xFFFFF8077D800000
24H2 capture 0x0
Microsoft's 24H2 rule Valid ImageBase values require enabled SeDebugPrivilege

Microsoft documents that the function can still succeed without the privilege enabled while returning an array of NULL addresses. Record the API result, array contents, and effective token together instead of printing only the first address.

Historical address excerpt
[*] KiSystemCall64 at 0xFFFFF80392AB8740
[*] Kernel Base Address at 0xFFFFF80392400000

The historical record combines the observed LSTAR value with the entry RVA from the same build to obtain a candidate base:

matching_build_address.pypython
observed_entry = 0xfffff80392ab8740
entry_rva = 0x6b8740
candidate_base = observed_entry - entry_rva
assert candidate_base == 0xfffff80392400000

0x6b8740 belongs to that build; it is not a universal constant. The loaded kernel, disk image, entry variant, and symbols must correspond. Subtraction yielding an aligned address is a consistency check, not image validation.

Entry and return state must be paired

flowchart LR
  accTitle: LSTAR selects an entry, not a complete context
  accDescr: A syscall on one logical processor selects an entry through LSTAR. Software must still establish the kernel stack and GS context and preserve the user return state.
  A["SYSCALL on one logical CPU"] --> B["LSTAR target"]
  B --> C["Entry code"]
  C --> D["Stack, GS and return-state handling"]

LSTAR is a model-specific register (MSR), numbered 0xc0000082. Changing one logical processor's entry state does not synchronize every CPU automatically. Migration and scheduling affect which state the next system call uses.

SYSCALL does not save RSP, and SYSRET does not restore it; software manages those stack transitions. The obligations represented by the historical entry and return stack diagrams are more informative than a list of build-specific gadget addresses.

Entry and return obligations4 rows
State Entry side Return side
GS base Establish the current context and swapgs count Pair transitions with the return mode
RSP Preserve its original relationship and establish a valid stack Restore the required user stack without guessing alignment changes
RIP and flags Separate the entry target from return state Satisfy the return instruction's architectural conditions
LSTAR and CR4 Save actual original values and the associated logical CPU Restore the matching state, not a universal constant

Check iretq frame consumption for the actual 64-bit execution mode rather than importing assumptions from another mode. Raising thread priority affects scheduling probability; it does not prove that interrupts or CPU migration are excluded. See the Intel architecture manuals for instruction and state definitions.

Changed bits between two recorded CR4 constants32-bit
SMAPSMEP
value0x00300000= 3145728
BitsFieldValue
20SMEP1
21SMAP1

This diagram shows changed bits, not an actual CR4 value. XORing the recorded constants 0x350ef8 and 0x050ef8 yields 0x300000, covering both SMEP bit 20 and SMAP bit 21, not SMEP alone. Another common annotation error concerns OR 0x40000: it sets RFLAGS.AC bit 18. AND 0xff clears several higher flag bits, not just IF.

Results, limits, and remediation checks

Historical result excerpt · whoami
Microsoft Windows [version 10.0.26100.4351]
$ whoami
autorité nt\système

The localized identity output supports SYSTEM execution in that run. Paths, the original user name, and deployment details are omitted. It does not establish reproducibility with default protections, nor correct token reference counting, thread migration handling, and every exit path.

The checks actually run locally covered four IOCTL round trips, 24 mapping/copy-length cases, six variable-payload sizes, four flag inputs, MSR packing, and RVA arithmetic. They are data models, not a new driver exploit.

Remediation verification4 steps
  1. 1

    Identify the product and version

    Apply the Lenovo update for the model and record the file actually loaded, not just the installer version.

  2. 2

    Keep system protections enabled

    Verify the runtime state of Memory Integrity and related controls. Do not treat results obtained with protections disabled as default behavior.

  3. 3

    Constrain each capability

    Check the device ACL, IOCTL access rights, caller identity, physical-resource range, and permitted MSR numbers and values.

  4. 4

    Check rejection and output

    Test unapproved callers, invalid lengths, count boundaries, and mapping failures. Record status, valid returned bytes, and actual side effects separately.

The durable fix belongs at the resource boundary. A valid signature and a valid message shape do not authorize arbitrary physical addresses or processor registers. Trace each controlled field to its final side effect, then establish who is allowed to request it.

References

NORMAL~/posts/binary/cve-2025-8061-lnvmsrio-ioctl-resource-boundaries.md§--
0%en