插上DMA板子,再攀作弊技术的一大高峰

插上DMA板子,再攀作弊技术的一大高峰

TangSong404 Lv3

引子:所谓的“插板子”究竟是什么

上一篇外挂博客的研究已经不能满足我的好奇心了,于是我终于走上了这最后一步!
image.png
通过外置的FPGA设备直接读取主机内存的作弊方式被称之为DMA外挂,也就是俗称的“插板子”。
板子是通过可以开发的固件实现功能的,但是想拥有一个好的固件,难的不是内存的读取,而是如何绕过游戏的反作弊。
一般的DMA板子都是通过PCIE接口读取主机内存的,然而一个未知的可读取内存的设备存在于计算机是很可疑的,所以我们要将其仿真为其他拥有PICE接口的正常设备来合理化其存在意义。如网卡,打印机,显卡,声卡等。
用于采集信息的设备中网卡最为便宜,且对于网卡仿真的FPGA项目早已经开源(也因此稳定性不高,最容易被检测),所以我们将以网卡固件为例进行讲解。

准备工作

硬件

一块CaptainDma75t板子(某鱼大约300)
8111网卡(大约10,多买几张防封备用)
副机(某鱼上有专卖的,大约500,用个人计算机也可以,最好有usb3.0接口)

官方软件:

TeleScan(用于采集网卡信息)
Vivado(用于开发FPGA)

小工具

Telescan2coeGui(前往https://github.com/un4ckn0wl3z/TeleScan2coeGui 仓库获取)
DMA-Flash-Tools(前往https://github.com/kilmu1337/DMA-Flash-Tools 仓库获取)

采集与配置

设备信息采集

插上8111网卡,注意一定不要热插拔!打开TeleScan软件进行扫描
选中扫描出的网卡,看到有6个Base Address寄存器
 2025-05-16 014526.png
选中每一个不全是0的寄存器,将其的值修改为最大值(右侧填满1)
 2025-05-16 014801.png
然后右键-> write-> DWORD at offset
 2025-05-16 015244.png
完成这一步后将其保存为.tlscan文件

配置流程

获取https://github.com/ekknod/pcileech-wifi/ 项目,我们总共有4个文件需要修改
1.pcileech_cfgspace
将你采集到的.tlscan文件拖入到Telescan2coeGui程序中
你将会得到一个output.coe,重命名为pcileech_cfgspace.coe
复制到项目下ip文件夹内,替换原有的文件即完成了这一步骤
2.pcileech_fifo
打开vivado,注意到底下的Tcl Console, cd到你项目的目录并运行source vivado_generate_project_captain_75T.tcl
打开项目后在vivado中找到pcileech_fifo.sv文件
查找rw[203]关键字,确保其值为0,并确保rw[206]值为1
3.pcileech_pice_cfg_a7
打开你之前保存的.tlscan文件,找到Device Serial Number Capability这一项
image.png
找到PCI Express Device Serial Number ,这个分为1st DW与 2nd DW,将2nd的值放在前1st的值放在后,获取到一个16位的编码
在vivado中找到pcileech_pice_cfg_a7.sv文件
查找关键字rw[127:64],将其值修改为上面得到的16位编码,这其实就是设备的DSN
同时还要确保rw[20]为1以及rw[21]为0
4.pcie_7x_0_core_top
在vivado中找到pcie_7x_0_core_top.v文件
这个文件有很多的parameter,要修改的地方非常多,总体而言就是仔细对照.tlscan,凡是其中涉及到的每一项都要修改

比如说最基础的CFG_VEND_ID,CFG_SUBSYS_ID,CLASS_CODE等参数一看便与teleScan中一一对应
image.png

比如说AER_BASE_PTR参数,主要观察AER,其实就对应了teleScan中Advanced Error Reporting这一项的缩写,BASE_PTR也就是括号内的offset的值
而AER_CAP_NEXTPTR参数,指的就是teleScan中Advanced Error Reporting下一项所对应的offset了
如果该项点开发现本身有Next_Item_Ptr属性,则NEXTPTR填入该值而非下一项所对应的offset
如果该项为最后一个,那么NEXTPTR应该填入000
image.png

又比如说BAR0到BAR5其实又对应了之前看到的Base Address Register的值,这里一一填入。注意如果某个BAR以04或0C结尾,下一个BAR应该全部填入F.
像我这样的初级教程,我们不做IO,那么要另外注意BAR0应该以00结尾

烧录与测速

固件生成与烧录

1.完成以上采集与配置后点击vivado中的Generate Bitstream,直接点OK耐心等待生成
2.前往项目中pcileech_enigma_x1\pcileech_enigma_x1.runs\impl_1目录,找到pcileech_enigma_x1_top.bin,这就是我们获取的固件了
3.将CaptainDma75t板子通过PCIE接口插入主机,并通过usb连接板子的UPDATE_PORT口(靠近金手指的一段)
4.将固件粘贴入DMA-Flash-Tools目录下的Flash Tools子目录,运行#####flash_ch347_75t#####.bat即可完成烧录。如果每个sector花费0-1ms这是不正常的,请重新运行.bat文件直到ms数在几百左右;如果卡在sector 33之后不动,这是正常的,请耐心等待直到出现success
5.如果烧录失败可能需要安装驱动CH341PAR,也在DMA-Flash-Tools目录下

副机与测速

DMA外挂存在的主要目的就是悄咪咪地读取内存而不被发现,所以如果我们通过其读取内存的程序运行在主机上,那前面都白忙活了
这就是副机——另一台用于运行代码读取主机内存的计算机存在的意义。
用副机测速是实战前非常重要的部分,用于检测板子是否可以正常、高效地使用

简单叙述一下驱动安装与测速过程:DMA板子通过DATA_PORT口连接副机,安装FTDI SuperSpeed-FIFO Bridge驱动,安装测速的系统运行库,运行下载的DMATest.exe完成测速

具体实现请看这一大佬的文章 https://flowus.cn/ddma/share/9d0bae1a-373b-4e43-94a7-9043d3519b72 ,其中还包括了下载链接与常见问题,非常方便

简易的实战

注意:代码运行在副机上
我使用了memprocfs的python api用于读取内存,这里介绍一下用例

1
2
3
vmm = memprocfs.Vmm(['-device', 'fpga']) # 通过板子连接主机
process = vmm.process('cs2.exe') # 获取进程
base_addr = next((m.base for m in process.module_list() if 'client.dll' in m.name.lower()), 0) # 获取dll地址
1
2
3
4
# 某个我封装好的用于读取指针数据的函数
def read_ptr(process, address):
pre = process.memory.read(address, 8) # 最重要的内存读取函数
return struct.unpack('<Q', pre)[0] if pre else 0
1
2
# memprocfs.FLAG_NOCACHE决定了你获取的是实时数据还是缓存数据,假如你感觉一段时间内多次读取的数据一模一样,可能就是没有这个参数
process.memory.read(entity["gsn"] + offsets["m_angAbsRotation"] + 4, 4, memprocfs.FLAG_NOCACHE)

下面实现了一个简易的cs2雷达外挂,可以实时获取人物在地图中的各种信息,并搭建了一个websocket服务器用于传输数据到渲染的机子上
有关offsets这里就不直接提供了,否则就是传播外挂了,有心者可以前往 https://github.com/IMXNOOBX/cs2-external-esp 找到相关脚本,不过要稍作修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import memprocfs, struct, os, json, threading, asyncio, websockets

points = []
points_lock = threading.Lock()

websocket_clients = set()

def read_offsets(filepath="/offsets/offsets.json"):
filepath = os.path.dirname(__file__) + filepath
if not os.path.exists(filepath):
raise FileNotFoundError(f"file not found: {filepath}")
with open(filepath, "r", encoding="utf-8") as file:
return json.load(file)

def read_ptr(process, address):
pre = process.memory.read(address, 8)
return struct.unpack('<Q', pre)[0] if pre else 0

def read_str(process, address):
raw = process.memory.read(address, 256)
return raw.split(b'\x00', 1)[0].decode('utf-8', errors='ignore')

def get_entity_arr(process, el_addr, lc_addr, offsets):
my_team = process.memory.read(lc_addr + offsets["m_iTeamNum"], 4)
my_name_addr = read_ptr(process, lc_addr + offsets["m_sSanitizedPlayerName"])
my_name = read_str(process, my_name_addr)

entity_array = []
for index in range(1, 64):
entry_addr = read_ptr(process, el_addr + (8 * (index & 0x7FFF) >> 9) + 16)
entity_addr = read_ptr(process, entry_addr + 120 * (index & 0x1FF))
if not entity_addr: continue

team = process.memory.read(entity_addr + offsets["m_iTeamNum"], 4)
if team != b'' and team != b'\x00\x00\x00\x00':
pawn_addr = struct.unpack('<I', process.memory.read(entity_addr + offsets["m_hPlayerPawn"], 4))[0]
entry2_addr = read_ptr(process, el_addr + 0x8 * ((pawn_addr & 0x7FFF) >> 9) + 16)
entity2_addr = read_ptr(process, entry2_addr + 120 * (pawn_addr & 0x1FF))
gsn_addr = read_ptr(process, entity2_addr + offsets["m_pGameSceneNode"])

weapon_addr = read_ptr(process, entity2_addr + offsets["m_pClippingWeapon"])

name_addr = read_ptr(process, entity_addr + offsets["m_sSanitizedPlayerName"])
name = read_str(process, name_addr)

who = 0
if team != my_team: who = 1

entity_array.append({
"entity": entity2_addr,
"gsn": gsn_addr,
"who": who,
"name": name,
"weapon": weapon_addr
})
return entity_array


def cheat_init(offsets):
vmm = memprocfs.Vmm(['-device', 'fpga'])
process = vmm.process('cs2.exe')
base_addr = next((m.base for m in process.module_list() if 'client.dll' in m.name.lower()), 0)
if not base_addr:
raise RuntimeError("client.dll base address not found!")

print(f"--base address {base_addr} get successfully!")
lc_addr = read_ptr(process, base_addr + offsets["dwLocalPlayerController"])
print(f"--local controller address {lc_addr} get successfully!")
el_addr = read_ptr(process, base_addr + offsets["dwEntityList"])
print(f"--global entity list address {el_addr} get successfully!")

entity_array = get_entity_arr(process, el_addr, lc_addr, offsets)
print("--entity address array get successfully!")

return process, entity_array


def cheat_updater(process, entity_array, offsets):
global points
while True:
temp_points = []
for entity in entity_array:
try:
health = struct.unpack('<i', process.memory.read(entity["entity"] + offsets["m_iHealth"], 4))[0]
x, y, _ = struct.unpack('<fff', process.memory.read(entity["entity"] + offsets["m_vOldOrigin"], 12, memprocfs.FLAG_NOCACHE))
yaw = struct.unpack('<f', process.memory.read(entity["gsn"] + offsets["m_angAbsRotation"] + 4, 4, memprocfs.FLAG_NOCACHE))[0]
weapon = "unknown"
weapon_addr = read_ptr(process, entity["entity"] + offsets["m_pClippingWeapon"])
if weapon_addr!=0:
first = read_ptr(process,weapon_addr + 0x10)
if first != 0:
second = read_ptr(process,first + 0x20)
if second!=0:
weapon = read_str(process, second)
if weapon.startswith("weapon_"):weapon = weapon[7:]
if health != 0:temp_points.append(( x, y , entity["who"], yaw ,entity["name"], health, weapon))
except:continue
with points_lock:
points = temp_points

async def websocket_handler(websocket, path):
websocket_clients.add(websocket)
try:
await websocket.wait_closed()
finally:
websocket_clients.remove(websocket)

async def websocket_broadcaster():
while True:
await asyncio.sleep(0.1)
with points_lock:
data = json.dumps(points)
for ws in websocket_clients.copy():
try:
await ws.send(data)
except:
websocket_clients.remove(ws)

def start_websocket_server():
asyncio.run(websocket_main())

async def websocket_main():
print("-- WebSocket server started at ws://0.0.0.0:8765")
async with websockets.serve(websocket_handler, "0.0.0.0", 8765):
await websocket_broadcaster()

if __name__ == "__main__":
offsets = read_offsets()
process, entity_array = cheat_init(offsets)

update_thread = threading.Thread(
target=cheat_updater, args=(process, entity_array, offsets), daemon=True
)
update_thread.start()

print("--cheat server running... press Ctrl+C to stop")

try:
asyncio.run(websocket_main())
except KeyboardInterrupt:
print("--server stopped")

后话

这是目前最简单的DMA外挂开发流程,并且我亲自测试过,对于国内某cs平台,毫不掩饰地使用透视,经历一周时间才封禁了我的账号。
但是这也是为了学习使用,开挂并不能给人带来真正的进步,本人非常不建议仅仅为了在游戏中追求快感而使用外挂,这对其他玩家是非常不公平的。
DMA也就玩到这了,之后再怎么升级也无非是固件开发与仿真技术的升级了,等以后有机会再研究吧。

  • Title: 插上DMA板子,再攀作弊技术的一大高峰
  • Author: TangSong404
  • Created at : 2025-05-15 00:00:00
  • Updated at : 2025-05-15 20:28:09
  • Link: https://www.tangsong404.top/2025/05/15/fun/dma_cheat/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments