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:
ClfsEncodeBlockdetects the bad value, zeroes the checksum, and returns an errorWriteMetadataBlockdoes not check the return value — the corrupted block (checksum=0) is flushed to disk
On the next OpenImage call:
ReadMetadataBlockcomparesDumpCountbetween 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 Offset | Field | Value | Purpose |
|---|---|---|---|
+0x006 | ValidSectorCount | 0x0001 | Trigger encode failure → zero checksum on write |
+0x070 | DumpCount (in LogBlockHeader) | 0x0002 | Higher than SHADOW → CONTROL selected first |
+0x084 | eExtendState | 0x0002 (ClfsExtendStateFlushingBlock) | Arm extension state machine |
+0x088 | iExtendBlock | 0x0002 | Non-zero, in-range — triggers extend path |
+0x08A | iFlushBlock | 0x0002 | Matches iExtendBlock |
+0x094 | cExtendSectors | 0x0001 | Forces extension logic |
CONTROL SHADOW Block (offset 0x400)
| File Offset | Field | Value | Purpose |
|---|---|---|---|
+0x406 | ValidSectorCount | 0x0002 | Valid (matches TotalSectors) |
+0x470 | DumpCount | 0x0001 | Lower than CONTROL → selected after CONTROL is zeroed |
+0x484 | eExtendState | 0x0002 | Continues extend path |
+0x488 | iExtendBlock | 0x0013 | OOB index — attacker-controlled |
+0x48A | iFlushBlock | 0x0013 | OOB index — attacker-controlled |
GENERAL / GENERAL SHADOW Blocks (offsets 0x800 / 0x4C00)
| File Offset | Field | Value | Purpose |
|---|---|---|---|
+0x1B98 | cbSymbolZone | 0x6543 | Overfills symbol zone → AllocSymbol returns 0xC0000023 → triggers ExtendMetadataBlock |
+0x9598 | cbSymbolZone | 0x6543 | Same 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)
| CVE | Patch Month | Technique | ITW |
|---|---|---|---|
| CVE-2022-24521 | April 2022 | Structure overlapping via iExtendBlock/iFlushBlock | Yes |
| Unknown | ~Sept 2022 | CLFS metadata corruption (undisclosed) | Yes |
| Unknown | ~Oct 2022 | CLFS metadata corruption (undisclosed) | Yes |
| CVE-2023-23376 | Feb 2023 | Index verification bypass (similar to #5) | Yes |
| CVE-2023-28252 | April 2023 | Encode/decode inconsistency → OOB increment | Yes |
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\SAMfor 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.GenericPDM:Trojan.Win32.GenericHEUR:Trojan-Ransom.Win32.GenericWin64.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:
| Function | Uses iExtendBlock/iFlushBlock? | Patched in CVE-2023-28252? |
|---|---|---|
WriteMetadataBlock | Yes | Yes |
GetControlRecord | Yes | Yes |
ReadMetadataBlock | Indirectly | Unclear |
FlushMetadataBlock | Check required | Unknown |
AllocSymbol failure path | Triggers extension | Unchanged |
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
