modem.sys — Missing zero-initialization of PDO-name pool buffer in ModemHandleSymbolicLink (CWE-908) fixed
KB5078752
1. Overview
- Unpatched Binary:
modem_unpatched.sys - Patched Binary:
modem_patched.sys - Overall Similarity Score: 0.9927
- Diff Statistics: 99 matched functions, 1 changed function, 98 identical functions, 0 unmatched functions in either direction.
- Verdict: The patch adds a
memsetthat zero-initializes the second kernel pool buffer allocated inModemHandleSymbolicLinkduring device-interface setup, matching the zeroing already applied to the first buffer. This is a CWE-908 (Use of Uninitialized Resource) hardening change. There is no demonstrable information disclosure: the uninitialized bytes are never read out, so the impact is defense-in-depth only. Severity: Low.
2. Vulnerability Summary
- Severity: Low (defense-in-depth hardening; no demonstrable disclosure)
- Vulnerability Class: Use of Uninitialized Resource (CWE-908)
- Affected Function:
ModemHandleSymbolicLink@0x1C00076CC
Root Cause & What Changed:
ModemHandleSymbolicLink builds a \DosDevices\ symbolic link and registers the modem device interface. When invoked in create mode (a3 != 0), it allocates two 0x202-byte PagedPool buffers with tag 'UMDM':
1. Destination.Buffer for the \DosDevices\<FriendlyName> link name. This buffer IS zeroed with memset(buffer, 0, MaximumLength) immediately after allocation in both builds (at 0x1C0007736).
2. DeviceName.Buffer for the Physical Device Object name (queried with DevicePropertyPhysicalDeviceObjectName). In the unpatched build this second buffer is not zeroed before use; in the patched build a memset(buffer, 0, 0x202) is inserted immediately after the allocation (at 0x1C00077E1–0x1C00077EE).
IoGetDeviceProperty fills only the actual PDO-name bytes plus the terminator; the tail of the 0x202-byte buffer retains stale pool contents in the unpatched build. However, the stale tail is never exposed: DeviceName.Length is set to ResultLength - 2, IoCreateSymbolicLink consumes only Length bytes as the link target, and the buffer is freed with ExFreePoolWithTag immediately afterward. No code reads the bytes beyond Length, and nothing copies them to a caller-observable location. The patch is therefore a hardening measure (zero-before-use, consistent with the sibling buffer), not the closure of a demonstrable leak.
Reachability & Data Flow:
1. DriverEntry (0x1C000D008) sets DriverObject->DriverExtension->AddDevice = ModemAddDevice (0x1C0006B60).
2. The PnP Manager invokes ModemAddDevice when a modem device is enumerated. ModemAddDevice (and the PnP dispatch routine ModemPnP at 0x1C0006EF0) call ModemHandleSymbolicLink(PhysicalDeviceObject, DeviceExtension+0x48, create).
3. In the create path, ModemHandleSymbolicLink allocates the 0x202-byte DeviceName.Buffer.
4. The buffer is passed as the PropertyBuffer to IoGetDeviceProperty(DevicePropertyPhysicalDeviceObjectName), which partially fills it.
5. IoCreateSymbolicLink(&Destination, &DeviceName) uses only DeviceName.Length bytes; the buffer is then freed.
This code runs only during PnP device setup/enumeration. It is not reachable from an untrusted IOCTL request surface.
3. Pseudocode Diff
// ModemHandleSymbolicLink, create path (a3 != 0)
// ... first buffer (Destination) allocated and memset(...,0,MaximumLength) in BOTH builds ...
// Second buffer for the PDO name
v9 = (WCHAR *)ExAllocatePoolWithTag(PagedPool, 0x202u, 0x4D444D55u); // 'UMDM'
DeviceName.Buffer = v9;
if ( v9 == nullptr ) { DeviceProperty = -1073741670; goto LABEL_14; }
// --- PATCHED VERSION ADDS: ---
memset(v9, 0, DeviceName.MaximumLength + 2LL); // zero entire 0x202-byte allocation
v10 = IoGetDeviceProperty(
PhysicalDeviceObject,
DevicePropertyPhysicalDeviceObjectName,
DeviceName.MaximumLength, // 0x200
DeviceName.Buffer, // (unpatched: uninitialized; patched: zeroed)
&ResultLength);
if ( v10 >= 0 )
{
DeviceName.Length += ResultLength - 2; // only valid PDO-name length is used
IoCreateSymbolicLink(&Destination, &DeviceName); // consumes DeviceName.Length bytes only
ExFreePoolWithTag(DeviceName.Buffer, 0); // buffer freed; tail never read
...
}
The only source-level change between the two builds is the added memset(v9, 0, DeviceName.MaximumLength + 2LL); and the corresponding decompiler variable rename (v9 → DeviceName.Buffer).
4. Assembly Analysis
Unpatched — second allocation, no zeroing before IoGetDeviceProperty:
00000001C00077A9 mov edx, 202h ; NumberOfBytes
00000001C00077AE mov dword ptr [rbp+DeviceName.Length], 2000000h ; Length=0, MaximumLength=0x200
00000001C00077B5 mov ecx, 1 ; PoolType = PagedPool
00000001C00077BA mov r8d, 4D444D55h ; Tag 'UMDM'
00000001C00077C0 call cs:__imp_ExAllocatePoolWithTag
00000001C00077CC mov [rbp+DeviceName.Buffer], rax
00000001C00077D0 test rax, rax
00000001C00077D3 jnz short loc_1C00077DF
00000001C00077D5 mov edi, 0C000009Ah ; STATUS_INSUFFICIENT_RESOURCES
00000001C00077DA jmp loc_1C00078AF
; --- no memset here: buffer tail stays uninitialized ---
00000001C00077DF movzx r8d, [rbp+DeviceName.MaximumLength] ; BufferLength = 0x200
00000001C00077E4 lea rcx, [rbp+arg_18] ; &ResultLength
00000001C00077E8 mov [rsp+50h+ResultLength], rcx
00000001C00077ED mov r9, rax ; PropertyBuffer (uninitialized)
00000001C00077F0 mov rcx, rsi ; DeviceObject
00000001C00077F3 mov edx, 0Bh ; DevicePropertyPhysicalDeviceObjectName
00000001C00077F8 call cs:__imp_IoGetDeviceProperty
Patched — memset inserted between allocation and IoGetDeviceProperty:
00000001C00077AD mov edx, 202h ; NumberOfBytes
00000001C00077B2 mov dword ptr [rbp+DeviceName.Length], 2000000h
00000001C00077B9 lea ecx, [rdi-1] ; PoolType = PagedPool (rdi=2, so ecx=1)
00000001C00077BC mov r8d, 4D444D55h ; Tag 'UMDM'
00000001C00077C2 call cs:__imp_ExAllocatePoolWithTag
00000001C00077CE mov [rbp+DeviceName.Buffer], rax
00000001C00077D2 test rax, rax
00000001C00077D5 jnz short loc_1C00077E1
00000001C00077D7 mov edi, 0C000009Ah
00000001C00077DC jmp loc_1C00078C3
00000001C00077E1 movzx r8d, [rbp+DeviceName.MaximumLength] ; 0x200
00000001C00077E6 xor edx, edx ; Val = 0
00000001C00077E8 add r8, rdi ; Size = MaximumLength + 2 = 0x202
00000001C00077EB mov rcx, rax ; buffer pointer
00000001C00077EE call memset ; THE FIX: zero entire 0x202 allocation
00000001C00077F3 movzx r8d, [rbp+DeviceName.MaximumLength] ; BufferLength = 0x200
00000001C00077F8 lea rax, [rbp+arg_18] ; &ResultLength
00000001C00077FC mov r9, [rbp+DeviceName.Buffer] ; PropertyBuffer (now zeroed)
00000001C0007800 mov edx, 0Bh ; DevicePropertyPhysicalDeviceObjectName
00000001C0007805 mov rcx, rsi ; DeviceObject
00000001C0007808 mov [rsp+50h+ResultLength], rax
00000001C000780D call cs:__imp_IoGetDeviceProperty
memset is the driver's internal fill helper at 0x1C0001A40.
5. Trigger Conditions
- A modem device must be enumerated on the system so the PnP Manager invokes
ModemAddDevice. This requires plugging in / presenting a modem device, or a privileged local operation such aspnputil /scan-devicesor a disable/re-enable cycle in Device Manager. ModemAddDevice(orModemPnP) callsModemHandleSymbolicLink(..., create), which allocates the 0x202-byteDeviceName.Bufferand, in the unpatched build, passes it toIoGetDevicePropertywithout zeroing.- There is no untrusted request path (IOCTL, etc.) that reaches this code; the trigger is device enumeration, which is a privileged / physical action.
6. Impact Assessment
- Class: Use of Uninitialized Resource (CWE-908). In the unpatched build the second pool buffer is not zeroed before it is handed to
IoGetDeviceProperty. - Demonstrable impact: None established. The bytes beyond the valid PDO-name length are never read:
DeviceName.Lengthis set fromResultLength,IoCreateSymbolicLinkcopies onlyLengthbytes as the link target, and the buffer is freed immediately after. No code reads the tail and nothing returns it to a caller-observable location. The change is best characterized as defense-in-depth hardening that brings the second allocation in line with the first (which was already zeroed in both builds). - What the patch does: inserts
memset(DeviceName.Buffer, 0, 0x202)so the entire allocation is zeroed before any use. - Claims of KASLR bypass, kernel-address disclosure, pool spraying/grooming, or a code-execution primitive are not supported by the binaries and are not made here.
7. Debugger Observation Notes
The difference between the two builds can be observed directly with a kernel debugger attached:
- Breakpoints (unpatched):
bp modem!ModemHandleSymbolicLink+0xF4(0x1C00077C0) — the secondExAllocatePoolWithTagcall.bp modem!ModemHandleSymbolicLink+0x12C(0x1C00077F8) — the followingIoGetDevicePropertycall.- What to inspect:
- At
+0xF4: step over the allocation;raxis the new 0x202-byte buffer.db rax L202shows the buffer contents (uninitialized pool bytes in the unpatched build). - At
+0x12C:r9holds the same buffer pointer; in the unpatched build no zeroing occurred between the two breakpoints, so the buffer is still uninitialized. In the patched build thememsetat0x1C00077EEhas already zeroed it. - Trigger setup: load the driver, then force a PnP re-enumeration of the modem device (
pnputil /scan-devices,devcon rescan, or a disable/re-enable cycle) soModemAddDeviceruns and callsModemHandleSymbolicLinkwith the create flag. - Expected observation: in the unpatched build,
db rax L202at+0x12Cshows non-zero pool bytes past the point whereIoGetDevicePropertywill write the PDO name; in the patched build the same region is all zeros. This confirms the missing initialization, but note the tail is not exposed outside the kernel in either build.
8. Changed Functions — Full Triage
ModemHandleSymbolicLink@0x1C00076CC(Similarity: 0.986, Change Type:hardening)- Behavioral Change: Added a
memset(0x1C0001A40) that zero-initializes the 0x202-byteDeviceName.Bufferfor the PDO name beforeIoGetDeviceProperty, matching the zeroing already applied to the first (Destination) buffer. CWE-908 hardening; no demonstrable disclosure. - Cosmetic/Register Shifts: The pool-type argument for the second allocation changed from
mov ecx, 1tolea ecx, [rdi-1](both yield 1 = PagedPool). TheResultLength - 2computation changed fromsub ax, 2tomov edi, 2; sub ax, di. These repurposerdito hold the constant2used in the newmemsetsize (MaximumLength + 2 = 0x202). - Jump Targets: Subsequent branch offsets shift by ~0x14 bytes to accommodate the inserted
memsetsequence. No behavioral change.
Independent function-level diff of both builds confirms this is the only function whose body changed. Every other diff line is a relocation of function-header addresses (the functions following ModemHandleSymbolicLink all shift by 0x14 bytes); no other security-relevant change was omitted.
9. Unmatched Functions
- No functions were added or removed. The entire patch is contained within
ModemHandleSymbolicLink.
10. Confidence & Caveats
- Confidence: High that the change is a single added
memsetzero-initializing the second pool buffer, and that the direction is correct (patched build is stricter). The missing zero-initialization is definitely present in the unpatched binary. - Caveats: No information-disclosure impact is demonstrable from the binaries. The uninitialized tail is never read out or returned to any caller-observable sink;
IoCreateSymbolicLinkuses only the validLength, and the buffer is freed immediately. The change is therefore defense-in-depth hardening (CWE-908), reachable only during PnP device setup/enumeration, not from an untrusted request. Severity is rated Low accordingly.