最近为了过某游戏的 EAC,折腾了一下 Cheat Engine 的内核驱动(DBK64)。目标很明确:不要签名,不要在设备树里创建显眼的 DBK64 设备,直接用 kdmapper 手动映射进内核

但这玩意儿坑是真的多,从 LNK 链接错误,到 CE 连不上驱动,再到 DBVM 死活加载不起来。这篇博客记录一下完整的修改和填坑过程。

为什么要这么折腾?

原版 CE 驱动有几个死穴:

  1. 签名问题:需要买 EV 证书,或者开测试模式(Test Signing),后者反作弊一抓一个准。
  2. 设备名\Device\DBK64 这个名字太招摇了,反作弊扫描设备树必死。
  3. 通信方式:标准的 DeviceIoControl 发送的 IOCTL 码特征太明显。

我的方案是:利用 kdmapper 手动映射驱动 + 劫持系统 Null 设备的 FastIO 进行隐蔽通信。


驱动层改造 (Driver)

原版驱动依赖 IoCreateDevice 创建设备,我们得把它扬了,改为“寄生”在 \Device\Null 上。

1. 实现 FastIO Hook (utils.c)

这是核心通信逻辑。为了隐蔽,采用了一种 Wrapper 协议:CE 客户端发送 IOCTL = 0,把真实的指令和数据打包在 InputBuffer 里。驱动拦截到 IOCTL=0 后再解包执行。

src 下新建 utils.c(记得在 templates/CMakeLists.txt.tpl 里加上它):

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
// utils.c 核心逻辑截取
BOOL MyDeviceIoControl(...) {
// 只拦截 IOCTL = 0 的请求,其他的放行给原驱动,防止系统异常
if (IoControlCode != 0 || InputBuffer == NULL) {
return FALSE;
}

// 解析 Wrapper 结构体
__try {
if (InputBufferLength < sizeof(CommInfo)) return FALSE;
PCommInfo pInfo = (PCommInfo)InputBuffer;
realControlCode = pInfo->ControlCode; // 取出真实 IOCTL
realInputBuffer = pInfo->inputBuffer; // 取出真实数据
} __except (1) { return FALSE; }

// 伪造 IRP 调用原始 DBK 处理函数
IRP FakeIRP = { 0 };
FakeIRP.Flags = realControlCode; // DBK 会从这里读控制码
FakeIRP.AssociatedIrp.SystemBuffer = realInputBuffer;

// ... 调用 DispatchIoctl ...
return TRUE;
}

// 替换 Null 驱动的 FastIO 指针
NTSTATUS Hook_Setting_Device_FastIoDeviceControl() {
// ... 获取 \Device\Null 对象 ...
InterlockedExchangePointer(
(PVOID*)&pDriver->FastIoDispatch->FastIoDeviceControl,
(PVOID)MyDeviceIoControl
);
return STATUS_SUCCESS;
}
````

### 2\. 重写 `DriverEntry` (`DBKDrvr.c`)

kdmapper 加载的驱动没有合法的 `DriverObject`,也没有注册表路径。所以:

1. **删除**所有注册表读取代码。
2. **删除** `IoCreateDevice`。
3. **关键修正**:设置 `loadedbydbvm = FALSE` 但 `IsManualMap = TRUE`。
* *坑点*:之前我为了让通信跑通强制设了 `loadedbydbvm = TRUE`,结果导致驱动以为 DBVM 已启动,在那瞎执行 VMCALL 指令,直接导致后续加载 DBVM 失败。必须解耦这两个标志位。

```c
NTSTATUS DriverEntry(...) {
// 初始化全局变量
IsManualMap = TRUE; // 告诉 Dispatcher 用 FakeIRP 模式解析
loadedbydbvm = FALSE; // DBVM 还没启动,别乱动 VMX 指令!
DBVM_Active = FALSE;

// ... 初始化调试器等 ...

// 挂钩子
Hook_Setting_Device_FastIoDeviceControl();

return STATUS_SUCCESS;
}

3. 处理 Process Watcher 报错

CE 启动时会尝试注册进程监视回调。但这在 Unbacked Memory (手动映射的内存) 里是不允许的,Windows 会直接拒绝访问。
解决方法:在 IOPLDispatcher.c 里直接“欺骗” CE。

1
2
3
4
5
6
7
case IOCTL_CE_STARTPROCESSWATCH:
if (IsManualMap) {
// 假装成功,实际上啥也没干,防止 CE 报错弹窗
ntStatus = STATUS_SUCCESS;
break;
}
// ... 原有逻辑

构建系统填坑 (CMake)

这个项目用 npmCMake,有些编译选项必须改,否则 kdmapper 加载直接蓝屏。

修改 templates/CMakeLists.txt.tpl

  1. 禁用安全检查/GS- (关掉缓冲区检查),/GUARD:NO (关掉 CFG)。手动映射的驱动没法处理这些 Cookie。
  2. 强制入口点/ENTRY:DriverEntry。如果不加,WDK 会默认用 KMDF 的入口,导致崩盘。
  3. 移除完整性检查/INTEGRITYCHECK:NO (虽然只是个标记,但在链接器参数里要处理好)。

客户端改造 (Cheat Engine Pascal源码)

驱动改好了,CE 客户端也得改,不然它还是傻乎乎地去连 \\.\DBK64

1. 修改通信协议 (DBK32functions.pas)

DeviceIoControl 函数魔改掉:

  1. **目标改为 \\.\Nul**:每次调用现开现关。
  2. 打包数据:把 IOCTL 和数据指针塞进结构体。
  3. 发送 IOCTL=0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type
TCommInfo = record
ControlCode: DWORD;
Padding: DWORD;
InputBuffer: Pointer;
end;

function DeviceIoControl(...): BOOL;
begin
// 打开 Nul 设备
RealHandle := CreateFileW('\\.\Nul', ...);

// 包装
Wrapper.ControlCode := dwIoControlCode;
Wrapper.InputBuffer := lpInBuffer;

// 发送 IOCTL 0
Result := Windows.DeviceIoControl(RealHandle, 0, @Wrapper, SizeOf(Wrapper), ...);
end;

2. 修复 DBVM 加载失败的问题

这是最搞心态的一步。驱动加载了,通信通了,但点击“加载 DBVM”时报错:

“I don’t know what you did… you didn’t load DBVM”

查了半天 DebugView,发现驱动报错 c0000034 (Object Name Not Found)
原因:我在精简 DBK32Initialize 代码时,手快把获取当前路径的代码删了!导致驱动去根目录下找 vmdisk.img

修复:在 DBK32Initialize 里把路径获取逻辑加回去,并强制跳过驱动加载检查,直接认为成功。

1
2
3
4
// DBK32Initialize 中必须保留:
getmem(apppath, 510);
GetModuleFileNameW(0, apppath, 250);
applicationpath := extractfilepath(apppath); // 驱动需要这个路径来找 vmdisk.img

调试与验证

为了看清驱动到底在干嘛,不仅要用 DebugView,还得改一下注册表开启内核日志打印:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter -> DEFAULT = 0xF

还要在驱动头文件里把 DbgPrint 宏注释掉,不然 Release 编译就把日志优化没了。

src\DBKFunc.h

1
2
3
#ifdef RELEASE
#define DbgPrint(...)
#endif

最终效果

  1. 运行 kdmapper,驱动加载成功。
  2. 打开修改版 CE,左上角出现熟悉的 **”DBK64 LOADED”**。
  3. DebugView 查看日志。
  4. 点击加载 DBVM,日志显示 vmxoffload 成功,CE 显示当前由于 DBVM 正在运行。

总结

这套方案通过寄生系统驱动伪装通信协议,实现了极高的隐蔽性。虽然过程折腾,但不仅学到了 kdmapper 的原理,还把 CE 的通信层摸透了。

最后提醒:调试完记得 bcdedit /set testsigning offbcdedit /debug off,不然反作弊系统会因为系统处于调试模式而踢人。