CVE-2022-37969 — CLFS OOB Write via SignaturesOffset Corruption

Last updated: 2026-04-10
Component: clfs.sys
Bug Class: OOB write (out-of-bounds write via corrupted SignaturesOffset field)
Patch: September 2022 Patch Tuesday
Exploited ITW: Yes — September 2022 (Zscaler ThreatLabz capture); Kaspersky also attributes to same author as CVE-2022-24521
Exploit Author: Same threat actor as CVE-2022-24521, Exploit #2 (Sept 2022), CVE-2023-23376, CVE-2023-28252
Related: Clfs, Cve 2022 24521, Cve 2023 28252, Primitives
Tags: clfs, oob-write, signatureoffset, kernel-mode, lpe, token-steal


Summary

CVE-2022-37969 is an elevation-of-privilege vulnerability in clfs.sys discovered as a zero-day ITW by Zscaler ThreatLabz (September 2022). Root cause: lack of strict bounds checking for the SignaturesOffset field in the Base Block header. A specially crafted BLF file causes CLFS to overwrite SignaturesOffset with an abnormally large value during ResetLog → FlushMetaData → ClfsEncodeBlockPrivate, which bypasses the cbSymbolZone bounds check in AllocSymbol, leading to an OOB write that corrupts CLFS_CONTAINER_CONTEXT.pContainer in an adjacent BLF’s kernel allocation.

Patch: Added bounds check in CClfsBaseFilePersisted::LoadContainerQ validating the sum of cbSymbolZone + CLFS_BASE_RECORD_HEADER end address does not exceed the valid symbol zone.


Root Cause Analysis

Step 1: SignaturesOffset Overwrite

The exploit crafts a BLF with:

  • A fake CLFS_CLIENT_CONTEXT at offset 0x23A0 in the base log file with eState = 0x20 (CLFS_LOG_SHUTDOWN)
  • Modified client context offset array (rgClients) pointing to the fake context
  • SignaturesOffset initially set to 0x00000050 (standard value for Base Block)

When CreateLogFile opens the crafted BLF:

  1. CClfsBaseFilePersisted::OpenImage → ReadMetadataBlock allocates a 0x7a00 bigpool for the base block
  2. AcquireClientContext reads the crafted client context, detects eState = CLFS_LOG_SHUTDOWN
  3. CClfsLogFcbPhysical::ResetLog is called — this writes 0xFFFFFFFF00000000 at base_block + 0x1BF8 (a “reset” value at the 13th sector position)
  4. The sector signature at base_block + 0x1BFC is the last 2 bytes of this, which becomes 0xFFFF
  5. FlushMetaData → WriteMetadataBlock → ClfsEncodeBlock → ClfsEncodeBlockPrivate is called
  6. ClfsEncodeBlockPrivate iterates over sectors, copies sector signatures to the SignaturesOffset array
  7. The 14th sector’s signature (0xFFFF) is copied to SignaturesOffset + 0xE*2 = SignaturesOffset + 0x1C
  8. Since SignaturesOffset = 0x50, this writes at 0x50 + 0x1C = 0x6C in the block header
  9. Field at +0x6C in the block header is SignaturesOffset — it gets overwritten to 0xFFFF0050

Step 2: cbSymbolZone Bounds Check Bypass

AllocSymbol checks:

if ((char *)&BaseLogRecord[1] + cbSymbolZone + size > 
    (char *)(&logBlockHeader->MajorVersion + logBlockHeader->SignaturesOffset))
    return 0xC0000023;

With SignaturesOffset = 0xFFFF0050, the upper bound becomes an astronomically large value → any cbSymbolZone passes the check, even a crafted large value like 0x0001114B.

Step 3: OOB Write Corrupts Adjacent Allocation

The exploit creates two BLF allocations spaced exactly 0x11000 apart (using SystemBigPoolInformation/NtQuerySystemInformation(0x42) to verify spacing):

  • MyLog.blf — the crafted BLF (malicious cbSymbolZone = 0x11000 + 0x14B)
  • MyLxg_xxx.blf — the victim BLF (normal, but allocated adjacent in kernel bigpool)

When AddLogContainer(MyLog.blf handle) is called:

  1. CClfsBaseFilePersisted::AllocSymbol uses the inflated cbSymbolZone
  2. memset at the corrupt offset writes zeros into CLFS_CONTAINER_CONTEXT + 0x18 (pContainer) of MyLxg_xxx.blf’s base block
  3. The pContainer field now points to attacker-controlled memory (heap spray at 0x10000)

Step 4: Controlled Pointer Dereference

When the container handle is closed:

  1. CClfsRequest::Close → CClfsBaseFilePersisted::RemoveContainer
  2. Gets CLFS_CONTAINER_CONTEXT, extracts pContainer
  3. Calls virtual functions on the corrupted CClfsContainer pointer
  4. Vtable is at attacker-controlled spray address → controlled code execution

Exploit Primitives

Pool Spray Technique

// Create MyLog.blf to get first bigpool allocation
CreateLogFile("MyLog.blf", ...)

// Keep creating BLFs until adjacent allocations are 0x11000 apart
for (int i = 0; ...) {
    sprintf(name, "MyLog_%03d.blf", i);
    CreateLogFile(name, ...);
    // Query SystemBigPoolInformation(0x42) to check spacing
    if (offset_between_last_two == 0x11000) break;
}
// cbSymbolZone = 0x11000 + 0x14B

Heap Spray (Win11 — Pipe Attribute AAR/AAW Path)

Memory region 0x10000 ~ 0x1010000:
  offset N*8+8 → address of AttributeValueSize in PipeAttribute object

Memory at 0xFFFFFFFF:
  → addr(SYSTEM_EPROCESS) & 0xFFFFFFFFFFFFFFF000

Memory at 0x5000000:
  +0x08 → ClfsEarlierLsn (function address)
  +0x18 → SeSetAccessStateGenericMapping (function address)

The fake CClfsContainer vtable at 0x5000000:

  • First virtual call → ClfsEarlierLsn (returns RDX = 0xFFFFFFFF)
  • Second virtual call → SeSetAccessStateGenericMapping(RCX=HeapSpray, RDX=0xFFFFFFFF)
    • Reads AttributeValueSize pointer from RCX+0x48 (HeapSpray)
    • Reads content of 0xFFFFFFFFSYSTEM_EPROCESS & 0xFFFFFFFFFFFFFFF000
    • Writes this to AttributeValue field (at AttributeValueSize + 8)

Result: AttributeValue now points to SYSTEM EPROCESS. Read via NtFsControlFile(0x110038) to get SYSTEM TOKEN.

Token Replacement (Two Invocations)

Invocation 1 (read SYSTEM token):

  • Set 0xFFFFFFFF = SYSTEM_EPROCESS & ~0xFFF
  • Trigger → AttributeValue → SYSTEM_EPROCESS
  • NtFsControlFile(0x110038) reads SYSTEM_EPROCESS → extract Token at +0x4B8 (Win11)

Invocation 2 (write token):

  • Set 0xFFFFFFFF = SYSTEM_TOKEN value
  • Set HeapSpray+0x48 = address of current_process_token_field - 8
  • Trigger → writes SYSTEM_TOKEN to (current_process_token_field - 8) + 8 = current_process_token_field
  • Process now has SYSTEM token

Windows 10 Path (PreviousMode)

On Windows 10 (no pipe attribute technique available), the exploit uses the classic PreviousMode decrement:

  1. Gain arbitrary decrement via corrupt vtable → ObDereferenceObject with controlled pointer
  2. Use decrement on KTHREAD.PreviousMode offset (0x232 on Win10)
  3. Decrement 1 → 0 (KernelMode) → NtRead/WriteVirtualMemory bypass
  4. Read SYSTEM EPROCESS (walk EPROCESS.ActiveProcessLinks)
  5. Write SYSTEM token to current process

BLF Modifications Summary

Offset in .blf    Field                         Original → Modified
0x9A8             rgClients[0] offset            0xF8 → crafted offset to fake CLFS_CLIENT_CONTEXT
0x23A0+0x78       fake ClientCtx.eState          0x04 → 0x20 (CLFS_LOG_SHUTDOWN)
0x1B98            cbSymbolZone                   0xF8 → 0x0001114B (or 0x11000+0x14B)
0x80C             Checksum (base block CRC32)     recalculated

Affected Versions

  • Windows 10 (all supported builds before September 2022 patch)
  • Windows 11 21H2 version 22000.918 (patched in September 2022)

CVE Series Context

CVE-2022-37969 is Exploit #2 in the Kaspersky 5-exploit series (Boris Larin), following CVE-2022-24521 (Exploit #1). It adapts the same general cbSymbolZone + adjacent allocation strategy but uses a different trigger mechanism (SetLogArchiveMode / fAttributes overlap in the original; eState=CLFS_LOG_SHUTDOWN → ResetLog here per Zscaler analysis).

See Cve 2022 24521 for Exploit #1, Cve 2023 28252 for Exploit #5.


References

  • Zscaler ThreatLabz — “Technical Analysis of Windows CLFS Zero-Day CVE-2022-37969” (Parts 1+2) — 2022-10-14, 2022-10-28
  • Core Security — “Understanding CVE-2022-37969” (detailed WinDbg walkthrough) — 2023-03
  • Kaspersky GReAT / Boris Larin — “Windows CLFS and five exploits (Exploit #2)” — securelist.com, 2023-12-21