ndistapi.sys — Uninitialized pool fields passed to TAPI provider callback (CWE-908) fixed
KB5073723
1. Overview
- Unpatched Binary:
ndistapi_unpatched.sys - Patched Binary:
ndistapi_patched.sys - Overall Similarity Score:
0.8228(82.28%) - Diff Statistics:
- Matched Functions: 49
- Changed Functions: 40
- Identical Functions: 9
- Unmatched (Unpatched): 0
- Unmatched (Patched): 0
- Verdict: The patch migrates the driver's own non-paged pool allocations from the non-zeroing
ExAllocatePoolWithTagto the zero-initializingExAllocatePool2. At two allocation sites (DoIoctlQuerySetWork,SendProviderShutdown) the driver builds a request structure, leaves several header fields uninitialized, and hands that structure to an in-kernel TAPI provider's request-handler callback. The unpatched code therefore exposes uninitialized non-paged pool bytes to the provider callback (CWE-908). The patch closes this by zero-initializing the buffer. This is a low-severity hardening fix: the uninitialized bytes are only reachable by the registered kernel provider driver, and no path returns them to the user-mode caller.
2. Vulnerability Summary
Finding 1: DoIoctlQuerySetWork (Low Severity)
- Vulnerability Class: Use of Uninitialized Memory (CWE-908)
- Affected Function:
DoIoctlQuerySetWork(IOCTL0x8FFF23C8/0x8FFF23CChandlers) - Root Cause: The unpatched code allocates the request buffer with
ExAllocatePoolWithTag(516 /* NonPagedPoolNxCacheAligned */, arg2[0xC]+0xEC, 'TAPI'), which does not zero-initialize memory. The driver populates specific header fields and the trailing data region but leaves gaps within the header uninitialized (offsets0x28-0x4F,0x54-0x57,0x5C-0x5F,0x6C-0xE7). The structure at offset+0x30is then passed to the TAPI provider's request-handler callback, so these uninitialized non-paged pool bytes are read by the provider. - Scope of exposure: The data region at
P+0xE8(the part copied back to the user-modeSystemBufferon synchronous success) is fully initialized before the callback: the drivermemmovesarg2[0xC]bytes of user input intoP+0xE8. Only the uninitialized header gaps (0x30..0xE7) reach the provider callback; they are not copied back to user mode by this driver. There is no demonstrable user-mode information disclosure in this function; the residual exposure is uninitialized pool data visible to the in-kernel provider. - The Fix: The patch replaces the allocation with
ExAllocatePool2(0x48, ...)(POOL_FLAG_NON_PAGED | POOL_FLAG_CACHE_ALIGNED), which zero-initializes the allocation by default, so the header gaps are zero when the structure reaches the provider. - Not part of the fix: The indirect call to the provider handler is Control-Flow-Guard protected in both builds (unpatched
__guard_dispatch_icall_fptr, patched out-of-line_guard_dispatch_icallthunk). CFG was not added by this patch. - Attacker-Reachable Entry Point:
\Device\NdisTapiviaIRP_MJ_DEVICE_CONTROL(METHOD_BUFFERED). - Data Flow (Call Chain):
NdisTapiDispatchreceivesIRP_MJ_DEVICE_CONTROL.DoIoctlQuerySetWorkvalidates the TAPI sub-code, allocates bufferP, and initializes the header fields it uses plus theP+0xE8data region (from user input).- The provider request handler is called via a CFG-guarded indirect call with
P+0x30; the provider reads a structure whose header gaps are uninitialized pool data in the unpatched build.
Finding 2: SendProviderShutdown (Low Severity)
- Vulnerability Class: Use of Uninitialized Memory (CWE-908)
- Affected Function:
SendProviderShutdown(invoked duringIRP_MJ_CLOSEviaDoIrpMjCloseWork, and duringNdisTapiDeregisterProvider) - Root Cause: In the unpatched build the caller allocates a
0xF0-byte buffer withExAllocatePoolWithTag(516, 0xF0, 'TAPI')(not zeroed) and passes it toSendProviderShutdown. The function initializes only specific fields (0x10,0x18,0x20,0x24,0x50,0x58,0x60,0x68,0xE8) and passes the structure at offset+0x30to the provider callback, exposing the uninitialized gaps. - The Fix: Allocation is pushed down into
SendProviderShutdownitself usingExAllocatePool2(0x48, 0xF0, 'TAPI')(zero-initialized), with a null-check that returnsSTATUS_INSUFFICIENT_RESOURCES. - Not part of the fix: The provider callback is CFG-guarded in both builds.
- Scope of exposure: This path runs during handle close / provider deregistration and returns nothing to user mode. The uninitialized bytes are only visible to the in-kernel provider callback. Low severity.
- Attacker-Reachable Entry Point: Closing the last handle to
\Device\NdisTapitriggersIRP_MJ_CLOSE, invokingDoIrpMjCloseWork. - Data Flow (Call Chain):
DoIrpMjCloseWork(unpatched) allocates the unzeroed0xF0-byte buffer and callsSendProviderShutdown.SendProviderShutdownpopulates selective fields and passesP + 0x30to the TAPI provider callback.
3. Pseudocode Diff
DoIoctlQuerySetWork
// === UNPATCHED ===
// Allocates pool WITHOUT zero-initialization (NonPagedPoolNxCacheAligned = 516)
PoolWithTag = ExAllocatePoolWithTag((POOL_TYPE)516, *(_DWORD *)(a2 + 12) + 236, 'IPAT');
v23 = PoolWithTag;
// Explicit field population (leaving header gaps uninitialized)
v23[9] = 0; // offset 0x24
v23[2..3] (qwords) = a1, v13; // offset 0x10 (IRP), 0x18 (provider)
v23[8] = requestId; // offset 0x20
memmove(v23 + 58, (a2 + 16), *(a2 + 12)); // offset 0xE8: user data (data region)
v23[20] = boolFlag; // offset 0x50
v23[22] = *(a2 + 4); // offset 0x58 (TAPI sub-code)
v23[12] (qword) = v23 + 58;// offset 0x60 -> data region
v23[26] = *(a2 + 12); // offset 0x68 (data size)
// GAPS NOT INITIALIZED: [0x28-0x4F], [0x54-0x57], [0x5C-0x5F], [0x6C-0xE7]
// Structure at +0x30 passed to provider via CFG-guarded indirect call:
v33 = ((__int64 (__fastcall *)(__int64, _DWORD *))v13[3])(v13[2], v23 + 12); // v23+12 == P+0x30
// On synchronous success the DATA region (already user-initialized) is copied back:
memmove(v37 + 4, v23 + 58, (unsigned int)v37[3]); // src = P+0xE8, not the header gaps
// === PATCHED ===
// ExAllocatePool2 zero-initializes the allocation by default
v23 = ExAllocatePool2(0x48, *(_DWORD *)(a2 + 12) + 236, 'IPAT');
// Same field population; header gaps are now zero. Same CFG-guarded indirect call.
SendProviderShutdown
// === UNPATCHED ===
// Caller (DoIrpMjCloseWork / NdisTapiDeregisterProvider) allocates an unzeroed buffer
void *P = ExAllocatePoolWithTag((POOL_TYPE)516, 0xF0, 'IPAT');
SendProviderShutdown(Provider, &newIrql, P);
// Inside SendProviderShutdown (arg3 == P):
*(P + 0x10) = 0;
*(P + 0x18) = Provider;
*(P + 0x24) = 1;
*(P + 0x50) = 1;
*(P + 0x58) = 0x7030119;
*(P + 0x60) = P + 0xE8;
*(P + 0x68) = 4;
// header gaps [0x28-0x4F], [0x54-0x57], [0x5C-0x5F], [0x6C-0xE7] NOT set
handler(Provider->ctx, P + 0x30); // CFG-guarded, receives uninitialized gaps
// === PATCHED ===
// Allocation moved inside SendProviderShutdown and zero-initialized
void *P = ExAllocatePool2(0x48, 0xF0, 'IPAT');
if (!P) return STATUS_INSUFFICIENT_RESOURCES;
// same field population; header gaps are now zero
4. Assembly Analysis
DoIoctlQuerySetWork (Unpatched, 0x1C000247C)
; Allocation (does NOT zero):
0x1C0002626 mov edx, ecx ; NumberOfBytes = data size + 0xEC
0x1C0002628 mov r8d, 49504154h ; Tag 'TAPI'
0x1C000262E mov ecx, 204h ; PoolType = NonPagedPoolNxCacheAligned
0x1C0002633 call cs:__imp_ExAllocatePoolWithTag
0x1C000263F mov r14, rax ; r14 = P
; Selective header initializations:
0x1C00026C9 mov [r14+24h], edi ; = 0
0x1C00026CD mov [r14+10h], r13 ; IRP pointer
0x1C00026D1 mov [r14+18h], r15 ; provider pointer
0x1C00026EA mov [r14+20h], eax ; request ID
0x1C00026F2 call memmove ; user data -> [r14+0E8h] (data region)
0x1C0002711 mov [r14+50h], eax ; flag
0x1C0002718 mov [r14+58h], eax ; TAPI sub-code
0x1C0002723 mov [r14+60h], rax ; -> [r14+0E8h]
0x1C000272F mov [r14+68h], eax ; data size
; header gaps [r14+28h..4Fh], [r14+54h..57h], [r14+5Ch..5Fh], [r14+6Ch..E7h] left uninitialized
; Provider callback (CFG-guarded), structure passed at r14+30h:
0x1C00027AA lea rdx, [r14+30h]
0x1C00027B2 call cs:__guard_dispatch_icall_fptr
; Copy-back to user on synchronous success reads the DATA region only:
0x1C00028B7 lea rdx, [r14+0E8h] ; Src = data region (user-initialized)
0x1C00028BE call memmove
DoIoctlQuerySetWork (Patched, 0x1400013D0)
; Zero-initializing allocation:
0x140001557 mov edx, eax ; NumberOfBytes = data size + 0xEC
0x140001559 mov ecx, 48h ; POOL_FLAG_NON_PAGED | POOL_FLAG_CACHE_ALIGNED
0x14000155E mov r8d, 49504154h ; Tag 'TAPI'
0x140001564 call cs:__imp_ExAllocatePool2 ; zero-initialized by default
0x140001570 mov r14, rax
; Same field population; header gaps are now zero.
; Provider callback (still CFG-guarded), structure passed at r14+30h:
0x1400016D5 lea rdx, [r14+30h]
0x1400016DD call _guard_dispatch_icall
SendProviderShutdown (Unpatched, 0x1C00020D8) and caller DoIrpMjCloseWork
; Caller DoIrpMjCloseWork allocates the unzeroed 0xF0 buffer:
0x1C0002222 mov edx, 0F0h ; NumberOfBytes
0x1C000222A mov r8d, 49504154h ; Tag
0x1C0002233 mov ecx, 204h ; NonPagedPoolNxCacheAligned
0x1C0002238 call cs:__imp_ExAllocatePoolWithTag
0x1C0002254 call SendProviderShutdown ; r8 = buffer
; Inside SendProviderShutdown (arg3 = r8):
0x1C00020ED and qword ptr [r8+10h], 0 ; = 0
0x1C00020F9 mov [r8+18h], rcx ; provider
0x1C0002102 mov [r8+24h], eax ; = 1
0x1C0002118 mov [r8+50h], eax ; = 1
0x1C000211C mov dword ptr [r8+58h], 7030119h
0x1C0002124 mov [r8+60h], r9 ; -> r8+0E8h
0x1C0002128 mov dword ptr [r8+68h], 4
; header gaps [r8+28h..4Fh], [r8+54h..57h], [r8+5Ch..5Fh], [r8+6Ch..E7h] left uninitialized
; Provider callback (CFG-guarded), structure at r8+30h (rsi == r8):
0x1C0002153 lea rdx, [rsi+30h]
0x1C000215B call cs:__guard_dispatch_icall_fptr
SendProviderShutdown (Patched, 0x140002B84)
; Allocation moved inside the function, zero-initialized:
0x140002B9E mov edx, 0F0h
0x140002BA3 mov ecx, 48h ; POOL_FLAG_NON_PAGED | POOL_FLAG_CACHE_ALIGNED
0x140002BA8 mov r8d, 49504154h ; Tag 'TAPI'
0x140002BAE call cs:__imp_ExAllocatePool2
0x140002BBA mov rdi, rax
0x140002BBD test rax, rax
0x140002BC0 jnz short loc_140002BCC ; else return STATUS_INSUFFICIENT_RESOURCES
; Provider callback (still CFG-guarded):
0x140002C2D call _guard_dispatch_icall
5. Trigger Conditions
To reach the uninitialized-header exposure in DoIoctlQuerySetWork:
- Obtain a handle to
\Device\NdisTapi(created viaWdmlibIoCreateDeviceSecure; the device SDDL governs which callers may open it). - A TAPI provider (WAN miniport driver) must be registered and matched by device ID, otherwise the IOCTL fails before the allocation.
- Construct an input buffer of at least
0x14bytes. - Set
buffer[0x04](TAPI sub-code) to a valid value in the range0x7030101..0x7030124. - Set
buffer[0x08]to a Device ID within a registered provider's range. - Set
buffer[0x0C](data size) to pass validation:>= lookup_table_minimum,<= 0x10000000,data_size + 0x13must not overflow 32-bit, and bothInputBufferLengthandOutputBufferLengthmust be>= data_size + 0x13. - Invoke
DeviceIoControlwith IOCTL0x8FFF23C8or0x8FFF23CC(METHOD_BUFFERED).
In the unpatched build, the allocated request structure has uninitialized header gaps when it is handed to the provider's request-handler callback. These bytes are not returned to the user output buffer by this driver; the output buffer receives only the P+0xE8 data region, which is initialized from the caller's own input before the callback.
6. Exploit Primitive & Development Notes
- Primitive: None demonstrable from this driver. The uninitialized non-paged pool bytes in the request structure are read only by the in-kernel TAPI provider callback; the unpatched driver does not copy any uninitialized header bytes back to the user-mode caller. Whether a specific provider propagates those bytes anywhere is provider-dependent and outside this binary.
- Nature of the fix: Defense hardening against use of uninitialized pool memory (CWE-908). Zero-initialization removes stale non-paged pool contents from the structure before it crosses the driver/provider boundary.
- What this is not: There is no evidence in either binary of a user-mode kernel information-disclosure primitive, a KASLR bypass, or a privilege-escalation chain arising from this change. Those are not claimed.
7. Debugger Playbook
For an analyst using a kernel debugger against the unpatched binary:
- Breakpoints:
bp ndistapi!DoIoctlQuerySetWork— function entry.bp 0x1C0002633— theExAllocatePoolWithTagcall (return valuerax, moved tor14, is the non-zeroed bufferP).bp 0x1C00027B2— the CFG-guarded indirect call to the provider handler;rdx = P+0x30is the structure the provider receives.- What to Inspect:
- At the allocation: capture
Pfromrax/r14. - Before
0x1C00027B2:dq r14+0x28 L10anddq r14+0x6C— in the unpatched build these header gaps contain stale non-paged pool data; in the patched build they are zero. - Note that the data region at
r14+0xE8is already initialized (user input copied by thememmoveat0x1C00026F2), so it does not exhibit uninitialized residue. - Key Instructions/Offsets:
- Buffer pointer:
r14(orP). - Uninitialized header ranges:
r14+0x28..0x4F,r14+0x54..0x57,r14+0x5C..0x5F,r14+0x6C..0xE7. - Vulnerable allocation:
0x1C0002633. Provider callback:0x1C00027B2. Copy-back of data region:0x1C00028BE. - Trigger Setup:
c HANDLE h = CreateFileW(L"\\\\.\\NdisTapi", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); DWORD buf[16] = {0}; buf[1] = 0x7030101; // valid TAPI sub-code buf[2] = DeviceID; // target provider device ID buf[3] = 0x40; // data size DWORD outBuf[256] = {0}; DWORD bytesReturned; DeviceIoControl(h, 0x8FFF23C8, buf, sizeof(buf), outBuf, sizeof(outBuf), &bytesReturned, NULL); - Expected Observation:
On the unpatched binary the header gaps at
r14+0x28..0xE7hold non-zero pool residue when the provider callback runs; on the patched binary those bytes are zero. The user-modeoutBufreflects only what the provider wrote into the data region, not the uninitialized header bytes.
8. Changed Functions — Full Triage
The patch shifts the driver's own pool allocations from ExAllocatePoolWithTag to ExAllocatePool2, drops the now-redundant memset calls that follow zeroing allocations, modernizes lookaside-list handling (ExAllocateFromNPagedLookasideList / ExFreeToNPagedLookasideList), and uses out-of-line CFG dispatch thunks. CFG protection on indirect provider calls is present in both builds.
- DoIoctlQuerySetWork (
0.9304): Security relevant (CWE-908, low). Request buffer switched to zero-initializingExAllocatePool2, so header gaps handed to the provider callback are no longer uninitialized pool data. - SendProviderShutdown (
0.5648): Security relevant (CWE-908, low). Allocation moved inside the function and switched toExAllocatePool2, with an added null-check. Same uninitialized-header class as above. - DoProviderInitComplete (
0.9519): Replaced legacy pool allocation withExAllocatePool2; the followingmemsetis retained in both builds. - SendProviderInitRequest (
0.9855): Replaced legacy pool allocation withExAllocatePool2. CFG-guarded indirect call present in both builds. - NdisTapiRegisterProvider (
0.9440): Replaced legacy pool allocation withExAllocatePool2; removed the now-redundantmemsetof the zeroed allocation. - DriverEntry (
0.6978): Replaced a pool allocation withExAllocatePool2and modernized lookaside-list cleanup. - NdisTapiUnload (
0.7264): Modernized lookaside-list cleanup; removed unused statistics counters. - NdisTapiDeregisterProvider (
0.8410): Refactored so the shutdown buffer is allocated insideSendProviderShutdownrather than here. - DoIrpMjCloseWork (
0.7512): Refactored; removed the0xF0-byte pool allocation (moved intoSendProviderShutdown). - DoGetProviderEventsWork (
0.9271): Modernized lookaside-list freeing; removed statistics counters. - NdisTapiIndicateStatus (
0.8733): Modernized lookaside-list allocation; removed statistics counters. - NdisTapiDispatch (
0.8603): Control-flow restructuring around theSTATUS_PENDINGcheck; IOCTL dispatch codes unchanged. - NdisTapiCompleteRequest (
0.9649): Register renaming and field-assignment reordering; equivalent logic. - NdisTapiCleanup (
0.9627): Field-assignment reordering; equivalent logic. - NdisTapiCancel (
0.8436): Linked-list traversal restructured; equivalent logic. - DoIoctlConnectWork (
0.9562): Provider-state comparison rewritten to an equivalent unsigned form (same accepted states). - DoLineCreateWork (
0.8988): Lock re-acquire / counter re-read restructured; equivalent logic. - memcpy / memset: Rebuilt by the compiler with different code generation.
9. Unmatched Functions
- Removed: None security-relevant.
- Added: None security-relevant. (The patched build additionally contains compiler-runtime and library helpers such as CPU-feature-dispatched
memsetvariants and CFG dispatch thunks; these are code-generation artifacts, not driver logic.)
10. Confidence & Caveats
- Confidence: High on the mechanism (in both
.asmand.c): the driver's own allocations move from non-zeroingExAllocatePoolWithTagto zero-initializingExAllocatePool2, and two of those buffers reach a provider callback with uninitialized header gaps in the unpatched build. - Severity: Low. The uninitialized bytes are exposed only to the in-kernel TAPI provider callback. No path in this driver returns uninitialized header bytes to the user-mode caller; the user output buffer receives only the initialized data region.
- Assumptions:
- Reaching the vulnerable allocation requires a registered, matching TAPI provider; without one the IOCTL fails early.
- Any onward propagation of the uninitialized bytes depends on the specific provider driver and is outside these binaries.