# CVE-2020-17087 — Windows Kernel `cng.sys` IOCTL 0x390400 Pool Buffer Overflow

---

## Summary

| **Product**           | Microsoft Windows — `cng.sys` (Kernel Cryptography Next Generation driver) |
|-----------------------|------------------------------------------------------------------|
| **Vendor**            | Microsoft Corporation |
| **Severity**          | Important (MSRC) — CVSS v3.1 **7.8 (High)** |
| **CVSS Vector**       | `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H` ([MSRC](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2020-17087)) |
| **CVE Title**         | Windows Kernel Local Elevation of Privilege Vulnerability (MSRC) |
| **Affected Versions** | Windows 10 2004 KB4579311 and previous (GP0 RCA); also Windows 7/8.1/Server per MSRC |
| **Tested Version**    | Windows 10 2004/20H2 — cng.sys 10.0.19041.264 (pre-patch; unchanged through 19041.572 per winbindex) vs 10.0.19041.630 (post-patch) |
| **Impact**            | Elevation of Privilege — kernel pool overflow converted to arbitrary kernel read/write and SYSTEM (ITW: Chrome sandbox escape) |
| **CVE ID**            | CVE-2020-17087 |
| **CWE**               | CWE-190: Integer Overflow or Wraparound (leading to CWE-787 out-of-bounds write) |
| **PoC Available**     | Yes — GP0 issue 2104 crash trigger; blue-team trigger variant (`poc_cve_2020_17087.c`) derived from it |
| **Exploit Available** | Full exploit technique public (PixiePoint Security, Oct 2021 — Segment Heap BlockSize attack); **exploited in the wild** before patch, chained with Chrome 0day CVE-2020-15999 |
| **Patch Available**   | Yes |
| **Patch Date**        | November 10, 2020 — KB4586781 |
| **Discovered**        | Mateusz Jurczyk & Sergei Glazunov (Google Project Zero); disclosed 2020-10-30 as ITW 0day |

---

## Root Cause

`cng!CfgAdtpFormatPropertyBlock()` formats a caller-controlled byte buffer
into a space-separated hex dump in Unicode. Each input byte becomes three
UTF-16 code units — high-nibble hex char, low-nibble hex char, space — i.e.
**6 output bytes per input byte**. The allocation size is computed as
`SrcLen * 6` **in 16-bit arithmetic**, but the conversion loop iterates
`SrcLen` times:

```c
// cng.sys 10.0.19041.264 — CfgAdtpFormatPropertyBlock @ 0x1c006245c (verified decompile)
uVar2 = param_2 * 6;                          // uVar2 is USHORT -> truncates!
puVar3 = (ushort *)BCryptAlloc((ulonglong)uVar2);
if (puVar3 != NULL) {
    puVar4 = puVar3;
    do {                                      // loop count = full param_2
        *puVar4   = (ushort)"0123456789abcdefT"[*param_1 >> 4];
        puVar4[1] = (ushort)"0123456789abcdefT"[*param_1 & 0xf];
        puVar4[2] = 0x20;                       // L' '
        param_1++; puVar4 += 3;
    } while (--uVar6 != 0);
}
```

For `SrcLen = 0x2AAB`: `(USHORT)(0x2AAB * 6) = 2` bytes allocated,
`0x2AAB * 6 = 0x10002` bytes written — a controlled-length linear overflow of
a NonPagedPoolNx allocation (tag `Cngb`), with overflow content constrained to
the pattern `XX 00 XX 00 20 00` where `XX ∈ [0x30–0x39, 0x61–0x66]` (hex
digits + `'T'`). Note the length also reaches the function truncated to 16
bits at the call site (`movzx ecx, di` in
`CfgAdtReportFunctionPropertyOperation`), bounding the effective input to
`0x2AAB–0xFFFF`.

### The patch (verified via headless Ghidra decompile, 2026-07-22)

The post-patch build (cng.sys 10.0.19041.630) adds one function,
`RtlUShortMult`, and rewrites the allocation to use it — safe 16-bit
multiplication that **returns an error on overflow** instead of truncating.
The function bails out before any allocation when `len * 6` exceeds `0xFFFF`:

```c
// cng.sys 10.0.19041.630 — same function @ 0x1c006245c (verified decompile)
lVar2 = RtlUShortMult(param_2, /* 6 */, local_res8);
if (lVar2 < 0)
    return lVar2;                              // overflow -> reject, no alloc
puVar3 = (ushort *)BCryptAlloc((ulonglong)local_res8[0]);
```

(The decompiler renders the constant operand ambiguously; the unchanged
output-length bookkeeping — `param_3[1] = out; param_3[0] = out - 2` — pins
the helper's semantics to `out = len * 6`.) A `BCryptFree` cleanup path on
failure was added as well. ghidriff confirms this is the **only** function
with code changes across the whole driver (79% similarity), with
`RtlUShortMult` the single added function.

---

## Complete Call Flow (userspace → vulnerable code)

```
[userspace, any integrity level]
Option A (raw):  CreateFileA("\\\\.\\GLOBALROOT\\Device\\Cng", GENERIC_READ|GENERIC_WRITE)
                 DeviceIoControl(hCng, 0x390400, inBuf, 0x1000 + DataBufferSize, ...)
Option B (API):  BCryptSetContextFunctionProperty()  (bcrypt.dll — documented Win32 API,
                 reaches the same CNG config IOCTL path)

[kernel]
KsecDispatch (ksecdd.sys → cng)
  └─ cng!CngDispatch
       └─ cng!CngDeviceControl                 // extracts IOCTL + buffers
            └─ cng!ConfigIoHandler_Safeguarded // sanity checks, copies input,
                                               // IoUnpack_SG_ParamBlock_Header
                                               //   (magic 0x1A2B3C4D required)
                 └─ cng!_ConfigFunctionIoHandler
                      └─ cng!CfgAdtReportFunctionPropertyOperation
                           │  param_9 = (USHORT)DataBufferSize   ← truncation #1 (call site)
                           └─ cng!CfgAdtpFormatPropertyBlock     ← THE BUG
                                ├─ BCryptAlloc((USHORT)(len*6))  ← truncation #2
                                └─ hex-dump loop writes len*6 bytes → POOL OVERFLOW
```

### Relevant interfaces

| Layer | Interface |
|---|---|
| Device | `\Device\Cng` (open via `\\.\GLOBALROOT\Device\Cng`) — no special privileges needed |
| Win32 API (alt. route) | `BCryptSetContextFunctionProperty` (bcrypt.dll) |
| IOCTL | `0x390400` (CNG config "set function property" operation) |
| Kernel entry | `cng!CngDispatch` → `cng!CngDeviceControl` |
| Vulnerable sink | `cng!CfgAdtpFormatPropertyBlock` → `BCryptAlloc` + hex-format loop |

### IOCTL input layout (from the GP0 PoC / PixiePoint writeup)

| Offset | Size | Field |
|---|---|---|
| 0x00 | 4 | magic = `0x1A2B3C4D` (required; any other value never reaches the bug) |
| 0x04 | 4 | `0x10400` |
| 0x08 | 4 | `1` |
| 0x10 | 8 | `0x100` |
| 0x18 | 4 | `3` |
| 0x20 | 8 | offset of context name → `0x200` (`L"FUNCTION"`) |
| 0x28 | 8 | `0x300` |
| 0x30 | 8 | offset of property name → `0x400` (`L"PROPERTY"`) |
| 0x38 | 4 | `0` |
| 0x40 | 8 | `0x500` |
| 0x48 | 8 | `0x600` |
| 0x50 | 4 | **DataBufferSize** — the overflow field (`≥ 0x2AAB` triggers) |
| 0x58 | 8 | offset of data buffer → `0x1000` |
| 0x60 | 8 | `0` |

---

## Exploitation Scenario

**ITW (Oct 2020):** used as the kernel stage of a Chrome exploit chain —
CVE-2020-15999 (FreeType heap overflow, Chrome RCE in renderer) →
CVE-2020-17087 to escape the sandbox to SYSTEM. Per GP0, the ITW sample
"uses the buffer overflow to establish an arbitrary read/write primitive in
kernel space with the help of Named Pipe objects".

**Public technique (PixiePoint Security, tested Win10 1903–20H2):** the
constrained overwrite (`XX 00 XX 00 20 00`) is turned into full kernel R/W
via the Segment Heap **BlockSize attack**:

1. Groom VS-allocator subsegments (128 KB, 23 usable pages) with named-pipe
   `_NP_DATA_QUEUE_ENTRY` sprays: groups `g3`/`g1`, target chunks
   (BlockSize `0x3E0`), holes (`0x7F0` + fill `0x7B0`).
2. Trigger with `DataBufferSize = 0x2BF9` → allocation `0x7D6`, write
   `0x107D6`; lands in a hole and overwrites a target chunk's pool-header
   **BlockSize `0x3E → 0x64`** ("ghost chunk" now `0x640` bytes).
3. Enable dynamic lookaside for both sizes; free/realloc the ghost chunk to
   get a repeatable **controlled linear overflow** into the adjacent named
   pipe chunk.
4. Corrupt `_NP_DATA_QUEUE_ENTRY.{QuotaInEntry, DataSize}` → OOB read with
   `PeekNamedPipe`; leak a valid root-queue pointer; flip `DataEntryType` to
   Unbuffered with a userland fake `_IRP` → **arbitrary kernel read**.
5. Leak `npfs` base, `nt!ExpPoolQuotaCookie`, `nt!RtlpHpHeapGlobals`,
   `nt!PsInitialSystemProcess`; craft fake `_EPROCESS` objects whose
   `QuotaBlock` points into `Token.Privileges`; use the Quota ProcessBilled
   overwrite for **arbitrary decrement** → flip on `SeDebugPrivilege`.
6. Inject into `winlogon.exe` → SYSTEM shell.

Pool-grooming caveat from the writeup: guard pages appear every ~0x10 pages,
so the exploit needs a 128 KB subsegment layout to get a ≥ 64 KB overflow
"runway" without touching a guard page.

---

## Detection & Hunting (blue team)

### High-signal telemetry

| Source | Indicator |
|---|---|
| **Crash dumps** (`C:\Windows\MEMORY.DMP`, minidumps) | Bugcheck with stack through `cng!CfgAdtpFormatPropertyBlock` / `cng!CfgAdtReportFunctionPropertyOperation` — on an *unpatched* host, a trigger PoC or exploit grooming failure shows exactly here (e.g. `KERNEL_SECURITY_CHECK_FAILURE` / `BAD_POOL_HEADER` 0xC2 / `PAGE_FAULT_IN_NONPAGED_AREA` in `cng.sys`) |
| Sysmon **Event 1** / Security 4688 | Processes opening `\\.\GLOBALROOT\Device\Cng` (rare outside crypto libraries) that then die with the machine — EDR device-open telemetry |
| Handle/device audit | Non-system processes with a handle to `\Device\Cng` issuing IOCTL `0x390400` (ETW Microsoft-Windows-Kernel-IO / driver telemetry if available) |
| Behavioral | Massive named-pipe creation bursts (hundreds–thousands of pipes) from one process = pool-grooming signature of the PixiePoint/ITW technique |
| Post-exploitation | Processes unexpectedly gaining `SeDebugPrivilege` (token-privilege auditing, Sysmon EID 10 on winlogon.exe) |

### Sigma rule (starter)

```yaml
title: CVE-2020-17087 CNG Pool Overflow - Suspicious Named Pipe Grooming
id: 3f8a1c20-1708-74c2-9a10-020201708700
status: experimental
logsource:
  product: windows
  category: pipe_created
detection:
  selection:
    # Exploit sprays hundreds-thousands of pipes for Segment Heap grooming
    PipeName|startswith: '\'
  condition: selection
level: low   # deploy as a correlation/counting rule: >200 pipe_created by one Image in <60s
```

```yaml
title: CVE-2020-17087 CNG Pool Overflow - Direct CNG Device Access
id: 3f8a1c20-1708-74c2-9a10-020201708701
status: experimental
logsource:
  product: windows
  category: process_creation   # pair with command-line / image-load hunting
detection:
  selection:
    CommandLine|contains:
      - 'GLOBALROOT\Device\Cng'
      - 'Device\Cng'
  condition: selection
level: high
```

### YARA (PoC marker — IOCTL buffer constants)

```yara
rule CVE_2020_17087_PoC_marker {
    meta:
        description = "Detects cng.sys IOCTL 0x390400 overflow PoC tooling"
        reference = "https://bugs.chromium.org/p/project-zero/issues/detail?id=2104"
    strings:
        $magic  = { 4D 3C 2B 1A }                        // 0x1A2B3C4D message magic (LE)
        $ioctl  = { 00 04 39 00 }                        // IOCTL 0x390400 (LE)
        $dev    = "GLOBALROOT\\Device\\Cng" ascii
        $sz1    = "DataBufferSize" ascii
    condition:
        uint16(0) == 0x5A4D and ($dev or ($magic and $ioctl)) or ($magic and $sz1)
}
```

### Crash-dump triage (what a trigger looks like)

```
cng!CfgAdtpFormatPropertyBlock+0x4e   call cng!BCryptAlloc
; pool header of the victim allocation: Tag 'Cngb', BlockSize tiny
; overflow bytes on the page: "30 00 30 00 20 00 64 00 64 00 20 00 ..."
```

---

## Remediation

1. **Apply KB4586781** (November 2020) or later; verify
   `cng.sys ≥ 10.0.19041.630` (Win10 2004/20H2). Any currently supported
   Windows build is long patched — this package is for legacy/forensic work.
2. On legacy hosts, alert on the pipe-grooming + CNG-device patterns above.
3. Crash dumps from unpatched hosts showing `cng!CfgAdtpFormatPropertyBlock`
   are near-conclusive evidence of trigger/exploit attempts — preserve them
   for IR.

---

## Timeline

| Date | Event |
|---|---|
| 2020-10-30 | Google Project Zero discloses the bug as an ITW 0day (issue 2104), chained with Chrome CVE-2020-15999 |
| 2020-11-10 | Microsoft November 2020 Patch Tuesday (KB4586781), CVE-2020-17087 assigned; fix = `RtlUShortMult` overflow check |
| 2021-02-04 | GP0 publishes the 0days-in-the-wild RCA |
| 2021-10-22 | PixiePoint Security publishes the full Segment Heap exploitation technique |
| 2026-07-22 | This analysis: blue-team package; fix **verified** via ghidriff + headless Ghidra decompilation (`RtlUShortMult` in `CfgAdtpFormatPropertyBlock`, single changed function) |

---

## References

- [Google Project Zero — 0days in the Wild RCA: CVE-2020-17087](https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2020/CVE-2020-17087.html)
- [GP0 issue 2104 + crash PoC](https://bugs.chromium.org/p/project-zero/issues/detail?id=2104)
- [PixiePoint Security — Exploiting the CNG.sys IOCTL 0x390400 Pool Overflow](https://www.pixiepointsecurity.com/blog/nday-cve-2020-17087/)
- [MSRC — CVE-2020-17087](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2020-17087)
- [OnlyFm252 diff report — cng_sys-kb4586781](/data/patch_diffs/cng_sys-kb4586781.md)

---

<sub>Analysis: patch verified 2026-07-22 via ghidriff (VersionTrackingDiff) of
cng.sys 10.0.19041.264 vs 10.0.19041.630
(Windows 10 2004/20H2), plus headless Ghidra decompilation of
`CfgAdtpFormatPropertyBlock` (@ 0x1c006245c in both builds) and
`CfgAdtReportFunctionPropertyOperation`. Root cause and IOCTL layout per the
GP0 issue 2104 PoC; exploitation per the PixiePoint Security writeup. The
`poc_cve_2020_17087.c` trigger is a pure-C port of the GP0 crash PoC —
WARNING: it bugchecks vulnerable systems on purpose.</sub>
