Manually mapping kernel-mode drivers used to be a common practice in the game hacking scene, especially when most anticheats were still relatively immature.
As a result, public projects often focused on modifying the g_CiOptions value to disable driver signature enforcement, loading their unsigned driver, and then applying some DKOM techniques to make detection more difficult.
This technique boomed in 2019, when kdmapper was released to the public.
This was a game-over for anticheats in that time, for the first few months , people have fun mapping random trash into kernel mode without even understanding how any of it worked (like any other top bypass method)
How this works?
Step 1: Load a Vulnerable Signed Driver
The process starts by loading a vulnerable driver, such as Intel’s iqvw64e.sys. This driver is legitimately signed, so Windows allows it to load even with DSE enabled.
This driver exposes functionality that can be exploited to perform arbitrary memory reads and writes in kernel mode — exactly what we need.
In intel_driver.cpp, the intel_driver::Load() function loads the signed Intel driver:
HANDLE intel_driver::Load() {
std::cout << "[<] Loading vulnerable driver" << std::endl;
// copy iqvw64e.sys to %TEMP% and use CreateService / StartService
...
return hDevice;
}
This returns a handle to the driver device, enabling later memory operations.
Step 2: Allocate Memory for the Target Driver
kdmapper uses the vulnerable driver to allocate non-paged pool memory (or similar) in kernel space, where it will manually load the unsigned driver.
This avoids calling NtLoadDriver, which would require the driver to be signed and properly registered.
After obtaining hDevice, main.cpp sends an IOCTL to allocate kernel memory:
SIZE_T allocSize = alignedSize(targetDriverImageSize);
if (!intel.LoadVulnerableDriver(...)) { /* error */ }
ULONG64 allocAddr;
if (!intel.AllocatePool(hDevice, allocSize, &allocAddr)) { /* error */ }
The vulnerable driver exposes a vulnerability that performs arbitrary kernel allocations.
Step 3: Map the Driver Manually
The unsigned driver (typically a .sys file) is parsed and mapped manually by kdmapper:
-
It loads the PE image into user-mode memory
-
Parses headers to locate sections, imports, and relocations
-
Copies sections to the allocated kernel memory
-
Resolves imports manually (or uses shellcode stubs)
-
Applies relocations for the new base address
At this point, the driver is "loaded" in memory, but hasn't been executed yet.
Again from main.cpp, the tool manually maps the driver into kernel memory:
ParsePEImage(targetDriverPath, &image, &size);
// resolve imports & relocations
intel.WriteMemory(hDevice, allocAddr, image, size);
// optionally skip header if `--copy-header` unset
All PE parsing, import resolution, relocation, and section copying is done in user-space; WriteMemory uses the driver to write into kernel memory.
Step 4: Prepare and Execute Shellcode
To start the driver, kdmapper writes a small shellcode stub into kernel memory that calls the mapped driver’s DriverEntry function.
-
The vulnerable driver is used to trigger execution of that shellcode
-
This calls the entry point with fake DRIVER_OBJECT and UNICODE_STRING parameters
From the system’s perspective, this driver was never "loaded", so it doesn’t show up in the standard driver list.
main.cpp builds minimal shellcode to trigger the driver's EntryPoint:
BYTE shellcode[] = { /* mov rax, EntryPoint; call rax; … */ };
intel.WriteMemory(hDevice, allocAddr - padding, shellcode, sizeof(shellcode));
intel.TriggerShellcode(hDevice, allocAddr - padding);
This executes the shellcode, passing fake DRIVER_OBJECT and UNICODE_STRING, effectively calling DriverEntry.
Step 5: Clean Up Traces
After the driver is mapped and running, kdmapper optionally:
-
Unloads the vulnerable driver (iqvw64e.sys)
-
Frees temporary memory
-
Erases leftover traces (e.g., zeroing shellcode)
if (freeAfter) {
intel.FreePool(hDevice, allocAddr, allocSize);
}
It also unhooks the vulnerable driver, clears PiDDBCacheTable, MmUnloadedDrivers, and other kernel traces to avoid detection
Detection
As expected, anti-cheat systems began evolving. At first, detection focused on simple indicators:
- IOCTL handlers pointing to unknown memory regions.
- System threads with invalid or suspicious stack traces.
- Executable memory regions not belonging to any known module.
Eventually, more sophisticated techniques emerged, including full memory scanning to proactively search for manually mapped drivers.
Modern Detection Concepts
While most mappers zero out PE headers to avoid basic scans, modern detection looks for patterns that remain:
- Compiler-generated functions like
_GSHandlerCheckCommon(used for buffer security checks), or_cpu_features_init(used bymemset). - Statically linked standard library routines like
memset,memcpy, and others, which often leave behind recognizable byte patterns. - Import wrappers generated by MSVC that perform indirect jumps (
jmp qword ptr [...]) viaFF 25opcodes.
These patterns often persist even when PE headers are stripped, offering detection vectors that can be used to flag suspicious memory regions.
Despite the promise of these techniques, detection isn’t perfect:
- Different compiler versions produce different patterns, requiring a wide range of signatures.
- False positives are possible, especially due to UEFI firmware blobs or legitimate kernel extensions.
- Some drivers may be so minimal (e.g.,
kdmapper’s HelloWorld) that no identifiable patterns are left behind.
Conclusion
Manual mapping with tools like kdmapper significantly lowered the barrier for kernel-mode access, especially in the game hacking and research communities. While it was highly effective against early anti-cheat implementations, the landscape has since evolved.
Modern detection approaches now require a deeper understanding of compiler behavior, memory heuristics, and low-level OS structures to remain effective.
Understanding how these techniques work—both offensively and defensively—is critical for researchers, red teamers, and developers working in security-sensitive environments.
