
【1】 出世本来大家都对抗的好好的你突然用硬件酱味打击这不胡闹吗galgame里不是这样的你应该先和我在R3上一决高下最后再在R0打的难舍难分... 相信大家对DMA的工作原理早就有过充分了解所以也不再赘述非要说就是12345678配置空间Configuration SpaceCPU 发起 → Root Complex → PCIe TLP(Config) → 设备用途枚举设备、分配 BAR、读写能力寄存器特点请求者是 CPU不经过 IOMMU内存访问DMA设备发起 → PCIe TLP(MemRd/MemWr) → [IOMMU] → 内存控制器 → 物理内存用途设备读写系统内存特点请求者是设备IOMMU 启用时经其翻译 后面要考是的就这样使用DMA可以很轻松的做到任意物理内存读取写入更重要的是它不会留下痕迹。和很多的技术手段一样本来DMA是老老实实作研究用的可是君子无罪怀璧其罪。总而言之DMA刚出来的时候确实是完完全全的碾压了市面上所有的反作弊没有人使用DMA去对抗反作弊反作弊也就没有对应的需求自然而然更加的海阔天空第一批DMA使用者也就切切实实享受到了新世界。2016年左右PCILeech发布 DMA读写从未如此简单: DMA读写从此工程化1234567fpga固件 收发 PCIe TLP|leechcore 传输层|vmm/MemProcs 物理内存 → 进程视图、符号解析|上位机程序 读目标数据19-21年这个时间段就依旧开始出现咋混共作弊的DMA作弊硬件流入市场配套服务也非常好固件配套服务也开始出现逐渐成熟化了对抗也进入了下一阶段。【2】大众总所周知追求小众是一件非常大众的事情而在反作弊对抗这块可以说大众了就再也没有翻身的余地了技术的泛滥与不断迭代让旧技术狠狠的被碾压过去不幸的很显然DMA也没有逃过时代车轮的碾压就在某个时间节点DMA的用户迎来了大爆发有人赚的盆满钵满固件教学等等雨后春笋一般冒出,设备成本的降低和名色大噪又反向助推用户数量的增长于是肉眼可见的DMA被盯上了。另一边反作弊厂商可以说是亚历山大了于是正式打响对抗战争。反作弊厂商首先想到的对抗方法是什么呢既然DMA是设备那显然他们可以先遍历检查一下123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687以下代码为伪代码还原示例ANSI_STRING a; UNICODE_STRING u;charpath[] \\Driver\\pci;RtlInitAnsiString(a, path);RtlAnsiStringToUnicodeString(u, a, TRUE);g_pci ObReferenceObjectByName(u, ...);//遍历 PCI 总线驱动自己的设备链表//g_pci 0x200 是 LIST_ENTRY 头node0 Flinknode8 Blink//记录基址 elem node - 0x280//配置空间头就嵌在 elem 0x04for(node *(PVOID*)(g_pci 0x200); ; node *(PVOID*)node) {Word vendor *(Word *)(elem 0x04);Word device *(Word *)(elem 0x06);if(vendor ! 0x8086)continue;// 判断是不是Intelif(device 0x7D0B) {if(device 0x7D0B || device 0x201D || device 0x28C0 ||device 0x467F || device 0x4C3D)gotomatched;}else{if(device 0x9A0B || device 0xA77F || device 0xAD0B)gotomatched;}continue;if(!(flags 8))continue;先拿这块设备内核 PDO再找到分配的 MMIO 资源PDEVICE_OBJECT pdo *(PDEVICE_OBJECT *)(elem 0x108);ULONGlen 0;status IoGetDeviceProperty(pdo,0x15/* DevicePropertyAllocatedResources */,0, NULL, len);if(status ! 0xC0000023/* STATUS_BUFFER_TOO_SMALL */) bail;PVOIDbuf ExAllocatePoolWithTag(NonPagedPool, 0x200,pciR);IoGetDeviceProperty(pdo, 0x15, 0x200, buf, len);PCM_RESOURCE_LIST rl buf;if(rl-List[0].PartialResourceList.PartialDescriptors[0].Type! 3/* CmResourceTypeMemory */) bail;bar ...[0].u.Memory.Start;barLen ...[0].u.Memory.Length;//越过 VMD 桥本身直读它下游总线的配置空间窗口if(barLen 0x100000) {p MmMapIoSpace(bar 0x100000, 4, MmNonCached);if(*(ULONG*)p 0xFFFFFFFF) {//总线 1 上没东西MmUnmapIoSpace(p, 4);ExFreePoolWithTag(buf, 0);}else{//上有设备 → 把它的完整 256 字节配置空间读下来MmUnmapIoSpace(p, 4);if(barLen 0x100000 0x1100) {out 0;sub_14001BAF0(bar 0x100000, out, elem);}}if(barLen 0x200000) {q MmMapIoSpace(bar 0x200000, 4, MmNonCached);if(*(ULONG*)q 0xFFFFFFFF) { MmUnmapIoSpace(q, 4); }else{//同上 }}ExFreePoolWithTag(buf, 0);}sub_14001BAF0(bar 0x100000, out, elem);}voidsub_14001BAF0(PHYSICAL_ADDRESS addr,ULONG*out,UCHAR*elem){void*p MmMapIoSpace(addr, 0x1100, MmNonCached);// 0x1400d7338scratch ExAllocatePoolWithTag(0x200, 0x1100,pcfg);// 0x1400d743di 0x1000;while(((UCHAR*)p)[i] 0xFF) i;// 全 0xFF 无设备sub_140026280((UCHAR*)p 1, (UCHAR*)p 0x1001, 0xFF);// 0x1400d768a*out 1;for(i 0; i 0x100; i)// 展开 256 条 store8elem[0x158 i] ((UCHAR*)p)[0x1000 i];// → elem[0x158..0x257]MmUnmapIoSpace(p, 0x1100);}那么这几个case 的十六进制常量是什么呢 通过查看PCI ID Repository我们可以看到123450x201D 0x467F 0x4C3D 0x9A0B Volume Management Device NVMe RAID Controller;0x28C0 Volume Management Device (VMD)0x7D0B Core Ultra 200H/200V Series Processors VMD0xA77F RST Volume Management Device Controller0xAD0B Core Ultra 200 Series Processors VMD;那就会有人问了反作弊拿这个elem 0x158-257又是干什么呢难道里面有什么可以判断是不是DMA的信息吗。于是我们就查一查12345678910111213141516171819202122status sub_14001C9D0(...);// 填充记录含直读配置空间if(status 0)gotodone;node *(PVOID*)(P 0x200);if(node (PVOID*)(P 0x200))gotoempty;// 空链out user_buffer;out[0x00..0x02] elem[0x00..0x02];*(WORD*)(out 0x03) *(WORD*)(elem 0x04);// VendorID*(WORD*)(out 0x05) *(WORD*)(elem 0x06);// DeviceID*(U64 *)(out 0x07) ((U64)*(ULONG*)(elem0x120) 32)| (U64)*(ULONG*)(elem0x11C);*(ULONG*)(out 0x0F) *(ULONG*)(elem 0x130);*(U64 *)(out 0x13) *(U64 *)(elem 0x128);*(ULONG*)(out 0x33) *(ULONG*)(elem 0x14C);*(ULONG*)(out 0x37) *(ULONG*)(elem 0x150);*(ULONG*)(out 0x3B) *(ULONG*)(elem 0x154);*(U64 *)(out 0x3F) *(U64 *)(elem 0x158);// ← 直读配置空间七点/* 一直到 out0x27E */*(ULONG*)r14 2;sub_14001BD00(P 0x210, out 0x27E, ...);直接查资料看看他都收集了哪些东西吧忘了说了这么多还没贴出来资料123456789101112131415161718typedefstruct_PCI_COMMON_HEADER0x00 Vendor IDUSHORTVendorID PCI_VENDOR_ID 0x000x02 Device IDUSHORTDeviceID PCI_DEVICE_ID 0x020x04 Command16 位 PCI_ENABLE_IO_SPACE 0x0001 / PCI_ENABLE_MEMORY_SPACE 0x0002 / PCI_ENABLE_BUS_MASTER 0x00040x06 Status PCI_STATUS_CAPABILITIES_LIST 0x0010 等0x08 Revision IDUCHARRevisionID0x09 ProgIf编程接口UCHARProgIf PCI_CLASS_PROG 0x090x0A SubClassUCHARSubClass0x0B BaseClassUCHARBaseClass PCI_CLASS_DEVICE 0x0a16 位含 SubClass0x0E HeaderType PCI_HEADER_TYPE 0x0e0普通设备1PCI 桥2CardBus0x10–0x27 BAR0–BAR56×32 位ULONGBaseAddresses[PCI_TYPE0_ADDRESSES]PCI_TYPE0_ADDRESSES 为 60x28 CIS / CardBus 指针ULONGCIS0x2C Subsystem Vendor IDUSHORTSubVendorID0x2E Subsystem IDUSHORTSubSystemID0x30 Expansion ROM 基址ULONGROMBaseAddress0x34 Capabilities PointerUCHARCapabilitiesPtr PCI_CAPABILITY_LIST 0x340x3C Interrupt LineUCHARInterruptLine0x3D Interrupt PinUCHARInterruptPin只读12345670x00 Vendor / Device ID 身份DMA 卡常报成网卡/声卡0x04 Commandbit1 Command寄存器0x08–0x0B Revision / ProgIF / SubClass / BaseClass 你是什么设备0x10–0x27 BAR0–BAR5 MMIO0x2C Subsystem Vendor / Device0x34 Capabilities Pointer 能力链头0x3C/0x3D Interrupt Line / PIN 中断什么是MMIO?MMIO就是 Memory-Mapped I/O0x10–0x27 那 6 个 32 位 BAR 是设备声明自己需要一段地址空间的寄存器12345#define PCI_BASE_ADDRESS_0 0x10#define PCI_BASE_ADDRESS_5 0x24#define PCI_BASE_ADDRESS_SPACE 0x01 /* 0 内存空间, 1 I/O 空间 */#define PCI_BASE_ADDRESS_MEM_TYPE_64 0x04 /* 64 位地址 */#define PCI_BASE_ADDRESS_MEM_PREFETCH 0x08 /* 可预取 */系统给这段空间分配物理地址后把基址写回 BAR之后 CPU 访问这段物理地址就是访问设备的板载寄存器。为什么收集MMIO设备内部的寄存器里包含 DMA 引擎的描述符地址、长度、启动位、完成状态。这些寄存器通常映射在 BAR 里CPU 只能通过这段 MMIO 去操作 DMA 。为什么收集 Command 寄存器1234567891011#define PCI_ENABLE_IO_SPACE 0x0001#define PCI_ENABLE_MEMORY_SPACE 0x0002 // bit1#define PCI_ENABLE_BUS_MASTER 0x0004 // bit2#define PCI_ENABLE_SPECIAL_CYCLES 0x0008#define PCI_ENABLE_WRITE_AND_INVALIDATE 0x0010#define PCI_ENABLE_VGA_COMPATIBLE_PALETTE 0x0020#define PCI_ENABLE_PARITY 0x0040 // (ro)#define PCI_ENABLE_WAIT_CYCLE 0x0080 // (ro)#define PCI_ENABLE_SERR 0x0100 // (ro)#define PCI_ENABLE_FAST_BACK_TO_BACK 0x0200 // (ro)#define PCI_DISABLE_LEVEL_INTERRUPT 0x0400当 PCI_ENABLE_BUS_MASTER 1 时该 Function 可以主动向总线发起 Memory Read / Write Request即 DMA 操作。当 PCI_ENABLE_BUS_MASTER 0 时该 Function 不允许发起任何 DMA 请求只能被动响应 CPU 的访问。还可以在用户态继续一下1234567HDEVINFO h SetupDiGetClassDevs(guid, NULL, NULL, DIGCF_ALLCLASSES | DIGCF_PRESENT);for(DWORDi 0; SetupDiEnumDeviceInfo(h, i, di); i) {SetupDiGetDeviceRegistryProperty(h, di, SPDRP_HARDWAREID,NULL, buf,sizeofbuf, NULL);//读取Vendor ID对比一下黑名单id}这里也有一些被拉黑的数据收集数据建立数据库狠狠的拉黑这些板子使用者。123109E Brooktree Corporation Bt848 / Bt878 / Bt879 视频采集 pci-ids 109E1CD7 Nanjing Magewell Electronics美乐威 Pro Capture / Eco Capture 采集卡 pci-ids 1CD7BDBD Blackmagic Design DeckLink / Intensity / UltraStudio 采集卡 pci-ids BDBD 注意都是采集卡下面也要考校验方案:1234567891011121314151617181920212223242526272829303132333435//没服务for(inti 0; i total; i, elem 0x848) {if(*(U32 *)(elem 0x808) 0)//设备没有绑定任何驱动服务//上报}//有服务for(inti 0; i total; i, elem 0x848) {if(*(U32 *)(elem 0x808) 0)continue;// 无服务 → 归 dma_nosrv 管if(*(U32 *)(elem 0x812) ! 786)continue;// ← 第二个判据if(ven_match(o, hwid(elem), o-vendor_list))report_event(7990, 102, 9015);}//假服务:boolvm ven_match(o, hwid(elem), o-vendor_list);// 子串查找boolvs svc_match(o, svc(elem), o-service_list);// 字符串全等if(vm vs) report_event(7990, 102, 9019);boolven_match(Type134 *o,char*hwid, vectorstring *list) {if(list-empty() || !hwid)returnfalse;for(each v in *list) {string pat VEN_ v;if(sub_181737594(hwid, pat.c_str()))// ← 在 hwid 里找这个子串returntrue;}returnfalse;}if(*(U32 *)(elem 0x00) 0 *(U32 *)(elem 0x04) 0 *(U32 *)(elem 0x08) ! 0 // ← 这一项要求非零*(U32 *)(elem 0x10) 0 *(U32 *)(elem 0x14) ! 0 all_zero(elem 0x18, 256) // ← 连续 256 字节全零total 1)// ← 记录总数恰好为 1这里给出参考表也不再赘述了核心对抗就是你伪造的我就校验这也是为什么某些垃圾固件确实会拉闸。12345678elem0x808 U32 nosrv / wrongsrv 0 无驱动服务elem0x812 U32 wrongsrv 786 服务起不来硬件 ID 字符串 string fakesrv VEN_厂商 子串 厂商身份服务名字符串 string fakesrv 全等 3 个虚拟机名 伪造服务名elem0x00/04/08/10/14 5×U32 hide 零/非零交替 空壳形态elem0x18..0x117 256 B hide 全零 该填未填记录总数inthide 1 / 变化 藏得太干净两来源 ID U32×2 cfg 互相相等 读取链被改【3】现在难道说DMA不小众了我就不玩了吗但那是弱者的思维。哪怕是在VTD大清洗后还是有相当数量的八旗子弟仍旧抱着DMA四死死不放手这也能理解毕竟双机硬件搭配依旧能解决很多问题比如签名软件特征等等。DMA为了应对反作弊厂商的严肃进攻也是做出了努力比如他们尝试伪装固件把收集到的公开的固件刷到自己这里或者伪装自己的ID用于对抗反作弊的黑固件数据库甚至是自己实现一个协议去对抗反作弊的行为检测。不过反作弊也迎来了史上最强VTD vs 现代最强DMA123456780xe87f test al, al0xe881 je 0x14000e8900xe883 cmp byte ptr [0x14003027A], 0 ; g_securePciEnabled0xe88a jne 0x14000e912 ; Secure PCI 已启用 → 跳过整段0xe890 cmp dword ptr [rbp - 0x7c], 0x10ec ; 未启用 → 检查 vendor Realtek0xe897 jne 0x14000e9120xe8a5 lea rdx, [r13 0x10] ; 遍历设备条目步长 0x140xe8c5 cmp byte ptr [rcx], 3 ; 子类型 31234567/* g_securePciEnabled 由 VslGetSecurePciEnabled() 写入0x14000D586 → 0x140016F5C */if(secure_pci_enabled)return;//直接过if(vendor ! 0x10EC)return;/* 遍历设备条目按子类型继续查 */系统已经启用了 Secure PCI就不管至于为什么只检查Realtak就懂的都懂了。不过反作弊也利用起了大杀器VTD还记得前面说的IOMMU吗这里就考到了DMA要访问是必须经过IOMMU的。在没有VTD时候DMA是这样访问的1设备 ──DMA──▶ 物理地址 0x0000_0000 / 内核代码 / 其他进程 / 其他 VM而在打开了VTD之后就是如此访问的1234567891011121314151617181920212223242526PCIe 设备│ DMA 请求带 IOVA Requester ID / BDF▼VT-d DMA Remapping EngineRoot Complex 里│ 1. 用 BDF 查 Root Entry → Context Entry│ 2. 找到这个设备的 I/O 页表Domain│ 3. IOVA → HPA 翻译│ 4. 检查 R/W/EXEC 权限▼物理内存0x00 SignatureDMAR0x04 Length0x08 Revision0x09 Checksum0x0A OEMID 6 字节0x10 OEM Table ID 8 字节0x18 OEM Revision0x1C Creator ID 4 字节0x20 Creator Revision0x24 HAW0x25 Flags0x26 Reserved0x28 Remapping Structures (DRHD/RMRR/ATSR/...)PCIE设备只能看到IOVA(I/O VirtualAddress)而不是直接拿到真实的物理地址了此时此刻真实的物理地址由VTD硬件查表页决定。而如果他要读取就需要系统在IOMMU页表里建映射表如果没有映射的话就会计入DMAR fault看到这里你应该也能感觉到VTD的姜维打击了还是那句话如果身份互换我会让你知道我的痛苦可以说打开VTD几乎DMA就没有还手之力了。123456789call NtQuerySystemInformation ;class0x4C SystemFirmwareTableInformation; 返回一串 4 字节 ACPI 表签名loop:cmp ecx, 0x52414D44 ;DMARje → TRUEcmp ecx, 0x53525649 ;IVRSje → TRUEend:→ FALSE当然DMA玩家也并非全无办法比如想办法解析DMAR表篡改数据或者伪造设备已经打开了VTD。比如我可以构造一张假的DMAR表:12345678定位 RSDP → 找到 XSDT 物理地址解析 XSDT → 收集所有 ACPI 表项地址扫描物理内存找 4KB 对齐且空闲的区域与现有表至少隔 4KB在该区域构造一份合法的 DMAR 表含 DRHD/RMRR 等子结构DRHD 寄存器基址选 4GB 的空闲 PCIe MMIO 页把新 DMAR 的物理地址写入 XSDT 条目替换或追加修正校验和回读验证观察系统 IOMMU 行为变化关键代码 取自kEv1nZ0/VTD-Bypass: 全自动过VTD检测12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273定位 RSDP → XSDT# 扫描物理内存低 1MB 找 RSDPforpainrange(0x000E0000,0x00100000,16):buflc.read(pa,36)ifbuf[0:8]bRSD PTR :rsdpbufbreakxsdt_pastruct.unpack_from(Q, rsdp,24)[0]# XsdtAddress at offset 24找空闲4KB对齐区域deffind_free_4k(start, end, used_list):找和 used_list 中每个地址至少隔 4KB 的空洞forpainrange(start, end,0x1000):conflictFalseforusedinused_list:ifabs(pa-used) 0x1000:conflictTruebreakifnotconflict:returnpareturn0构造 DMAR 表defbuild_dmar(dmar_pa):dmarbytearray(48)# 表头 48 字节struct.pack_into(4s, dmar,0, bDMAR)struct.pack_into(I, dmar,4,48)dmar[8]1# Revision# OEMID / OEM Table ID / Creator ID 模仿原固件# Checksum 后面算returndmardefcalc_checksum(buf):return(-sum(buf)) 0xFFdmarbuild_dmar(dmar_pa)dmar[9]calc_checksum(dmar)DRHD 基址选 4GBMMIOdefpick_drhd_base():选 4GB 的空闲 PCIe MMIO 页forpainrange(0x100000000,0x10000000000,0x1000):ifnotis_mmio_mapped(pa)andnotis_used(pa):returnpareturn0写入新 DMAR 并挂进 XSDT# 写入 DMAR 表体lc.write(dmar_pa, bytes(dmar))# 替换 XSDT 中的 DMAR 条目fori, entryinenumerate(entries):iflc.read(entry,4)bDMAR:lc.write(xsdt_pa36i*8, struct.pack(Q, dmar_pa))break# 修正 XSDT 校验和xsdt_datalc.read(xsdt_pa, length)xsdt_data[9]calc_checksum(xsdt_data)lc.write(xsdt_pa, xsdt_data)最后再回读验证一下verifylc.read(dmar_pa,4)assertverifybDMAR,DMAR 写入失败verify_xsdtlc.read(xsdt_pa36i*8,8)assertstruct.unpack(Q, verify_xsdt)[0]dmar_pa,XSDT 条目更新失败当然大部分反作弊也会在用API判断完后检查DMAR表也可以利用固件元数据不一致来判断环境是否可信。123456789101112*(U32*)(dmar0x0B) 0x0B OEMID(0x0A,6) 之内*(U16*)(dmar0x0F) 0x0F OEMID 末 OEMTableID 首*(U32*)(dmar0x1D) 0x1D CreatorID(0x1C) 之内RtlCompareMemory(dmar0x0B, sdt0x0B, 6) 0x0B 起 6 B伪代码:PVOIDdmar FindAcpiTable(DMAR);PVOIDroot FindAcpiTable(XSDT);boolvt_d_seen (dmar ! NULL);boolsame_oem vt_d_seen RtlCompareMemory(dmar 0x0B,root 0x0B, 6) 0;