Make a PCILeech DMA card enumerate as a Realtek RTL8168H Gigabit-Ethernet
controller, convincingly enough to satisfy the Linux r8169 driver's probe at
the config-space and BAR level.
This is a fork of ufrisk/pcileech-fpga v4.14 targeting the EnigmaX1 board (Artix-7 XC7A75T). PCILeech is a DMA read/write framework used for memory forensics and hardware red-teaming. A stock PCILeech card is trivially fingerprinted because it presents a generic, known device identity. The work here replaces that identity with a specific, real NIC's: every register the driver touches during enumeration is backed by emulation that reproduces what a genuine RTL8168H returns.
I built it while studying how host software and kernel-level anti-cheat detect DMA hardware by its PCIe footprint, namely which config-space fields, capability layouts, and register-access timings give a counterfeit endpoint away. It is a device-identity-emulation project, not a networking project. No packets are ever sent or received.
A PCIe endpoint is identified long before any driver-specific traffic. During enumeration the host reads the configuration space (vendor/device IDs, BARs, capability chains), and the matching driver then walks the device's register file over the BARs. To pass as an RTL8168H you have to get three things right:
- Identity. The 4 KB configuration space a real card exposes: IDs, class code, BAR sizing, and the full PCI + PCI-Express capability chains.
- Register file. The BAR-mapped registers the
r8169driver reads and writes duringprobe/hw_start, including the ones gated behind flag-polling handshakes (PHY/MDIO, ERI, OCP). - Behaviour over time. Registers that self-clear, lock/unlock, count, or complete after a delay, with timing inside the driver's poll budgets.
None of this is in upstream PCILeech. Stock builds answer BAR reads with zeros.
host --USB3--> FT601 --> PCILeech FIFO/COM --> PCIe Gen2 x1 hard block
(DMA tooling) |
+-----------+-----------+
config-space shadow BAR controller
(4 KB COE BRAM) +-------+-------+
BAR0/BAR2 BAR4
RTL8168 register MSI-X
file + state machines table
The PCILeech base (FT601 USB3 bridge, FIFO/COM, the Xilinx 7-series PCIe core, TLP engines) is unchanged. Three things were added or rewired:
- A shadow configuration space.
pcileech_fifo.svflips the upstreamCFGTLP ZERO DATAflag (rw[203], line 290) so config-TLP reads are served from a 4 KB BRAM initialised byip/pcileech_cfgspace.coeinstead of returning zeros. The IDs in the PCIe hard block and_pcie_core_configwere set to10EC:8168, rev0x15, subsystem1458:E000(Gigabyte). - A BAR0/BAR2 register file.
pcileech_bar_impl_rtl8168.svserves both BARs from one 1024x32 dual-port BRAM (COE-initialised reset values) behind a writemask ROM, with eight state machines intercepting the registers that aren't plain storage. - A BAR4 MSI-X region (
pcileech_bar_impl_msix.sv) and an MSI request path threaded up to the PCIe core'scfg_interrupt(pcileech_pcie_cfg_a7.sv).
pcileech_tlps128_bar_controller.sv was rewired to share one instance across
BAR0/BAR2 and demux the response with a 2-cycle pipeline matching the register
file's read latency. Everything runs in the single clk_pcie domain, so the
MSI path needs no clock-domain crossing.
The reset values, register offsets, and handshake protocols were lifted from
the Linux r8169 driver source (drivers/net/ethernet/realtek/r8169_main.c),
not from a datasheet. The driver is the thing that has to be satisfied, so it
is the authority on what the chip must look like.
- Chip version. The driver identifies the MAC by reading
TxConfig(0x40) and computingxid = (txconfig >> 20) & 0xfcf, then matching a table. The emulatedTxConfigof0x54100780yieldsxid = 0x541, which matches{ 0x7cf, 0x541, RTL_GIGA_MAC_VER_46, "RTL8168h/8111h" }, so the driver binds the 8168H path and runsrtl_hw_start_8168h_1. The XID bits are held read-only by the writemask so the host can't perturb the identity. - Flag-polling handshakes. PHYAR (0x60), ERIAR/ERIDR (0x74/0x70), and
GPHY_OCP (0xB8) are not registers; they are request/complete protocols. The
driver writes a command with bit 31 set and spins until the hardware clears
(write) or sets (read) bit 31. Each is a small FSM (
rtl8168_phyar_emu.sv,rtl8168_eriar_emu.sv,rtl8168_gphy_ocp_emu.sv) that mirrors the command, waits a completion delay, then flips the flag. PHYAR/OCP take ~20 us, ERI ~3-4 us, all well inside the driver's poll budgets. - Locks and self-clears.
Cfg9346(0x50) unlocks the MAC and Config registers only after the host writes0xC0, exactly asrtl_unlock_config_regsexpects (rtl8168_cfg9346_emu.sv).ChipCmd(0x37) reset (bit 4) self-clears after 625 cycles (rtl8168_chipcmd_emu.sv), which the driver polls with a 100 ms timeout. - Shared PHY state. PHYAR and GPHY_OCP address the same 32x16 PHY register file (BMCR/BMSR/PHYID/ANAR and the rest), since the 8168H driver reaches the PHY through both paths.
The per-register breakdown (offset, reset value, writemask, completion delay, protocol) is in docs/REGISTERS.md.
| Area | Detail |
|---|---|
| Identity | VID 10EC, DID 8168, Rev 0x15, class 020000, subsystem 1458:E000 |
| Config space | 4 KB shadow BRAM; std caps PM (D1/D2+), MSI, PCIe (Gen1/x1, MPS 128B), MSI-X, VPD; ext caps AER, VC, DSN, LTR, L1SS (all version 1); per-bit writemask + W1C masks |
| BAR0 / BAR2 | I/O 256 B / MMIO 4 KB, shared register file, RMW behind writemask with skid buffer, Cfg9346-gated MAC/Config writes |
| BAR4 | 16 KB MSI-X table (4 vectors) + PBA backing store |
| State machines | PHYAR, GPHY_OCP, ERIAR/ERIDR, ChipCmd (reset self-clear), Cfg9346 (lock), IntrMask/Status (W1C + LinkChg), free-running timer |
| Timing | LFSR-jittered completion delays on the polling handshakes |
- Cross-checked against primary sources. Every config-space DWORD,
register offset, and handshake protocol was checked against the
r8169_main.csource and against reallspci -nnvvxxxdumps of10ec:8168cards: identity, BAR sizing, capability offsets/order, MSI and MSI-X fields, and the PHYAR/ERIAR/GPHY_OCP/Cfg9346/ChipCmd protocols. - Brought up on hardware. On the EnigmaX1 the
r8169driver binds against the card and completes config-space and BAR-level interrogation.
| Field | Value |
|---|---|
| VID / DID | 10EC / 8168 |
| RevID | 0x15 |
| SVID / SSID | 1458 / E000 (Gigabyte) |
| XID | 0x541, maps to RTL_GIGA_MAC_VER_46 (RTL8168h/8111h) |
| TxConfig | 0x54100780 |
| PHY ID | 001C:C800 (RTL8168H internal PHY) |
| DSN | 00:E0:4C:FF:FE:68:00:01 (EUI-64 from MAC, per-unit) |
| BARs | 0: I/O 256 B, 2: Mem64 4 KB, 4: Mem64 16 KB |
| MSI-X | 4 vectors, table BAR4+0x000, PBA BAR4+0x800 |
| Cap chain | PM, MSI, PCIe, MSI-X, VPD |
| Ext caps | AER, VC, DSN, LTR, L1SS |
Vivado 2023.2+ WebPACK. From the Tcl Shell:
source vivado_generate_project.tcl -notrace
source vivado_build.tcl -notraceProject generation creates the COE-backed BRAM/ROM IP (bram_bar_rtl8168,
drom_bar_writemask, bram_bar4_msix, and the cfgspace cores). Build takes
about an hour; flash with vivado_flash.tcl over the USB update port
(IS25LP128F). Per-unit MAC/DSN/subsystem customisation is in
BUILD.md.
src/
pcileech_bar_impl_rtl8168.sv BAR0/BAR2 register file + state-machine mux
pcileech_bar_impl_msix.sv BAR4 MSI-X table / PBA
rtl8168_phyar_emu.sv PHYAR, MDIO PHY register access
rtl8168_gphy_ocp_emu.sv GPHY_OCP, OCP PHY access (shared PHY file)
rtl8168_eriar_emu.sv ERIAR/ERIDR, ERI register access
rtl8168_chipcmd_emu.sv ChipCmd with RST self-clear
rtl8168_cfg9346_emu.sv Cfg9346 config lock/unlock
rtl8168_intr_emu.sv interrupt mask/status + MSI trigger
rtl8168_timer_emu.sv free-running timer
rtl8168_lfsr.sv 16-bit Galois LFSR for jitter
pcileech_*.sv upstream PCILeech (see Provenance)
ip/
pcileech_cfgspace*.coe 4 KB config space + writemask + W1C masks
pcileech_bar_rtl8168.coe BAR register reset values
pcileech_bar_writemask.coe BAR per-bit write mask
docs/REGISTERS.md per-register map (offset/reset/mask/protocol)
| Resource | Used | Available | % |
|---|---|---|---|
| LUT | ~12,500 | 47,200 | 26 |
| FF | ~9,500 | 94,400 | 10 |
| BRAM 36Kb | ~25 | 105 | 24 |
| GTP | 1 | 4 | 25 |
Approximate, post-implementation.
- MSI-X is exercised with a single vector; multi-vector delivery is untested.
- ASPM L1 is declared in LnkCap and left to the Xilinx PCIe core; the design does not manage L1 entry itself.
- The handshake jitter magnitudes are tuned for this board; another board may need different values.
MIT, (c) 2025 Advit Arora. See LICENSE.