在折腾内核驱动加载的时候,不想花几百刀去买 EV 证书,又不想每次都开测试模式(TestSigning)。这年头,BYOVD (Bring Your Own Vulnerable Driver) 几乎是标准操作了。大家最熟悉的一般是 kdmapper 利用那个著名的 Intel 网卡驱动漏洞。

但其实 Cheat Engine (CE) 自带的那个 dbk64.sys 也是个狠角色。它本身是签了名的合法驱动,但为了给 CE 提供强大的内存修改功能,它暴露出来的 IOCTL 接口简直就是给黑客留的后门。

今天就来扒一扒 CECheater 这个项目,看看它是怎么“借”用 DBK 驱动,把我们自己的未签名驱动(Payload)强行塞进内核并跑起来的。

核心原理:它是怎么“偷渡”的?

简单来说,Windows 给了 dbk64.sys 一张合法的“门禁卡”(数字签名)。CECheater 不需要自己进门,而是把 Payload 递给已经进门的 DBK,让 DBK 在里面帮我们办事。

整个流程可以拆解为这几步:

  1. 加载工具人:先把正版 dbk64.sys 加载起来。
  2. 申请地皮:利用 DBK 的接口在内核里申请一块内存(RWX 权限)。
  3. **手动映射 (Manual Map)**:把我们的未签名驱动文件(.sys),像拼图一样手动在用户层解析好,填到刚才申请的内核内存里。
  4. 执行入口:再利用 DBK 的执行接口,跳转到我们驱动的 DriverEntry

下面结合源码细节通过去分析。

1. 搞定“工具人” (DBK 驱动加载)

dbk64.sys 比较矫情,它不像普通驱动直接 SC create 就能跑,它启动时会检查注册表里的一堆配置。

DBKControl.cpp 里可以看到,加载前必须手动伪造这些注册表项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 伪造服务注册表项
std::wstring subKey = Format(L"SYSTEM\\CurrentControlSet\\Services\\%ws", DBK_SERVICE_NAME);
// ...
RegSetValueEx(hKey, L"A", 0, REG_SZ, ...); // \Device\DBK64
RegSetValueEx(hKey, L"B", 0, REG_SZ, ...); // \DosDevices\DBK64
RegSetValueEx(hKey, L"C", 0, REG_SZ, ...); // \BaseNamedObjects\DBKProcList60
````

如果这些 A、B、C、D 键值不对,驱动就会加载失败。加载成功后,我们就能拿到 `\\.\DBK64` 的句柄,开始发号施令了。

## 2\. 内核借地 (Allocation)

有了句柄,第一件事是找内核要内存。这里用到了 IOCTL `IOCTL_CE_ALLOCATEMEM_NONPAGED` (0x0826)。

在 `IOCtlCode.h` 里可以看到 CE 暴露了多少危险功能:

```cpp
#define IOCTL_CE_READMEMORY CTL_CODE(..., 0x0800, ...)
#define IOCTL_CE_WRITEMEMORY CTL_CODE(..., 0x0801, ...)
#define IOCTL_CE_EXECUTE_CODE CTL_CODE(..., 0x083c, ...) // 重点是这个
#define IOCTL_CE_ALLOCATEMEM_NONPAGED CTL_CODE(..., 0x0826, ...)

通过 DBK_AllocNonPagedMem 函数,我们在内核非分页池(NonPagedPool)中拿到了一块带有执行权限的内存地址 pKernelImage。这就是我们 Payload 的新家。

3. 手动映射 (The Hard Part)

这一步是整个项目的核心。因为我们不是通过系统加载器(OS Loader)加载驱动,所以 PE 文件里的所有地址都是错的,导入表也是空的。我们必须手动干系统加载器的活。

代码主要在 MemLoadDriver.cppDBK_LoadMyDriver 函数中。

修复重定位 (Fix Relocation)

驱动编译时的基址(ImageBase)和我们在内核申请到的地址肯定不一样。
FixRelocation 函数会遍历 PE 的重定位表,计算出偏移量 delta,然后把代码里所有硬编码的绝对地址都修正过来。

1
2
3
4
// 计算偏移
ULONG_PTR llDelta = (ULONG_PTR)pBaseAddress - pImageNtHeaders->OptionalHeader.ImageBase;
// 遍历 Block 修改地址...
*(PULONG_PTR)CONVERT_RVA(...) += llDelta;

修复导入表 (Fix Imports)

我们的驱动可能会调用 ntoskrnl.exe 里的 DbgPrint 或者 PsCreateSystemThread。系统加载时会自动填好这些函数地址,但现在必须手动填。

FixImports 函数通过 GetDriverAddress(获取内核模块基址)和 GetDriverExportFuncByName(解析导出表)来找到这些函数的真实内核地址,并填入我们驱动的 IAT 表中。

这一步做完,把处理好的 PE 镜像通过 DBK_WriteProcessMem 写入刚才申请的内核内存中,驱动就“就位”了。

4. 激活 (Execution)

最后一步是让 CPU 跳过去执行我们的 DriverEntry

这里有个很有意思的设计。MemLoadDriver.cpp 里准备了两段 Shellcode:

  1. **shellcode_CallIoCreateDriver**:这段 Shellcode 会模拟调用 IoCreateDriver。这会让我们的未签名驱动在内核对象管理器里注册一个合法的 DriverObject。这对于需要处理 IOCTL 的复杂驱动来说是必须的。
  2. **shellcode_JmpDriverEntry**:简单粗暴,直接 jmp 到入口点。适合那种跑完就退的测试驱动。

最后通过 DBK_ExecuteCode 发送 IOCTL,DBK 驱动会在内核态执行这段 Shellcode,从而拉起我们的 Payload。

1
2
3
4
// 构造 Shellcode 并让 DBK 执行
if (!DBK_ExecuteCode((UINT64)pKernelShellcode)) {
LOG("DBK_ExecuteCode failed");
}

实战踩坑记录 (重点!)

原理听起来简单,但实操起来全是坑。如果直接写个驱动扔进去,99% 会蓝屏或者没反应。以下是调试时遇到的几个关键点:

1. 必须禁用缓冲区安全检查 (/GS-)

这是最容易被忽视的。VS 默认开启 /GS,编译器会在函数入口插入 __security_init_cookie
但是!手动映射的驱动没人帮你初始化这个 Cookie。驱动一跑,Cookie 检查失败,直接自爆。
解决:CMakeLists.txt 或 VS 属性里,务必加上 /GS-

2. 入口点必须是 DriverEntry

如果你用 KMDF 框架,入口点其实是 FxDriverEntry;如果你开了 /GS,入口点是 GsDriverEntry。这俩都需要环境支持,手动映射搞不定。
解决: 强制链接器选项 /ENTRY:DriverEntry,把那些初始化壳子全扒掉。

3. “僵尸驱动” 问题

如果你用 IoCreateDriver 方式加载,驱动会注册一个设备名(如 \Device\MyDriver)。如果你调试时驱动崩了或者没卸载干净,第二次加载时 IoCreateDriver 会因为名字冲突直接返回失败,导致你以为代码没执行,其实是入口都没进去。
解决: 调试阶段改个驱动名再加载,或者确保卸载干净。

4. 虚假的 Win11 兼容性

虽然项目声称支持 Win11,但如果开了 **HVCI (内核隔离)**,DBK 申请 RWX 内存的操作可能会失败,或者执行时被拦截。要在 Win11 上顺畅玩耍,最好先关掉内核隔离,或者在编译时加上 /guard:cf- 禁用控制流保护。

总结

CECheater 本质上就是把 Cheat Engine 当作了一个通过合法签名的“加载器”。相比于利用漏洞(Exploit)的 kdmapper,这种方式利用的是 Feature,相对稳定一些,但反作弊(EAC/BE)对 dbk64.sys 的监控也更严。

对于驱动开发学习者来说,研究这个项目的代码(特别是 PE 解析和 Shellcode 构造部分)比单纯跑通一个 Demo 要有价值得多。


本文代码参考自 CECheater 项目源码及调试日志。仅供安全研究,请勿用于非法用途。