pciidex.sys — MMIO BAR mapped executable via deprecated MmMapIoSpace, non-executable protection enforced with MmMapIoSpaceEx (CWE-732) — fixed
KB5073723
1. Overview
- Unpatched Binary:
pciidex_unpatched.sys - Patched Binary:
pciidex_patched.sys - Overall Similarity Score:
0.9154(91.54%) - Diff Statistics:
- Matched Functions: 195
- Changed Functions: 115
- Identical Functions: 80
- Unmatched Functions (Unpatched/Patched): 0 / 2
- Verdict: The patch replaces the deprecated
MmMapIoSpaceAPI withMmMapIoSpaceExin theGenPopulateAccessRangefunction, adding an explicit page-protection argument (0x204=PAGE_NOCACHE | PAGE_READWRITE) so that mapped PCI IDE MMIO regions are non-executable (NX) rather than mapped read-write-execute. This is a genuine, correctly-directed defense-in-depth hardening; it is not a self-contained exploitable vulnerability. Severity: Low.
2. Vulnerability Summary
Finding: Executable MMIO Mapping via Deprecated API (CWE-732)
- Severity: Low (defense-in-depth hardening)
- Vulnerability Class: Incorrect Permission Assignment for Critical Resource / Weak Memory Protection on MMIO mapping
- Affected Function:
GenPopulateAccessRange(unpatched@ 0x1C0008FB0, patched@ 0x1C000A120)
Root Cause Analysis:
The unpatched pciidex.sys driver uses the deprecated MmMapIoSpace() API to map PCI IDE controller memory resources (from CmResourceTypeMemory descriptors) into kernel virtual address space during device start. MmMapIoSpace takes only a cache type (here MmNonCached) and does not allow the caller to specify page protection; the resulting mapping is read-write-execute, i.e. the pages carry no explicit Non-Executable (NX) attribute.
The patch replaces this call with MmMapIoSpaceEx(), passing 0x204 (PAGE_NOCACHE | PAGE_READWRITE) as the Protect argument. This makes the mapped I/O region explicitly non-executable, which is Microsoft's documented reason for introducing MmMapIoSpaceEx and deprecating MmMapIoSpace. The direction of the change is correct: only the patched build enforces NX on these mappings.
This is a hardening change, not a directly reachable exploit. The physical addresses and lengths come from PCI BAR resource descriptors assigned by the PnP/PCI bus driver, not from an IOCTL or other attacker-controlled request. Turning the executable MMIO mapping into a code-execution primitive would additionally require an attacker to control the physical memory contents behind the BAR (for example a malicious or DMA-capable device, or an emulated/pass-through device in a virtualized guest) and a separate control-flow hijack to redirect execution into the mapped virtual address. None of that is demonstrable from this driver alone.
Attacker-Reachable Entry Point & Data Flow:
1. The vulnerability is triggered during the standard Plug and Play (PnP) enumeration of a PCI IDE controller.
2. The OS sends an IRP_MJ_PNP request with the minor code IRP_MN_START_DEVICE (ControllerStartDevice).
3. The driver processes the translated resource requirements, eventually calling GenPopulateAccessRange to map hardware resources.
4. GenPopulateAccessRange evaluates the resource descriptors. For memory descriptors (CmResourceTypeMemory / type 3), it extracts the physical address and length.
5. The function passes this physical address and length to the vulnerable MmMapIoSpace API, mapping the memory without explicit NX enforcement.
3. Pseudocode Diff
The following decompilation shows the resource mapping logic in GenPopulateAccessRange for memory-type resources. v10 walks the resource descriptors; *((_BYTE *)v10 - 12) is the descriptor Type (3 = CmResourceTypeMemory), v8[5 * v9 + 1] is the physical start address, and *v10 is the length.
/* Unpatched GenPopulateAccessRange @ 0x1C0008FB0 */
if ( *((_BYTE *)v10 - 12) == 3 ) // CmResourceTypeMemory
{
// Deprecated API: only a cache type is supplied (MmNonCached);
// the resulting mapping carries no explicit non-executable protection.
v11 = MmMapIoSpace(*(PHYSICAL_ADDRESS *)&v8[5 * v9 + 1], (unsigned int)*v10, MmNonCached);
*(_QWORD *)(a2 + 24 * v2) = v11;
*(PHYSICAL_ADDRESS *)(a2 + 24 * v2 + 8) = MmGetPhysicalAddress(v11);
*(_DWORD *)(a2 + 24 * v2 + 16) = *v10;
*(_BYTE *)(a2 + 24 * v2 + 20) = 1;
}
/* Patched GenPopulateAccessRange @ 0x1C000A120 */
if ( *((_BYTE *)v10 - 12) == 3 ) // CmResourceTypeMemory
{
// Modern API: 516 == 0x204 == PAGE_NOCACHE | PAGE_READWRITE.
// The mapping is explicitly read-write and non-executable.
v11 = (void *)MmMapIoSpaceEx(*(_QWORD *)&v8[5 * v9 + 1], (unsigned int)*v10, 516);
*(_QWORD *)(a2 + 24 * v2) = v11;
*(PHYSICAL_ADDRESS *)(a2 + 24 * v2 + 8) = MmGetPhysicalAddress(v11);
*(_DWORD *)(a2 + 24 * v2 + 16) = *v10;
*(_BYTE *)(a2 + 24 * v2 + 20) = 1;
}
4. Assembly Analysis
The change is localized to the resource mapping loop inside GenPopulateAccessRange.
Unpatched Assembly
; Unpatched GenPopulateAccessRange @ 0x1C0008FB0, Type==3 (CmResourceTypeMemory) path:
00000001C0009017 mov edx, [rdi] ; NumberOfBytes (length)
00000001C0009019 lea rcx, [r14+r14*4]
00000001C000901D mov rcx, [r12+rcx*4+4] ; PhysicalAddress from descriptor
00000001C0009022 xor r8d, r8d ; CacheType = MmNonCached (0); no page protection arg
00000001C0009025 call cs:__imp_MmMapIoSpace
Patched Assembly
; Patched GenPopulateAccessRange @ 0x1C000A120, Type==3 (CmResourceTypeMemory) path:
00000001C000A18B mov edx, [rdi] ; NumberOfBytes (length)
00000001C000A18D lea rcx, [r14+r14*4]
00000001C000A191 mov rcx, [r12+rcx*4+4] ; PhysicalAddress from descriptor
00000001C000A196 mov r8d, 204h ; Protect = PAGE_NOCACHE|PAGE_READWRITE (explicit NX)
00000001C000A19C call cs:__imp_MmMapIoSpaceEx
5. Trigger Conditions
To reach the vulnerable code path:
- Obtain a handle or mechanism to interact with the PnP subsystem (requires administrative privileges by default).
- Force the target PCI IDE controller (handled by
pciidex.sys) to process anIRP_MN_START_DEVICErequest. This can be done by restarting the device. - The device must supply resource requirements of type
CmResourceTypeMemory(Type3). - No specific race conditions or timing constraints are required.
- Observable Effect: In the unpatched binary, inspecting the Page Table Entry (
PTE) of the returned kernel Virtual Address (VA) via a debugger shows the mapping without the NX (Execute-Disable) bit; in the patched binary the same mapping is non-executable. This is the only directly observable difference between the two builds.
6. Impact & Notes
- Nature of the change: A defense-in-depth hardening. The unpatched build maps PCI IDE memory BARs read-write-execute; the patched build maps them read-write, non-executable. There is no self-contained exploit primitive in this driver.
- Preconditions any exploitation would require (not demonstrable here): an attacker able to control the physical memory contents behind the mapped BAR (for example a malicious or DMA-capable device, or an emulated/pass-through device in a virtualized guest), plus a separate control-flow hijack to redirect kernel execution into the mapped VA. The physical address and length passed to the mapping call originate from PCI resource descriptors assigned during PnP enumeration, not from attacker-supplied requests.
- Environmental mitigations:
- HVCI / VBS: Hypervisor-Protected Code Integrity enforces NX on kernel pages regardless of the OS kernel's request, so the two builds are equivalent when VBS/HVCI is enabled.
- SMEP / SMAP: Prevent user-mode pages from being executed/accessed in kernel mode, but do not affect kernel-mapped MMIO regions.
7. Debugger PoC Playbook
Follow these steps using a kernel debugger (WinDbg/KD) attached to the unpatched target to confirm the behavior.
Breakpoints
Set breakpoints on the vulnerable function and the deprecated API itself:
bp pciidex!GenPopulateAccessRange
bp nt!MmMapIoSpace
Why: GenPopulateAccessRange confirms we have entered the correct resource mapping logic. Breaking on nt!MmMapIoSpace allows us to intercept the exact moment the vulnerable mapping occurs.
What to Inspect at Each Breakpoint
- At
pciidex!GenPopulateAccessRange: rcxholdsarg1(the resource list pointer).rdxholdsarg2(the output access range array pointer).- Trace execution to the
Type == 3branch to confirm a memory resource is being processed. - At
nt!MmMapIoSpace: rcx= Physical Address from the PCI BAR descriptor.rdx= Length of the mapping.r8d=0(MmNonCached), demonstrating the lack of explicit page protection.- Step over (
p) the call and inspect the return value inrax(this is the mapped kernel VA).
Key Instructions/Offsets
0x1C0008FB0: Entry point ofGenPopulateAccessRange(unpatched).0x1C0009025: TheMmMapIoSpacecall site in the unpatched binary (theType == 3/CmResourceTypeMemorybranch).0x1C000A19C: The correspondingMmMapIoSpaceExcall site in the patched binary, preceded bymov r8d, 204hat0x1C000A196.
Trigger Setup
No custom script or IOCTL packet is required to trigger this flaw. It occurs during normal device initialization.
1. Open an elevated command prompt or Device Manager.
2. Identify the PCI IDE Controller (e.g., Standard SATA AHCI Controller in IDE mode).
3. Disable and re-enable the device, or run pnputil /restart-device *DeviceID*.
4. The breakpoints will hit automatically.
Expected Observation
After MmMapIoSpace returns, take the pointer in rax and run the WinDbg extension command:
!pte <rax>
Observe the PTE output. On the unpatched system the Execute-Disable (NX) bit is not set, so the mapped MMIO region is executable. On the patched system (using MmMapIoSpaceEx with 0x204), the mapping is non-executable. This confirms the direction of the fix; it is not, by itself, a code-execution demonstration.
8. Changed Functions — Full Triage
Out of 115 changed functions, the overwhelming majority are compiler-level optimizations, register re-allocations, and basic block reordering. Below are the notable behavioral and security-adjacent changes:
- GenPopulateAccessRange (
0.9901similarity): Security Relevant. ReplacedMmMapIoSpacewithMmMapIoSpaceExto enforce explicit NX protection (0x204) on mapped memory. - ControllerInitControllerInfo (
0.9217): Behavioral. Changed how PCI IDE channel mode flags are initialized at offset0x1ac; patched version uses individual byte writes rather than a single 32-bit write. No functional difference. - PciIdeRegReadDwordDeviceKey (
0.9714): Behavioral. Function signature gained a 5th parameter. Registry DWORD values are now written directly to a caller-provided buffer instead of relying on a local stack variable, eliminating potential stack-residue leaks. - PciIdeChannelEnabled (
0.9219): Behavioral. Updated to account for the new signature ofPciIdeRegReadDwordDeviceKey. Control flow simplified. - GenPnpFdoRemoveDevice (
0.7963): Behavioral. Device teardown refactored.IoDetachDevice/IoDeleteDeviceinline code was extracted toGenPnpFreeFdo(). PDO removal paths unified. - GenPnpPdoStartDevice (
0.9786): Behavioral. State flag masking updated to re-read a value afterChannelStartDevicecompletes, fixing a minor state machine correctness issue using stale data. - ControllerStartMiniport, FdoSystemPowerDownCompletion, PciIdeUnload, RemovePdoExtension, PciIdeXSetBusData, ControllerQueryBusRelations, ChannelBuildDeviceId, ChannelQueryResourceRequirements, ControllerInterruptControl: Cosmetic. These functions exhibit compiler-driven changes such as variable renaming (
rsitordi), branch swapping, jump target offset shifts, and tail-call restructuring. The underlying logic is identical.
9. Unmatched Functions
No functions were removed. Two functions exist only in the patched build, both refactoring extractions of code that was previously inlined in existing functions:
- GenPnpFreeFdo (
@ 0x1C000919C): FDO teardown helper extracted from the device-removal path (set state, unlink from the global list,IoDetachDevice/IoDeleteDevice). Same operations as the inlined unpatched code. - PdoCompleteSystemPowerIrp (
@ 0x1C0002B40): system power-IRP completion helper (records the requested power state, callsPoSetPowerStatewhen it changes, thenPoStartNextPowerIrpandIofCompleteRequest), extracted from the PDO power dispatch path.
Neither introduces new security behavior.
10. Confidence & Caveats
- Confidence Level: High. The API replacement (
MmMapIoSpace->MmMapIoSpaceExwith0x204) is present in both the decompilation and the disassembly ofGenPopulateAccessRange, matches at the exact call sites (0x1C0009025vs0x1C000A19C), and aligns with Microsoft's deprecation ofMmMapIoSpacein favour ofMmMapIoSpaceEx. - Severity rationale (Low): The change enforces NX on MMIO mappings but does not, on its own, constitute a reachable exploit. The mapped physical addresses come from PnP-assigned PCI BAR resource descriptors rather than attacker-controlled input, and turning an executable MMIO page into code execution would require both control of the mapped physical memory contents and a separate control-flow hijack. Under HVCI/VBS the two builds behave identically.
- Verification Note: The executable-vs-NX difference is observable via
!pteon the returned VA in the two builds; this confirms the fix direction but is not a code-execution demonstration.