Blog / kernel-mode · · 12 min read · Signed KyTech Research

Manual Mapping Kernel Drivers: How Unsigned Code Reaches Ring 0

How HVCI and the signed third-party driver Blocklist changed the manual-mapping landscape on Windows 11 24H2.

Manual mapping a kernel driver means loading unsigned code into ring 0 without ever calling ZwLoadDriver. The mapper skips the Service Control Manager, skips IopLoadDriver, skips the signature check in MiCheckSystemImage, and hands the kernel a fully relocated PE image to execute. For roughly a decade this was the default answer to Microsoft's Driver Signature Enforcement: pay a signed but signed third-party driver to do the heavy lifting, jump to your entry point, and let the loader driver unload itself. In 2026, that path is mostly closed. This post walks through how manual mapping actually works, what killed it on Windows 11 24H2, and why KyTech pays the WHQL bill instead of chasing the treadmill.

The legitimate load path exists for a reason

When a service starts a kernel driver the normal way, control flows through a fairly deep stack. User mode calls StartService, which talks to services.exe. The SCM issues NtLoadDriver against the registry key under HKLM\SYSTEM\CurrentControlSet\Services\<name>. Inside the kernel, IopLoadDriver opens the image via ZwOpenSection with SEC_IMAGE, and the memory manager routes through MiCreateImageFileMap, which invokes MiCheckSystemImage. That is where Driver Signature Enforcement lives. If the PE is not signed by a certificate chained to a Microsoft cross-cert (or a WHQL-attested cert on 1607+), the mapping fails with STATUS_INVALID_IMAGE_HASH and the driver never gets an INIT section, let alone a DriverEntry call.

Everything about manual mapping is an attempt to skip this stack. If you can already execute code in kernel mode, DSE never runs, because DSE only guards the section-creation path used by IopLoadDriver. Getting into kernel mode without triggering it is the whole game.

The signed-third-party-driver primitive

The standard technique is to ride a legitimately-signed third-party driver. Pick a driver that is signed (so it loads normally), exposes an IOCTL interface to user mode, and has a bug that grants arbitrary kernel read, arbitrary kernel write, and ideally the ability to allocate executable non-paged memory. Intel's iqvw64e.sys (the NAL driver shipped with the Ethernet diagnostic toolkit) is the canonical example. Its MmMapIoSpace wrapper accepted user-controlled physical addresses without validation, letting anyone with SeLoadDriverPrivilege map arbitrary physical memory as writable and executable.

A control interaction with a mapper driver looks like this from user mode:

typedef struct _MAP_REQUEST {
    PHYSICAL_ADDRESS PhysicalAddress;
    ULONG            Size;
    PVOID            OutMappedVa;   // filled by driver
    ULONG            Protect;       // PAGE_EXECUTE_READWRITE
} MAP_REQUEST, *PMAP_REQUEST;

#define IOCTL_MAP_PHYSICAL \
    CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)

HANDLE h = CreateFileW(L"\\\\.\\Nal",
                       GENERIC_READ | GENERIC_WRITE,
                       0, NULL, OPEN_EXISTING, 0, NULL);

MAP_REQUEST req = {0};
req.PhysicalAddress.QuadPart = target_pa;
req.Size    = 0x2000;
req.Protect = PAGE_EXECUTE_READWRITE;

DWORD bytes = 0;
DeviceIoControl(h, IOCTL_MAP_PHYSICAL, &req, sizeof(req),
                &req, sizeof(req), &bytes, NULL);

Once the mapper hands back a kernel virtual address for the physical page you asked for, you have a primitive equivalent to WriteProcessMemory against the kernel. Repeat that primitive across the whole payload PE and you have manual mapping.

What a manual-mapped image actually looks like

A properly loaded driver lives in a section object that PsLoadedModuleList tracks, with a KLDR_DATA_TABLE_ENTRY describing it, valid pool tags, per-section page protections that respect the PE's characteristics, and a PLDR_DATA_TABLE_ENTRY PatchGuard treats as a first-class citizen. A manually mapped image lives in a single slab of NonPagedPoolExecute (or a physical alias thereof) with none of that context:

+---------------------------------------------------+
| ExAllocatePool2(NonPagedPoolExecute, 0x14000)     |
+---------------------------------------------------+
| PE headers stripped / zeroed                      |  offset 0x0000
+---------------------------------------------------+
| .text  (relocated, imports resolved by loader)    |  offset 0x1000
+---------------------------------------------------+
| .rdata (RO in a real load, RWX here)              |  offset 0x8000
+---------------------------------------------------+
| .data  (RW)                                       |  offset 0xB000
+---------------------------------------------------+
| .pdata (unwind info, usually left in place)       |  offset 0xD000
+---------------------------------------------------+
       ^                                       ^
       |                                       |
   DriverEntry called here, then RIP           end of blob
   returns and the mapper frees nothing

Nothing in PsLoadedModuleList points at this region. Nothing in MmUnloadedDrivers will ever hold its name. If it takes a bugcheck, the stack trace shows nt!<returned_from_pool+0xNN> with no module attribution. That is a feature for the mapper and a liability for the operator, because kernel telemetry (ETW Microsoft-Windows-Kernel-Process, NT Kernel Logger image-load events) never sees it either. A signed driver we shipped as part of the KyTech product line would be catalog-registered, versioned, and revocable. A manually mapped blob is a ghost, until something looks for ghosts.

The manual-mapper canon

Public manual-mapper reference implementations are what most researchers learn from. The canonical educational examples on GitHub abuse a signed third-party diagnostic driver (Intel's iqvw64e Ethernet Diagnostic driver is the historical prototype), extract the vulnerable IOCTL, allocate a pool of NonPagedPoolExecute, relocate the target PE against that base, resolve imports against the running kernel's exports (walking KeServiceDescriptorTable and the export directory of ntoskrnl.exe), and call the entry point with a synthetic DRIVER_OBJECT set to NULL. That last detail is a giveaway: a real DriverEntry receives a valid PDRIVER_OBJECT from IoCreateDriver, and mappers that pass NULL will crash any driver that expected to register an unload routine or an IRP dispatch table. Practical mappers either pass a stub or expect the payload to know it is being mapped.

The clean-up phase is where mappers get creative. Reference implementations zero the PE headers so RtlPcToFileHeader can't attribute callbacks back to the region, then leak the pool allocation (freeing it would tear down the still-executing image). Some forks patch NonPagedPoolExecute to NonPagedPoolNx after entry to defeat casual pool scanners. All of these tricks depend on Microsoft not looking at the specific memory region, which was a reasonable bet in 2019 and is a poor bet in 2026.

HVCI and the death of RWX kernel memory

Hypervisor-Protected Code Integrity, the kernel-mode component of Virtualization-Based Security, moved the DSE check out of the kernel and into a Secure Kernel running in VTL1. When HVCI is on, the NT kernel cannot mark a page as executable without asking the Secure Kernel first, and the Secure Kernel refuses unless the page contents match a signed Code Integrity policy. This is not a syscall the mapper can spoof. The EPT (Extended Page Tables) permissions are owned by the hypervisor. If your pool allocation is NonPagedPoolExecute under HVCI, you got the allocation, but the underlying physical page has EPT execute permission denied. The instant RIP lands on the first byte of your .text, EPT throws a violation and the box bugchecks with SECURE_KERNEL_ERROR (0x18b) or HYPERVISOR_ERROR (0x20001), depending on where the fault takes you.

HVCI ships enabled by default on any Windows 11 install that meets the hardware bar (TPM 2.0, VT-x/AMD-V, SLAT, IOMMU). Microsoft documents the exact enablement path and the group-policy, MDM, and UEFI knobs that lock it on, including the registry flags OEMs stamp before first boot. On Windows 11 24H2 it is enabled by default on more SKUs than 23H2, and OEM images increasingly ship with it locked on via UEFI variables. For a manual-mapped payload to run, HVCI has to be off. Turning HVCI off requires either kernel-mode code (which is what you were trying to get) or a UAC-elevated reboot cycle the user has to approve. The Vanguard anti-cheat that Riot pushed into League of Legends in 2024 refuses to run on any system where HVCI or Memory Integrity is disabled, and Fortnite added a similar check for competitive playlists.

MiShowBadMapper and the physical-alias trick

Some mappers side-stepped the RWX-allocation problem by allocating writable pool, then creating a second virtual mapping of the same physical pages with execute permissions via MmMapIoSpace(..., MmCached) or by walking the client's page tables directly. That technique class is what the Windows 11 24H2 memory-manager change targeted — see our note on the Win11 24H2 MmMapIoSpace wall.

Starting in Windows 11 24H2, MiShowBadMapper runs on any MmMapIoSpace call whose target physical range overlaps memory the kernel has already classified as write-back cacheable RAM (as opposed to true MMIO). If the alias would produce a cache-attribute mismatch or an executable view of a WB kernel page, the call returns NULL and (in checked builds) prints a diagnostic. Drivers riding this primitive stopped working in the 24H2 general availability window. Every published mapper that used the physical-alias route needs a rewrite or an entirely new vulnerability.

Older mapper substrates that relied on a handshake IOCTL over a signed third-party driver still connect and return successful DeviceIoControl status codes, but the actual mapping call downstream fails and the client sees a NULL pointer where the mapped VA was supposed to be.

The signed third-party driver Blocklist

Microsoft ships a DriverSiPolicy.p7b under %SystemRoot%\System32\CodeIntegrity\ that HVCI (and, on 24H2, an always-on subset even without HVCI) consults before allowing a driver to load. The Microsoft-recommended driver block rules are published on GitHub and updated roughly quarterly, and the list includes essentially every historically abused signed-driver-abuse candidate: iqvw64e.sys, RTCore64.sys (MSI Afterburner), gdrv.sys (Gigabyte), AsIO.sys (ASUS), WinRing0x64.sys, HpqKbFiltr.sys, and a long tail of similar drivers. The list also blocks specific hashes of vulnerable versions of otherwise-legitimate drivers, so simply obtaining an older build does not help.

To load one of these drivers today you need either an HVCI-disabled system or a WDAC policy that overrides the blocklist. On a stock Windows 11 24H2 install with Smart App Control on, none of the standard mapper drivers load. This does not eliminate the technique. It moves it to the fringe: whoever finds the next unpatched, un-blocklisted, still-signed driver with a usable IOCTL gets a window measured in weeks or months before Microsoft catalogs and revokes it. That is not a business model. It is a treadmill.

Comparison: signed vs manually mapped

Property Signed WHQL driver Manually mapped payload
Load path ZwLoadDriver -> IopLoadDriver Signed-third-party-driver IOCTL -> pool allocation -> jump
Visible in PsLoadedModuleList Yes No
Bugcheck attribution Module name in stack trace nt!<pool+offset>, unattributed
Survives HVCI Yes No (RWX or physical alias both blocked)
PatchGuard integration First-class None; hooks caught within seconds
Windows Update revocation Certificate revocable Not revocable, but blocklist kills loader
Development cost EV cert (~$400/yr) + WHQL submission Zero, until the treadmill catches you
Time to production Weeks (WHQL queue) Hours
Lifetime Years Weeks to months per driver

The last row is why serious kernel-mode software ships signed. The first row is why the signed-third-party-driver market keeps existing, because there are use cases (research, ephemeral tooling, adversarial red-teaming) that cannot pay the WHQL cost.

A minimal in-driver stub

For completeness, here is roughly what a mapper payload has to do differently from a normally-loaded driver. It cannot use IRP dispatch tables (no DRIVER_OBJECT), it cannot rely on the loader for imports, and it must resolve its own kernel APIs at runtime:

NTSTATUS DriverEntry(PVOID unused1, PVOID unused2)
{
    UNREFERENCED_PARAMETER(unused1);
    UNREFERENCED_PARAMETER(unused2);

    // No IoCreateDevice, no IoCreateSymbolicLink.
    // The mapper did not give us a DRIVER_OBJECT.
    // Resolve exports by walking ntoskrnl's export table
    // from a base we already computed on the client side
    // and passed via a shared page.
    PVOID nt_base = FindKernelBase();
    tExAllocatePool2 pExAllocatePool2 =
        (tExAllocatePool2)GetExport(nt_base, "ExAllocatePool2");

    // Register a process-creation callback so we have somewhere
    // to run periodic work without owning a device.
    PsSetCreateProcessNotifyRoutineEx(OnProcessCreate, FALSE);

    // Return STATUS_SUCCESS to whatever the mapper's stub does
    // with our return value (usually nothing).
    return STATUS_SUCCESS;
}

Every design choice here (no device, callback-driven, self-resolved imports) exists because the driver was not loaded through the normal path. Every one of those choices is also a signal a defender can look for. A kernel module with no DRIVER_OBJECT, no \Device\... object, no entry in PsLoadedModuleList, and a callback that resolves back to unattributed pool memory is not subtle.

What is left of manual mapping in 2026

The realistic remaining scope for manual mapping on current Windows:

  • HVCI-off systems (older hardware, enterprise images that opted out for compatibility with legacy drivers, or research VMs).
  • Newly discovered signed-driver-abuse candidates in the window between disclosure and blocklist update. This window has been shrinking. Microsoft has automated a lot of the blocklist pipeline in the last two years.
  • Custom signed third-party drivers signed under stolen or leaked EV certs. When one of these gets caught, the whole cert gets revoked, and the operator loses everything signed under it.
  • WDAC-permissive environments where an admin explicitly whitelisted a mapper's helper driver.

For any product that expects to run for more than a quarter on a random consumer machine, this list is not a foundation.

How KyTech handles this

KyTech was founded in 2025 by two engineers who had spent enough time reversing anti-cheat kernel components to know the manual-map treadmill was ending. Instead of chasing third-party signed drivers, our kernel component ships as a properly signed driver: EV code-signing cert, WHQL attestation, the whole SCM load path Microsoft describes in the kernel-mode code signing requirements, a real DriverEntry, a real device object, and callbacks registered through ObRegisterCallbacks and PsSetCreateProcessNotifyRoutineEx the way the DDK intended. That takes a few extra weeks per revision and costs real money in cert fees, and it means the driver survives HVCI, survives Smart App Control, and doesn't vanish the next time Microsoft ships a policy update.

Our product line, Apex Legends, Counter-Strike 2, Overwatch 2, Black Ops 7, Forza Horizon 6, and Roblox, is spoofer-adjacent tooling that has to coexist with each title's anti-cheat rather than fight it. The Apex-only HWID spoofer we currently have in beta uses the same signed-driver architecture as the rest of the product family. That flagship build ships as KyTech Apex, and it never touches the manual-map path this post walks through. You can see the current lineup on our purchase page. For a deeper look at how the 2026 signing regime reshaped the market, see our companion post on Windows kernel driver signing in 2026. Manual mapping is a technique worth understanding. It is not one worth building a business on.

Signed by KyTech Research

We still play these games and we still push every build in production. If something in here is wrong, and eventually something will be, ping us in Discord and we will fix it.

Enough theory. get in.

The loader is one click away. Ring-0 kernel driver, polymorphic per download, memory-only injection. Six games across VAC, EAC, Ricochet, Byfron, and Warden.

Get in ›