ntfs.sys — Negative-value input validation in three NtSetInformationFile handlers promoted from Velocity-feature-gated to unconditional (gate g_Feature_2614832441 / EvaluateCurrentState removed)
KB5094123
1. Overview
| Item | Value |
|---|---|
| Unpatched binary | ntfs_unpatched.sys |
| Patched binary | ntfs_patched.sys |
| Overall similarity | 0.9929 |
| Matched functions | 4046 |
| Changed functions | 3 |
| Identical functions | 4043 |
| Unmatched (unpatched / patched) | 0 / 0 |
Verdict (corrected): The patch removes a Windows Velocity feature gate — EvaluateCurrentState() at 0x1C00303D0, which is EvaluateFeature(&g_Feature_2614832441_60270648...) returning cachedstate != 1 — from three NtSetInformationFile handlers. In the unpatched build that gate made the rejection of an attacker-supplied negative 64-bit value conditional on the feature state; in the patched build the rejection is unconditional. This is a delivered defense-in-depth input-validation hardening, and the feature (both the EvaluateCurrentState helper and the g_Feature_2614832441 descriptor) is fully retired from the patched image.
Two facts materially lower the severity relative to the original engine report:
- The check is on by default even in the unpatched build.
EvaluateCurrentState()returnstrue(validation active, negatives rejected) unless the feature is explicitly Disabled (cachedstate == 1). The accept-negatives path therefore requires a non-default feature-disable (a servicing/Velocity kill-switch), which an unprivileged local caller does not control. The patch's effect is to remove that kill-switch and make the validation permanent. - No memory-corruption or privilege-escalation primitive is demonstrated in the binary. The negative value is accepted into NTFS position/size/allocation bookkeeping, but the report's downstream claim (pool corruption in
NtfsAddAllocation/NtfsDeleteAllocation→ LPE) is not evidenced by any actual corrupting write in the disassembly. That exploit chain, and all associated KASLR / pool-spray / token-steal / SMEP / HVCI / CFG narrative, has been removed as unsubstantiated.
Of the 3 changed functions, all three are the three affected handlers and share one root cause: a < 0 validation that was feature-flag gated is now applied unconditionally. The diff is tight; there are no relocation or heuristic-mispairing artifacts to discount.
Corrected severity: Low (defense-in-depth input validation). Corrected classification: over-claimed-but-real — a genuine, delivered validation change on attacker-supplied NtSetInformationFile input, with the severity and exploit narrative grossly over-stated in the original report.
2. Verified Change Summary
All three handlers are dispatched from NtfsCommonSetInformation (switch on FileInformationClass). Each takes a user-supplied FILE_*_INFORMATION buffer, extracts a signed 64-bit field, and (pre-patch) rejected it if negative only when the Velocity gate was in the validating state. The patch deletes the gate call and rejects a negative value in every case.
2.1 LOW — NtfsSetAllocationInfo (sub_1C00DFA10), FileAllocationInformation (class 19)
- User value:
v13 = **(a3 + 0x18)= attacker-controlledAllocationSize(FILE_ALLOCATION_INFORMATION). - Bound:
*(Vcb + 0x1E18)(Vcb = *(Scb + 0xB0),Scb = a4) = maximum file size. - Unpatched: gate-true branch checks
v13 > max || v13 < 0; gate-false branch checks onlyv13 > max(the< 0test is absent). - Patched: single unconditional guard
if (v13 > max || v13 < 0) return STATUS_INVALID_PARAMETER. - Proven primitive: when the feature is disabled, a negative
AllocationSizeis accepted. It routes to the shrink pathNtfsPrepareToShrinkFileSize(a1, Scb, a4, v13)(a negative value is< *(Scb + 0x20), the current allocation, so the grow branch is skipped). No downstream corrupting write is demonstrated; the impact is a negative size entering NTFS allocation bookkeeping under a non-default configuration.
2.2 LOW — NtfsSetEndOfFileInfo (sub_1C011B620), FileEndOfFileInformation (class 20)
- User value:
v12 = **(a3 + 0x18)= attacker-controlledEndOfFile(FILE_END_OF_FILE_INFORMATION). - Unpatched:
if (EvaluateCurrentState() && v12 < 0) reject— the&&short-circuits when the gate is false, so a negative value is accepted. - Patched:
if (v12 < 0) reject— unconditional. - Proven primitive: a negative stream
EndOfFile/FileSizeaccepted into NTFS size handling under a non-default configuration. Downstream size/valid-data-length confusion is plausible but not demonstrated in the binary.
2.3 LOW — NtfsSetPositionInfo (sub_1C01D2D10), FilePositionInformation (class 14)
- User value:
*v2wherev2 = *(a2 + 0x18)= attacker-controlledCurrentByteOffset(FILE_POSITION_INFORMATION). - Unpatched:
if (EvaluateCurrentState() && *v2 < 0) reject; else { *(a1 + 0x68) = *v2; return 0; }— a negative offset is stored toFileObject+0x68when the gate is false. - Patched:
if (*v2 >= 0) { store; return 0; } else reject— unconditional non-negative check. - Proven primitive: a negative
FileObject->CurrentByteOffsetstored under a non-default configuration. Any impact requires a later position-relative read/write to consume the offset; not demonstrated.
3. Pseudocode Diff (from IDA Hex-Rays, both builds)
3.1 NtfsSetAllocationInfo (sub_1C00DFA10)
// UNPATCHED @ 0x1C00DFA10 (ntfs_unpatched.c:95389-95418)
v13 = **(_QWORD **)(a3 + 24); // user AllocationSize
v14 = EvaluateCurrentState(); // Velocity gate, sub_1C00303D0
v16 = *(_QWORD *)(a4 + 176); // Vcb (Scb+0xB0)
if ( v14 ) { // gate TRUE (validating)
if ( v13 > *(_QWORD *)(v16 + 7704) || v13 < 0 ) // full check: > max || < 0
return 0xC000000D; // STATUS_INVALID_PARAMETER (trace 0xF379F)
}
else if ( v13 > *(_QWORD *)(v16 + 7704) )// gate FALSE: ONLY upper bound, no < 0
return 0xC000000D; // (trace 0xF37A6)
// negative v13 then reaches NtfsPrepareToShrinkFileSize(a1, Scb, a4, v13)
// PATCHED @ 0x1C00DFA10 (ntfs_patched.c:95383-95393)
v14 = **(_QWORD **)(a3 + 24); // user AllocationSize
if ( v14 > *(_QWORD *)(*(_QWORD *)(a4 + 176) + 7704LL) || v14 < 0 ) // unconditional
return 0xC000000D; // (trace 0xF379A)
// EvaluateCurrentState() is gone
3.2 NtfsSetEndOfFileInfo (sub_1C011B620)
// UNPATCHED @ 0x1C011B620 (ntfs_unpatched.c:135330-135338)
v12 = **(_QWORD **)(a3 + 24); // user EndOfFile
if ( EvaluateCurrentState() && v12 < 0 ) // rejected ONLY when gate TRUE
return 0xC000000D; // (trace 0xF3AD5)
// PATCHED @ 0x1C011B600 (ntfs_patched.c:135313-135321)
v12 = **(_QWORD **)(a3 + 24);
if ( v12 < 0 ) // negative ALWAYS rejected
return 0xC000000D; // (trace 0xF3AC6)
3.3 NtfsSetPositionInfo (sub_1C01D2D10)
// UNPATCHED @ 0x1C01D2D10 (ntfs_unpatched.c:224079-224090)
v2 = *(_QWORD **)(a2 + 24); // user FILE_POSITION_INFORMATION
if ( EvaluateCurrentState() && (__int64)*v2 < 0 ) { // reject only when gate TRUE
return 0xC000000D; // (trace 0xF366A)
} else {
*(_QWORD *)(a1 + 104) = *v2; // store CurrentByteOffset (FileObject+0x68)
return 0;
}
// PATCHED @ 0x1C01D2CE0 (ntfs_patched.c:224062-224073)
v2 = *(_QWORD **)(a2 + 24);
if ( (__int64)*v2 >= 0 ) { // unconditional non-negative check
*(_QWORD *)(a1 + 104) = *v2;
return 0;
} else {
return 0xC000000D; // (trace 0xF3668)
}
Common change: each handler drops the call EvaluateCurrentState and promotes the < 0 (and, for allocation, > max) test from gate-conditional to unconditional. No fallback path survives.
4. Assembly Analysis (spot-checked against the .asm ground truth)
4.1 NtfsSetPositionInfo — corrected listing
Unpatched @ 0x1C01D2D10 (verbatim from ntfs_unpatched.asm):
0x1C01D2D10 mov [rsp+arg_0], rbx
0x1C01D2D15 push rdi
0x1C01D2D16 sub rsp, 20h
0x1C01D2D1A mov rbx, [rdx+18h] ; rbx = user FILE_POSITION_INFORMATION buffer
0x1C01D2D1E mov rdi, rcx ; rdi = arg1 (FileObject/Fcb)
0x1C01D2D21 call EvaluateCurrentState ; 0x1C00303D0 Velocity gate
0x1C01D2D26 test eax, eax
0x1C01D2D28 jz loc_1C01D2D52 ; gate FALSE -> store (skip sign check)
0x1C01D2D2A cmp qword ptr [rbx], 0 ; sign check reached only when gate TRUE
0x1C01D2D2E jge loc_1C01D2D52 ; >=0 -> store
0x1C01D2D30 mov cl, cs:NtfsStatusDebugFlags
0x1C01D2D36 mov ebx, 0C000000Dh ; STATUS_INVALID_PARAMETER
0x1C01D2D3B test cl, cl
0x1C01D2D3D jz loc_1C01D2D4E
0x1C01D2D3F mov r8d, 0F366Ah
0x1C01D2D45 mov edx, ebx
0x1C01D2D47 xor ecx, ecx
0x1C01D2D49 call NtfsStatusTraceAndDebugInternal
0x1C01D2D4E mov eax, ebx
0x1C01D2D50 jmp loc_1C01D2D5B
loc_1C01D2D52:
0x1C01D2D52 mov rax, [rbx]
0x1C01D2D55 mov [rdi+68h], rax ; FileObject->CurrentByteOffset = value
0x1C01D2D59 xor eax, eax ; STATUS_SUCCESS
0x1C01D2D5B ...
Note: the store (mov [rdi+68h], rax) sits at the shared target loc_1C01D2D52, reached by both the gate-false jz and the non-negative jge. (The earlier engine report listed the store on the jz fall-through, before the cmp/jge, and inserted a phantom xor eax,eax there; that ordering is incorrect. The logic is nonetheless equivalent to the C above.)
Patched @ 0x1C01D2CE0 (verbatim):
0x1C01D2CE0 sub rsp, 28h
0x1C01D2CE4 mov rax, [rdx+18h]
0x1C01D2CE8 mov rdx, [rax] ; CurrentByteOffset
0x1C01D2CEB test rdx, rdx
0x1C01D2CEE jns loc_1C01D2D13 ; >=0 -> store (UNCONDITIONAL)
0x1C01D2CF0 ... ; else reject 0xC000000D (trace 0xF3668)
loc_1C01D2D13:
0x1C01D2D13 mov [rcx+68h], rdx
0x1C01D2D17 xor eax, eax
0x1C01D2D19 add rsp, 28h
0x1C01D2D1D retn
EvaluateCurrentState is gone; the sign test runs on every call.
4.2 NtfsSetAllocationInfo (sub_1C00DFA10)
- Unpatched:
call EvaluateCurrentState;test eax,eax; gate-true path doescmp v13,[Vcb+0x1E18]and the< 0test; gate-false path does the upper-bound compare only. Gate-false reject block trace id0xF37A6. - Patched:
EvaluateCurrentStateremoved; a single guard applies> max || < 0, reject trace id0xF379A.
4.3 NtfsSetEndOfFileInfo (sub_1C011B620)
- Unpatched:
call EvaluateCurrentState;test eax,eax;je <accept>(gate false skips the sign check);test rdi,rdi; jns <accept>reached only when gate true. Reject trace0xF3AD5. - Patched:
EvaluateCurrentStateremoved;test v12,v12; js <reject>unconditional. Reject trace0xF3AC6.
5. The Feature Gate: EvaluateCurrentState / g_Feature_2614832441
// ntfs_unpatched.c:37173
_BOOL8 EvaluateCurrentState()
{
EvaluateFeature(&g_Feature_2614832441_60270648_FeatureDescriptorDetails);
return g_Feature_2614832441_60270648_cachedstate != 1; // true unless state == Disabled(1)
}
This is a standard Windows Implementation Library (WIL) staged-feature helper. EvaluateFeature resolves and caches the feature's enabled state; EvaluateCurrentState returns true whenever the cached state is not 1 (Disabled). In WIL terms the states are roughly Default(0) / Disabled(1) / Enabled(2), so the helper returns true (validation active) for Default and Enabled, and false (validation skipped) only when the feature is explicitly turned off.
Consequences:
- In the unpatched build the negative-value rejection is active by default; it is bypassed only if the g_Feature_2614832441 kill-switch is set to Disabled via a servicing/Velocity override (not attacker-controllable from an unprivileged context).
- In the patched build there are zero callers of EvaluateCurrentState and zero references to g_Feature_2614832441 anywhere in the image — the helper and the descriptor are fully retired. This confirms a delivered fix (the kill-switch is gone), not a staged-and-retained one.
6. Severity Assessment (proven primitive only)
- Proven primitive: three
NtSetInformationFilehandlers accept an attacker-supplied negative 64-bit value (CurrentByteOffset/AllocationSize/EndOfFile) into NTFS position/size/allocation state when the Velocity validation feature is Disabled. The patch makes the rejection unconditional and retires the feature. - Reachability: local, requires a handle to a file on an NTFS volume (write/append for allocation and EOF; any handle for position). Reachable by a local user, but the accept-negatives behavior additionally requires the non-default feature-disabled state.
- Demonstrated impact: rejection of malformed (negative) input. No out-of-bounds access, pool corruption, information disclosure, or privilege escalation is demonstrated in the disassembly. The value reaching
NtfsPrepareToShrinkFileSize/ the size path /FileObject+0x68is confirmed, but no corrupting write derived from the negative magnitude is shown. - Rating: Low. This is a defense-in-depth input-validation hardening that was on-by-default and is now unconditional. It is a genuine, security-relevant change on attacker-supplied input, which is why it is not dismissed as churn; but nothing in the binary supports a High/Medium memory-safety rating.
7. Rejected / Over-claimed Statements (removed from the corrected report)
The following claims from the original engine report are not supported by the binary and have been removed:
- "Out-of-bounds / pool corruption in nonpaged pool." No corrupting write is shown. The downstream
NtfsAddAllocation/NtfsDeleteAllocation"cluster arithmetic → run-array overflow" is inferred, not traced. - "Local privilege escalation to SYSTEM." No control-flow or data-only EoP primitive is demonstrated.
- KASLR defeat, pool grooming/spray, function-pointer/
LIST_ENTRYoverwrite, token-privilege overwrite, SMEP/SMAP/CFG/CET/HVCI/VBS bypass discussion, "Debugger PoC Playbook" as evidence. All of this is speculative exploit-development narrative with no basis in the diff. - Severity HIGH / HIGH / MEDIUM. Corrected to Low for all three (proven primitive is input validation).
- "The shared gate helper
EvaluateCurrentStatestill exists (it is called elsewhere)." False — it has no callers and does not exist as a standalone routine in the patched image;g_Feature_2614832441is entirely absent. - Verbatim position-handler assembly. The store was mis-placed on the
jzfall-through; corrected in §4.1.
What is retained as substantiated: the exact per-function code diffs, the identity and semantics of the Velocity gate, the trace ids, the entry points and dispatch chain, and the fact that the patch delivers unconditional negative-value validation.
8. Changed Functions — Full Triage
- NtfsSetPositionInfo (sub_1C01D2D10 → sub_1C01D2CE0) (similarity 0.6821, security_relevant): the
EvaluateCurrentState()gate that short-circuited the>= 0check onCurrentByteOffsetis removed; the sign check is now unconditional. The low similarity reflects the removed call plus control-flow restructuring (branch collapse) and the small function size, not mispairing. - NtfsSetAllocationInfo (sub_1C00DFA10 → sub_1C00DFA10) (similarity 0.975, security_relevant): the gate-false branch that checked only the upper bound is removed; the guard is now a single unconditional
size > max || size < 0. - NtfsSetEndOfFileInfo (sub_1C011B620 → sub_1C011B600) (similarity 0.99, security_relevant): the compound
EvaluateCurrentState() && v12 < 0test is replaced by an unconditionalv12 < 0rejection.
Non-security changes: none. All three changed functions are the three affected handlers; the remaining 4043 functions are byte-identical. No relocation artifacts or heuristic mispairings.
9. Unmatched Functions
- Removed (unpatched only): none reported by the matcher. Note, however, that the
EvaluateCurrentStatehelper and theg_Feature_2614832441descriptor are effectively gone from the patched image (no callers / no references); the matcher counts them as matched-but-not-flagged, but they are dead-and-dropped after the three handlers stopped calling them. - Added (patched only): none.
- Implication: the fix is in-place and subtractive — each handler had its
EvaluateCurrentState()call and the associated conditional branch removed, promoting the negative-value validation to unconditional. The Velocity kill-switch is retired entirely.
10. Confidence & Caveats
- Confidence: high (on what the change is); the exploitability question is deliberately bounded to what the binary proves.
- Rationale:
- The diff is minimal and unambiguous across all three functions: remove
call EvaluateCurrentState, make the< 0(and, for allocation,> max) test unconditional. Confirmed in both the Hex-Rays C and the raw.asmfor the position handler; confirmed in the C for allocation and EOF. EvaluateCurrentStateis definitively a WIL/Velocity gate overg_Feature_2614832441, returningcachedstate != 1, and is retired in the patched image.- Corrections applied: severity downgraded HIGH/HIGH/MEDIUM → Low across the board; fabricated pool-corruption/LPE/exploit-mitigation narrative removed; false "gate still called elsewhere" claim corrected; mis-ordered position assembly corrected.
- Open questions (not resolvable from this diff alone):
- The default resolved state of
g_Feature_2614832441on shipping builds (Default vs Enabled) — both makeEvaluateCurrentState()return true, i.e. validation active; only an explicit Disable exposes the accept-negatives path. - Whether a negative
AllocationSize/EndOfFilereaching the size/allocation path can be driven to an actual out-of-bounds write — unproven here; would require tracingNtfsPrepareToShrinkFileSize/NtfsAddAllocation/NtfsDeleteAllocationand reproducing a fault. If demonstrated, severity could rise; absent that, it stands as input-validation hardening.