csc.sys — CscDclMRxTransition LVIEW null check made unconditional, closing a possible NULL-pointer dereference (CWE-476) fixed
KB5078885
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | csc_unpatched.sys |
| Patched binary | csc_patched.sys |
| Overall similarity | 0.9877 |
| Matched functions | 1415 |
| Changed functions | 1 |
| Identical functions | 1414 |
| Unmatched (either direction) | 0 / 0 |
Verdict: A single, surgical patch makes an existing rdi (LVIEW) NULL check unconditional in CscDclMRxTransition, closing a possible kernel NULL-pointer dereference (CWE-476). In the unpatched build the NULL check at both guard points is gated behind a WIL velocity/servicing feature (Feature_4082332987); when that check evaluates to 0, control branches past the test rdi, rdi NULL check into the block that dereferences rdi. The patch removes the feature gate and performs the NULL check unconditionally. Practical impact is bounded: at most a local kernel denial of service (bug check), and only if a NULL LVIEW pointer can reach this path with the feature disabled — which is not demonstrated here, and the LVIEW pointer is internal driver state rather than attacker-supplied input.
2. Vulnerability Summary
Finding #1 — NULL Pointer Dereference in CscDclMRxTransition
| Attribute | Value |
|---|---|
| Severity | Low |
| Class | NULL pointer dereference (CWE-476) / potential local kernel DoS; velocity-feature-gated NULL check made unconditional |
| Affected function | CscDclMRxTransition (sub_1C005FF5C unpatched, sub_1c005ddcc patched) |
| Primitive | Kernel NULL deref → bug check (local DoS) if reachable; no memory-corruption or code-execution primitive |
Root cause. The function reads rdi (a CSC LVIEW / logical-view pointer) from *(*(arg1+0x38)+0x70) at 0x1C005FFA1. Given the callees (CscAcquireLViewLock, CscLViewIsRootGhosted, CscReleaseLViewLock, CscFlushLViewWasPreviouslyOffline) this is a logical-view object. The pre-patch code guards the main block with:
if ((op - 1) <= 2) {
if (Feature_4082332987__private_IsEnabledDeviceUsage() == 0 || rdi != NULL) {
// block that dereferences rdi
}
}
sub_1c0007a70 is Feature_4082332987__private_IsEnabledDeviceUsage, a WIL velocity/servicing feature check (the mechanism Windows uses to stage a change so it can be rolled back server-side). It reads the global Feature_4082332987__private_featureState, tests bit 0x10, and if the bit is clear falls back to Feature_4082332987__private_IsEnabledFallback → wil_details_IsEnabledFallback, which can return 0. In the assembly the gate is call feature; test eax,eax; jz block_entry: when the feature check returns 0 (the feature-disabled path) the jz jumps past the test rdi, rdi NULL check and enters the block, so rdi is dereferenced without the NULL check through:
CscAcquireLViewLock(rdi)(sub_1c000c340) at0x1C005FFF3— first crash siteCscLViewIsRootGhosted(rdi)(sub_1c00654b8),CscReleaseLViewLock(rdi)(sub_1c000c49c), and related LVIEW calls off a NULL base.
The identical pattern repeats at the function exit gate (0x1C0060538), where CscFlushLViewWasPreviouslyOffline(rdi) (sub_1c00474b0) is called at 0x1C0060549 with the same potentially-NULL rdi.
The fix removes both feature-check gates and performs an unconditional test rdi, rdi NULL check before the dereferences. The corrected guard is effectively:
if ((op - 1) <= 2 && rdi != NULL) // rdi MUST be non-NULL
Entry point and data flow:
- An FSCTL /
DeviceIoControlrequest against the CSC device reaches the CSC FSCTL handler. CscFsCtl(sub_1c004c760) receives theRX_CONTEXT.CscDclInternalFsControl(sub_1c004c938) routes the request and callsCscDclMRxTransitionat0x1C004CDA7.CscDclMRxTransition(sub_1c005ff5c) dereferencesrdi = *(*(arg1+0x38)+0x70)when the feature-gated branch skips the NULL check.
Reachability caveat: rdi is loaded from internal redirector/LVIEW state reached through RX_CONTEXT, not from a user-supplied buffer. The rest of the function dereferences rdi in many places without a NULL check, which indicates rdi is normally non-NULL; the two guarded points are defensive. Whether a low-privileged caller can actually drive *(*(arg1+0x38)+0x70) to NULL on this path, with the feature disabled, is not established here.
(The MINIRDR dispatch table is populated by CscInitializeDispatchTable (sub_1c008f338); DriverEntry is a separate function at 0x1C008F950.)
3. Pseudocode Diff
// === UNPATCHED CscDclMRxTransition (sub_1c005ff5c) ===
void CscDclMRxTransition(RX_CONTEXT* ctx, ULONG op) // arg1=rcx, arg2=edx/r8d
{
void* tmp = ctx->field_0x38; // mov rax, [rcx+0x38]
LVIEW* rdi = *(void**)(tmp + 0x70); // mov rdi, [rax+0x70] <-- CAN BE NULL
if (op - 1 <= 2) // op in {1,2,3}
{
// In asm: call feature; test eax,eax; jz block --> when the feature
// check returns 0 (disabled), the branch jumps PAST the `test rdi, rdi` NULL check.
if (Feature_4082332987__private_IsEnabledDeviceUsage() == 0 || rdi != NULL)
{
CscAcquireLViewLock(rdi); // deref (0x1C005FFF3), NULL not checked on feature-disabled path
CscLViewIsRootGhosted(rdi); // deref (0x1C005FFFB)
CscReleaseLViewLock(rdi); // ...
}
}
// Exit path — same feature-gated pattern:
if (Feature_4082332987__private_IsEnabledDeviceUsage() == 0 || rdi != NULL)
CscFlushLViewWasPreviouslyOffline(rdi); // CRASH if rdi==NULL (0x1C0060549)
}
// === PATCHED CscDclMRxTransition (sub_1c005ddcc) ===
void CscDclMRxTransition(RX_CONTEXT* ctx, ULONG op)
{
void* tmp = ctx->field_0x38;
LVIEW* rdi = *(void**)(tmp + 0x70); // still possibly NULL
if (op - 1 <= 2 && rdi != NULL) // FIX: unconditional NULL check, feature call removed
{
CscAcquireLViewLock(rdi); // safe
...
}
if (rdi != NULL) // FIX: unconditional NULL check at exit
CscFlushLViewWasPreviouslyOffline(rdi); // safe
}
Key changes:
- Removed: two call Feature_4082332987__private_IsEnabledDeviceUsage (sub_1c0007a70) invocations.
- Added: test rdi, rdi / jz at both guard points (0x1C005DE52 entry, 0x1C005E39F exit in the patched binary).
- All other deltas are module-base relocation (patched function moves from 0x1C005FF5C to 0x1C005DDCC) and the resulting call-target address shifts.
4. Assembly Analysis
4.1 Vulnerable function — unpatched CscDclMRxTransition (sub_1c005ff5c)
; ====== Entry block ======
0x1C005FF5C: mov rax, rsp
0x1C005FF67: mov [rax+0x18], r8d ; arg2 (operation code)
0x1C005FF6B: mov [rax+0x8], rcx ; arg1 (RX_CONTEXT)
...
; rdi = LVIEW pointer
0x1C005FF85: mov rax, [rcx+0x38] ; rax = *(arg1+0x38)
0x1C005FFA1: mov rdi, [rax+0x70] ; rdi = LVIEW pointer <-- NULL possible
...
; ====== Vulnerable entry gate @ 0x1C005FFE2 ======
0x1C005FFD3: cmp ecx, 0x2 ; (arg2-1) <= 2 (op in {1,2,3})
0x1C005FFD6: jbe 0x1C005FFE2
0x1C005FFE2: call Feature_4082332987__private_IsEnabledDeviceUsage
0x1C005FFE7: test eax, eax
0x1C005FFE9: jz 0x1C005FFF0 ; feature==0 -> ENTER BLOCK, skipping NULL check
; ^^^^ THIS IS THE BUG
0x1C005FFEB: test rdi, rdi ; only reached when feature!=0
0x1C005FFEE: jz 0x1C005FFD8
; ----- Block 0x1C005FFF0: first dereference of possibly-NULL rdi -----
0x1C005FFF0: mov rcx, rdi ; rcx = rdi
0x1C005FFF3: call CscAcquireLViewLock ; deref rdi -> CRASH if NULL
0x1C005FFF8: mov rcx, rdi
0x1C005FFFB: call CscLViewIsRootGhosted ; another deref of rdi
...
; ====== Vulnerable exit gate @ 0x1C0060538 ======
0x1C0060538: call Feature_4082332987__private_IsEnabledDeviceUsage
0x1C006053D: test eax, eax
0x1C006053F: jz 0x1C0060546 ; feature==0 -> skip NULL check, proceed to deref
0x1C0060541: test rdi, rdi
0x1C0060544: jz 0x1C006054E
0x1C0060546: mov rcx, rdi ; rcx = rdi (possibly NULL)
0x1C0060549: call CscFlushLViewWasPreviouslyOffline ; dereferences rdi -> CRASH if NULL
4.2 Helper — Feature_4082332987__private_IsEnabledDeviceUsage (sub_1c0007a70)
0x1C0007A70: sub rsp, 0x28
0x1C0007A74: and qword [rsp+..], 0x0
0x1C0007A7A: mov eax, cs:Feature_4082332987__private_featureState ; WIL feature state
0x1C0007A84: test al, 0x10 ; bit 0x10
0x1C0007A86: jz 0x1C0007A8D ; if clear -> fallback path
0x1C0007A88: and eax, 0x1 ; if set, return bit 0
0x1C0007A8B: jmp 0x1C0007A9C
0x1C0007A8D: mov rcx, [rsp+..]
0x1C0007A92: mov edx, 0x3
0x1C0007A97: call Feature_4082332987__private_IsEnabledFallback ; -> wil_details_IsEnabledFallback, may return 0
0x1C0007A9C: add rsp, 0x28
0x1C0007AA0: retn
This is a WIL staged-feature check (Feature_4082332987), not an Offline-Files-specific flag. When bit 0x10 of Feature_4082332987__private_featureState is clear, it falls back to Feature_4082332987__private_IsEnabledFallback, which can return 0 — the condition that makes the entry/exit gates skip the rdi NULL check.
4.3 Patched equivalent — CscDclMRxTransition (sub_1c005ddcc)
; Entry gate
0x1C005DE43: cmp ecx, 0x2
0x1C005DE46: jbe 0x1C005DE52
0x1C005DE48: mov esi, 0xC000000D
0x1C005DE4D: jmp 0x1C005E392
0x1C005DE52: test rdi, rdi ; <-- DIRECT NULL CHECK (feature call removed)
0x1C005DE55: jz 0x1C005DE48 ; rdi==0 -> error path, block skipped
0x1C005DE57: mov rcx, rdi
0x1C005DE5A: call CscAcquireLViewLock ; safe — rdi is non-NULL
; Exit gate
0x1C005E39F: test rdi, rdi ; <-- DIRECT NULL CHECK (no feature call)
0x1C005E3A2: jz 0x1C005E3AC
0x1C005E3A4: mov rcx, rdi
0x1C005E3A7: call CscFlushLViewWasPreviouslyOffline ; safe
Side-by-side, the swap is unambiguous: call Feature_4082332987__private_IsEnabledDeviceUsage + test eax,eax + jz is replaced by test rdi, rdi + jz at both guard locations.
5. Trigger Conditions
To reach and fire the bug:
- Reach the FSCTL dispatch interface. Issue an FSCTL /
DeviceIoControlagainst the CSC device that routes throughCscFsCtl(sub_1c004c760) →CscDclInternalFsControl(sub_1c004c938) →CscDclMRxTransition(sub_1c005ff5c). Confirm the exact operation codes against theCscDclInternalFsControl/CscFsCtldispatch before building a PoC. - Force the WIL feature check to return 0.
Feature_4082332987__private_IsEnabledDeviceUsage(sub_1c0007a70) must return0, i.e. the staged featureFeature_4082332987is disabled:Feature_4082332987__private_featureStatebit0x10clear andFeature_4082332987__private_IsEnabledFallback(wil_details_IsEnabledFallback) returning0. This is the WIL feature-staging state, not an Offline-Files registry flag. - Supply a NULL LVIEW pointer. The request must arrive on an
RX_CONTEXTwhose*(*(arg1+0x38)+0x70)(the LVIEW pointer loaded intordi) is NULL. - Pass
arg2 ∈ {1,2,3}. Thecmp ecx, 0x2 / jbecheck at0x1C005FFD6requires the operation code (arg2 - 1) to be ≤ 2. - Observe the crash. Expected result: a kernel access violation (
0xC0000005,STATUS_ACCESS_VIOLATION) faulting on a small offset from NULL, with the faulting instruction insideCscAcquireLViewLock(called at0x1C005FFF3) orCscFlushLViewWasPreviouslyOffline(called at0x1C0060549), surfacing as a kernel bug check.
Reachability note: whether a NULL LVIEW pointer can be presented to CscDclMRxTransition via the FSCTL path (and with the feature disabled) must be validated empirically against the dispatch codes.
6. Exploit Primitive & Development Notes
Primitive. If the feature-disabled path is reached with rdi == NULL, the result is a kernel NULL-pointer dereference → bug check (local denial of service). On modern Windows the NULL page is reserved and not user-mappable, so the impact is bounded to a crash; there is no memory-corruption, write, or information-leak primitive here.
No escalation primitive is provided by this bug. The dereference is of a NULL base pointer, not attacker-controlled memory. Reaching code execution would require an unrelated way to map the zero page (blocked by NULL-page protection on Windows 8+) plus separate primitives, none of which this change provides. The report does not claim a code-execution path.
Reachability is not established. As noted in Section 2, rdi (the LVIEW pointer) is internal redirector state, not a user-supplied value, and the function dereferences it unguarded elsewhere — consistent with it normally being non-NULL. Absent a demonstrated way for a caller to present a NULL LVIEW on this path with the feature disabled, the practical severity is Low.
7. Debugger PoC Playbook
For a follow-on agent with WinDbg/KD attached to the unpatched csc.sys, here is the concrete plan.
7.1 Symbols & setup
.reload /f csc.sys
lm m csc ; confirm base
? csc!CscDclMRxTransition - csc ; offset 0x5ff5c from module base
If you only have the raw file, addresses in the JSON assume a loaded base of 0x1c000000; rebase to the runtime base via ? csc + 0x5ff5c.
7.2 Breakpoints (ready-to-paste)
bp csc!CscDclMRxTransition ; function entry — inspect rcx, r8d/edx
bp csc+0x5ffe2 ; vulnerable entry gate (call Feature_4082332987__private_IsEnabledDeviceUsage)
bp csc+0x5fff3 ; first NULL-deref site (call CscAcquireLViewLock)
bp csc+0x60538 ; vulnerable exit gate
bp csc+0x60549 ; second NULL-deref site (call CscFlushLViewWasPreviouslyOffline)
bp csc!Feature_4082332987__private_IsEnabledDeviceUsage ; observe return value
bp csc+0x5ffa1 ; the "mov rdi, [rax+0x70]" — where rdi gets loaded
Why each one matters:
- CscDclMRxTransition — capture the RX_CONTEXT (rcx) and op (r8d/edx).
- csc+0x5ffe2 — the gate. If the instruction here is call Feature_4082332987__private_IsEnabledDeviceUsage, you are on the vulnerable binary; if it is test rdi, rdi, you are on the patched one.
- csc+0x5fff3 — first dereference; if you reach here with rdi==0, the bug fires inside CscAcquireLViewLock.
- csc!Feature_4082332987__private_IsEnabledDeviceUsage — observe what the feature check returns.
7.3 What to inspect at each breakpoint
| BP | Register / memory to inspect |
|---|---|
csc!CscDclMRxTransition entry |
rcx = RX_CONTEXT*; r8d/edx = op (must be 1/2/3) |
csc+0x5ffa1 (post load) |
rdi — the LVIEW pointer that may be NULL. Inspect poi(poi(rcx+0x38)+0x70) |
csc!Feature_4082332987__private_IsEnabledDeviceUsage |
On exit, al — if 0, the NULL check is skipped. Reads Feature_4082332987__private_featureState (bit 0x10) |
csc+0x5ffe2 |
Right before the call: rdi, ecx (should already be op-1 and ≤ 2) |
csc+0x5fff3 |
rcx — should equal rdi; if 0, the deref inside CscAcquireLViewLock faults |
7.4 Trigger setup from user mode
- Put the WIL staged feature
Feature_4082332987into the disabled state soFeature_4082332987__private_IsEnabledDeviceUsagereturns 0 (Feature_4082332987__private_featureStatebit0x10clear and the fallback returning 0). - Issue an FSCTL /
DeviceIoControlagainst the CSC device that routes throughCscFsCtl→CscDclInternalFsControl→CscDclMRxTransition, with an operation codearg2 ∈ {1,2,3}and anRX_CONTEXTwhose*(*(arg1+0x38)+0x70)(the LVIEW pointer) is NULL. Confirm the exact FSCTL codes against theCscFsCtl/CscDclInternalFsControldispatch. - Confirm the request reached the dispatch by hitting
csc!CscFsCtlfirst, then stepping intoCscDclInternalFsControl→CscDclMRxTransition.
7.5 Expected observation confirming the bug
EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - STATUS_ACCESS_VIOLATION
FAULTING_IP: csc!CscAcquireLViewLock ; deref of NULL LVIEW pointer
READ_ADDRESS: small offset from NULL
STACK_TEXT:
csc!CscAcquireLViewLock
csc!CscDclMRxTransition ; the call at 0x1C005FFF3
csc!CscDclInternalFsControl
csc!CscFsCtl
Run !analyze -v to confirm; kv to see the chain back through CscFsCtl.
7.6 Struct / offset notes
RX_CONTEXT:+0x38→ pointer to the structure whose+0x70field holds the LVIEW pointer.- Structure at
*(ctx+0x38): +0x70→ LVIEW pointer (loaded intordiat0x1C005FFA1).Feature_4082332987__private_featureState:- WIL staged-feature state read by
sub_1c0007a70; bit0x10gates the fast path, otherwise the fallback decides. - Operation code (
arg2): - Values
1,2,3all satisfy(arg2-1) <= 2at0x1C005FFD6.
8. Changed Functions — Full Triage
Only one function changed.
| Function | Similarity | Type | Note |
|---|---|---|---|
CscDclMRxTransition (sub_1C005FF5C) |
0.9703 | Security-relevant | Two Feature_4082332987__private_IsEnabledDeviceUsage feature-check gates replaced with direct test rdi, rdi NULL checks at entry (0x1C005DE52) and exit (0x1C005E39F). Fixes the feature-gated missing NULL check where a feature==0 result branches past test rdi, rdi into the block that dereferences rdi. |
Cosmetic / non-behavioral deltas folded into the same change:
- Module-base relocation (the function moves from 0x1C005FF5C to 0x1C005DDCC).
- Callee-address shifts from the relocation: CscAcquireLViewLock (sub_1c000c340 → sub_1c000b540), CscFlushLViewWasPreviouslyOffline (sub_1c00474b0 → sub_1c0045320) — same target functions at shifted addresses.
No other functions in the binary exhibit behavioral change.
9. Unmatched Functions
None. unmatched_unpatched = 0, unmatched_patched = 0. No sanitizer was removed and no mitigation helper was added; the fix is entirely in-line within CscDclMRxTransition (sub_1C005FF5C).
10. Confidence & Caveats
Confidence: High.
Rationale:
- The diff is unambiguous and direction-correct: two call Feature_4082332987__private_IsEnabledDeviceUsage sequences (unpatched) are replaced by two test rdi, rdi sequences (patched) at the same guard points. The vulnerable pattern is present only in the unpatched binary.
- The missing-NULL-check flaw is plainly visible in the assembly: call feature; test eax,eax; jz block skips the test rdi, rdi at 0x1C005FFEB (entry) / 0x1C0060541 (exit) whenever the feature check returns 0.
- sub_1c0007a70 is confirmed as Feature_4082332987__private_IsEnabledDeviceUsage, a WIL staged-feature helper (reads Feature_4082332987__private_featureState, tests bit 0x10, falls back to Feature_4082332987__private_IsEnabledFallback → wil_details_IsEnabledFallback). It is not an Offline-Files registry flag.
Assumptions made (require manual verification before PoC):
1. Call-chain routing. The reachable path is CscFsCtl (sub_1c004c760) → CscDclInternalFsControl (sub_1c004c938, call at 0x1C004CDA7) → CscDclMRxTransition (sub_1c005ff5c), confirmed by the call sites. The MINIRDR dispatch table is populated by CscInitializeDispatchTable (sub_1c008f338); DriverEntry is a separate function at 0x1C008F950.
2. FSCTL codes. The exact operation codes that route through CscDclInternalFsControl to CscDclMRxTransition must be confirmed against the CSC dispatch.
3. Trigger route feasibility. Reaching a NULL LVIEW pointer (*(*(arg1+0x38)+0x70) == NULL) at CscDclMRxTransition with the feature disabled must be validated empirically.
4. Feature state forcing. Confirm how Feature_4082332987__private_featureState bit 0x10 and the fallback are driven to make the check return 0 (WIL feature-staging configuration).
What a researcher should verify manually:
- The operation code that maps to arg2 ∈ {1,2,3}.
- Whether a low-privileged user can reach the FSCTL dispatch (CscFsCtl) with a NULL LVIEW context.
- Whether any earlier in-driver check blocks the NULL-LVIEW case before CscDclMRxTransition.
- The exact FSCTL codes that route to CscFsCtl (sub_1c004c760).