# Root Cause Analysis — CVE-2021-31969

## Metadata

| Field | Value |
|---|---|
| **CVE** | CVE-2021-31969 |
| **Binary** | cldflt.sys (Windows Cloud Files Mini Filter Driver) |
| **Severity** | Important (CVSS 7.8) |
| **Impact** | Elevation of Privilege |
| **Bug Class** | CWE-190: Integer Underflow → Paged Pool Overflow |
| **Patch** | KB5003646 (June 2021) |
| **Exploit Author** | Chen Le Qi (STAR Labs SG) |

## 1. Executive Summary

CVE-2021-31969 is an integer underflow in `cldflt.sys` `HsmpRpiDecompressBuffer` that results in a controlled paged pool overflow from a 0x20-byte allocation. The `cstmDataSize` field in cloud filter reparse data controls the pool allocation size (`cstmDataSize + 8`). Without a minimum value check, a value < 4 causes `allocatedSize - 12` to underflow to `0xFFFFFFF4`, which is passed as `UncompressedBufferSize` to `RtlDecompressBuffer`. By marking the LZNT1 header as uncompressed, `RtlDecompressBuffer` acts as `memcpy` with attacker-controlled size and content.

## 2. Vulnerable Function

**HsmpRpiDecompressBuffer** in `cldflt.sys`

### Pre-patch (KB5003217)

```c
v7 = *(_DWORD *)(a1 + 8);
someSize = HIWORD(v7);
// No minimum check on someSize
allocatedSize = someSize + 8;
allocatedMem = ExAllocatePoolWithTag(PagedPool, someSize + 8, 'pRsH');
// ...
v3 = RtlDecompressBuffer(
    COMPRESSION_FORMAT_LZNT1,
    (PUCHAR)allocatedMem + 12,    // uncompressed_buffer
    allocatedSize - 12,           // BUG: underflows if allocatedSize < 12
    (PUCHAR)(a1 + 12),            // compressed_buffer (user-controlled)
    a2 - 12,                      // compressed_size
    (PULONG)va);
```

### Post-patch (KB5003646)

```c
someSize = *(_WORD *)(a1 + 10);
if (someSize >= 4u) {   // NEW: minimum size check
    // ... rest of function
}
```

## 3. Root Cause

The `cstmDataSize` WORD field at offset +2 in `HSM_REPARSE_DATA` controls the allocation. With `cstmDataSize = 0`:
- `allocatedSize = 0 + 8 = 8`
- Pool allocation: 0x8 bytes → rounded up to 0x20 by pool allocator
- `UncompressedBufferSize = 8 - 12 = 0xFFFFFFF4` (unsigned underflow)

The LZNT1 compressed buffer is user-controlled. By setting the first WORD of the buffer to specify "uncompressed" data, `RtlDecompressBuffer` simply copies the data verbatim. Since the LZNT1 format supports multiple chunks of up to 0x1000 bytes each, and `HsmpRpReadBuffer` retrieves up to 0x4000 bytes of reparse data, the attacker can overflow approximately 4 pages past the 0x20 allocation.

## 4. Reachability / Attack Surface

### Call chain

```
Attacker opens directory in sync root
  -> NTFS post-create callback
    -> HsmFltPostCREATE
      -> HsmpSetupContexts
        -> HsmpRpReadBuffer
          -> FltFsControlFile(FSCTL_GET_REPARSE_POINT)
          -> HsmpRpiDecompressBuffer
            -> RtlDecompressBuffer(underflowed size)  // OVERFLOW
```

### Prerequisites

1. Register cloud sync root via `CfRegisterSyncRoot()` + `CfConnectSyncRoot()` (attaches cldflt.sys minifilter)
2. Create directory in sync root
3. Set reparse data with `FSCTL_SET_REPARSE_POINT_EX` (pre-op blocks `FSCTL_SET_REPARSE_POINT`)
4. Craft HSM_REPARSE_DATA with `cstmDataSize = 0`, `flag = 0x8000`, LZNT1 header marking data as uncompressed
5. Close and reopen the directory handle to trigger decompression path

### Pool constraints

- Allocation size: 0x20 bytes (LFH-managed for common sizes)
- Overflow content and size: fully attacker-controlled
- Maximum overflow: ~0x4000 bytes (limited by `HsmpRpReadBuffer` retrieval size)

## 5. Exploitation (STAR Labs — Chen Le Qi)

### Challenge

The 0x20 allocation falls in the Low Fragmentation Heap (LFH), which only places it adjacent to other 0x20-sized chunks. Finding a 0x20-sized object that provides both read and write primitives is impractical.

### Strategy: overflow past LFH into VS subsegment

1. **LFH exhaustion spray**: Allocate many `_TERMINATION_PORT` objects (0x20 each, paged pool) via `NtRegisterThreadTerminatePort` to fill existing LFH buckets and force new segment allocation.
2. **VS subsegment spray**: Allocate `_WNF_STATE_DATA` and `_TOKEN` objects in the Variable Size allocator, hoping they land contiguous to an LFH bucket.
3. **Overflow**: Write 4 pages of DWORDs with value `0x1000` past the LFH allocation. If a WNF object is hit, its `AllocatedSize` and `DataSize` fields are overwritten to `0x1000`, granting relative page read/write.
4. **Identify corruption**: Query all WNF state names; objects returning `STATUS_BUFFER_TOO_SMALL` have enlarged `DataSize`. Check that the adjacent `_TOKEN` is undamaged by reading its `TokenId`.

### Arbitrary read (via _TOKEN)

Set `Token->BnoIsolationHandlesEntry` to a usermode buffer with forged `IsolationPrefix.Buffer` and `MaximumLength`. Call `NtQueryInformationToken(TokenBnoIsolation)` — kernel copies data from the forged pointer to usermode output buffer.

### Arbitrary write (via _TOKEN)

Set `Token->PrimaryGroup` to one byte before a null to make the DynamicPart offset calculation yield +2. Set `Token->DynamicPart` to `(target_address - 8)`. Call `NtSetInformationToken(TokenDefaultDacl)` with a crafted ACL buffer — `SepAppendDefaultDacl` writes the buffer to the computed address.

### PreviousMode null

Target `_KTHREAD + 0x232` (PreviousMode). Once zeroed, the exploit thread can use `NtReadVirtualMemory`/`NtWriteVirtualMemory` directly for kernel R/W.

### Token theft

Walk `ActiveProcessLinks` from any leaked EPROCESS → find System (PID 4) → copy Token to exploit process.

### Success rate

~1 in 15 attempts. Primary failure mode: WNF corruption also damages the adjacent TOKEN.

## 6. Related Vulnerabilities

- **CVE-2023-36036**: Same function (`HsmpRpiDecompressBuffer`), different root cause — missing maximum cap on `cstmDataSize` allowing OOB. Exploited in the wild. Patched November 2023.

## 7. Detection

### YARA rule

```yara
rule CVE_2021_31969_cldflt_pool_overflow {
    meta:
        description = "Detects exploit artifacts for CVE-2021-31969 (cldflt.sys integer underflow)"
        cve = "CVE-2021-31969"
        author = "OnlyFm252"
    strings:
        $sync_reg = "CfRegisterSyncRoot" ascii
        $reparse_ex = "FSCTL_SET_REPARSE_POINT_EX" ascii
        $terminate = "NtRegisterThreadTerminatePort" ascii
        $wnf_create = "NtCreateWnfStateName" ascii
        $dup_token = "DuplicateTokenEx" ascii
        $set_token = "NtSetInformationToken" ascii
        $bno = "TokenBnoIsolation" ascii
    condition:
        uint16(0) == 0x5A4D and
        3 of ($sync_reg, $reparse_ex, $terminate) and
        2 of ($wnf_create, $dup_token, $set_token, $bno)
}
```

### Sigma rule

```yaml
title: CVE-2021-31969 cldflt.sys Pool Overflow Exploitation
id: a9b7c2d1-2021-31969-cldflt-pool
status: experimental
description: Detects cloud filter sync root registration followed by reparse point manipulation
references:
    - https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31969
    - https://starlabs.sg/blog/2023/11-exploitation-of-a-kernel-pool-overflow-from-a-restrictive-chunk-size-cve-2021-31969/
logsource:
    product: windows
    category: driver_load
detection:
    selection_driver:
        ImageLoaded|endswith: '\cldflt.sys'
    filter_normal:
        Image|contains:
            - 'OneDrive'
            - 'svchost.exe'
    condition: selection_driver and not filter_normal
falsepositives:
    - Third-party cloud file sync providers
level: medium
```

## 8. References

- MSRC: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-31969
- STAR Labs writeup: https://starlabs.sg/blog/2023/11-exploitation-of-a-kernel-pool-overflow-from-a-restrictive-chunk-size-cve-2021-31969/
- LZNT1 spec: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-xca/94164d22-2928-4417-876e-d193766c4db6
