csvnsflt.sys — Use-After-Free race from unlocked global reads in CsvCamSetSecurityInfo (CWE-416, CWE-362) fixed
KB5078752
1. Overview
- Unpatched Binary:
csvnsflt_unpatched.sys - Patched Binary:
csvnsflt_patched.sys - Overall Similarity: 0.9912
- Diff Statistics: 137 matched functions, 1 changed function, 136 identical functions, 0 unmatched functions in either direction.
- Verdict: The patch fixes a use-after-free that is caused by a lock imbalance in
CsvCamSetSecurityInfo. Two code regions in the unpatched build read and update global objects with no lock, while other regions of the same function free and replace those objects while holdingSigningLockexclusive. The patch adds a shared-lock acquire/release around both previously-unlocked regions so readers are serialized against the exclusive writer.
2. Vulnerability Summary
- Severity: High
- Vulnerability Class: Use-After-Free caused by a data race / missing synchronization (CWE-416, CWE-362)
- Affected Function:
CsvCamSetSecurityInfo @ 0x1C0013304
Root Cause:
CsvCamSetSecurityInfo manages two process-wide objects, both protected by the same lock SigningLock (an RWLock wrapping an ERESOURCE):
EncryptionManager, a pointer to anEncryptionobject. This is a0x28-byte PagedPool allocation (tag0x41435643) that holds BCrypt algorithm/key/hash handles and buffers. It is freed byEncryption::\scalar deleting destructor'`, which also releases its embedded BCrypt handles and buffers.ReplayCacheManagerObject, a0x1010-byte PagedPool allocation (tag0x41435643) that holds the global sequence-number state. It is freed byExFreePoolWithTag.
In the unpatched build two regions of this function touch these globals with no lock held:
- At
0x1C0013332the function readsEncryptionManagerinto a register with no lock, then dereferences the captured pointer at+0x20(0x1C0013341) and+0x18(0x1C0013348) and passes the+0x18field tomemcmp. - At
0x1C0013617the sequence-number update loop readsReplayCacheManagerObjectwith no lock, dereferences it at+0x18/+0x10/+0x4, and writes it withxchg [rbx+0x20](0x1C001363F) andlock cmpxchg [rbx+0x18](0x1C0013645).
Meanwhile, the replacement paths in the same function acquire SigningLock exclusive (ExAcquireResourceExclusiveLite) before freeing the old object and installing a new one: the old EncryptionManager is freed at 0x1C0013401; the old ReplayCacheManagerObject is freed at 0x1C001359F. Because the reader regions hold no lock, a second concurrent invocation can free the object between the moment the pointer is captured and the moment its fields are read or written. The region 1 race is a use-after-free read of a freed Encryption object; the region 2 race is a use-after-free write (interlocked update) into a freed ReplayCacheManager object.
The memcmp at 0x1C0013353 compares user-supplied bytes against the object's +0x18 buffer and returns only an equal / not-equal boolean (memcmp(...) == 0); it does not return the compared bytes to the caller, so this path is not an information-disclosure channel.
The patch wraps both previously-unlocked regions in KeEnterCriticalRegion + ExAcquireResourceSharedLite(SigningLock) ... ExReleaseResourceLite + KeLeaveCriticalRegion, so the reads and the interlocked writes are serialized against the exclusive writer that frees the objects.
Attacker-Reachable Entry Point & Data Flow:
1. User-Mode API: A caller sends a message with FilterSendMessage(hPort, buffer, ...) to the minifilter communication port.
2. Message Notify Callback: The registered callback NfltMessage @ 0x1C0005F60 receives the message. It validates the length, allocates a pool copy (tag 0x634D664E), ProbeForReads the input, copies it with memmove, and switches on the first DWORD.
3. Opcode Dispatch: When the first DWORD equals 6 and the message length is >= 0x14, the callback calls CsvCamSetSecurityInfo(buffer+4) at 0x1C0006299.
4. Vulnerable Region: CsvCamSetSecurityInfo performs the unlocked global reads/updates described above.
3. Pseudocode Diff
// === UNPATCHED CsvCamSetSecurityInfo @ 0x1C0013304 (region 1: unlocked read) ===
if ( a1[2] != 0 )
{
v3 = 1;
// EncryptionManager is read and dereferenced with NO lock held:
if ( EncryptionManager != nullptr
&& ((v6 = *((_DWORD *)EncryptionManager + 8), a1[2] != v6) // deref +0x20
? (v7 = false)
: (v7 = memcmp(a1 + 3, *((const void **)EncryptionManager + 3), v6) == 0), // deref +0x18, boolean result
v7) )
{
v3 = 0;
}
else
{
// allocate + Encryption::Initialize, then under EXCLUSIVE lock free the old object:
KeEnterCriticalRegion();
ExAcquireResourceExclusiveLite(*SigningLock, 1u);
if ( EncryptionManager != nullptr )
Encryption::`scalar deleting destructor'(EncryptionManager); // FREE old object
EncryptionManager = v9; // install new object
ExReleaseResourceLite(*SigningLock);
KeLeaveCriticalRegion();
}
}
// ... region 2: the sequence-number loop reads/writes ReplayCacheManagerObject with NO lock ...
// === PATCHED CsvCamSetSecurityInfo @ 0x1C0013304 (region 1 now locked) ===
if ( a1[2] > 0 )
{
KeEnterCriticalRegion();
ExAcquireResourceSharedLite(*SigningLock, 1u); // NEW: shared lock before the read
if ( EncryptionManager != nullptr )
{
v6 = *((_DWORD *)EncryptionManager + 8);
if ( a1[2] != v6 )
v7 = false;
else
v7 = memcmp(a1 + 3, *((const void **)EncryptionManager + 3), v6) == 0;
// ...
}
ExReleaseResourceLite(*SigningLock); // NEW: release after the read
KeLeaveCriticalRegion();
}
// ... region 2: the sequence-number loop is likewise wrapped in a shared lock ...
4. Assembly Analysis
Unpatched — region 1: unlocked read of EncryptionManager
00000001C001331C mov eax, [rcx+8] ; eax = arg1[2] (user-supplied size field)
00000001C001332A test eax, eax
00000001C001332C jz loc_1C001342F
00000001C0013332 mov rdx, cs:?EncryptionManager@@3PEAVEncryption@@EA ; read global with NO lock held
00000001C001333C test rdx, rdx
00000001C001333F jz short loc_1C001336D
00000001C0013341 mov ecx, [rdx+20h] ; deref of possibly-freed object
00000001C0013344 cmp eax, ecx
00000001C0013346 jnz short loc_1C001335F
00000001C0013348 mov rdx, [rdx+18h] ; Buf2 = pointer field of possibly-freed object
00000001C001334C mov r8d, ecx ; Size
00000001C001334F lea rcx, [rsi+0Ch] ; Buf1 = user data
00000001C0013353 call memcmp ; returns equal/not-equal only (boolean)
Unpatched — the exclusive-lock free that region 1 races
00000001C00133D1 mov rbx, cs:?SigningLock@@3PEAVRWLock@@EA
00000001C00133D8 call cs:__imp_KeEnterCriticalRegion
00000001C00133E9 call cs:__imp_ExAcquireResourceExclusiveLite ; EXCLUSIVE
00000001C00133F5 mov rcx, cs:?EncryptionManager@@3PEAVEncryption@@EA
00000001C00133FF jz short loc_1C0013406
00000001C0013401 call ??_GEncryption@@QEAAPEAXI@Z ; frees old EncryptionManager
00000001C001340D mov cs:?EncryptionManager@@3PEAVEncryption@@EA, rdi ; install new object
00000001C0013417 call cs:__imp_ExReleaseResourceLite
00000001C0013423 call cs:__imp_KeLeaveCriticalRegion
Unpatched — region 2: unlocked read/update of ReplayCacheManagerObject
00000001C0013617 mov edi, [rsi+4]
00000001C001361A mov rbx, cs:?ReplayCacheManagerObject@@3PEAV... ; read global with NO lock held
00000001C0013621 mov esi, [rbx+18h] ; deref of possibly-freed object
00000001C001363F xchg rax, [rbx+20h] ; write into possibly-freed object
00000001C0013645 lock cmpxchg [rbx+18h], edi ; interlocked write into possibly-freed object
The old ReplayCacheManagerObject is freed under the exclusive lock at 0x1C001359F (ExFreePoolWithTag), which region 2 can race.
Patched — shared lock added around region 1
00000001C001333A mov rbx, cs:?SigningLock@@3PEAVRWLock@@EA
00000001C0013344 call cs:__imp_KeEnterCriticalRegion ; NEW
00000001C0013356 call cs:__imp_ExAcquireResourceSharedLite ; NEW shared lock before the read
00000001C0013362 mov rdx, cs:?EncryptionManager@@3PEAVEncryption@@EA
00000001C001336E mov eax, [rdx+20h]
00000001C0013376 mov rdx, [rdx+18h]
00000001C0013381 call memcmp
00000001C00133A4 call cs:__imp_ExReleaseResourceLite ; NEW release after the read
00000001C00133B0 call cs:__imp_KeLeaveCriticalRegion ; NEW
Patched — shared lock added around region 2
00000001C0013641 mov rbx, cs:?SigningLock@@3PEAVRWLock@@EA
00000001C0013648 call cs:__imp_KeEnterCriticalRegion ; NEW
00000001C0013659 call cs:__imp_ExAcquireResourceSharedLite ; NEW shared lock before the loop
00000001C0013668 mov rbx, cs:?ReplayCacheManagerObject@@3PEAV...
00000001C001366F mov esi, [rbx+18h]
00000001C001368D xchg rax, [rbx+20h]
00000001C0013693 lock cmpxchg [rbx+18h], edi
00000001C00136DD call cs:__imp_ExReleaseResourceLite ; NEW release after the loop
00000001C00136E9 call cs:__imp_KeLeaveCriticalRegion ; NEW
5. Trigger Conditions
- Session Establishment: Obtain a handle to the minifilter communication port via
FilterConnectCommunicationPort. - Payload Formatting: Construct a message of at least
0x14bytes. The first DWORD is the opcode and must equal6. Starting at offset+0x4the callback passes the buffer toCsvCamSetSecurityInfo, which reads its fields at+0x8(the size compared againstEncryptionManager+0x20) and+0xC(the bytes passed tomemcmp). - Race Execution: Issue concurrent opcode-6 messages.
- Reader interleaving: A message whose size/data match the current
EncryptionManagerkeeps execution on the unlocked read path (region 1), and the follow-on sequence-number update (region 2) reads/writesReplayCacheManagerObjectunlocked. - Writer interleaving: A message whose size or data mismatch drives the allocation/replace path, which acquires the exclusive lock and frees the old object before installing a new one.
- Timing: The writer must free the object during the window between the reader capturing the global pointer and dereferencing it (region 1) or performing the interlocked update (region 2).
- Observable Effect: Dereferencing or writing the freed PagedPool object can raise an unhandled kernel exception (for example a page fault when the freed page is unmapped or reclaimed), leading to a bugcheck.
6. Impact
- Primitive: Kernel use-after-free on PagedPool objects (tag
0x41435643). Region 1 is a stale read of a freedEncryptionobject (fields+0x20and+0x18, with the+0x18value used as a buffer pointer formemcmp). Region 2 is a stale write (xchgandlock cmpxchg) into a freedReplayCacheManagerobject. - Not an information-disclosure channel: The
memcmpon the region-1 path returns only a boolean equal/not-equal result and does not return the compared memory contents to the caller. The unlocked reads do not surface object bytes to user mode. - Reachable impact: Memory corruption (a stale read of, and stale interlocked writes into, freed pool). The demonstrable consequence is kernel instability / bugcheck (denial of service). Turning this race into privilege escalation would require reclaiming the freed allocation with controlled contents within the race window; that further exploitation is not demonstrated here and depends on pool state and timing not established from the static diff.
- Reachability caveats: The trigger assumes a caller can open and send messages to the driver's communication port; the port's security descriptor and the driver's presence (it loads on Cluster Shared Volumes nodes) determine who can reach the code. These should be confirmed on the target configuration.
7. Debugger Playbook
This section lists real instruction addresses in the unpatched binary for observing the race. Assume the analysis image base 0x1C0000000; adjust for the runtime load base.
Breakpoints
bp csvnsflt_unpatched!NfltMessage ; 0x1C0005F60 — message dispatch entry
bp csvnsflt_unpatched!CsvCamSetSecurityInfo ; 0x1C0013304 — function entry
bp 0x1C0013332 ; region 1: unlocked capture of EncryptionManager
bp 0x1C0013341 ; region 1: first deref of the captured pointer
bp 0x1C0013401 ; exclusive-lock free of the old EncryptionManager
bp 0x1C001361A ; region 2: unlocked read of ReplayCacheManagerObject
bp 0x1C001359F ; exclusive-lock free of the old ReplayCacheManagerObject
What to Inspect
- At
NfltMessage(0x1C0005F60):rdxpoints to the user message; after the internalmemmovecopy, the first DWORD selects the opcode. Confirm opcode6routes toCsvCamSetSecurityInfo. - At
0x1C0013332(region-1 capture):rdxreceives the currentEncryptionManagervalue with no lock held. Set a write breakpoint on theEncryptionManagerglobal to observe when a concurrent thread replaces it. - At
0x1C0013401(free):rcxholds the object being freed. If it equals the value captured at0x1C0013332in the racing thread, the region-1 use-after-free is imminent. - At
0x1C0013341(deref): A fault here on a freed/unmapped page confirms the region-1 use-after-free fired. - At
0x1C001361A/0x1C001363F(region 2): Observe the unlocked read and thexchg/lock cmpxchgwrites; racing the free at0x1C001359Fcorrupts freed pool.
Struct & Offset Notes
- Message layout:
[+0x0] opcode(6); the struct at+0x4is passed toCsvCamSetSecurityInfoasarg1, witharg1[2](+0x8) the size andarg1[3](+0xC) the compared bytes. Encryptionobject:+0x20is the size compared against the user size;+0x18is the buffer pointer passed tomemcmp.ReplayCacheManagerobject:+0x18and+0x20are the sequence-number fields updated by the interlocked operations.
8. Changed Functions — Full Triage
CsvCamSetSecurityInfo @ 0x1C0013304(Similarity: 0.9045 | Change Type: Security Relevant)- Changes: Added
KeEnterCriticalRegion+ExAcquireResourceSharedLite(SigningLock)...ExReleaseResourceLite+KeLeaveCriticalRegionaround theEncryptionManagerread/compare region (previously fully unlocked) and around theReplayCacheManagerObjectsequence-number update loop (previously fully unlocked). The exclusive-lock free/replace paths were already locked in the unpatched build; the change closes the reader side of the lock imbalance. The remaining differences are register-allocation shifts caused by the added blocks.
9. Unmatched Functions
- Added: None.
- Removed: None.
10. Confidence & Caveats
- Confidence Level: High that the change is a genuine synchronization fix. The unpatched build clearly reads and updates
EncryptionManagerandReplayCacheManagerObjectwithout holdingSigningLock, while the free/replace paths hold it exclusive; the patched build adds a matching shared lock around both regions. Direction is correct (patched is stricter). - What is demonstrated: A reachable kernel use-after-free (stale read of a freed
Encryptionobject and stale interlocked writes into a freedReplayCacheManagerobject) driven by racing concurrent port messages. - What is not demonstrated: A full privilege-escalation chain. The region-1
memcmpresult is only a boolean and is not an information-leak channel. Converting the use-after-free into controlled memory corruption would require reclaiming the freed pool with attacker-controlled contents inside the race window, which is not established from the static diff. - To verify on a live target: the communication port name and its security descriptor (who may send messages), the exact object sizes for pool reclaim, and the presence of the driver on the target (Cluster Shared Volumes nodes).
DONE