fbwf.sys — pool out-of-bounds write from unaccounted stream-name append in FbwfGenerateFileName (CWE-787) fixed
KB5073723
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 to0xFFFE - volume_len(sub r15d, esi@0x1C0001E94), and writes the current path bounded by that remainder viaFbwfGetCurrentPath(@0x1C0001EAD). After this,siholdsvolume_len + path_len. If the file object has a stream (cmp [r14+70h], r12@0x1C0001EB5), the code writes a':'at byte offsetsi(mov [rax+rcx*2], dx@0x1C0001ECF) and thenmemmoves the stream name atbuffer + si + 2(@0x1C0001E1C) — with no check thatsi + 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 anfbwf-monitored volume is opened/queried. FbwfGenerateFileNameallocates the fixed0xFFFEbuffer and copies volume name + path.- The stream branch appends
':'+ stream name atvolume_len + path_lenwith no bounds check. - When
volume_len + path_lenapproaches0xFFFE, the appended bytes overflow the pool allocation. - Fix: The patched function pre-measures the full length before allocating. It calls
FbwfGetCurrentPathin measure mode (r8d = 0@0x140002C2F), formsebx = volume_len + path_len(0x140002C3F/0x140002C42), and when a stream is present adds the colon and stream length:lea r15d, [rbx+2](@0x140002C6E) thenlea r15d, [r15+rax*2](@0x140002C72). It allocates exactly that size withExAllocatePool2(@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-bitcountfrom 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-1and returnsSTATUS_INTEGER_OVERFLOW(0xC0000095, @0x140004E5B). Non-positive counts are rejected up front (test r15, r15; jle default@0x140004DB2) and the0x7FFFFFFFFFFFFFFFsentinel 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 fromZwQuerySystemInformation(@0x140004DDB); it does not size a pool allocation. The unpatchedFbwfPortMessagecontains no0xFB2F0001case, nomul/imul, and noZwQuerySystemInformationcall. 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
FilterSendMessagewith IOCTL0xFB2F0001. FltMgrforwards the message toFbwfPortMessage.- 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 toRtlInitUnicodeString(@0x14000DDB5).RtlInitUnicodeStringcomputesLengthby 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 validationLength + 2 <= 0x80(unpatchedadd rsi, 2; cmp rsi, 0x80@0x1C000EAD1; patchedlea 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'sCurrentByteOffset, andFILE_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 < lengthrejects withSTATUS_INVALID_PARAMETER). The patched code restructures these checks (flattening the nestedHighPart == -1test into a directQuadPart == -2comparison) 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 viaFbwfGetCurrentPathbounded to0xFFFE(mov r8d, r12d@0x1C00114F1;call FbwfGetCurrentPath@0x1C00114FA). It setsr14to the end of the path inside that buffer (lea r14, [rdi+rax*2]@0x1C001151C) and thenmemmoves the directory-entry name (the copy held in[rbx+8], size =name_len + 2from[rsp+Size]) tobuffer + path_offset(call memmove@0x1C001152C) with no check thatpath_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) andmemmoves it at the same path offsetr14(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 fixed0xFFFEbuffer 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 callbackFbwfPreCreate, which callsDirTreeNode::AddDirEntry(the same overload is also reached fromFbwfRestoreDeletedFile). 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 reachesFbwfPreCreate, which callsDirTreeNode::AddDirEntryto insert the new entry (also reached fromFbwfRestoreDeletedFile). AddDirEntryallocates the fixed0xFFFEbuffer and fills it with the current directory path viaFbwfGetCurrentPath.- 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_lenfills the0xFFFEbuffer, the appended name bytes overflow the paged-pool allocation. - Fix: The patched function pre-measures the exact required size before allocating. It calls
FbwfGetCurrentPathin measure mode (xor r8d, r8d;lea r9, [rsp+var_58];call FbwfGetCurrentPath@0x140014046/0x140014056), adds4(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 at0x14001409F/0x1400140A5), folds it in (add r15d, edx@0x1400140A8), and allocates exactly that many bytes withExAllocatePool2(mov ecx, 102h;call ExAllocatePool2@0x1400140BC/0x1400140C1) before performing the identical copies (memmove@0x140014133and0x140014199). 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)
- On a volume monitored by the
fbwfminifilter, create or reference a file that carries an alternate data stream (path:stream). - Arrange for the file's full path to be long enough that
volume_len + path_lenapproaches the fixed0xFFFE-byte buffer (a deeply nested directory path). - Trigger the minifilter name-generation path (file create / name query) so
FbwfGenerateFileNameruns on the option-byte 1/2 branch. - Observation: On the unpatched build, once the volume name and path fill the fixed buffer, the
':'and stream-name writes land past the0xFFFEpool 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)
- Obtain a handle to the
fbwfcommunication port viaFilterConnectCommunicationPort. - Construct an input buffer with the IOCTL code
0xFB2F0001selecting the scratch-space handler. - Populate the
countfield in the message body. Provide an input buffer length (arg3) >= 8 bytes and an output buffer length (arg5) >= 8 bytes. - Set
countsuch thatcount * block_sizewould overflow 64-bit arithmetic (forblock_size = 0x1000, acountof0x0010000000000000or larger). - Send the message via
FilterSendMessage. - 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)
- Open a handle to a file residing on a volume monitored by the
fbwfminifilter. - Construct a write operation using
NtWriteFileorZwWriteFile. - Supply a
LARGE_INTEGERByteOffset of0xFFFFFFFFFFFFFFFE(use current position) or0xFFFFFFFFFFFFFFFF(append). - Execute the write request.
- Observation: Both builds substitute the current-position or end-of-file offset for these sentinels and apply the same
length + offsetwrap guard. Behavior is identical between builds; there is no security-relevant difference to exercise here.
Trigger 4: Pool Out-of-Bounds Write (DirTreeNode::AddDirEntry)
- On a volume monitored by the
fbwfminifilter, create a file or directory whose parent directory path is deeply nested so the current path nearly fills the fixed0xFFFE-byte buffer. - Give the created entry a name (and/or drive a write-through entry) so the appended name pushes
path_len + name_lenpast0xFFFE. - Trigger the create so
FbwfPreCreatecallsDirTreeNode::AddDirEntryto insert the entry. - Observation: On the unpatched build, once the current path fills the fixed
0xFFFEbuffer, the entry-name (0x1C001152C) and, if present, the write-through-name (0x1C001157A)memmoves land past the0xFFFEallocation, corrupting adjacent paged pool. On the patched build the allocation is pre-sized topath_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_lenbytes 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 full0xFFFEbuffer, sinceFbwfGetCurrentPathis 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 to0x1C0001E4D.- Set a breakpoint at the fixed allocation (
0x1C0001E61) and at the streammemmove(0x1C0001E1C). - What to Inspect:
- At the allocation: confirm the size operand is
0xFFFEregardless of the file path. - At
0x1C0001EB2:si=volume_len + path_len. Ifsiis near0xFFFE, the pending colon/stream writes will overflow. - At
0x1C0001ECF/0x1C0001E1C: confirm the destination offset (si, thensi+2) plus the stream length exceeds0xFFFE. - Key Instructions/Offsets:
0x1C0001E53: fixed0xFFFEsize loaded beforeExAllocatePoolWithTag.0x1C0001ECF:':'write at offsetsiwith no bound check.0x1C0001E1C: stream-namememmoveatbuffer + si + 2with no bound check.- Trigger Setup:
- On an
fbwf-protected volume, reference a file with an alternate data stream whose full path is deeply nested sovolume_len + path_lenapproaches0xFFFE. - 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. Inspectrdx(input buffer) andr8d(input size).- Set a breakpoint at the
mulin the scratch-space handler (0x140004E45) to observe thecount * block_sizecomputation. - What to Inspect:
- At the entry point:
r8dmust be >= 8.rdxpoints to the user input copied to pool. Check[rdx]for the IOCTL code. - At the multiply: confirm
count(from user input,r15) andblock_size(from volume metadata) before the overflow test at0x140004E4F. - 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 at0x1C000A746.0x140004DBB: the0xFB2F0001case selector.0x140004E45: themul.0x140004E5B:STATUS_INTEGER_OVERFLOWon overflow.- Trigger Setup:
- Open the minifilter port from user mode using
FilterConnectCommunicationPort. - Send a packet via
FilterSendMessagewith buffer length >= 8 and IOCTL0xFB2F0001, withcountset to overflow the multiply. - Expected Observation:
- With an overflow-triggering
count, the overflow test fails, the size is forced to-1, and the handler returnsSTATUS_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 fixed0xFFFEand appends a':'plus the alternate-data-stream name atvolume_len + path_lenwith no bounds check, allowing a paged-pool out-of-bounds write when the path fills the buffer. Patched pre-measuresvolume_len + path_len + 2 + stream_len*2and allocates exactly that withExAllocatePool2. This is a load-bearing security fix.DirTreeNode::AddDirEntry(Security Relevant): Unpatched allocates a fixed0xFFFEpaged-pool buffer (ExAllocatePoolWithTag@0x1C00114C4), fills it with the current directory path viaFbwfGetCurrentPath, then appends the directory-entry name (memmove@0x1C001152C) and the write-through-entry name (memmove@0x1C001157A) at the running path offset with no check thatpath_len + name_lenstays within0xFFFE, allowing a paged-pool out-of-bounds write when the path fills the buffer. Patched pre-measurespath_len + 4 + max(entry_name_len, write_through_name_len)and allocates exactly that withExAllocatePool2(@0x1400140C1). A second load-bearing security fix (see Finding 5), the same CWE-787/CWE-131 class asFbwfGenerateFileName.FbwfPortMessage(Security Relevant): Adds an overflow-checkedcount * block_sizecomputation in a new scratch-space handler (IOCTL0xFB2F0001), returningSTATUS_INTEGER_OVERFLOWon overflow. The product feedsTotalBytescache 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 withRtlInitUnicodeString, which performs the same unbounded null-terminator scan. Both builds then apply the identicalLength + 2 <= 0x80check 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 samelength + offsetwrap guard; no new offset validation is introduced.FbwfNormalizeNameComponent(Behavioral): Replaced a fixed0xFFFEExAllocatePoolWithTagbuffer with a dynamically sizedExAllocatePool2allocation ofmax(component_len, name_len) + 1bytes. 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 fixed0xFFFEalready 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 explicitFinalCompressedSize >= 0x1000check to validate compression results.FbwfExtendFileSize(Behavioral): Replaced legacyExAllocatePoolWithTagwith 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
0xFFFEallocation and the unaccounted stream-name append in the unpatchedFbwfGenerateFileName, and the pre-measured size (including the stream) in the patched build; the same fixed0xFFFEallocation with an unaccounted entry-name/write-through-name append in the unpatchedDirTreeNode::AddDirEntry, and the pre-measuredpath_len + 4 + max(name lengths)allocation in the patched build; the patched-only overflow-checked multiplication inFbwfPortMessagefeedingTotalBytesaccounting; the equivalentRtlInitUnicodeString-vs-loop length computation inFbwfCheckAndTranslateVolumeName; and the equivalentFbwfWriteoffset 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
0xFFFEbuffer, combined with an alternate data stream on the target file. - For Finding 2, the attacker is assumed to have sufficient privileges to open the
FilterSendMessagecommunication port; depending on the port ACLs this may require medium-integrity administrative access. The exact communication port name should be verified dynamically (e.g., viafltmc 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_lento the0xFFFEboundary and confirm the overflow length (2 + stream_len) against the pool block. - For Finding 2, mapping the exact byte offset of the
countfield within the0xFB2F0001message body and the target volume'sblock_sizeis required to force the multiply to overflow.