Overview

  • Unpatched Binary: fbwf_unpatched.sys
  • Patched Binary: fbwf_patched.sys
  • Overall Similarity Score: 0.6656
  • Diff Statistics:
  • Matched Functions: 173 (124 changed, 49 identical)
  • Unmatched (Unpatched): 0
  • Unmatched (Patched): 0

Verdict: The load-bearing security fix is in FbwfGenerateFileName. The unpatched build allocates a fixed 0xFFFE-byte pool buffer, fills it with the volume name plus the current path (bounded to the remaining space), and then, when the target file has an alternate data stream, appends a ':' separator and the stream name at the running offset with no size check. Because the fixed buffer was never sized to hold the appended stream, a sufficiently long path leaves the colon and stream-name writes past the end of the allocation, corrupting adjacent paged pool. The patched build first computes the exact required size (volume length + path length + 2 for the colon + stream length) and allocates that, so the appended data always fits.

A second, sibling instance of the same bug and the same fix exists in DirTreeNode::AddDirEntry (the directory-entry name-construction path). The unpatched build allocates the same fixed 0xFFFE-byte paged-pool buffer, fills it with the current directory path via FbwfGetCurrentPath, and then appends the directory-entry name and the write-through-entry name at the running path offset with no size check. The patched build pre-measures path_len + 4 + max(entry_name_len, write_through_name_len) and allocates exactly that with ExAllocatePool2 before performing the identical copies. This is a second load-bearing CWE-787 fix; see Finding 5.

Two further changes are defensive rather than corrective: FbwfPortMessage gains a brand-new IOCTL 0xFB2F0001 handler that performs overflow-checked count * block_size arithmetic (a new feature that returns STATUS_INTEGER_OVERFLOW on overflow; the product only updates a cache-accounting counter, not an allocation). FbwfCheckAndTranslateVolumeName replaces a manual wide-string length loop with RtlInitUnicodeString, which performs the identical unbounded null-terminator scan; the two builds behave the same. FbwfWrite's byte-offset sentinel handling is a structural refactor with no behavioral change.


2. Vulnerability Summary

Finding 1: Pool Out-of-Bounds Write in FbwfGenerateFileName (Medium)

  • Severity: Medium
  • Vulnerability Class: Out-of-Bounds Write / Incorrect Buffer Size Calculation (CWE-787, root cause CWE-131)
  • Affected Function: FbwfGenerateFileName (Unpatched: 0x1C0001CB0 / Patched: 0x140002A80)
  • Root Cause: In the option-byte 1/2 name-construction path, the unpatched function allocates a fixed 0xFFFE (65534) byte pool buffer (mov r15d, 0FFFEh @ 0x1C0001E53, ExAllocatePoolWithTag @ 0x1C0001E61). It copies the volume name (memmove @ 0x1C0001E8F, size = word[rdx+0x18]), sets the remaining budget to 0xFFFE - volume_len (sub r15d, esi @ 0x1C0001E94), and writes the current path bounded by that remainder via FbwfGetCurrentPath (@ 0x1C0001EAD). After this, si holds volume_len + path_len. If the file object has a stream (cmp [r14+70h], r12 @ 0x1C0001EB5), the code writes a ':' at byte offset si (mov [rax+rcx*2], dx @ 0x1C0001ECF) and then memmoves the stream name at buffer + si + 2 (@ 0x1C0001E1C) — with no check that si + 2 + stream_len <= 0xFFFE. The buffer was sized to hold only the volume name and path, so when those fill it the colon and stream-name bytes are written past the allocation into adjacent paged pool.
  • Attacker-Reachable Entry Point: File create/name-query on an fbwf-protected volume for a file bearing an alternate data stream, reaching the minifilter name-generation path.
  • Call Chain:
  • A file with an alternate data stream (path:stream) on an fbwf-monitored volume is opened/queried.
  • FbwfGenerateFileName allocates the fixed 0xFFFE buffer and copies volume name + path.
  • The stream branch appends ':' + stream name at volume_len + path_len with no bounds check.
  • When volume_len + path_len approaches 0xFFFE, the appended bytes overflow the pool allocation.
  • Fix: The patched function pre-measures the full length before allocating. It calls FbwfGetCurrentPath in measure mode (r8d = 0 @ 0x140002C2F), forms ebx = volume_len + path_len (0x140002C3F/0x140002C42), and when a stream is present adds the colon and stream length: lea r15d, [rbx+2] (@ 0x140002C6E) then lea r15d, [r15+rax*2] (@ 0x140002C72). It allocates exactly that size with ExAllocatePool2 (@ 0x140002C8B) before performing the same copies, so the appended stream can never exceed the buffer.

Finding 2: Integer Overflow Hardening in FbwfPortMessage Scratch-Space Sizing (Low)

  • Severity: Low
  • Vulnerability Class: Integer Overflow in Size Computation, defensively hardened (CWE-190)
  • Affected Function: FbwfPortMessage (Unpatched: 0x1C0009E00 / Patched: 0x140003DB0)
  • Root Cause: The patched handler adds a scratch-space adjustment path (reached only for IOCTL 0xFB2F0001, cmp r13d, 0FB2F0001h @ 0x140004DBB) that reads a user-supplied 64-bit count from the message body (mov r15, [rcx+r9] @ 0x140004DB0) and multiplies it by the volume's block size (mov r8d, [rcx+14h] @ 0x140004E1F; mul r15 @ 0x140004E45). The multiplication is guarded by an overflow test (test rdx, rdx; jnz @ 0x140004E4F) that, on overflow, forces the size to -1 and returns STATUS_INTEGER_OVERFLOW (0xC0000095, @ 0x140004E5B). Non-positive counts are rejected up front (test r15, r15; jle default @ 0x140004DB2) and the 0x7FFFFFFFFFFFFFFF sentinel is special-cased (cmp r15, rax @ 0x140004DFF). The resulting product feeds a running cache-accounting counter (mov cs:TotalBytes, r9 @ 0x140004F68), compared against system memory information from ZwQuerySystemInformation (@ 0x140004DDB); it does not size a pool allocation. The unpatched FbwfPortMessage contains no 0xFB2F0001 case, no mul/imul, and no ZwQuerySystemInformation call. Both builds bound-check the IOCTL dispatch index identically (lea eax, [reg+0x4e0fffe]; cmp eax, 0xb; ja default).
  • Attacker-Reachable Entry Point: FilterSendMessage (User mode) -> FltMgr -> FbwfPortMessage
  • Call Chain:
  • User-mode process connects to the filter communication port via FilterConnectCommunicationPort.
  • Process calls FilterSendMessage with IOCTL 0xFB2F0001.
  • FltMgr forwards the message to FbwfPortMessage.
  • The handler extracts count, rejects non-positive values, and multiplies it by the volume block size under the overflow check.
  • On overflow the request returns STATUS_INTEGER_OVERFLOW; otherwise the product updates the scratch-space accounting total.

Finding 3: FbwfCheckAndTranslateVolumeName String-Length Refactor (No Security-Relevant Change)

  • Severity: Informational
  • Vulnerability Class: N/A (source-level refactor)
  • Affected Function: FbwfCheckAndTranslateVolumeName (Unpatched: 0x1C000EA7C / Patched: 0x14000DD6C)
  • Root Cause: The unpatched code computes the length of the volume-name string with an inlined wcslen-style loop (inc r9; cmp [rbx+r9*2], r15w; jnz @ 0x1C000EAB7). The patched code replaces that loop with a call to RtlInitUnicodeString (@ 0x14000DDB5). RtlInitUnicodeString computes Length by scanning for the terminating null wide character with no upper bound — it performs the exact same unbounded scan the manual loop did and adds no bounds checking. After computing the length, both builds apply the identical validation Length + 2 <= 0x80 (unpatched add rsi, 2; cmp rsi, 0x80 @ 0x1C000EAD1; patched lea rcx, [rdx+2]; cmp rcx, 0x80 @ 0x14000DDCF) and take the same subsequent code path (FltGetVolumeFromName, the drive-letter check, memmove). There is no behavioral difference between the two builds; this is a source-level refactor (inlined loop replaced by a library call), not a fix.
  • Attacker-Reachable Entry Point: FilterSendMessage -> FltMgr -> FbwfPortMessage -> FbwfCheckAndTranslateVolumeName

Finding 4: FbwfWrite Byte-Offset Handling (No Security-Relevant Change)

  • Severity: Informational
  • Vulnerability Class: N/A (structural refactor)
  • Affected Function: FbwfWrite (Unpatched: 0x1C0005990 / Patched: 0x14000B480)
  • Root Cause: Both builds handle the two sentinel byte-offset values identically: FILE_USE_FILE_POINTER_POSITION (QuadPart == -2) substitutes the file object's CurrentByteOffset, and FILE_WRITE_TO_END_OF_FILE (HighPart == -1 && LowPart == -1) substitutes the end-of-file offset. Both builds also perform the same wrap guard on the resulting write range (length + offset < length rejects with STATUS_INVALID_PARAMETER). The patched code restructures these checks (flattening the nested HighPart == -1 test into a direct QuadPart == -2 comparison) but adds no new offset validation. This is a refactor with no security-relevant behavioral difference.
  • Attacker-Reachable Entry Point: NtWriteFile / WriteFile -> IO Manager -> FltMgr -> FbwfWrite

Finding 5: Pool Out-of-Bounds Write in DirTreeNode::AddDirEntry (Medium)

  • Severity: Medium
  • Vulnerability Class: Out-of-Bounds Write / Incorrect Buffer Size Calculation (CWE-787, root cause CWE-131)
  • Affected Function: DirTreeNode::AddDirEntry (the (_FbwfDirNameEntry*, wchar_t const*, _FbwfVolumeEntry*, ulong) overload) (Unpatched: 0x1C00112D0 / Patched: 0x140013E0C)
  • Root Cause: In the directory-entry insertion path, the unpatched function allocates a fixed 0xFFFE (65534) byte paged-pool buffer (mov r12d, 0FFFEh @ 0x1C00114B6; ExAllocatePoolWithTag @ 0x1C00114C4) and fills it with the current directory path via FbwfGetCurrentPath bounded to 0xFFFE (mov r8d, r12d @ 0x1C00114F1; call FbwfGetCurrentPath @ 0x1C00114FA). It sets r14 to the end of the path inside that buffer (lea r14, [rdi+rax*2] @ 0x1C001151C) and then memmoves the directory-entry name (the copy held in [rbx+8], size = name_len + 2 from [rsp+Size]) to buffer + path_offset (call memmove @ 0x1C001152C) with no check that path_len + name_len + 2 <= 0xFFFE. When a write-through/short-name entry is present ([rbx+10h] non-NULL), it measures that name's length (inc rsi; cmp [rdx+rsi*2], cx; jnz @ 0x1C0011561) and memmoves it at the same path offset r14 (lea r8d, [rsi+rsi] @ 0x1C001156A; add r8, 2; call memmove @ 0x1C001157A), again with no bounds check. Because the buffer was sized to hold only the path, a directory whose path fills the fixed 0xFFFE buffer leaves the appended entry-name/write-through-name bytes past the end of the allocation, corrupting adjacent paged pool.
  • Attacker-Reachable Entry Point: File/directory create on an fbwf-protected volume reaching the minifilter pre-create callback FbwfPreCreate, which calls DirTreeNode::AddDirEntry (the same overload is also reached from FbwfRestoreDeletedFile). The appended name (arg2) is the created entry's name, so its length is attacker-influenced.
  • Call Chain:
  • A file/directory create on an fbwf-protected volume reaches FbwfPreCreate, which calls DirTreeNode::AddDirEntry to insert the new entry (also reached from FbwfRestoreDeletedFile).
  • AddDirEntry allocates the fixed 0xFFFE buffer and fills it with the current directory path via FbwfGetCurrentPath.
  • It appends the directory-entry name (and, when present, the write-through/short-name entry) at the running path offset with no bounds check.
  • When path_len fills the 0xFFFE buffer, the appended name bytes overflow the paged-pool allocation.
  • Fix: The patched function pre-measures the exact required size before allocating. It calls FbwfGetCurrentPath in measure mode (xor r8d, r8d; lea r9, [rsp+var_58]; call FbwfGetCurrentPath @ 0x140014046/0x140014056), adds 4 (add edx, 4 @ 0x14001405F), takes the maximum of the directory-entry name length and the write-through/short-name length (cmp r15, rax; ja @ 0x140014088/0x14001408B, recomputed at 0x14001409F/0x1400140A5), folds it in (add r15d, edx @ 0x1400140A8), and allocates exactly that many bytes with ExAllocatePool2 (mov ecx, 102h; call ExAllocatePool2 @ 0x1400140BC/0x1400140C1) before performing the identical copies (memmove @ 0x140014133 and 0x140014199). The appended names can no longer exceed the buffer.

3. Pseudocode Diff

FbwfGenerateFileName (option-byte 1/2 path, stream branch)

Focus: fixed-size allocation with an unaccounted stream-name append vs. exact pre-measured sizing.

// UNPATCHED (0x1C0001CB0)
buffer = ExAllocatePoolWithTag(PagedPool, 0xFFFE, tag);   // fixed size
memmove(buffer, volume_name, volume_len);                 // volume name
remaining = 0xFFFE - volume_len;
path_len  = FbwfGetCurrentPath(vol, buffer + volume_len, remaining, 0); // bounded to remaining
off = volume_len + path_len;
if (file_object->stream != NULL) {
    buffer[off/2] = ':';                                  // NO check off+2 <= 0xFFFE
    off += 2;
    stream_len = wcslen(stream_name) * 2;
    memmove(buffer + off, stream_name, stream_len);       // NO check off+stream_len <= 0xFFFE
}
// volume+path fill the fixed buffer, so the colon + stream write past it.


// PATCHED (FbwfGenerateFileName @ 0x140002A80)
path_len = FbwfGetCurrentPath(vol, 0, 0, &len);           // measure only
size = volume_len + path_len;
if (file_object->stream != NULL)
    size = size + 2 + stream_len * 2;                     // account for colon + stream
buffer = ExAllocatePool2(0x102, size, tag);               // exact size
// ... then copy volume, path, ':' and stream into a buffer that fits.

FbwfPortMessage (IOCTL 0xFB2F0001 scratch-space handler)

Focus: overflow-checked multiplication of a user count by the volume block size (patched-only feature).

// UNPATCHED (0x1C0009E00)
// No 0xFB2F0001 case exists; no user-count * block_size multiplication
// and no ZwQuerySystemInformation call anywhere in this function.


// PATCHED (FbwfPortMessage @ 0x140003DB0)
int64_t count = *(int64_t*)(msg_body);            // user-supplied count
if (count <= 0) goto default;                     // reject non-positive
if (count == 0x7fffffffffffffff) { ... }          // MAX_VALUE special-cased
uint32_t block_size = *(uint32_t*)(vol_meta + 0x14);
uint64_t product = (uint64_t)block_size * count;  // mul r15
if (overflow /* rdx != 0 */) {                    // 128-bit overflow test
    product = -1;
    status  = STATUS_INTEGER_OVERFLOW;            // 0xC0000095
}
// product updates cs:TotalBytes cache accounting; it does NOT size an allocation.

FbwfCheckAndTranslateVolumeName

Focus: inlined length loop vs. RtlInitUnicodeString — equivalent behavior.

// UNPATCHED (0x1C000EA7C)
int64_t r9 = -1;
do { r9 += 1; } while (arg1[r9] != 0);     // wcslen: scans until null wchar
int32_t len_bytes = r9 * 2;
if (len_bytes + 2 <= 0x80) { ... }


// PATCHED (FbwfCheckAndTranslateVolumeName @ 0x14000DD6C)
RtlInitUnicodeString(&vname, arg1);        // internally the same unbounded null scan
uint64_t len_bytes = vname.Length;
if (len_bytes + 2 <= 0x80) { ... }
// Same scan, same 0x80 check, same downstream path. No behavioral difference.

DirTreeNode::AddDirEntry (directory-entry name-construction path)

Focus: fixed-size allocation with an unaccounted name append vs. exact pre-measured sizing.

// UNPATCHED (0x1C00112D0)
buffer = ExAllocatePoolWithTag(PagedPool, 0xFFFE, tag);   // fixed size
path_len = FbwfGetCurrentPath(vol, buffer, 0xFFFE, 0);    // fill path, bounded to 0xFFFE
dst = buffer + path_len;                                  // r14 = end of path
memmove(dst, entry_name, name_len + 2);                   // NO check path_len+name_len+2 <= 0xFFFE
if (write_through_entry != NULL) {
    wt_len = wcslen(write_through_name) * 2;
    memmove(dst, write_through_name, wt_len + 2);         // NO check path_len+wt_len+2 <= 0xFFFE
}
// the path can fill the fixed buffer, so the name/write-through append writes past it.


// PATCHED (DirTreeNode::AddDirEntry @ 0x140013E0C)
path_len = FbwfGetCurrentPath(vol, 0, 0, &len);           // measure only
size = path_len + 4;
size += max(entry_name_len, write_through_name_len);      // account for the appended name
buffer = ExAllocatePool2(0x102, size, tag);               // exact size
// ... then fill path and copy the name(s) into a buffer that fits.

4. Assembly Analysis

FbwfGenerateFileName Fixed Allocation vs. Sized Allocation

The unpatched build pins the allocation at 0xFFFE and later appends the stream without adjusting for it; the patched build folds the stream length into the size before allocating.

; UNPATCHED @ 0x1C0001E4D  fixed allocation, then unchecked stream append
0x1C0001E53  mov     r15d, 0FFFEh              ; fixed buffer size
0x1C0001E61  call    __imp_ExAllocatePoolWithTag
0x1C0001E8F  call    memmove                  ; volume name (size = word[rdx+18h])
0x1C0001E94  sub     r15d, esi                ; remaining = 0xFFFE - volume_len
0x1C0001EAD  call    FbwfGetCurrentPath       ; path, bounded to remaining
0x1C0001EB2  add     si, ax                   ; si = volume_len + path_len
0x1C0001EB5  cmp     [r14+70h], r12           ; stream present?
0x1C0001ECF  mov     [rax+rcx*2], dx          ; write ':' at offset si (NO bound check)
0x1C0001E1C  call    memmove                  ; stream name at buffer+si+2 (NO bound check)

; PATCHED @ 0x140002C21  measure first, size includes the stream
0x140002C2F  call    FbwfGetCurrentPath       ; r8d = 0 -> measure path length only
0x140002C42  add     ebx, [rsp+68h+var_40]    ; ebx = volume_len + path_len
0x140002C6E  lea     r15d, [rbx+2]            ; + ':' separator
0x140002C72  lea     r15d, [r15+rax*2]        ; + stream_len*2
0x140002C8B  call    __imp_ExAllocatePool2    ; allocate exactly r15d bytes

FbwfPortMessage Overflow Check (Patched-Only)

The IOCTL dispatch bounds check is present in both builds; the overflow-checked multiplication exists only in the patched 0xFB2F0001 handler.

; IOCTL dispatch bounds check  PRESENT IN BOTH BUILDS
; UNPATCHED @ 0x1C000A746          PATCHED @ 0x140004733
lea     eax, [r15 + 0x4e0fffe]     lea     eax, [r13 + 0x4e0fffe]
cmp     eax, 0xb                   cmp     eax, 0xb
ja      default_case               ja      default_case

; PATCHED-ONLY: 0xFB2F0001 scratch-space handler overflow check
0x140004DBB  cmp     r13d, 0FB2F0001h        ; select scratch-space case
0x140004E1F  mov     r8d, [rcx+14h]          ; block_size (volume metadata)
0x140004E45  mul     r15                     ; count * block_size (128-bit)
0x140004E4F  test    rdx, rdx                ; overflow if high half != 0
0x140004E5B  mov     ebx, 0C0000095h         ; STATUS_INTEGER_OVERFLOW
0x140004F68  mov     cs:TotalBytes, r9       ; product updates cache accounting
; No corresponding multiplication exists in the unpatched function.

FbwfCheckAndTranslateVolumeName Length Computation

The unpatched inlined loop and the patched RtlInitUnicodeString call resolve the string length the same way, and both feed the same <= 0x80 check.

; UNPATCHED @ 0x1C000EAB7  inlined wcslen loop
mov     r9d, 0FFFFFFFFh
loop_start:
inc     r9
cmp     [rbx + r9*2], r15w        ; read wide char, compare to 0
jnz     short loop_start          ; scans until null wchar (no upper bound)
add     r9d, r9d                  ; length in bytes
add     rsi, 2
cmp     rsi, r12 ; 0x80           ; Length + 2 <= 0x80

; PATCHED @ 0x14000DDB5  RtlInitUnicodeString (same unbounded scan internally)
call    __imp_RtlInitUnicodeString
movzx   edx, [rbp+DestinationString.Length]
lea     rcx, [rdx+2]
cmp     rcx, r12 ; 0x80           ; Length + 2 <= 0x80  (identical check)

DirTreeNode::AddDirEntry Fixed Allocation vs. Pre-Measured Allocation

The unpatched build pins the allocation at 0xFFFE and appends the entry/write-through name at the running path offset without accounting for it; the patched build measures the path and folds in the larger of the two name lengths before allocating.

; UNPATCHED @ 0x1C00114B6  fixed allocation, then unchecked name append
0x1C00114B6  mov     r12d, 0FFFEh              ; fixed buffer size
0x1C00114C4  call    cs:__imp_ExAllocatePoolWithTag
0x1C00114F1  mov     r8d, r12d                 ; path bound = 0xFFFE
0x1C00114FA  call    FbwfGetCurrentPath        ; fill directory path
0x1C001151C  lea     r14, [rdi+rax*2]          ; r14 = end of path
0x1C0011520  mov     r8, [rsp+78h+Size]        ; size = name_len + 2
0x1C0011525  mov     rdx, [rbx+8]              ; entry-name copy
0x1C001152C  call    memmove                   ; append name at path end (NO bound check)
0x1C001156A  lea     r8d, [rsi+rsi]            ; write-through name length
0x1C0011573  add     r8, 2
0x1C001157A  call    memmove                   ; append write-through name at path end (NO bound check)

; PATCHED @ 0x140014041  measure first, size includes the appended name
0x140014046  xor     r8d, r8d                  ; r8d = 0 -> measure path length only
0x140014056  call    FbwfGetCurrentPath        ; measure path length into var_58
0x14001405F  add     edx, 4                    ; path_len + 4
0x140014088  cmp     r15, rax                  ; entry_name_len vs write-through_name_len
0x14001408B  ja      loc_1400140A8             ; r15 = max(entry_name_len, write_through_name_len)
0x1400140A8  add     r15d, edx                 ; + (path_len + 4)
0x1400140BC  mov     ecx, 102h
0x1400140C1  call    cs:__imp_ExAllocatePool2  ; allocate exactly r15d bytes

5. Trigger Conditions

Trigger 1: Pool Out-of-Bounds Write (FbwfGenerateFileName)

  1. On a volume monitored by the fbwf minifilter, create or reference a file that carries an alternate data stream (path:stream).
  2. Arrange for the file's full path to be long enough that volume_len + path_len approaches the fixed 0xFFFE-byte buffer (a deeply nested directory path).
  3. Trigger the minifilter name-generation path (file create / name query) so FbwfGenerateFileName runs on the option-byte 1/2 branch.
  4. Observation: On the unpatched build, once the volume name and path fill the fixed buffer, the ':' and stream-name writes land past the 0xFFFE pool allocation, corrupting adjacent paged pool. On the patched build the buffer is sized to include the stream, so the writes stay in bounds.

Trigger 2: Integer Overflow Rejection (FbwfPortMessage)

  1. Obtain a handle to the fbwf communication port via FilterConnectCommunicationPort.
  2. Construct an input buffer with the IOCTL code 0xFB2F0001 selecting the scratch-space handler.
  3. Populate the count field in the message body. Provide an input buffer length (arg3) >= 8 bytes and an output buffer length (arg5) >= 8 bytes.
  4. Set count such that count * block_size would overflow 64-bit arithmetic (for block_size = 0x1000, a count of 0x0010000000000000 or larger).
  5. Send the message via FilterSendMessage.
  6. Observation: On the patched build the overflow test detects the overflow and the call returns STATUS_INTEGER_OVERFLOW (0xC0000095) with no state change. There is no undersized allocation and no pool corruption. The unpatched build has no such handler.

Trigger 3: FbwfWrite Sentinel Offset Handling (No Change)

  1. Open a handle to a file residing on a volume monitored by the fbwf minifilter.
  2. Construct a write operation using NtWriteFile or ZwWriteFile.
  3. Supply a LARGE_INTEGER ByteOffset of 0xFFFFFFFFFFFFFFFE (use current position) or 0xFFFFFFFFFFFFFFFF (append).
  4. Execute the write request.
  5. Observation: Both builds substitute the current-position or end-of-file offset for these sentinels and apply the same length + offset wrap guard. Behavior is identical between builds; there is no security-relevant difference to exercise here.

Trigger 4: Pool Out-of-Bounds Write (DirTreeNode::AddDirEntry)

  1. On a volume monitored by the fbwf minifilter, create a file or directory whose parent directory path is deeply nested so the current path nearly fills the fixed 0xFFFE-byte buffer.
  2. Give the created entry a name (and/or drive a write-through entry) so the appended name pushes path_len + name_len past 0xFFFE.
  3. Trigger the create so FbwfPreCreate calls DirTreeNode::AddDirEntry to insert the entry.
  4. Observation: On the unpatched build, once the current path fills the fixed 0xFFFE buffer, the entry-name (0x1C001152C) and, if present, the write-through-name (0x1C001157A) memmoves land past the 0xFFFE allocation, corrupting adjacent paged pool. On the patched build the allocation is pre-sized to path_len + 4 + max(entry_name_len, write_through_name_len), so the copies stay in bounds.

6. Exploit Primitive & Development Notes

Primitive 1: Kernel Paged-Pool Out-of-Bounds Write (FbwfGenerateFileName)

The unaccounted stream-name append yields a paged-pool out-of-bounds write. When volume_len + path_len fills the fixed 0xFFFE buffer, the driver writes a ':' (2 bytes) followed by the stream name (up to stream_len bytes) past the end of the allocation.

  • Exploitation Strategy: The overflow length is 2 + stream_len bytes and the stream-name content is attacker-influenced, giving a controlled linear write into whatever follows the allocation in paged pool. Practical reachability is constrained: the path must be long enough (with the volume name) to consume nearly the full 0xFFFE buffer, since FbwfGetCurrentPath is bounded to the remaining space. Under those conditions the write corrupts adjacent pool, which can be steered toward a pool-based exploitation primitive or, at minimum, a bugcheck. The patched build removes the primitive by sizing the allocation to include the appended stream.

Primitive 2: None (Overflow-Checked Size Computation, FbwfPortMessage)

The multiplication in the FbwfPortMessage scratch-space handler does not yield an exploit primitive. The product of count * block_size updates the TotalBytes cache-accounting counter and is compared against ZwQuerySystemInformation memory sizes, not used to size a pool allocation. On the patched build the overflow test rejects any overflow with STATUS_INTEGER_OVERFLOW, and the unpatched build does not contain this handler at all. There is no undersized allocation and therefore no pool-overflow write primitive to develop here.


7. Debugger PoC Playbook

This playbook provides actionable guidance for triggering the finding with an attached kernel debugger (KD/WinDbg).

Finding 1: FbwfGenerateFileName Pool Out-of-Bounds Write

  • Breakpoints:
  • bp fbwf_unpatched!FbwfGenerateFileName (0x1C0001CB0) — catches name generation. Follow the option-byte 1/2 path to 0x1C0001E4D.
  • Set a breakpoint at the fixed allocation (0x1C0001E61) and at the stream memmove (0x1C0001E1C).
  • What to Inspect:
  • At the allocation: confirm the size operand is 0xFFFE regardless of the file path.
  • At 0x1C0001EB2: si = volume_len + path_len. If si is near 0xFFFE, the pending colon/stream writes will overflow.
  • At 0x1C0001ECF / 0x1C0001E1C: confirm the destination offset (si, then si+2) plus the stream length exceeds 0xFFFE.
  • Key Instructions/Offsets:
  • 0x1C0001E53: fixed 0xFFFE size loaded before ExAllocatePoolWithTag.
  • 0x1C0001ECF: ':' write at offset si with no bound check.
  • 0x1C0001E1C: stream-name memmove at buffer + si + 2 with no bound check.
  • Trigger Setup:
  • On an fbwf-protected volume, reference a file with an alternate data stream whose full path is deeply nested so volume_len + path_len approaches 0xFFFE.
  • Expected Observation:
  • With Special Pool enabled for fbwf.sys, the append triggers an immediate bugcheck (SPECIAL_POOL_DETECTED_MEMORY_CORRUPTION). Without Special Pool, the colon and stream bytes silently corrupt adjacent paged pool. On the patched build the allocation is sized to include the stream, so the writes stay in bounds.

Finding 2: FbwfPortMessage Overflow-Checked Multiplication

  • Breakpoints:
  • bp fbwf_patched!FbwfPortMessage (0x140003DB0) — catches all user-mode messages. Inspect rdx (input buffer) and r8d (input size).
  • Set a breakpoint at the mul in the scratch-space handler (0x140004E45) to observe the count * block_size computation.
  • What to Inspect:
  • At the entry point: r8d must be >= 8. rdx points to the user input copied to pool. Check [rdx] for the IOCTL code.
  • At the multiply: confirm count (from user input, r15) and block_size (from volume metadata) before the overflow test at 0x140004E4F.
  • Key Instructions/Offsets:
  • 0x140004733: the IOCTL dispatch bounds check (lea eax, [r13+0x4e0fffe]; cmp eax, 0xb; ja) — the same guard is present in the unpatched build at 0x1C000A746.
  • 0x140004DBB: the 0xFB2F0001 case selector. 0x140004E45: the mul. 0x140004E5B: STATUS_INTEGER_OVERFLOW on overflow.
  • Trigger Setup:
  • Open the minifilter port from user mode using FilterConnectCommunicationPort.
  • Send a packet via FilterSendMessage with buffer length >= 8 and IOCTL 0xFB2F0001, with count set to overflow the multiply.
  • Expected Observation:
  • With an overflow-triggering count, the overflow test fails, the size is forced to -1, and the handler returns STATUS_INTEGER_OVERFLOW (0xC0000095). No allocation is sized from the product and no pool corruption occurs.

8. Changed Functions — Full Triage

  • FbwfGenerateFileName (Security Relevant): Unpatched sizes the name buffer at a fixed 0xFFFE and appends a ':' plus the alternate-data-stream name at volume_len + path_len with no bounds check, allowing a paged-pool out-of-bounds write when the path fills the buffer. Patched pre-measures volume_len + path_len + 2 + stream_len*2 and allocates exactly that with ExAllocatePool2. This is a load-bearing security fix.
  • DirTreeNode::AddDirEntry (Security Relevant): Unpatched allocates a fixed 0xFFFE paged-pool buffer (ExAllocatePoolWithTag @ 0x1C00114C4), fills it with the current directory path via FbwfGetCurrentPath, then appends the directory-entry name (memmove @ 0x1C001152C) and the write-through-entry name (memmove @ 0x1C001157A) at the running path offset with no check that path_len + name_len stays within 0xFFFE, allowing a paged-pool out-of-bounds write when the path fills the buffer. Patched pre-measures path_len + 4 + max(entry_name_len, write_through_name_len) and allocates exactly that with ExAllocatePool2 (@ 0x1400140C1). A second load-bearing security fix (see Finding 5), the same CWE-787/CWE-131 class as FbwfGenerateFileName.
  • FbwfPortMessage (Security Relevant): Adds an overflow-checked count * block_size computation in a new scratch-space handler (IOCTL 0xFB2F0001), returning STATUS_INTEGER_OVERFLOW on overflow. The product feeds TotalBytes cache accounting, not a pool allocation; the IOCTL dispatch bounds check is present in both builds. Defensive CWE-190 hardening on a new feature.
  • FbwfCheckAndTranslateVolumeName (Behavioral): Replaced the inlined wide-string length loop with RtlInitUnicodeString, which performs the same unbounded null-terminator scan. Both builds then apply the identical Length + 2 <= 0x80 check and the same downstream logic. Source-level refactor with no behavioral difference.
  • FbwfWrite (Behavioral): Restructured the byte-offset sentinel handling (FILE_USE_FILE_POINTER_POSITION, FILE_WRITE_TO_END_OF_FILE). Both builds substitute the current/end offset for these sentinels and apply the same length + offset wrap guard; no new offset validation is introduced.
  • FbwfNormalizeNameComponent (Behavioral): Replaced a fixed 0xFFFE ExAllocatePoolWithTag buffer with a dynamically sized ExAllocatePool2 allocation of max(component_len, name_len) + 1 bytes. The copy operations into the pool buffer are not newly bounds-checked, and the only length check present (cmp eax, [Length], guarding the caller's output buffer) is identical in both builds. The fixed 0xFFFE already accommodated realistic single-component/path inputs; this is defensive fixed-to-dynamic sizing plus allocation-API churn, not a demonstrable overflow fix.
  • FbwfPreCreate (Behavioral): Refactored file creation logic to include explicit share access checks (IoCheckLinkShareAccess) and added validations for malformed file names/related file objects.
  • FbwfCompress (Behavioral): Migrated memory management to Lookaside Lists and added an explicit FinalCompressedSize >= 0x1000 check to validate compression results.
  • FbwfExtendFileSize (Behavioral): Replaced legacy ExAllocatePoolWithTag with zero-flag semantics (ExAllocatePool2) and implemented Control Flow Guard (guard_dispatch_icall) for indirect calls.
  • FbwfQuerySecurity (Cosmetic): Structural field offset shifts and renaming of internal wrapper functions to standard library calls (e.g., memcpy).

9. Unmatched Functions

  • Added/Removed: None.
  • Implication: The patch is entirely contained within the modification of existing functions. No new mitigation callbacks or wrapper functions were introduced at the compilation level, meaning security enhancements are localized to the modified logic.

10. Confidence & Caveats

  • Confidence: High. The assembly shows the fixed 0xFFFE allocation and the unaccounted stream-name append in the unpatched FbwfGenerateFileName, and the pre-measured size (including the stream) in the patched build; the same fixed 0xFFFE allocation with an unaccounted entry-name/write-through-name append in the unpatched DirTreeNode::AddDirEntry, and the pre-measured path_len + 4 + max(name lengths) allocation in the patched build; the patched-only overflow-checked multiplication in FbwfPortMessage feeding TotalBytes accounting; the equivalent RtlInitUnicodeString-vs-loop length computation in FbwfCheckAndTranslateVolumeName; and the equivalent FbwfWrite offset handling and wrap guard across both builds.
  • Assumptions:
  • The fbwf.sys (File-Based Write Filter) driver is active, which is standard on Windows Preinstallation Environment (WinPE) or kiosk-style embedded Windows deployments.
  • For Finding 1, reachability requires a file path long enough that the volume name plus path fill the fixed 0xFFFE buffer, combined with an alternate data stream on the target file.
  • For Finding 2, the attacker is assumed to have sufficient privileges to open the FilterSendMessage communication port; depending on the port ACLs this may require medium-integrity administrative access. The exact communication port name should be verified dynamically (e.g., via fltmc instances).
  • Manual Verification Required:
  • A reverse engineer writing a Finding 1 proof-of-concept will need to construct a path/stream combination that drives volume_len + path_len to the 0xFFFE boundary and confirm the overflow length (2 + stream_len) against the pool block.
  • For Finding 2, mapping the exact byte offset of the count field within the 0xFB2F0001 message body and the target volume's block_size is required to force the multiply to overflow.