Building the First Open-Source DMA HWID Spoofer
Why I Built This
Every DMA-based HWID spoofer out there is closed-source, paid, or both. Some sell for thousands of dollars. The techniques aren’t secret — they’re well-documented across research blogs and forums — but nobody had put together a complete, open-source implementation.
So I built one. Written in Rust, covering 12 hardware spoofing modules, with PatchGuard bypass and DSE patching. The full source is on GitHub.
This post walks through the reverse engineering process, the kernel internals I had to understand, and how each piece fits together.
What is DMA and Why Does It Matter
Direct Memory Access (DMA) is a hardware feature that allows devices to read and write system memory without going through the CPU. In normal operation, this is how your NVMe drive, GPU, and network card move data efficiently.
But when you plug in an FPGA-based PCIe device (like a PCIeSquirrel or similar board), you get the same unrestricted access to physical memory. The OS doesn’t mediate these reads and writes — they happen at the hardware level, below the kernel.
This means you can:
- Read and write any physical memory address
- Modify kernel structures in real-time
- Bypass software-based protections entirely
The tool uses MemProcFS by ufrisk as the DMA engine interface. It abstracts the FPGA communication and provides a clean API for memory operations:
pub struct Dma<'a> {
vmm: Vmm<'a>,
}
impl<'a> Dma<'a> {
pub fn new() -> Result<Self> {
let args = vec!["-device", "fpga", "-waitinitialize"];
let vmm = Vmm::new("vmm.dll", &args)?;
Ok(Self { vmm })
}
pub fn read_phys(&self, addr: u64, size: usize) -> Result<Vec<u8>> {
self.vmm.mem_read_ex(addr, size, FLAG_NOCACHE)
}
pub fn write_phys(&self, addr: u64, data: &[u8]) -> Result<()> {
self.vmm.mem_write(addr, data)
}
}
The Reverse Engineering Process
I spent a significant amount of time in IDA Pro disassembling Windows 11 kernel drivers. The goal was to find the exact memory offsets and structures needed to locate and modify hardware identifiers.
Finding Kernel Structures
The first step for every spoofing module is the same: find where Windows stores the data in kernel memory. This means:
- Loading the target driver (e.g.,
ntoskrnl.exe,CI.dll,nvlddmkm.sys,dxgkrnl.sys) into IDA Pro - Identifying exported functions and following cross-references
- Mapping out the internal data structures and their offsets
- Validating offsets against the live system via DMA reads
For example, SMBIOS tables are stored at a physical address referenced by ntoskrnl.exe. I found the offset by tracing how WmipFindSMBiosStructure accesses the SMBIOS entry point, then confirmed it by reading the physical memory and parsing the SMBIOS header format.
Build-Specific Offsets
One of the biggest challenges is that kernel structure offsets change between Windows builds. The spoofer detects the Windows build number and selects the correct offsets:
let build_number = Self::get_build_number(dma)?;
let cip_init_va = if build_number < WIN10_1709_BUILD {
scanner.find_cip_initialize_pre_1709(ci_init_offset)?
} else if build_number < WIN11_24H2_BUILD {
scanner.find_cip_initialize_post_1709(ci_init_offset)?
} else {
scanner.find_cip_initialize_24h2(ci_init_offset)?
};
This was built and tested specifically on Windows 11 Pro Build 26100 (24H2). Supporting other builds would require extracting their offsets — which is why the project is open-source.
Defeating Windows Kernel Protections
Before you can modify anything in kernel memory, you need to deal with two major protections: Driver Signature Enforcement (DSE) and PatchGuard (Kernel Patch Protection / KPP).
Patching DSE via CI.dll
DSE prevents unsigned drivers from loading. The enforcement lives in CI.dll, specifically in a global variable called g_CiOptions. When this value is 0x00000006, DSE is enabled. Set it to 0x00000000, and signature checks are disabled.
The challenge is finding g_CiOptions in memory. It’s not exported. I had to trace through CI.dll in IDA Pro:
- Find the exported function
CiInitialize - Follow the call chain into
CipInitialize(an internal function) - Locate the
movinstruction that referencesg_CiOptions - Extract the RVA and resolve it to a virtual address
The pattern scanning differs across Windows versions. Microsoft restructured CipInitialize in 24H2, so I had to write a separate scanner for that build:
pub fn disable_dse(&mut self) -> Result<()> {
let va = self.find_g_ci_options()?;
let current = self.dma.read_u32(KERNEL_PID, va)?;
// Save original for restoration
self.original_value = Some(current);
// Patch to disabled
self.dma.write(KERNEL_PID, va, &DSE_DISABLED.to_le_bytes())?;
// Verify the write
let verify = self.dma.read_u32(KERNEL_PID, va)?;
if verify != DSE_DISABLED {
return Err(anyhow!("DSE patch verification failed"));
}
Ok(())
}
This approach is well-documented in the security research community. The blog post “The dusk of g_CiOptions” by cryptoplague covers the evolution of this technique, including how VBS (Virtualization-Based Security) changes the game.
Bypassing PatchGuard (KPP)
PatchGuard is Microsoft’s kernel integrity monitor. It periodically checks critical kernel structures and BSODs the system if it detects tampering. If you patch g_CiOptions and PatchGuard notices before you restore it — blue screen.
The bypass works in 5 steps:
- Disable PG timers — Walk each processor’s
KPRCBtimer table, decrypt the DPC pointers usingKiWaitNever/KiWaitAlways, identify PatchGuard-owned timers, and null their DPC entries - Restore
KiBalanceSetManagerPeriodicDpc— PatchGuard hijacks this legitimate DPC; we restore the original deferred routine - Patch
KiSwInterruptDispatch— Replace the entry point with aretinstruction to prevent PG from triggering via software interrupts - Patch
KiMcaDeferredRecoveryService— Another PG trigger point, sameretpatch - Clear
MaxDataSize— Zero out this value to prevent PG context allocation
The DPC decryption is the most interesting part. PatchGuard encrypts its timer DPC pointers using a multi-step XOR/rotate scheme:
fn decrypt_dpc(&self, timer_addr: u64, encrypted_dpc: u64) -> u64 {
let rotated = encrypted_dpc ^ self.ki_wait_never;
let rotated = rotated.rotate_left((self.ki_wait_never & 0xFF) as u32);
let xored = rotated ^ timer_addr;
let swapped = xored.swap_bytes();
swapped ^ self.ki_wait_always
}
I found the KiWaitNever and KiWaitAlways offsets by disassembling ntoskrnl.exe in IDA Pro and tracing the timer initialization code. These values are read from fixed offsets relative to the kernel base.
The 12 Spoofing Modules
With kernel protections out of the way, the actual spoofing can begin. Each module targets a different hardware identifier that Windows (and anti-cheat / fingerprinting systems) uses to identify a machine.
SMBIOS — The Motherboard Identity
SMBIOS (System Management BIOS) tables contain your motherboard serial, system UUID, chassis info, BIOS vendor, and more. Windows stores these in physical memory, referenced by a pointer in ntoskrnl.exe.
I reversed WmipFindSMBiosStructure in IDA to find the physical address pointer and table length. The spoofer reads the raw SMBIOS data, parses each table type (BIOS, System, Baseboard, Chassis, Processor, Memory), and overwrites the string fields with generated values.
The tricky part is that SMBIOS strings are stored as null-terminated sequences after the fixed-length header. You have to walk the string table carefully and write replacements that are exactly the same length as the originals — otherwise you corrupt the table structure.
Disk Serials — NVME, RAID, CLASSPNP
Disk serial numbers are stored in driver-specific structures. The spoofer handles three different storage drivers:
- NVME (
stornvme.sys) — Serial stored in the NVMe identify controller data structure - RAID (
storport.sys/ RAID controllers) — Serial in the RAID unit extension - CLASSPNP (
classpnp.sys) — The generic class driver that exposes serials to WMI queries
Each required separate IDA sessions to trace how the serial gets from the physical device into the driver’s internal structures.
NVIDIA GPU UUIDs
This was one of the more complex modules. NVIDIA GPU UUIDs are stored in the nvlddmkm.sys driver’s memory space. The UUID format follows NVIDIA’s convention: GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
The spoofer enumerates GPU devices, scans the driver’s memory region for UUID patterns, and ranks candidates by confidence level (Exact, High, Medium, Low) based on surrounding memory context. You can also provide a known UUID from nvidia-smi output and the tool will locate it by signature scanning.
NIC MAC Addresses
MAC address spoofing targets Intel WiFi adapters specifically (Netwtw10.sys and similar). The spoofer locates the adapter’s internal structure in the driver and overwrites the MAC bytes. Generated MACs use real manufacturer OUI prefixes (Intel, Realtek, etc.) to look legitimate.
Monitor EDID via DXGKRNL
EDID (Extended Display Identification Data) contains your monitor’s serial number and manufacturer info. Windows caches this in two places: the registry and the dxgkrnl.sys kernel driver’s memory.
The spoofer patches both. The DXGKRNL approach required reversing how the display kernel caches EDID blocks — finding the adapter objects and their associated EDID buffers in memory.
The Rest
The remaining modules follow similar patterns:
- Volume Serials — Spoofs volume GUIDs stored in the mount manager
- USB Device IDs — Modifies USB storage device serial numbers in driver memory
- TPM Identity — Patches TPM endorsement key references in the registry
- EFI Variables — Modifies EFI platform data stored in kernel memory
- Boot Configuration — Spoofs boot timestamps and identifiers
- ARP Cache — Manipulates the kernel’s ARP table entries
- Registry Traces — Cleans up hardware ID traces left in the Windows registry
Realistic HWID Generation
Spoofing is useless if the generated values look fake. Anti-cheat systems check for patterns — a MAC address with an unregistered OUI prefix, a disk serial that doesn’t match any manufacturer’s format, or a SMBIOS UUID of all zeros will get flagged instantly.
The spoofer includes a seed-based HWID generator that produces realistic values:
- MAC addresses use real OUI prefixes from Intel, Realtek, Broadcom, and other manufacturers
- Disk serials follow manufacturer-specific patterns (WD, Samsung, Seagate, NVMe)
- SMBIOS serials match the format used by AMI, Phoenix, and other BIOS vendors
- UUIDs/GUIDs follow RFC 4122 format with proper version bits
The seed system means you can regenerate the same set of hardware IDs consistently. Change the seed, get a completely new identity. This is stored in hwid_seed.json and can be exported/imported.
Code Cave Injection
For the PatchGuard barricade (an experimental MmAccessFault hook), the spoofer needs to inject code into kernel memory without allocating new pages — which would be suspicious. Instead, it finds “code caves”: unused regions of padding bytes within legitimate kernel modules.
pub fn find_best_codecave(vmm: &Vmm, min_size: usize) -> Result<CodeCave> {
// Scan loaded kernel modules for regions of 0x00 or 0xCC bytes
// that are large enough to hold our shellcode
}
This technique is well-known in exploit development — you’re reusing space that’s already mapped and executable, so no new memory allocations show up in kernel auditing.
Why Rust
Most tools in this space are written in C or C++. I chose Rust mostly because I like writing Rust. Let’s be honest — when you’re writing raw bytes to kernel memory addresses through DMA, “memory safety” isn’t really the selling point. The core operations are all FFI calls into memprocfs and most of the interesting code is unsafe by nature.
That said, Rust still made the project more pleasant to work on:
- Error handling — The
Resulttype forces you to handle every failure path. When a DMA read fails or an offset is wrong, you get a clean error message instead of a silent corruption or segfault. Debugging kernel-level tools is painful enough without chasing undefined behavior in your own code. - Tooling —
cargojust works. Building, dependency management, cross-compilation — it’s all smoother than wrestling with CMake or Visual Studio project files. - FFI — Rust’s
libloadingcrate makes it straightforward to dynamically loadvmm.dll,leechcore.dll, andFTD3XX.dllat runtime.
The tradeoff is that Rust’s ecosystem for Windows kernel internals is basically nonexistent. Most of the struct definitions, constants, and offsets had to be written from scratch based on IDA output. No crate is going to hand you KPRCB layouts or PatchGuard timer structures.
References and Prior Work
This project builds on research and tools from the community:
- MemProcFS by ufrisk — The DMA engine that makes all of this possible
- PCILeech by ufrisk — The original DMA attack framework and FPGA firmware
- “The dusk of g_CiOptions” — Deep dive into DSE bypass evolution
- Guided Hacking’s HWID Spoofer RE — Reverse engineering walkthrough of how spoofers work
- SecHex-Spoofy — An earlier open-source (software-based) HWID spoofer
Disclaimer
This project is for educational and research purposes only. It demonstrates the security implications of DMA devices and the level of access they provide when system protections are disabled.
DMA attacks are a real threat vector. Understanding how they work is essential for building defenses — IOMMU enforcement, Secure Boot, VBS, and Credential Guard all exist to mitigate exactly this class of attack.
The full source code is available at github.com/vibheksoni/dma-spoofer. PRs welcome — especially for supporting additional Windows builds.
Enjoyed this post? Buy me a coffee ☕ to support my work.
Need a project done? Hire DevHive Studios 🐝