From SMM to DMA: Alternative Paths to Arbitrary R/W
The hypervisor approach works beautifully when I control the target machine -- load a driver, virtualize from underneath, intercept everything. But sometimes I can't do that. Maybe the machine has Secure Boot locked down, maybe there's no driver loading path, maybe I need to read memory on a machine I can only physically touch. That's where DMA comes in.
Direct Memory Access lets a PCIe device read and write system RAM without involving the CPU at all. The device generates memory transactions on the PCIe bus, and the memory controller fulfills them. From the CPU's perspective, nothing happened -- no interrupt, no page fault, no TLB entry, no trace in any CPU-visible data structure. The data just moves.
This post covers three hardware approaches I've researched and partially built: an eNVMe controller on a NanoPC-T6, a flashable Intel i210 NIC, and IOMMU bypass from SMM context.
PCIe bus mastering 101
Every PCIe device can be a bus master -- meaning it can initiate memory transactions without being asked. This is how NICs receive packets (DMA write to a ring buffer in RAM), how NVMe drives transfer sectors (DMA read/write to PRPs), and how GPUs access vertex buffers.
The device has a set of Base Address Registers (BARs) that the BIOS or OS maps into the physical address space. But the device can also generate arbitrary physical addresses on the bus. If I control the device's firmware, I control what addresses it reads and writes. That's arbitrary physical memory access from a PCIe slot.
// PCIe Transaction Layer Packet (TLP) for a memory read
// This is what goes on the wire when a device reads host RAM
struct TlpMemoryRead {
fmt_type: u8, // 0x00 = 3DW MRd, 0x20 = 4DW MRd
length: u16,
requester_id: u16,
tag: u8, // transaction ID for completion matching
address: u64,
}
NanoPC-T6 with eNVMe
The NanoPC-T6 is a single-board computer with an RK3588 SoC that has a PCIe 3.0 x1 port. I can plug it into the target machine's M.2 slot (via an adapter) and it shows up as an NVMe device. But instead of actually being a storage device, it runs custom firmware that generates arbitrary memory read/write TLPs.
The RK3588 has a PCIe endpoint controller mode -- meaning I can program it to act as any PCIe device class. I configure it as an NVMe controller (class code 0x010802) so the target's BIOS allocates BARs for it during enumeration, but my firmware ignores NVMe commands and instead uses the PCIe controller's DMA engine to read target RAM.
// RK3588 PCIe endpoint DMA -- reading target host RAM
fn dma_read_host(pcie: &PcieEndpoint, host_pa: u64, buf: &mut [u8]) {
// configure DMA channel: source = host physical addr
pcie.dma.write_sar(host_pa);
// destination = local SRAM on the RK3588
pcie.dma.write_dar(buf.as_ptr() as u64);
pcie.dma.write_size(buf.len() as u32);
// fire -- generates MRd TLPs on the PCIe bus
pcie.dma.start_read();
while !pcie.dma.is_complete() {}
// buf now contains host RAM contents at host_pa
}
The transfer rate is limited by PCIe 3.0 x1 bandwidth -- about 985 MB/s theoretical, maybe 800 MB/s in practice. That's more than enough for targeted reads of kernel structures. A full EPROCESS walk takes microseconds.
Intel i210 NIC with flashable firmware
The Intel i210 is a gigabit NIC with a flashable SPI NOR chip on board. The NIC's firmware runs on an embedded ARC core and controls all DMA operations. If I flash custom firmware, the NIC becomes a DMA device that I control remotely over Ethernet.
This is less exotic than the NanoPC approach -- I can buy an i210 PCIe card for $30, flash it with a modified firmware image, plug it into the target, and send commands over the network. The NIC's DMA engine generates the memory read/write TLPs, and I receive the results as crafted Ethernet frames.
The i210's DMA engine is designed for packet ring buffers, so I abuse the TX descriptor ring. Each TX descriptor has a physical address field (the buffer to transmit). Instead of pointing it at a real packet buffer, I point it at the kernel address I want to read. The NIC DMAs in the "packet" data (which is actually kernel memory), wraps it in an Ethernet frame, and sends it out the wire to my receiver.
IOMMU bypass from SMM
The IOMMU (AMD calls it AMD-Vi, Intel calls it VT-d) is supposed to prevent exactly this kind of attack. It interposes on PCIe transactions and checks whether the requesting device is allowed to access the target address. Each device gets a Device Table entry that defines its allowed memory ranges.
But if I'm running code in SMM (System Management Mode -- ring -2), I have access to the IOMMU's configuration registers. I can reprogram the Device Table to grant any device access to any physical address range. Or I can just disable the IOMMU entirely.
// AMD-Vi (IOMMU) Device Table Entry
// each PCIe device has one -- controls its DMA permissions
struct DeviceTableEntry {
flags: u64,
page_table_root: u64,
domain_id: u16,
// ... interrupt remapping fields ...
}
// from SMM context: disable IOMMU protection for a device
fn iommu_bypass_device(dev_id: u16) {
let dte = get_device_table_entry(dev_id);
// clear Valid and Translation Valid bits
dte.flags &= !0x3;
// flush IOMMU TLB for this device
iommu_invalidate_dte(dev_id);
// now this device can DMA to any physical address
}
The combination is powerful: SmmInfect gives me SMM code execution, I reprogram the IOMMU to allow my DMA device through, then the DMA device does the actual memory reads. Two-stage attack where each stage covers the other's weaknesses -- SMM can't easily exfiltrate large amounts of data (no network access), but it can reconfigure hardware. DMA can move data fast but is normally blocked by the IOMMU.
When to use what
These approaches aren't mutually exclusive. The hypervisor is best when I have driver-loading capability and want low-latency, high-bandwidth memory access with full stealth. DMA is best when I need physical-access-only attacks or when the target has Secure Boot/HVCI locked down. SMM is the ultimate privilege escalation but requires firmware flashing.
In practice, I use the hypervisor for day-to-day work and keep the DMA hardware as a fallback. The NanoPC-T6 lives in a drawer until I need it. The i210 is more practical for remote scenarios -- plug it in, walk away, send commands over the network later.
The real nightmare scenario for defenders is all three combined: SmmInfect persists in firmware across reboots, disables the IOMMU for a planted DMA device, and that device provides arbitrary read/write to an external attacker. No software on the target machine can detect or prevent any of this. The attack surface is entirely below the OS.