LnvMSRIO.sys 3.1.0.36 将用户请求中的地址和寄存器编号带入物理内存映射与 MSR 操作。CVE-2025-8061 的重点不是复杂的内存破坏,而是已经签名、可以装载的驱动仍可能暴露过宽的特权资源接口。
以下结合历史反编译视图、Windows 11 24H2 运行记录与离线布局模型,分别检查设备可达性、IOCTL 缓冲区、复制方向和处理器状态。没有重新装载该驱动,也没有在当前机器上修改 MSR 或执行内核载荷。
公告范围与样本前提
| 项目 | 结论 |
|---|---|
| 漏洞 | CVE-2025-8061,CWE-782 |
| 厂商产品范围 | Dispatcher 3.0 与 3.1 Driver |
| 版本边界 | 受影响版本低于 3.1.0.41 |
| 单独排除 | Dispatcher 3.2 Driver |
| 分析样本 | LnvMSRIO.sys 3.1.0.36,x64 |
| 历史终端系统版本 | 10.0.26100.4351,Windows 11 24H2 |
Lenovo 公告 与厂商提交的 CVE 记录 将其描述为本地已认证用户可触达的访问控制问题,并明确排除开启 Core Isolation Memory Integrity 的系统。更新应按对应机型的修复表选择包,而不是只比较文件名。厂商记录感谢 YiShun Zeng 与 Luis Casvella 独立报告问题。
历史验证关闭了 HVCI 相关保护,这个条件必须与权限变化结果一起保留。签名界面显示签名方 Lenovo 且校验成功,但签名只提供来源与完整性证据,不证明每个 IOCTL 都有适当的操作约束。
从设备句柄追到 IOCTL
flowchart LR accTitle: 从初始化到资源操作 accDescr: 驱动初始化建立设备与符号链接,注册设备控制分发,再将获准请求送入物理内存或 MSR 处理函数。 A["DriverEntry"] --> B["初始化"] B --> C["设备与 WinMsrDev 链接"] B --> D["MajorFunction 0x0e"] C --> E["已有设备句柄"] E --> D D --> F["IOCTL 分发"] F --> G["物理内存分支"] F --> H["MSR 分支"]
初始化视图建立 \DosDevices\WinMsrDev,并将 DriverObject->MajorFunction[0x0e] 指向设备控制处理函数。已有设备能被打开,与普通用户能够安装驱动是两种条件;后者还涉及装载权限和系统策略。
四个常量按照 CTL_CODE 位布局 解码如下。Access 是句柄所需访问权,不是程序的完整授权策略。
| IOCTL | 分支 | Function | Access | Method |
|---|---|---|---|---|
| 0x9c406104 | 物理读取 | 0x841 | 1:FILE_READ_DATA | 0:METHOD_BUFFERED |
| 0x9c40a108 | 物理写入入口 | 0x842 | 2:FILE_WRITE_DATA | 0:METHOD_BUFFERED |
| 0x9c402084 | MSR 读取 | 0x821 | 0:FILE_ANY_ACCESS | 0:METHOD_BUFFERED |
| 0x9c402088 | MSR 写入 | 0x822 | 0:FILE_ANY_ACCESS | 0:METHOD_BUFFERED |
四者的 DeviceType 都是 0x9c40。MSR 两项的 FILE_ANY_ACCESS 仍要求调用方已经取得设备句柄,故审计记录应包括实际设备安全描述符、打开权限和返回值。MSR 写入常量是 0x9c402088,不要与物理读取项混用。
同一个 SystemBuffer 的两个角色
四项均使用 METHOD_BUFFERED。I/O 管理器提供的 Irp->AssociatedIrp.SystemBuffer 承担输入和输出角色,其容量取输入、输出长度较大者;这与用户态 DeviceIoControl 接收两个指针不矛盾。
| 长度 | 含义 |
|---|---|
| InputBufferLength | 驱动可按输入格式读取的字节数 |
| OutputBufferLength | 调用方提供的输出容量 |
| IoStatus.Information | 驱动报告的有效返回字节数 |
缓冲区容量大,不代表全部字节都是输入数据,也不代表输出都已经初始化。字段定义见 Microsoft 缓冲区文档。历史伪代码的部分参数类型与命名不一致,以下恢复数据偏移和方向,不把函数原型当成可直接编译的 ABI。
| 偏移 | 名称 | 类型 | 大小 |
|---|---|---|---|
| 0x00 | physical_address | uint64_t | 8 |
| 0x08 | operation_type | uint32_t | 4 |
| 0x0c | count | uint32_t | 4 |
物理读取路径要求 0x10 字节输入。起始八字节作为物理地址,后两个字段参与映射长度和复制分支。MmMapIoSpace 返回对应的内核虚拟映射;它的地址参数是物理地址值,并非用户指针,实际用途和失败返回仍须遵循 API 合约。
映射长度与复制长度存在差异
读取视图计算 operation_type * count 作为映射长度,而三个包装器并非都按这个积复制。图中包装器参数顺序为 src, dst, count,内部才调用常规的 memcpy(dst, src, length)。
| operation_type | 映射长度表达式 | 包装器实际复制字节数 |
|---|---|---|
| 1 | 1 × count | count |
| 2 | 2 × count | count << 1 |
| 8 | 8 × count | count << 2 |
因此 8 分支不应按字段值直接解释为每项复制八字节。该视图中的乘积表现为 32 位值,复制包装器则先零扩展计数再移位;离线模型在边界输入上显示两者可能进一步分离。这是需要返回机器码与边界测试核实的审计点,不据此另行宣告已经验证了越界漏洞。
flowchart LR accTitle: 映射与复制是两项操作 accDescr: MmMapIoSpace 将物理区间映射到内核;读分支从映射区复制到系统缓冲区,已确认的写分支方向相反。 P["物理区间"] -->|"MmMapIoSpace"| M["内核映射"] M -->|"读取"| O["SystemBuffer 输出"] I["SystemBuffer 负载 +16"] -->|"写入分支"| M
写入入口的格式是 16 字节控制头加实际负载。含 Data[1] 的自然对齐 C 结构可能带尾部填充,sizeof 不是任意负载长度的通用表达式;请求长度应单独计算,并检查加法与乘法边界。
另一个重要差异出现在写入视图的 8 分支:它把映射区域作为 memcpy_wrapper3 的源,把输入缓冲区 +16 作为目标,与普通写入示意的方向相反。该分支仅有反编译证据,尚缺原始指令与运行对照;因此图中的写方向仅用于相符分支,不把整条入口的所有模式统称为已证实的任意写。
即使取得物理内存操作,也还没有自动获得任意内核虚拟地址读写。映射关系、页面类型及对象所在物理页均需额外证据;这里不通过扫描物理地址补足这些前提。
MSR 字段要按字节布局
读取分支从输入取得 32 位 MSR 编号,执行 rdmsr 后组合 EDX:EAX,形成 64 位结果。写入记录则使用 +0 的四字节编号与 +4 的八字节值:这是 12 字节紧凑布局。
register0x00–0x03value0x04–0x0B
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 只是字节示例,不用于寄存器写入。Win64 本地布局检查中,默认结构将八字节值放到 +8,总长 16;按一字节对齐或显式序列化才得到 +4、总长 12。
同样,sizeof(pointer) 只返回指针宽度,不是 MSR 编号字段长度。驱动碰巧接受多出的字节并不证明封装正确。寄存器白名单、值域、输入输出长度及异常路径应分别检查,字符串名字或结构长度都不是资源授权。
地址结果必须绑定同一内核构建
历史模块查询的两个结果可整理为下表。第二个结果需要结合令牌特权解释,而非直接断言 24H2 删除了模块地址查询。
| 记录 | 结果或条件 |
|---|---|
| 较早环境截图 | 0xFFFFF8077D800000 |
| 24H2 截图 | 0x0 |
| Microsoft 的 24H2 规则 | 返回有效 ImageBase 需要已启用 SeDebugPrivilege |
Microsoft 文档 明确指出:缺少已启用的特权时,函数仍可返回成功,但数组中的地址全部为 NULL。因此应同时记录 API 返回值、数组内容和有效令牌,而不是只打印第一个地址。
[*] KiSystemCall64 at 0xFFFFF80392AB8740
[*] Kernel Base Address at 0xFFFFF80392400000这组历史记录用 LSTAR 观察值配合同一构建的入口 RVA 计算候选基址:
observed_entry = 0xfffff80392ab8740
entry_rva = 0x6b8740
candidate_base = observed_entry - entry_rva
assert candidate_base == 0xfffff803924000000x6b8740 是该构建的值,不是通用常量。内存内核与磁盘映像、入口变体及符号必须对应;减得一个对齐地址只是相容性检查,不替代映像验证。
进入内核与恢复上下文成对出现
flowchart LR accTitle: LSTAR 选择入口而非完整上下文 accDescr: 逻辑处理器上的系统调用通过 LSTAR 选择入口,软件仍需处理内核栈、GS 与用户返回状态。 A["某逻辑 CPU 上的 SYSCALL"] --> B["LSTAR 目标"] B --> C["入口代码"] C --> D["栈、GS 与返回状态处理"]
LSTAR 是模型特定寄存器(model-specific register,MSR),编号为 0xc0000082。修改某逻辑处理器的入口状态,不等于自动同步全部 CPU;迁移和调度会影响后续系统调用使用哪个状态。
SYSCALL 本身不保存 RSP,SYSRET 也不恢复 RSP,这些栈处理由软件完成。历史进入与返回栈图涉及的约束可整理如下,而不是只保留一串构建相关的指令片段地址。
| 状态 | 进入侧 | 返回侧 |
|---|---|---|
| GS 基址 | 明确当前上下文及 swapgs 次数 | 与返回模式保持配对 |
| RSP | 保存原始关系并建立有效栈 | 恢复所需用户栈,不依赖猜测的对齐量 |
| RIP 与标志 | 区分入口地址和返回状态 | 满足返回指令的架构条件 |
| LSTAR 与 CR4 | 保存实际原值及所属逻辑 CPU | 恢复对应状态,而非写入通用常量 |
iretq 的帧消费应按实际 64 位执行模式核对,不把其他模式的经验直接套入。提高线程优先级也只影响调度概率,不是禁止中断或固定 CPU 的证据。相关指令与状态定义见 Intel 架构手册。
| 位 | 字段 | 值 |
|---|---|---|
| 20 | SMEP | 1 |
| 21 | SMAP | 1 |
这个位图展示差值,不是实际 CR4 值。历史常量 0x350ef8 与 0x050ef8 异或得到 0x300000,涉及 SMEP bit 20 和 SMAP bit 21 两项,而非只有 SMEP。另一个常见注释误差是 OR 0x40000:它置位 RFLAGS.AC bit 18;AND 0xff 则清除多项高位标志,不只是 IF。
结果、局限与修复检查
Microsoft Windows [version 10.0.26100.4351]
$ whoami
autorité nt\système终端中的本地化身份输出支持该次运行已取得 SYSTEM 身份;路径、原始用户名及部署细节已省去。记录并不证明默认保护配置可复现,也不证明令牌引用计数、线程迁移和所有退出路径均已正确处理。
本地实际运行的检查包括 4 个 IOCTL 编码往返、24 组映射与复制长度、6 个可变负载尺寸、4 组标志输入、MSR 布局和 RVA 算术。它们是数据模型,不是一次新的驱动利用。
- 1
确认产品与版本
按机型应用 Lenovo 公告中的修复包,记录实际装载文件,而不仅是安装包版本。
- 2
保留系统保护
核对 Memory Integrity 等实际运行状态,不把关闭保护后的结果当成默认系统表现。
- 3
约束每项能力
核对设备 ACL、IOCTL 访问权、调用方身份、物理资源范围和允许的 MSR 编号与值。
- 4
检查拒绝与输出
分别验证无权调用、错误长度、计数边界和映射失败,记录返回状态、有效字节数及实际副作用。
可靠修复落在资源接口最前端:合法签名和合法消息形状都不足以授予任意物理地址或处理器寄存器访问权。将每个可控字段追到最终副作用,再确认谁有权触发它,才是本案可迁移的审阅方法。