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:

  1. The check is on by default even in the unpatched build. EvaluateCurrentState() returns true (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.
  2. 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-controlled AllocationSize (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 only v13 > max (the < 0 test is absent).
  • Patched: single unconditional guard if (v13 > max || v13 < 0) return STATUS_INVALID_PARAMETER.
  • Proven primitive: when the feature is disabled, a negative AllocationSize is accepted. It routes to the shrink path NtfsPrepareToShrinkFileSize(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-controlled EndOfFile (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/FileSize accepted 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: *v2 where v2 = *(a2 + 0x18) = attacker-controlled CurrentByteOffset (FILE_POSITION_INFORMATION).
  • Unpatched: if (EvaluateCurrentState() && *v2 < 0) reject; else { *(a1 + 0x68) = *v2; return 0; } — a negative offset is stored to FileObject+0x68 when the gate is false.
  • Patched: if (*v2 >= 0) { store; return 0; } else reject — unconditional non-negative check.
  • Proven primitive: a negative FileObject->CurrentByteOffset stored 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 does cmp v13,[Vcb+0x1E18] and the < 0 test; gate-false path does the upper-bound compare only. Gate-false reject block trace id 0xF37A6.
  • Patched: EvaluateCurrentState removed; a single guard applies > max || < 0, reject trace id 0xF379A.

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 trace 0xF3AD5.
  • Patched: EvaluateCurrentState removed; test v12,v12; js <reject> unconditional. Reject trace 0xF3AC6.

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 NtSetInformationFile handlers 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+0x68 is 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_ENTRY overwrite, 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 EvaluateCurrentState still exists (it is called elsewhere)." False — it has no callers and does not exist as a standalone routine in the patched image; g_Feature_2614832441 is entirely absent.
  • Verbatim position-handler assembly. The store was mis-placed on the jz fall-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 >= 0 check on CurrentByteOffset is 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 < 0 test is replaced by an unconditional v12 < 0 rejection.

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 EvaluateCurrentState helper and the g_Feature_2614832441 descriptor 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 .asm for the position handler; confirmed in the C for allocation and EOF.
  • EvaluateCurrentState is definitively a WIL/Velocity gate over g_Feature_2614832441, returning cachedstate != 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_2614832441 on shipping builds (Default vs Enabled) — both make EvaluateCurrentState() return true, i.e. validation active; only an explicit Disable exposes the accept-negatives path.
  • Whether a negative AllocationSize/EndOfFile reaching the size/allocation path can be driven to an actual out-of-bounds write — unproven here; would require tracing NtfsPrepareToShrinkFileSize / NtfsAddAllocation / NtfsDeleteAllocation and reproducing a fault. If demonstrated, severity could rise; absent that, it stands as input-validation hardening.