CVE-2023-28252 — CLFS OOB Write → Arbitrary Increment → LPE (Nokoyawa)

Last updated: 2026-04-10
Severity: High
CVSS: 7.8
Component: clfs.sys — Common Log File System driver
Affected versions: Windows 10, 11; Server 2008/2012/2016/2019/2022
Bug class: OOB write (arbitrary increment primitive)
Privilege required: Medium IL (standard user)
Privilege gained: User → SYSTEM
Patch: April 11, 2023 Patch Tuesday — CClfsBaseFile::GetControlRecord + WriteMetadataBlock
Public exploit: Yes (Fortra PoC, Metasploit module)
ITW: Yes — Nokoyawa ransomware, same exploit author as 4 prior CLFS CVEs
Related: Clfs, Primitives, Pool Internals, Integer Overflows


Vulnerability Summary

A logic flaw in CLFS’s encode/decode validation allows an attacker to force a metadata block’s checksum to zero by triggering a specific error path in WriteMetadataBlock. On the next file open, ReadMetadataBlock falls back to the shadow block (whose checksum is valid). The shadow block contains attacker-controlled iExtendBlock / iFlushBlock index fields — these are out-of-bounds array indices fed into an arithmetic expression that produces an OOB offset into m_rgBlocks[]. The result is an arbitrary increment of one byte at a kernel-controlled address, which the exploit escalates into PreviousMode manipulation and full token steal.


Root Cause Analysis

The Encode/Decode Inconsistency

ClfsDecodeBlockPrivate validates TotalSectorCount and block flags but does not validate ValidSectorCount.
ClfsEncodeBlockPrivate validates all three including ValidSectorCount.

When WriteMetadataBlock calls ClfsEncodeBlock on a block with an invalid ValidSectorCount:

  • ClfsEncodeBlock detects the bad value, zeroes the checksum, and returns an error
  • WriteMetadataBlock does not check the return value — the corrupted block (checksum=0) is flushed to disk

On the next OpenImage call:

  • ReadMetadataBlock compares DumpCount between CONTROL and CONTROL_SHADOW
  • CONTROL block has checksum=0 → treated as invalid → SHADOW block is selected
  • SHADOW block is entirely attacker-controlled

The OOB Index in WriteMetadataBlock

Inside WriteMetadataBlock, the attacker-controlled iFlushBlock field (from the shadow CONTROL record) is used to index m_rgBlocks[]:

; iFlushBlock = 0x13 (controlled)
lea  rcx, [rsi*2]     ; rcx = 0x26
add  rcx, rsi         ; rcx = 0x39  (= iFlushBlock * 3)
lea  r8,  [rcx*8]     ; r8  = 0x1C8 (= iFlushBlock * 24)
; m_rgBlocks has only 6 entries × 0x30 bytes = 0xC0 bytes total
; iFlushBlock=0x13 → offset 0x1C8 → OOB by 0x108 bytes
mov  r14, qword ptr [m_rgBlocks_base + r8 + rcx]   ; OOB read of pbImage ptr
inc  qword ptr [rax + r14]                           ; increment byte at loaded address

The OOB read loads pbImage from an address 0x108 bytes past the end of m_rgBlocks — which in a groomed heap lands inside an attacker-controlled _CLFS_CONTAINER_CONTEXT or log block object. The attacker sets pbImage in that fake structure to point to their target. The inc then increments one byte at that target address.


Exact BLF Modifications

These are the precise field patches needed (relative to BLF file start):

CONTROL Block (offset 0x000)

File OffsetFieldValuePurpose
+0x006ValidSectorCount0x0001Trigger encode failure → zero checksum on write
+0x070DumpCount (in LogBlockHeader)0x0002Higher than SHADOW → CONTROL selected first
+0x084eExtendState0x0002 (ClfsExtendStateFlushingBlock)Arm extension state machine
+0x088iExtendBlock0x0002Non-zero, in-range — triggers extend path
+0x08AiFlushBlock0x0002Matches iExtendBlock
+0x094cExtendSectors0x0001Forces extension logic

CONTROL SHADOW Block (offset 0x400)

File OffsetFieldValuePurpose
+0x406ValidSectorCount0x0002Valid (matches TotalSectors)
+0x470DumpCount0x0001Lower than CONTROL → selected after CONTROL is zeroed
+0x484eExtendState0x0002Continues extend path
+0x488iExtendBlock0x0013OOB index — attacker-controlled
+0x48AiFlushBlock0x0013OOB index — attacker-controlled

GENERAL / GENERAL SHADOW Blocks (offsets 0x800 / 0x4C00)

File OffsetFieldValuePurpose
+0x1B98cbSymbolZone0x6543Overfills symbol zone → AllocSymbol returns 0xC0000023 → triggers ExtendMetadataBlock
+0x9598cbSymbolZone0x6543Same in shadow copy

Full Exploitation Sequence

1. CreateLogFile("MyLog.blf", OPEN_ALWAYS)
   → Creates clean BLF with valid metadata

2. Patch BLF with above field modifications (direct file I/O)

3. CreateLogFile("MyLog.blf", OPEN_EXISTING)
   → Triggers OpenImage → ReadMetadataBlock selects CONTROL (DumpCount=2)
   → ExtendMetadataBlock executes (eExtendState=FlushingBlock, iExtendBlock=2)
   → WriteMetadataBlock calls ClfsEncodeBlock on CONTROL with ValidSectors=1
   → ClfsEncodeBlock fails → zeros checksum → writes to disk (return value ignored)
   → CONTROL block now has checksum=0 on disk

4. AddLogContainer("C:\Users\Public\.container0")
   → AddContainer → AddSymbol → FindSymbol → AllocSymbol
   → cbSymbolZone=0x6543 overflows available space → returns 0xC0000023
   → Error triggers ExtendMetadataBlock again
   → ReadMetadataBlock now selects CONTROL_SHADOW (CONTROL checksum=0)
   → CONTROL_SHADOW has iFlushBlock=0x13 (attacker-controlled)
   → WriteMetadataBlock executes OOB index arithmetic
   → r8 offset = 0x13 * 24 = 0x1C8 (past end of m_rgBlocks[6])
   → OOB read loads pbImage from attacker-groomed fake container object
   → inc [rax + r14] increments one byte at attacker's chosen kernel address

5. The increment target = low byte of a CLFS log block header pointer
   → Increments 0x14 → 0x15, redirecting to attacker's fake _CLFS_CONTAINER_CONTEXT
   → Fake container has pbImage pointing to controlled user-space address (e.g., 0x5000000)

From Increment Primitive to AAR/AAW

Windows 10 Path: PreviousMode Overwrite

Increment targets _KTHREAD.PreviousMode offset
→ Set PreviousMode = KernelMode (0)
→ NtWriteVirtualMemory now accepts kernel addresses → full AAW
→ NtReadVirtualMemory now accepts kernel addresses → full AAR

Windows 11 Path: Pipe Attribute Corruption

Increment targets PIPE_ATTRIBUTE structure field
→ Corrupt pipe attribute linked-list pointer
→ NtFsControlFile(FSCTL_PIPE_GET_HANDLE_ATTRIBUTE) → reads from attacker address → AAR
→ NtFsControlFile(FSCTL_PIPE_SET_HANDLE_ATTRIBUTE) → writes to attacker address → AAW

Both paths give arbitrary kernel read/write with no SMEP bypass needed (pure data-only).


Pool Grooming

1. Allocate 10 named pipes → occupy NonPaged/Paged pool at target size
2. Close specific handles → create holes of correct size
3. Open malformed BLF → CLFS allocates m_rgBlocks structure into a hole
4. Fake _CLFS_CONTAINER_CONTEXT placed via separate allocation at offset 0x1570
   relative to m_rgBlocks base

Sizing: CLFS metadata block allocations are sized by TotalSectorCount * 0x200
→ Attacker controls TotalSectorCount → controls allocation size → matches pipe buffer holes

KASLR Defeat

NtQuerySystemInformation(SystemHandleInformation, ...)
// or SystemModuleInformation for kernel base
// Requires Medium IL — standard user session qualifies
// Leaks kernel object addresses → compute ntoskrnl base → find _KTHREAD offsets

Same technique documented in PuzzleMaker (2021) and MysterySnail (2021) campaigns by same threat actor cluster.


Token Steal (Final Stage)

1. NtQuerySystemInformation(SystemProcessInformation) → get SYSTEM process EPROCESS addr
2. Read System.Token via AAR primitive
3. Write System.Token to CurrentProcess.Token via AAW
→ Current process is now SYSTEM

HVCI-compatible: no shellcode, no code injection, pure data writes.


Patch Analysis

Microsoft’s fix in April 2023 added two checks:

CClfsBaseFile::GetControlRecord: validates that computed offsets (from iExtendBlock / iFlushBlock) are within the m_rgBlocks array bounds (max index = cBlocks - 1 = 5). Any index ≥ 6 is rejected.

CClfsBaseFilePersisted::WriteMetadataBlock: checks return value of ClfsEncodeBlock — if encoding fails, the block is not written to disk. This prevents the CONTROL block checksum zeroing that causes fallback to the attacker-controlled shadow.

Patch bypassable? The root issue (encode/decode inconsistency, unchecked return values) was present in multiple callsites. CVE-2023-23376 (two months earlier) had an almost identical bypass — the same group exploited a sibling code path. History strongly suggests variant patches are incomplete.


The Five CLFS CVEs (Same Exploit Author, ~June 2022 – April 2023)

CVEPatch MonthTechniqueITW
CVE-2022-24521April 2022Structure overlapping via iExtendBlock/iFlushBlockYes
Unknown~Sept 2022CLFS metadata corruption (undisclosed)Yes
Unknown~Oct 2022CLFS metadata corruption (undisclosed)Yes
CVE-2023-23376Feb 2023Index verification bypass (similar to #5)Yes
CVE-2023-28252April 2023Encode/decode inconsistency → OOB incrementYes

Pattern: Each fix narrowly patches the specific bypass found. The attacker consistently finds the next sibling code path within weeks. Kaspersky notes at least 32 CLFS CVEs since 2018 total (all researchers combined).


Threat Actor Profile

  • Cluster: Sophisticated cybercrime/ransomware affiliate, likely FIN12-adjacent
  • TTPs: Cobalt Strike BEACON (custom loaders) → CLFS LPE → Nokoyawa ransomware OR “Pipemagic” backdoor (MSBuild delivery)
  • Post-exploitation: reg save HKLM\SAM for credential extraction
  • Sectors targeted: Retail/wholesale, energy, manufacturing, healthcare, software development
  • Geographic scope: Global
  • Connection to prior campaigns: Same KASLR leak technique (NtQuerySystemInformation) as PuzzleMaker and MysterySnail — suggesting either shared tooling or same developer

Artifacts / Detection

On-disk indicators:

C:\Users\Public\.container*
C:\Users\Public\MyLog*.blf
C:\Users\Public\p_*

Kaspersky detections:

  • PDM:Exploit.Win32.Generic
  • PDM:Trojan.Win32.Generic
  • HEUR:Trojan-Ransom.Win32.Generic
  • Win64.Agent.*

WinDbg hunting:

ba w1 clfs!CClfsBaseFilePersisted::WriteMetadataBlock+<offset>
// Look for iFlushBlock > 5 at the index validation point
!pool @rcx   // inspect pool chunk receiving OOB access

Variant Analysis

Per the five-CVE pattern, all CLFS m_rgBlocks index consumers are suspects:

FunctionUses iExtendBlock/iFlushBlock?Patched in CVE-2023-28252?
WriteMetadataBlockYesYes
GetControlRecordYesYes
ReadMetadataBlockIndirectlyUnclear
FlushMetadataBlockCheck requiredUnknown
AllocSymbol failure pathTriggers extensionUnchanged

Recommendation: audit all functions that receive iExtendBlock/iFlushBlock from a CLFS_CONTROL_RECORD — specifically any path reachable via the shadow block after CONTROL checksum zeroing.


References

  • Kaspersky GReAT — “Nokoyawa ransomware attacks with Windows zero-day” — securelist.com/nokoyawa-ransomware-attacks-with-windows-zero-day/109483/
  • Kaspersky GReAT — “Windows CLFS and five exploits used by ransomware operators (Exploit #5 – CVE-2023-28252)” — securelist.com/windows-clfs-exploits-ransomware-cve-2023-28252/111601/
  • Google Project Zero — 0days-in-the-wild RCA — googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2023/CVE-2023-28252.html
  • uhg.sg — “A Deep Dive into CVE-2023-28252” — blog.uhg.sg/article/23.html
  • Fortra PoC — github.com/fortra/CVE-2023-28252
  • Metasploit module — rapid7/metasploit-framework exploits/windows/local/cve_2023_28252_clfs_driver.rb
  • MSRC Advisory — msrc.microsoft.com/update-guide/vulnerability/CVE-2023-28252