1. Overview

  • Unpatched Binary: dfs_unpatched.sys
  • Patched Binary: dfs_patched.sys
  • Overall Similarity Score: 0.9075
  • Diff Statistics:
    • Matched Functions: 112
    • Changed Functions: 79
    • Identical Functions: 33
    • Unmatched (Unpatched/Patched): 0 / 0
  • Verdict: The patch contains no security-relevant fix. The most-changed function, DfsReferralParseStringEntry, is refactored from a 6-argument to a 3-argument interface, but the alignment math and bounds checks are functionally identical in both builds. The remaining changes are routine modernizations: pool API updates (ExAllocatePoolWithTagExAllocatePool2), CFG _guard_dispatch_icall enforcement, stack zero-initialization, and WPP/ETW tracing refactoring.

2. Vulnerability Summary

  • Severity: None (no security-relevant change)
  • Vulnerability Class: None — this patch makes no security-relevant change to the referral-parsing path
  • Affected Functions: DfsReferralParseStringEntry (sub_1C0001E04 / sub_140002210), DfsFltFsctlGetReferralEx (sub_1C0002240 / sub_140001CB8)

Root Cause Analysis: There is no truncation vulnerability. The 2-byte alignment padding computed for the referral string is the value ((ptr + 1) & 0xFFFE) - ptr, which is 0 or 1 depending solely on the low bit of the pointer. Because only the low bit matters, computing it in 16-bit registers is exact; there is no loss of information versus 64-bit arithmetic. Both builds compute this value the same way using 16-bit register operations:

  • Unpatched: the caller DfsFltFsctlGetReferralEx computes it (and r9w, di; sub r9w, dx) and passes it to DfsReferralParseStringEntry as r9w (arg4), along with the remaining size (arg5) and start pointer (arg6).
  • Patched: DfsReferralParseStringEntry reads the pointer from *arg1 and the size from *arg2, then computes the identical alignment internally (and ax, di; sub ax, r11w — still 16-bit register operations).

Both builds then run the identical four bounds checks on the parsed length: (size - alignment) >= 2, length even, (size - alignment - 2) >= length, and remaining >= second_alignment. The parser returns the same validated UNICODE_STRING.Length in both builds, and the caller's downstream pool allocation (length1 + length2 + 0xBC) and two memmove copies operate on the same validated lengths. The change is a pure interface refactoring (moving the alignment computation and the initial size check from caller into callee); it does not add, remove, or alter any bounds check, and it does not fix an out-of-bounds condition.

Entry Point and Call Chain (for reference): 1. FSCTL_DFS_GET_REFERRALS_EX (0x601b0) is delivered to the DFS minifilter control device. 2. DfsFltHandleDeviceRequests routes the I/O control request. 3. DfsFltHandleFsControl identifies the FSCTL and dispatches it. 4. DfsFltFsctlGetReferralEx handles the request and calls the parser. 5. DfsReferralParseStringEntry validates and parses the referral string with the bounds checks described above. 6. Control returns to DfsFltFsctlGetReferralEx, which allocates a pool buffer sized to the validated lengths and copies the strings into it.


3. Pseudocode Diff

The diff below shows the alignment computation being shifted from the caller into the callee. The computed value is identical in both builds; the copy size is the validated out_str.Length returned by the parser.

// === UNPATCHED: DfsFltFsctlGetReferralEx (Caller) ===
// alignment padding is 0 or 1 (depends only on the low bit of data_ptr)
int alignment = ((data_ptr + 1) & 0xFFFE) - (data_ptr & 0xFFFF);   // == data_ptr & 1

// caller passes alignment, size and pointer to the parser
result = DfsReferralParseStringEntry(&ptr, &size, &out_str, alignment, remaining_size, data_ptr);

if (result >= 0) {
    // allocation is sized to the validated parsed lengths
    int32_t alloc_size = out_str.Length + out_str_2.Length + 0xBC;
    void* pool_buf = ExAllocatePoolWithTag(NonPagedPoolNx, alloc_size, tag);

    // copy uses the same validated length the allocation was sized from
    memmove(&pool_buf[0xB0], out_str.Buffer, out_str.Length);
}

// === PATCHED: DfsFltFsctlGetReferralEx (Caller) ===
// alignment/size handling now done inside the parser
result = DfsReferralParseStringEntry(&ptr, &size, &out_str);
// ...

// === PATCHED: DfsReferralParseStringEntry (Callee) ===
int64_t r11 = *arg1;               // read pointer
int32_t r9  = *arg2;               // read size
// same 0/1 alignment padding, computed with 16-bit register ops (and ax,di; sub ax,r11w)
uint16_t alignment = ((r11 + 1) & 0xFFFE) - (uint16_t)r11;

4. Assembly Analysis

Unpatched DfsFltFsctlGetReferralEx (sub_1C0002240, Caller)

The alignment calculation for the first call occurs at 0x1c0002343. The 16-bit register ops (r9w, dx) compute the 2-byte alignment padding (0 or 1); this is exact because the padding depends only on the pointer's low bit.

; UNPATCHED CALLER KEY SEQUENCE (first parser call)
00000001C000232C  lea     rdx, [rbx+8Ch]                   ; rdx = data pointer (arg1+0x8c)
00000001C0002333  mov     [rbp+47h+var_80], ecx            ; save size
00000001C0002336  lea     r9d, [rdx+1]                     ; addr + 1
00000001C000233A  mov     [rbp+47h+Buf2], rdx              ; save data pointer
00000001C000233E  mov     edi, 0FFFEh
00000001C0002343  and     r9w, di                          ; (addr+1) & 0xfffe (16-bit)
00000001C0002347  sub     r9w, dx                          ; alignment padding = 0 or 1
00000001C000234B  movzx   eax, r9w                         ; alignment (0 or 1)
00000001C000234F  cmp     ecx, eax
00000001C0002351  jb      loc_1C00026FA                    ; error if size < alignment
00000001C0002357  mov     [rsp+0C0h+var_98], rdx           ; arg6 = data pointer
00000001C000235C  lea     r8, [rbp+47h+DestinationString]  ; arg3 = output UNICODE_STRING
00000001C0002360  mov     dword ptr [rsp+0C0h+var_A0], ecx ; arg5 = size
00000001C0002364  lea     rdx, [rbp+47h+var_80]            ; arg2 = &size
00000001C0002368  lea     rcx, [rbp+47h+Buf2]              ; arg1 = &pointer
00000001C000236C  call    DfsReferralParseStringEntry

Unpatched DfsReferralParseStringEntry (sub_1C0001E04, Callee)

The function receives the alignment padding (0 or 1) in r9w and uses it to index the length field. The caller has already checked size >= alignment before this call.

; UNPATCHED CALLEE ENTRY (0x1c0001e04)
00000001C0001E0E  mov     r10d, [rsp+arg_20]        ; arg5 = remaining size
00000001C0001E16  mov     r11, [rsp+arg_28]         ; arg6 = start pointer (data_ptr)
00000001C0001E1E  movzx   eax, r9w                  ; alignment padding (0 or 1)
00000001C0001E22  sub     r10d, eax                 ; r10d = size - alignment
00000001C0001E25  movzx   eax, r9w                  ; reload alignment
00000001C0001E29  cmp     r10d, 2                   ; check if (size - align) >= 2
00000001C0001E2D  jb      short loc_1C0001E8B       ; if not, error
00000001C0001E2F  movzx   r9d, word ptr [rax+r11]   ; read length from [alignment + start_ptr]
00000001C0001E34  test    r9b, 1                    ; length must be even
00000001C0001E38  jnz     short loc_1C0001E8B
; ... returns the validated length in DestinationString.
; The patched callee (0x140002210) computes the same 0/1 alignment
; internally (and ax,di; sub ax,r11w) and runs the identical checks.

5. Trigger Conditions

This section lists the input the FSCTL path parses. No overflow results: both builds enforce the same bounds, and the and r9w, di; sub r9w, dx sequence can only ever yield 0 or 1 (the 2-byte alignment padding), so it cannot make the pool allocation smaller than the copied data.

  1. Context: The function checks ExGetPreviousMode(); the path is reached from KernelMode or through a kernel-mode DFS component via IRP_MJ_FILE_SYSTEM_CONTROL.
  2. Device Handle: A handle to the DFS minifilter control device (e.g., \\.\DfsFlt or equivalent).
  3. IOCTL: FSCTL_DFS_GET_REFERRALS_EX (0x601b0).
  4. Buffer Layout: The input buffer must be at least 0x88 bytes, containing:
    • Offset 0x00: Version field (2 or 0x17).
    • Offset 0x08-0x0b: Non-zero fields to pass version-specific validation.
    • Offset 0x80: 32-bit size field (>= 9 and size + 0x84 <= total buffer size).
    • Offset 0x86: Flags (bit 0 controls secondary string parsing).
    • Offset 0x88: Second 32-bit size field (size + 0x8c <= total buffer size).
    • Offset 0x8c: String referral data begins.
  5. Alignment value: The and r9w, di; sub r9w, dx sequence produces 0 or 1 regardless of the full 64-bit address, because 2-byte alignment padding depends only on the pointer's low bit.
  6. Length validation: The parser rejects the string unless (size - alignment) >= 2, the length is even, and (size - alignment - 2) >= length. The pool allocation is then sized length1 + length2 + 0xBC and each memmove copies exactly lengthN bytes, so the copy always fits.
  7. Observable Effect: None. The request is parsed and either succeeds or returns STATUS_INVALID_PARAMETER/STATUS_INSUFFICIENT_RESOURCES; there is no memory-safety violation on this path in either build.

6. Exploit Primitive & Development Notes

  • Primitive: None on this path. The pool allocation is sized to length1 + length2 + 0xBC and the two memmove copies move exactly length1 and length2 bytes into that buffer, so the copy size cannot exceed the allocation size. There is no attacker-controlled size/allocation mismatch, and the alignment value is bounded to 0/1 in both builds. No non-paged pool overflow is reachable here.
  • Modernizations present in the patch (not exploitable conditions):
    • ExAllocatePool2: Pool allocations were migrated from ExAllocatePoolWithTag to ExAllocatePool2, which zero-initializes by default.
    • CFG (_guard_dispatch_icall): Indirect-call sites (e.g., in FinalizeRequest) were routed through the CFG dispatch stub.
    • Zero-init: Several functions now memset stack structures before use. These are hardening/refactoring changes, not fixes for a specific reachable bug.

7. Debugger PoC Playbook

Attach a kernel debugger (WinDbg/KD) to the target machine running dfs_unpatched.sys. These breakpoints let a reviewer confirm that the alignment value is always 0 or 1 and that the copy size never exceeds the allocation, i.e. that there is no overflow on this path.

Breakpoints

Set these breakpoints to observe the parse and allocation path:

; 1. Break on FSCTL dispatch to catch 0x601b0 specifically
bp dfs_unpatched!DfsFltHandleFsControl ".if (@ecx == 0x601b0) { echo '[+] Hit FSCTL_DFS_GET_REFERRALS_EX' } .else { gc }"

; 2. Break at the alignment computation in the caller (first parser call)
bp dfs_unpatched!DfsFltFsctlGetReferralEx+0x103

; 3. Break at the parser function entry
bp dfs_unpatched!DfsReferralParseStringEntry

; 4. Break at the pool allocation to check sizes
bp dfs_unpatched!DfsFltFsctlGetReferralEx+0x25a

What to Inspect at Each Breakpoint

  • At DfsFltFsctlGetReferralEx+0x103 (0x1c0002343):
    • Inspect rdx (the data pointer, arg1+0x8c).
    • Single-step through the and r9w, di; sub r9w, dx instructions.
    • Confirm r9w is 0 when rdx is even and 1 when rdx is odd — it is the 2-byte alignment padding and can be nothing else.
  • At DfsReferralParseStringEntry Entry:
    • Inspect r9w (arg4): always 0 or 1.
    • Inspect r11 (qword [rsp+0x30]) to see the base pointer.
  • At DfsReferralParseStringEntry Internal Instruction (0x1c0001e2f):
    • Examine word [rax+r11]. This is the 16-bit length field read at [alignment + base]. The subsequent checks require it to be even and to fit within the remaining size.
  • At DfsFltFsctlGetReferralEx+0x25a (0x1c000249a, the ExAllocatePoolWithTag call):
    • Inspect r13d (or trace into ExAllocatePoolWithTag). The size is length1 + length2 + 0xBC.
    • Trace to the memmove calls (0x1c0002548 and 0x1c0002587). The Size argument (r8) is lengthN, which is always <= allocation size. No overflow occurs.

Struct/Offset Notes

  • Input Buffer + 0x00: Version (2 or 0x17)
  • Input Buffer + 0x80: Size_Field_1 (Offset/Size validation)
  • Input Buffer + 0x86: Flags
  • Input Buffer + 0x88: Size_Field_2 (Data bounds)
  • Input Buffer + 0x8c: String Data Origin

8. Changed Functions — Full Triage

Below is the triage of all changed functions. Many represent broader security modernizations.

  • DfsReferralParseStringEntry (sub_1C0001E04 / sub_140002210, Sim: 0.7521): [Behavioral/Refactor] Interface changed from 6 arguments to 3. The alignment computation and the initial size check moved from the caller into this function, but the alignment value (0/1 padding, 16-bit register ops in both builds) and all four bounds checks are functionally identical. No security-relevant change.
  • DfsFltFsctlGetReferralEx (sub_1C0002240 / sub_140001CB8, Sim: 0.8267): [Behavioral] Updated the call to match the new 3-argument parser interface and updated pool APIs from ExAllocatePoolWithTag to ExAllocatePool2. The allocation/copy path is unchanged in behavior.
  • DfsFltFormPrefix (sub_1C000B104 / sub_14000D68C, Sim: 0.9126): [Behavioral] The *arg1 <= 0 empty-prefix guard (cmp [ptr],0; jbe skip) before DfsFltConcatenateFilePath is present in BOTH builds. The only change is the pool API (ExAllocatePoolWithTagExAllocatePool2). Not security-relevant.
  • DfsFltFsctlGetReferral (Sim: 0.9154): [Behavioral] Routine API updates to ExAllocatePool2 and WPP tracing refactoring.
  • DfspFltAddDfsVolume (Sim: 0.8023): [Behavioral] Updated pool APIs (ExAllocatePool2) and refactored lock release primitives.
  • UpdateUserBuffer (Sim: 0.8889): [Behavioral] Refactored loop structures for MDL-based buffer mapping. Functionally equivalent.
  • UpCallCreateQueueEx / UpCallCompleteQueueRequest (Sim: ~0.65-0.88): [Behavioral] Queue structure layout changed slightly (field offsets shifted). Pool APIs updated.
  • FinalizeRequest (Sim: 0.9291): [Behavioral] Added _guard_dispatch_icall() for Control Flow Guard (CFG) enforcement on indirect calls.
  • DfsFltFsctlAttachToFileSystem (Sim: 0.9315): [Behavioral] Improved cleanup paths and added memset zero-initialization of stack variables before use.
  • DfsFltFsctlTranslatePath (Sim: 0.9888): [Behavioral] Corrected UNICODE_STRING pointer arithmetic and zero-initialized stack memory.
  • Cosmetic/Minor Changes (DfsFltFsctlSetDfsnShareInfo, DfspFltConvertStringSidToSid, DfsFltHandleFsControl, DfsFltFsctlDetachFromFileSystem): Primarily consist of zero-initializing structures before use, WPP tracing updates, and control flow flattening.

9. Unmatched Functions

There were 0 added and 0 removed functions between the unpatched and patched binaries. All remediations occurred inline within existing functions.


10. Confidence & Caveats

  • Confidence Level: High that this patch contains no security fix on the referral-parsing path. The alignment padding is 0/1 (low bit only) and is computed with 16-bit register operations in both builds; the four bounds checks and the allocation/copy sizing are identical.
  • Nature of the change: DfsReferralParseStringEntry and its caller DfsFltFsctlGetReferralEx were refactored (6-arg → 3-arg interface) with no behavioral difference in validation. Remaining changes across the binary are modernizations: ExAllocatePool2 migration, CFG _guard_dispatch_icall, stack zero-initialization, and WPP/ETW tracing refactoring.
  • Context note: ExGetPreviousMode() == KernelMode gates DfsFltFsctlGetReferralEx (0x1c0002240), so the path is reached from kernel context; this is unchanged between builds and is not itself a vulnerability.