# Root Cause Analysis — CVE-2026-20871

## Overview

| | |
|---|---|
| CVE | CVE-2026-20871 |
| Binary | dwmcore.dll (Desktop Window Manager Composition Core) |
| Vulnerability class | CWE-416: Use After Free |
| Impact | Elevation of Privilege (Standard User → SYSTEM) |
| CVSS 3.1 | 7.8 (High) — AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Pre-patch version | 10.0.26100.7309 (December 2025) |
| Post-patch version | 10.0.26100.7623 (January 2026, KB5074109) |
| Feature flag | Feature_1732988217 |
| Credited researcher | Anonymous working with Trend Zero Day Initiative |
| Independent analysis | [Joe Desimone / Elastic Security Labs — "Patch diff to SYSTEM"](https://www.elastic.co/security-labs/patch-diff-to-system) |

## Executive Summary

A use-after-free in the Desktop Window Manager's `CSynchronousSuperWetInk` destructor allows a local attacker to achieve code execution as SYSTEM. The destructor conditionally calls `RemoveSource()` based on `IsSuperWetCompatible()`, but an attacker can flip the object's `LookupMode` property between registration (where it was `2`, making `IsSuperWetCompatible()` return true) and destruction (where it's now `0`, returning false). This causes `RemoveSource()` to be skipped, leaving a dangling pointer in `CSuperWetInkManager::localStrokesVector`. DWM's next render pass dereferences the freed vtable via `DirtyActiveInk`, giving the attacker RIP control.

A full exploit chain including heap spray, CFG-compatible gadgets, and arbitrary code execution was independently developed and published by Elastic Security Labs.

## Root Cause

### Conditional Unregistration in Destructor

The core bug is a state mismatch between registration and unregistration:

```c
// PRE-PATCH — dwmcore.dll 10.0.26100.7309
CSynchronousSuperWetInk::~CSynchronousSuperWetInk(this)
{
    *(undefined ***)this = &_vftable_;
    bVar2 = IsSuperWetCompatible(this);   // checks LookupMode
    if (bVar2) {
        CSuperWetInkManager::RemoveSource(
            *(this->channel + 0x290),     // manager pointer
            (CSuperWetSource *)this);      // removes from localStrokesVector
    }
    // ... object freed here ...
}
```

### The State Flip Attack

The `IsSuperWetCompatible()` check reads the object's `LookupMode` field. An attacker controls this field via `CMD_SET_PROPERTY` (propId 10) through the DirectComposition batch buffer interface:

1. **Registration** (during `Draw`): `LookupMode=2` → `IsSuperWetCompatible()` returns TRUE → object pointer added to `CSuperWetInkManager::localStrokesVector`
2. **Flip**: Attacker sends `CMD_SET_PROPERTY` propId 10, value 0 → `LookupMode=0`
3. **Destruction** (when ink trail released): `IsSuperWetCompatible()` returns FALSE → `RemoveSource()` **skipped** → pointer remains in vector
4. **Object freed**: Standard C++ destructor chain frees the 288-byte heap allocation
5. **UAF trigger**: DWM's render loop calls `CSuperWetInkManager::DirtyActiveInk`, which iterates `localStrokesVector` and executes `(*(code **)((*puVar4)->vtable + 0x50))()` — virtual call through the dangling pointer

### The Dangling Pointer Dereference

```c
// In CSuperWetInkManager::DirtyActiveInk (render loop)
for (each entry in localStrokesVector) {
    pcVar2 = *(code **)((*puVar4)->vtable + 0x50);  // read vtable from freed memory
    (*pcVar2)();                                      // call through it → RIP control
}
```

Since the attacker controls the 288-byte freed allocation (via heap spray), they control the fake vtable pointer at offset 0, and thus the function pointer at vtable+0x50.

### Sibling Fix: CDelegatedInkCanvas Destructor

The same pattern exists in `CDelegatedInkCanvas::~CDelegatedInkCanvas`:

```c
// PRE-PATCH
if (*(longlong *)(this + 0xc0) != 0) {
    CSuperWetInkManager::RemoveSource(...);
}

// POST-PATCH
bVar1 = Feature_1732988217::__private_IsEnabled();
if ((bVar1) || (*(longlong *)(this + 0xc0) != 0)) {
    CSuperWetInkManager::RemoveSource(...);    // always runs when flag on
}
```

Both destructors are fixed under the same feature flag — they share the same `localStrokesVector` dangling-pointer consequence.

## The Fix

```c
// POST-PATCH — dwmcore.dll 10.0.26100.7623
CSynchronousSuperWetInk::~CSynchronousSuperWetInk(this)
{
    *(undefined ***)this = &_vftable_;
    bVar2 = Feature_1732988217::__private_IsEnabled();
    if (!bVar2) {
        bVar2 = IsSuperWetCompatible(this);
        if (!bVar2) goto LAB_0;   // skip only when flag OFF and not compatible
    }
    // When flag is ON: RemoveSource() runs UNCONDITIONALLY
    CSuperWetInkManager::RemoveSource(
        *(this->channel + 0x290),
        (CSuperWetSource *)this);
LAB_0:
    // ... cleanup ...
}
```

The fix inverts the logic: when `Feature_1732988217` is enabled (production rollout), `RemoveSource()` always executes regardless of `LookupMode`. The `IsSuperWetCompatible()` check only gates `RemoveSource()` when the flag is disabled (kill-switch fallback).

## Published Exploit Chain (Elastic Security Labs)

The following is a summary of the full exploit chain published by [Elastic Security Labs](https://www.elastic.co/security-labs/patch-diff-to-system). This is documented for blue-team awareness.

### Trigger Sequence

1. Create D3D11 device + DXGI swap chain + DirectComposition device
2. Create `CSynchronousSuperWetInk` via `CreateDelegatedInkTrailForSwapChain()` (resource type 0xa8, 288 bytes)
3. Create `CSuperWetInkVisual` (type 0xa5), connect via `CMD_SET_REFERENCE` (propId 0x34)
4. Set `LookupMode=2` via `CMD_SET_PROPERTY` (propId 10) → registers with manager
5. Present frames → DWM registers pointer in `localStrokesVector`
6. Set `LookupMode=0` → flip the guard condition
7. Release ink trail → destructor skips `RemoveSource()`, frees object
8. Present more frames → `DirtyActiveInk` dereferences freed pointer

### Heap Spray (GetRECT)

Reclaim the 288-byte allocation using an 18-RECT array on `CRegionGeometry` (type 0x81) via `CMD_SET_BUFFER_PROPERTY` (0x0F, propId 5). Same LFH bucket (34), 72 controlled int32s = full byte-level control.

### Gadget Chain (CFG-Compatible)

1. **`__fnINSTRING` (user32.dll)**: Converts relative offsets to absolute (solving ASLR without a leak), dispatches inner function pointer
2. **`CStdAsyncStubBuffer2_Disconnect` (combase.dll)**: Two sequential vtable calls — first calls `VirtualProtect` to make spray page RWX, second jumps to inline shellcode calling `WinExec("cmd.exe", SW_SHOW)`

DWM runs at System integrity → cmd.exe spawns as SYSTEM.

## CVSS 4.0 Vector

```
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
```

| Metric | Value | Rationale |
|--------|-------|-----------|
| AV:L | Local | Requires local code execution |
| AC:L | Low | Deterministic trigger — no race condition |
| AT:N | None | DWM always running, no special conditions |
| PR:L | Low | Standard user can create DComp resources |
| UI:N | None | No user interaction required |
| VC:H/VI:H/VA:H | High | Full SYSTEM code execution |

## Detection

### YARA Rule

```yara
rule CVE_2026_20871_DWM_SuperWetInk_UAF
{
    meta:
        description = "Detects tools exploiting CVE-2026-20871 (DWM SuperWetInk UAF)"
        author      = "OnlyFm252 / STAR Labs SG"
        date        = "2026-07-22"
        cve         = "CVE-2026-20871"
        severity    = "critical"

    strings:
        $dcomp_create   = "DCompositionCreateDevice" ascii wide
        $ink_trail      = "CreateDelegatedInkTrailForSwapChain" ascii wide
        $nt_batch       = "NtDCompositionProcessChannelBatchBuffer" ascii wide
        $nt_commit      = "NtDCompositionCommitChannel" ascii wide
        $win32u         = "win32u" ascii wide nocase
        $d3d11          = "D3D11CreateDevice" ascii wide
        $dxgi_swap      = "CreateSwapChain" ascii wide
        $virtual_prot   = "VirtualProtect" ascii
        $winexec        = "WinExec" ascii
        $cmd_exe        = "cmd.exe" ascii wide

    condition:
        uint16(0) == 0x5A4D and
        filesize < 2MB and
        ($dcomp_create or $ink_trail) and
        ($nt_batch or $nt_commit) and
        ($d3d11 or $dxgi_swap) and
        2 of ($virtual_prot, $winexec, $cmd_exe)
}

rule CVE_2026_20871_GetRECT_HeapSpray
{
    meta:
        description = "Detects GetRECT heap spray pattern used in CVE-2026-20871 exploit"
        author      = "OnlyFm252 / STAR Labs SG"
        date        = "2026-07-22"
        cve         = "CVE-2026-20871"

    strings:
        $batch_buf    = "ProcessChannelBatchBuffer" ascii wide
        $region_geom  = { A5 00 00 00 }   /* resource type 0xa5 (CSuperWetInkVisual) */
        $set_buf_prop = { 0F 00 00 00 }   /* CMD_SET_BUFFER_PROPERTY */
        $rect_count   = { 12 00 00 00 }   /* 18 RECTs */
        $fnINSTRING   = "__fnINSTRING" ascii
        $stub_disc    = "CStdAsyncStubBuffer2_Disconnect" ascii

    condition:
        uint16(0) == 0x5A4D and
        $batch_buf and
        2 of ($region_geom, $set_buf_prop, $rect_count) and
        ($fnINSTRING or $stub_disc)
}
```

### Sigma Rule

```yaml
title: CVE-2026-20871 DWM DirectComposition Ink Trail Exploitation
id: a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d
status: experimental
description: >
    Detects suspicious DirectComposition activity characteristic of
    CVE-2026-20871 exploitation — process creating DComp ink trails
    followed by SYSTEM-level child process spawning from dwm.exe.
author: OnlyFm252 / STAR Labs SG
date: 2026/07/22
references:
    - https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-20871
    - https://www.elastic.co/security-labs/patch-diff-to-system
tags:
    - attack.privilege_escalation
    - attack.t1068
    - cve.2026.20871
logsource:
    product: windows
    category: process_creation
detection:
    selection:
        ParentImage|endswith: '\dwm.exe'
        IntegrityLevel: System
    filter_legitimate:
        Image|endswith:
            - '\LogonUI.exe'
            - '\consent.exe'
            - '\WerFault.exe'
    condition: selection and not filter_legitimate
level: critical
falsepositives:
    - Legitimate DWM child processes (rare)

---
title: CVE-2026-20871 DWM Crash or Restart After DirectComposition Activity
id: b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e
status: experimental
description: >
    Detects DWM crash or unexpected restart, which may indicate failed
    exploitation of CVE-2026-20871 (heap corruption from UAF).
author: OnlyFm252 / STAR Labs SG
date: 2026/07/22
tags:
    - attack.privilege_escalation
    - attack.t1068
logsource:
    product: windows
    service: system
detection:
    selection:
        EventID:
            - 7031  # Service terminated unexpectedly
            - 7034  # Service terminated unexpectedly
        Provider_Name: 'Service Control Manager'
        param1|contains: 'Desktop Window Manager'
    condition: selection
level: high
falsepositives:
    - Legitimate DWM crashes due to driver issues
    - GPU driver updates causing DWM restart
```

### Sysmon Configuration

```xml
<!-- CVE-2026-20871: DWM SuperWetInk UAF detection -->

<!-- Event 1: Suspicious child process from dwm.exe -->
<ProcessCreate onmatch="include">
    <ParentImage condition="end with">dwm.exe</ParentImage>
</ProcessCreate>

<!-- Event 7: DComp-related DLL loads in unusual processes -->
<ImageLoad onmatch="include">
    <ImageLoaded condition="end with">dcomp.dll</ImageLoaded>
</ImageLoad>
<ImageLoad onmatch="include">
    <ImageLoaded condition="end with">win32u.dll</ImageLoaded>
</ImageLoad>

<!-- Event 10: Process access to dwm.exe -->
<ProcessAccess onmatch="include">
    <TargetImage condition="end with">dwm.exe</TargetImage>
    <GrantedAccess condition="is">0x1F0FFF</GrantedAccess>
</ProcessAccess>

<!-- Event 11: Suspicious PE drops near exploitation -->
<FileCreate onmatch="include">
    <TargetFilename condition="contains">\AppData\Local\Temp\</TargetFilename>
    <Image condition="end with">dwm.exe</Image>
</FileCreate>
```

## Version Matrix

| Version | File Version | Status |
|---------|-------------|--------|
| Pre-patch (Dec 2025) | 10.0.26100.7309 | Vulnerable |
| Post-patch (Jan 2026, KB5074109) | 10.0.26100.7623 | Fixed (Feature_1732988217) |

## References

- [MSRC Advisory: CVE-2026-20871](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-20871)
- [Elastic Security Labs — "Patch diff to SYSTEM"](https://www.elastic.co/security-labs/patch-diff-to-system) by Joe Desimone
- [MITRE CWE-416: Use After Free](https://cwe.mitre.org/data/definitions/416.html)

---
*Analysis by OnlyFm252 / STAR Labs SG — 2026-07-22*
