本文翻译自 How Kernel Anti-Cheats Work: A Deep Dive into Modern Game Protection,原载于 Hacker News。
引言
毫不夸张地说,现代内核级反作弊系统是运行在消费级 Windows 设备上最复杂的软件之一。它们运行在软件可用的最高特权级别,拦截本为合法安全产品设计的内核回调,扫描大多数程序员整个职业生涯都未曾触及的内存结构——而且这一切都在游戏运行时透明地进行。
如果你曾经好奇 BattlEye 究竟如何检测到外挂,或者为什么 Vanguard 坚持要在 Windows 启动前加载,又或者 PCIe DMA 设备绕过所有这些保护意味着什么——这篇文章就是为你准备的。
为什么用户态保护不够用?
用户态反作弊的根本问题在于信任模型。用户态进程运行在 ring 3,完全受制于内核的权威。任何完全在用户态实现的保护都可以被任何运行在更高特权级别的东西绕过——在 Windows 中,这意味着 ring 0(内核驱动)或更低级别(hypervisor、固件)。
一个用户态反作弊如果调用 ReadProcessMemory 来检查游戏内存完整性,可以被一个 hook 了 NtReadVirtualMemory 并返回虚假数据的内核驱动击败。一个通过 EnumProcessModules 枚举已加载模块的用户态反作弊,可以被一个修补 PEB 模块列表的驱动击败。用户态进程对发生在其上层的操作完全视而不见。
外挂开发者早在大多数反作弊工程师愿意采取行动之前就理解了这一点。很长一段时间里,内核是外挂的专属领地。内核模式外挂可以直接操作游戏内存,无需经过任何用户态反作弊可以拦截的 API。它们可以轻松地从用户态枚举 API 中隐藏自己的存在。它们可以拦截并伪造用户态反作弊可能执行的任何检查的结果。
反制措施是不可避免的:将反作弊移入内核。
军备竞赛的演进
这种升级是无情的。用户态外挂让位于内核外挂。内核反作弊作为回应出现。外挂开发者开始利用具有漏洞的合法签名驱动程序来实现内核执行,而无需加载未签名驱动(即 BYOVD 攻击)。反作弊以阻止列表和更严格的驱动枚举作为回应。外挂开发者转向 hypervisor,在内核之下运行并虚拟化整个操作系统。反作弊添加了 hypervisor 检测。外挂开发者开始使用 PCIe DMA 设备通过硬件直接读取游戏内存,完全不触及操作系统。对此的回应仍在开发中。
每一次升级都要求攻击方投入更多的资金和专业知识,这产生了一个重要效果:过滤掉休闲作弊者。一个 30 美元的内核外挂订阅对许多人来说是可及的。一个定制的 FPGA DMA 设置需要数百美元,并且需要重要的技术知识来配置。军备竞赛虽然让反作弊工程师感到沮丧,但确实服务于实际目标:使作弊变得足够昂贵和困难,以至于大多数作弊者不会费心。
主流反作弊系统
四大系统主导着竞技游戏领域:
-
BattlEye:被 PUBG、彩虹六号:围攻、DayZ、Arma 和数十款其他游戏使用。其内核组件是
BEDaisy.sys,一直是详细公开逆向工程工作的对象,最著名的是 secret.club 研究人员和 back.engineering 博客的工作。 -
EasyAntiCheat (EAC):现归 Epic Games 所有,用于堡垒之夜、Apex 英雄、Rust 等众多游戏。其架构与 BattlEye 的三层设计大致相似,但在实现细节上有显著差异。
-
Vanguard:Riot Games 的专有反作弊系统,用于瓦罗兰特和英雄联盟。它的特点是在系统启动时加载其内核组件(
vgk.sys)而不是在游戏启动时,并且对驱动程序白名单采取激进立场。 -
FACEIT AC:用于 Counter-Strike 的 FACEIT 竞技平台。它是一个内核级系统,在竞技社区中以有效的外挂检测而享有盛誉,并且一直是学术分析的主题,研究了内核反作弊软件的架构特性。
内核反作弊的三层架构
现代内核反作弊普遍遵循三层架构:
-
内核驱动:运行在 ring 0。注册回调、拦截系统调用、扫描内存、强制执行保护。这是实际上有权力做任何有意义事情的组件。
-
用户态服务:作为 Windows 服务运行,通常具有
SYSTEM权限。通过 IOCTL 与内核驱动通信。处理与后端服务器的网络通信、管理封禁执行、收集和传输遥测数据。 -
游戏注入 DLL:注入到(或由)游戏进程加载。执行用户态检查、与服务通信,并作为专门应用于游戏进程的保护端点。
这种关注点分离既是架构性的也是安全性的。内核驱动可以做任何用户态组件无法做到的事情,但它不能轻松建立网络连接或实现复杂的应用逻辑。服务可以做这些事情但不能直接拦截系统调用。游戏内 DLL 可以直接访问游戏状态,但运行在不可信的 ring 3 上下文中。
通信通道
-
IOCTL(I/O 控制码)是用户态和内核驱动之间的主要通信机制。用户态进程打开驱动设备对象的句柄并使用控制码调用
DeviceIoControl。驱动在其IRP_MJ_DEVICE_CONTROL分发例程中处理此请求。 -
命名管道用于服务与游戏注入 DLL 之间的 IPC。命名管道比通过内核路由所有内容更快更简单,并允许服务向游戏组件推送通知而无需轮询。
-
共享内存段使用
NtCreateSection创建并通过NtMapViewOfSection映射到服务进程和游戏进程中,允许高带宽、低延迟的数据共享。遥测数据(输入事件、时序数据)可以由游戏 DLL 写入共享环形缓冲区,由服务读取而无需每个事件的开销。
启动时 vs 运行时驱动加载
启动时和运行时驱动加载之间的区别比看起来更重要。
BattlEye 和 EAC 在游戏启动时加载其内核驱动。BEDaisy.sys 及其 EAC 等效组件被注册为按需启动驱动,并通过 ZwLoadDriver 从服务在游戏启动时加载。它们在游戏退出时卸载。
Vanguard 在系统启动时加载 vgk.sys。驱动被配置为启动启动驱动(注册表中的 SERVICE_BOOT_START),意味着 Windows 内核在系统大部分初始化之前加载它。这给了 Vanguard 一个关键优势:它可以观察其后加载的每个驱动程序。任何在 vgk.sys 之后加载的驱动都可以在其代码以有意义的方式运行之前被检查。在正常驱动初始化阶段加载的外挂驱动是加载到一个 Vanguard 已经监控的系统中。
启动时加载的实际意义也是为什么 Vanguard 需要系统重启才能启用:驱动必须在系统其余部分初始化之前就位,这意味着它不能在事后加载而不重启。
内核回调与监控
这是内核反作弊所做一切的基础。Windows 内核暴露了一套丰富的回调注册 API,本意是为安全产品设计的,反作弊使用了每一个。
ObRegisterCallbacks
ObRegisterCallbacks 也许是进程保护最重要的 API。它允许驱动注册一个回调,当打开或复制指定对象类型的句柄时被调用。对于反作弊目的,感兴趣的对象类型是 PsProcessType 和 PsThreadType。
当外挂调用 OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, gameProcessId) 时,反作弊的 ObRegisterCallbacks 预操作回调会触发。回调检查目标进程是否是受保护的游戏进程。如果是,它会从所需访问权限中剥离 PROCESS_VM_READ、PROCESS_VM_WRITE、PROCESS_VM_OPERATION 和 PROCESS_DUP_HANDLE。外挂收到一个句柄,但这个句柄对于读取或写入游戏内存是无用的。外挂的 ReadProcessMemory 调用将以 ERROR_ACCESS_DENIED 失败。
PsSetCreateProcessNotifyRoutineEx
PsSetCreateProcessNotifyRoutineEx 允许驱动注册一个回调,在全系统每个进程创建和终止事件时触发。回调接收进程的 PEPROCESS、PID 和包含被创建进程详细信息(映像名称、命令行、父 PID)的 PPS_CREATE_NOTIFY_INFO 结构。
值得注意的是,Ex 变体(在 Windows Vista SP1 中引入)提供了映像文件名和命令行,而原始的 PsSetCreateProcessNotifyRoutine 不提供。
反作弊使用此回调来检测系统上生成的外挂工具进程。如果在游戏运行时创建了已知的外挂启动器或注入器进程,反作弊可以立即标记它。一些实现还设置 CreateInfo->CreationStatus 为失败代码以直接阻止进程启动。
PsSetCreateThreadNotifyRoutine
PsSetCreateThreadNotifyRoutine 在全系统每个线程创建和终止时触发。反作弊专门使用它来检测受保护游戏进程中的线程创建。当游戏进程中创建新线程时,回调触发,反作弊可以检查线程的起始地址。
在游戏进程中创建的、其起始地址不在任何已加载模块地址范围内的线程是注入代码的强烈指示。合法线程在模块代码内启动。注入线程通常在 shellcode 或手动映射的 PE 代码中启动,这些代码没有模块支持。
PsSetLoadImageNotifyRoutine
PsSetLoadImageNotifyRoutine 在映像(DLL 或 EXE)被映射到任何进程时触发。它提供映像文件名和包含基地址和大小的 PIMAGE_INFO 结构。
此回调在 IRQL PASSIVE_LEVEL 运行。回调在映像被映射后但在其入口点执行之前触发,这给反作弊提供了在任何代码运行之前扫描映像的机会。
CmRegisterCallbackEx
CmRegisterCallbackEx 为注册表操作注册回调。反作弊使用它来监控可能表明外挂正在配置自己或试图修改反作弊设置的注册表修改。
用于文件系统监控的 MiniFilter 驱动
minifilter 驱动位于文件系统过滤器栈中,拦截进出文件系统驱动的 IRP 请求。反作弊使用 minifilter 来监控外挂文件投递(将已知的外挂可执行文件或 DLL 写入磁盘),检测对其自己驱动程序文件的读取(这可能表明试图在验证之前修补磁盘上的驱动二进制文件),并强制执行文件访问限制。
内存保护与扫描
内核驱动可以做的远不止注册回调。它可以主动扫描游戏进程的内存和全系统内存池中的外挂痕迹。
句柄访问阻止
如 ObRegisterCallbacks 部分所述,保护游戏内存免受外部读写的主要机制是从打开到游戏进程的句柄中剥离 PROCESS_VM_READ 和 PROCESS_VM_WRITE。这对任何使用标准 Win32 API(ReadProcessMemory、WriteProcessMemory)的外挂都有效,因为这些最终调用 NtReadVirtualMemory 和 NtWriteVirtualMemory,它们需要适当的句柄访问权限。
然而,内核模式外挂可以完全绕过这一点。它可以直接调用 MmCopyVirtualMemory(一个未导出但可定位的内核函数)或直接操作页表条目来访问游戏内存,而无需经过基于句柄的访问控制系统。这就是为什么仅靠句柄保护是不够的,也是为什么内核级外挂需要内核级反作弊响应。
周期性内存完整性哈希
反作弊定期对游戏可执行文件及其核心 DLL 的代码段(.text 段)进行哈希。在游戏启动时计算基线哈希,周期性的重新哈希与基线比较。如果哈希改变,说明有人写入了游戏代码,这是代码修补的强烈指示(常用于通过修补游戏逻辑来启用无后座、加速或自瞄功能)。
启发式扫描:检测手动映射代码
最有趣的内存扫描是手动映射代码的启发式检测。当合法 DLL 加载时,它出现在进程的 PEB 模块列表中、InLoadOrderModuleList 中,并有一个相应的 VAD_NODE 条目,其 MemoryAreaType 表明映射来自文件。手动映射绕过正常的加载器,所以映射的代码在内存中显示为匿名私有映射或具有可疑特征的文件支持映射。
关键启发式是:找到进程中所有可执行内存区域,然后将每个区域与已加载模块列表进行交叉引用。不对应任何已加载模块的可执行内存是可疑的。
VAD 树遍历
VAD(虚拟地址描述符)树是内存管理器用于跟踪进程中分配的所有内存区域的内核内部结构。每个 VAD_NODE(实际上是内核术语中的 MMVAD 结构)包含有关区域的信息:其基地址和大小、其保护、是否是文件支持的(以及如果是,哪个文件)以及各种标志。
反作弊直接遍历 VAD 树而不是依赖 ZwQueryVirtualMemory,因为 VAD 树不能像模块列表可以被操纵那样从内核模式轻易隐藏。
VAD 遍历的力量在于它捕获手动映射的代码,即使外挂已经操纵了 PEB 模块列表或 LDR_DATA_TABLE_ENTRY 链来隐藏自己。VAD 是用户态代码无法直接修改的内核结构。
反注入检测
CreateRemoteThread 注入
经典的注入技术:在目标进程中调用 CreateRemoteThread,以 LoadLibraryA 作为线程起始地址,DLL 路径作为参数。这通过 PsSetCreateThreadNotifyRoutine 可以轻易检测:新线程的起始地址将是 LoadLibraryA(或者更准确地说是它在 kernel32.dll 中的地址),并且调用者进程不是游戏本身。
一个更微妙的检查是创建线程的 CLIENT_ID。当调用 CreateRemoteThread 时,内核记录哪个进程创建了线程。反作弊可以检查游戏进程中的线程是否是由外部进程创建的,这是注入的可靠指示。
APC 注入
QueueUserAPC 和底层的 NtQueueApcThread 允许将异步过程调用排队到调用者具有 THREAD_SET_CONTEXT 访问权限的任何进程中的线程。当目标线程进入可警报等待时,APC 触发并在目标线程上下文中执行任意代码。
内核级别的检测利用 KAPC 结构。每个线程都有一个内核 APC 队列和一个用户 APC 队列。反作弊可以检查游戏进程线程的待处理 APC 队列以检测可疑的 APC 目标。
反射式 DLL 注入和手动映射
反射式 DLL 注入在 DLL 内部嵌入一个反射式加载器,当执行时,在不使用 LoadLibrary 的情况下将 DLL 映射到内存中。DLL 解析自己的 PE 头、解析导入、应用重定位并调用 DllMain。结果是一个完全功能的 DLL 在内存中,但永远不会出现在 InLoadOrderModuleList 中。
检测方法:具有有效 PE 头的可执行内存(检查 MZ 魔术字节和 e_lfanew 指定偏移处的 PE\0\0 签名)但没有相应的模块列表条目。这是一个可靠的指示。
使用 RtlWalkFrameChain 的栈遍历
当 BEDaisy 想要检查线程的调用栈时,它使用 APC 机制在线程处于用户模式时捕获栈帧。APC 在游戏线程上下文中触发并调用 RtlWalkFrameChain 或 RtlCaptureStackBackTrace 来捕获返回地址链。
BEDaisy 将内核 APC 排队到受保护进程的线程。APC 内核例程在 APC_LEVEL 运行,捕获线程的栈,然后将每个返回地址与已加载模块列表进行分析。指向任何已加载模块之外的返回地址是栈上有注入代码的强烈指示,这表明线程当前正在执行注入代码或从中返回。
Hook 检测
Hook 是用户态外挂拦截和操纵游戏与操作系统交互的主要机制。检测它们是反作弊的核心功能。
IAT Hook 检测
PE 文件的导入地址表(IAT)包含导入函数的地址。当进程加载时,加载器通过在导出 DLL 中查找每个导入函数并将函数的地址写入 IAT 来解析这些地址。IAT hook 用指向攻击者控制的代码的指针覆盖这些条目之一。
检测方法:对于每个 IAT 条目,将解析的地址与正确 DLL 的磁盘导出所说的地址进行比较。
Inline Hook 检测
Inline hook 用 JMP(相对近跳操作码 0xE9,或通过内存指针的间接跳 0xFF 0x25)修补函数的前几个字节,将执行重定向到攻击者代码,后者通常执行其修改然后跳回原始代码(”蹦床”模式)。
检测涉及读取每个受监控函数的前 16-32 字节并检查:
0xE9(JMP rel32)0xFF 0x25(JMP [rip+disp32]) - 64 位 hook 常见0x48 0xB8 ... 0xFF 0xE0(MOV RAX, imm64; JMP RAX) - 绝对 64 位跳转序列0xCC(INT 3) - 软件断点,也可以是 hook 点
反作弊读取磁盘 PE 文件并将函数序言的磁盘字节与当前内存中的内容进行比较。任何差异都表明修补。
SSDT 完整性检查
系统服务描述符表(SSDT)是内核的系统调用调度表。当用户态进程执行 syscall 指令时,内核使用系统调用号(放在 EAX 中)索引到 SSDT 并调用相应的内核函数。修补 SSDT 将系统调用重定向到攻击者控制的代码。
SSDT hook 是一种经典技术,在 64 位 Windows 中引入 PatchGuard(内核补丁保护,KPP)后变得明显更难。PatchGuard 监控 SSDT(以及许多其他结构),如果检测到修改则触发 CRITICAL_STRUCTURE_CORRUPTION bug 检查(0x109)。因此,SSDT hook 在 64 位 Windows 中基本上已消亡。然而,反作弊仍然作为纵深防御措施验证 SSDT 完整性。
IDT 和 GDT 监控
中断描述符表(IDT)将中断向量映射到其处理程序例程。全局描述符表(GDT)定义内存段。两者都是处理器级结构,在所有配置上都不能轻易被 PatchGuard 单独保护。
在内核级别运行的外挂可以尝试替换 IDT 条目来拦截特定中断,这可用于控制流拦截或作为隐蔽通道。反作弊验证 IDT 条目指向预期的内核位置。
驱动级保护
检测未签名和测试签名驱动
在启用了安全启动的正确配置的 Windows 系统上,所有内核驱动必须由 Microsoft 信任的证书签名。测试签名模式(使用 bcdedit /set testsigning on 启用)允许加载自签名驱动,是开发和外挂部署的常见技术。
反作弊通过读取 Windows 启动配置和检查反映 DSE 当前是否强制执行的内核变量来检测测试签名模式。一些反作弊如果启用了测试签名则拒绝启动。
BYOVD 攻击
自带易受攻击驱动(Bring Your Own Vulnerable Driver)是 2024-2026 年加载未签名内核代码的主流技术。攻击工作原理如下:
-
攻击者找到一个具有漏洞的合法签名驱动(通常是危险的 IOCTL 处理程序,允许任意内核内存读/写,或使用攻击者控制的参数调用
MmMapIoSpace)。 -
攻击者加载此合法驱动(它通过 DSE,因为它有有效签名)。
-
攻击者利用合法驱动中的漏洞实现任意内核代码执行。
-
使用该内核执行,攻击者禁用 DSE 或直接映射其未签名的外挂驱动。
常见的 BYOVD 目标包括来自 MSI、Gigabyte、ASUS 和各种硬件供应商的驱动。这些驱动通常有暴露直接物理内存读/写能力的 IOCTL 处理程序,这正是攻击者所需要的全部。
反作弊驱动阻止列表
对抗 BYOVD 的主要防御是已知易受攻击驱动的阻止列表。Microsoft 易受攻击驱动阻止列表(维护在 DriverSiPolicy.p7b 中)内置在 Windows 中并通过 Windows Update 分发。反作弊维护自己更积极的阻止列表。
特别是 Vanguard 以主动将已加载驱动集与其阻止列表进行比较而闻名,如果存在阻止列表中的驱动则拒绝允许受保护的游戏启动。这是强制执行的,因为一些 BYOVD 攻击涉及加载易受攻击的驱动并立即在使用后卸载它,所以在游戏启动时进行预扫描可以覆盖大多数情况。
PiDDBCache 和 PiDDBLock
这是内核外挂开发者和反作弊工程师都非常关心的更有趣的内部机制之一。
PiDDBCacheTable 是一个内核内部 AVL 树,缓存有关先前加载驱动的信息。当驱动加载时,内核存储一个以驱动的 TimeDateStamp(来自 PE 头)和 SizeOfImage 为键的条目。此缓存用于快速查找驱动是否曾经被见过。结构是由 PiDDBLock(一个 ERESOURCE 锁)保护的 RTL_AVL_TABLE。
不通过正常加载路径手动映射驱动的外挂开发者试图擦除或修改相应的 PiDDBCacheTable 条目来隐瞒他们的驱动曾经被加载过。反作弊通过以下方式检测:
-
验证
PiDDBCacheTable的一致性 - 如果驱动在内存中(通过池标签扫描或其他方式找到)但没有PiDDBCacheTable条目,该条目可能被擦除了。 -
监控
PiDDBLock的非内核线程的意外获取。 -
将所有已知加载驱动的时间戳/大小组合与
PiDDBCacheTable条目进行比较。
MmUnloadedDrivers
MmUnloadedDrivers 是一个内核数组(也不导出),维护最后 50 个卸载驱动的循环缓冲区,存储它们的名称、起始地址、结束地址和卸载时间戳。此结构允许驱动活动的调试和取证。
成功加载然后卸载内核驱动的外挂开发者经常试图将其在 MmUnloadedDrivers 中的条目清零或损坏来隐藏痕迹。反作弊通过以下方式检测:
-
维护自己预期的
MmUnloadedDrivers条目的影子副本。 -
检测循环缓冲区中间的异常零填充条目(故意擦除的签名)。
-
将
MmUnloadedDrivers与其他内核时间戳和日志进行交叉引用。
BigPool 分配
当内核分配超过大约 4KB(更准确地,当它超过池分配器管理的阈值)时,它被管理为”大池分配”,在 PoolBigPageTable 中跟踪。反作弊扫描此表来查找由手动映射驱动进行的内存分配。手动映射驱动通常为其代码和数据段进行大分配;这些在大池表中显示为分配地址但没有相应的已加载驱动。
技术是枚举所有大池条目,然后将每个分配的地址与已加载驱动地址范围列表进行交叉引用。不在任何驱动范围内的、大小适合驱动代码段的分配是可疑的。
反调试保护
反作弊代码本身是逆向工程的高价值目标。分析反作弊驱动的逆向工程师需要使用内核调试器,反作弊会积极检测。
NtQueryInformationProcess 检查
在用户态级别(在游戏注入的 DLL 中),反作弊使用 NtQueryInformationProcess 和多个信息类:
-
ProcessDebugPort(7):如果通过DebugActiveProcess附加了调试器,返回非零值。 -
ProcessDebugObjectHandle(30):如果存在调试对象,返回调试对象的句柄。 -
ProcessDebugFlags(31):NoDebugInherit标志;检查其反转可以揭示调试器存在。
内核调试器检测
内核驱动检查内核导出变量 KdDebuggerEnabled 和 KdDebuggerNotPresent。在附加了 WinDbg(或任何内核调试器)的系统上,KdDebuggerEnabled 为 TRUE,KdDebuggerNotPresent 为 FALSE。
一些反作弊更进一步,直接检查 KDDEBUGGER_DATA64 结构和共享内核数据页(KUSER_SHARED_DATA)中的调试器相关标志。
线程隐藏检测
带有 ThreadHideFromDebugger (17) 的 NtSetInformationThread 在线程的 ETHREAD 结构中设置一个标志(CrossThreadFlags.HideFromDebugger)。一旦设置,内核不会为该线程向任何附加的调试器传递调试事件。线程基本上对 WinDbg 不可见:线程中的断点不会触发调试器通知,异常不会被转发。
反作弊使用它来保护自己的线程。然而,它们也检测外挂是否使用它来隐藏自己的注入线程。检测方法是通过内核枚举(不是通过可能被 hook 的用户态 API)枚举系统中的所有线程,并检查每个线程的 CrossThreadFlags 中的 HideFromDebugger 位。游戏进程中反作弊自己没有隐藏的隐藏线程是一个危险信号。
基于时序的反调试
单步调试(通过 EFLAGS 中的 TF 标志)和硬件断点显著增加指令执行之间的时间。反作弊使用基于 RDTSC 指令的时序来检测:
阈值 EXPECTED_MAXIMUM_CYCLES 根据已知的 CPU 行为校准。单步每条指令可能增加数千个周期(由于调试异常处理),使时序差异变得明显。
硬件断点检测
x86-64 调试寄存器(断点地址 DR0-DR3,状态 DR6,控制 DR7)在内核模式下可访问。读取它们允许检测调试器设置的硬件断点。
反作弊扫描所有线程的保存调试寄存器状态(可通过 KeGetContextThread 获取的 CONTEXT 结构或直接从 KTHREAD::TrapFrame 访问)以查找非反作弊本身设置的活动硬件断点。
基于 Hypervisor 的调试器检测
基于 Type-1 hypervisor 的调试器(如在 Windows VM 中运行用于隔离调试的自定义 hypervisor)明显更难检测。主要的检测向量是:
-
CPUID 检查:hypervisor 存在位(执行 CPUID 叶 1 时 ECX 的第 31 位)表示存在 hypervisor。可以使用 CPUID 叶
0x40000000查询 hypervisor 供应商。VMware 返回 “VMwareVMware”,VirtualBox 返回 “VBoxVBoxVBox”。未知的供应商字符串是可疑的。 -
MSR 时序:在 VM 中执行
RDMSR比本机执行引入额外的开销。反作弊对 MSR 读取进行计时并标记异常。 -
CPUID 指令时序:
CPUID指令本身在虚拟化环境中是特权指令,必须由 hypervisor 处理,引入可测量的延迟。
DMA 作弊与检测
DMA 作弊代表了当前反作弊军备竞赛的前沿,它们确实很难仅用软件解决。
什么是 DMA 作弊?
PCIe DMA(直接内存访问)作弊使用 PCIe 连接的设备——通常是开发 FPGA 板——可以通过 PCIe 总线直接读取主机系统的物理内存,而无需 CPU 参与。pcileech 框架及其 LeechCore 库为这些设备提供软件栈。设备物理上出现在 PCIe 总线上,通过 PCIe TLP(事务层包)协议获取对主机物理内存的访问,并通过将虚拟地址转换为物理地址(使用页表,页表也在物理内存中,可以被设备读取)来读取游戏进程内存。
攻击机器(运行作弊软件)与受害机器(运行游戏)物理分离。所有作弊逻辑在攻击者的机器上运行。游戏机器没有来自作弊的进程、驱动、内存分配。从纯软件角度来看,游戏机器完全干净。
PCIe 内部机制
PCIe 通信围绕 TLP 结构化。来自 DMA 设备的内存读取 TLP 包含要读取的物理地址和请求的字节数。PCIe 根复杂通过读取指定的物理内存并在完成 TLP 中返回数据来服务此请求。这完全是硬件级别的,CPU 不参与服务请求。
IOMMU 作为防御
IOMMU(Intel VT-d,AMD-Vi)是一个硬件单元,使用设备特定的页表(类似于 CPU 用于用户态地址转换的页表)转换来自 PCIe 设备的 DMA 地址。如果 IOMMU 启用并正确配置,PCIe 设备只能访问操作系统通过 IOMMU 页表显式授予它的物理内存。
理论上,这是对抗 DMA 攻击的硬件级防御。
实际上,IOMMU 防御有重大缺口。许多游戏主板默认禁用 IOMMU。即使启用,IOMMU 配置复杂,许多系统的 IOMMU 策略配置不当,留下大范围物理内存可访问。关键的是,成功冒充合法 PCIe 设备的 DMA 固件(例如,操作系统已授予 IOMMU 访问权限的 USB 控制器或网卡)可能使用合法设备的授予权限通过 IOMMU 访问内存。
DMA 固件模仿
复杂的 DMA 作弊固件设计为模仿合法设备的 PCIe 设备 ID、供应商 ID、子系统 ID 和 BAR0 配置。运行定制固件的 Xilinx FPGA 可以向 BIOS 和操作系统展示自己,例如,作为一个 USB 主机控制器。操作系统为该设备加载合法驱动(提供 IOMMU 覆盖),FPGA 固件使用该覆盖执行 DMA 读取。
反作弊试图通过枚举所有 PCIe 设备并验证每个设备的报告特征是否匹配预期硬件来检测这一点。但在没有特定固件级证明的情况下,很难区分合法硬件和正确模仿的 FPGA。
安全启动和 TPM 作为部分缓解措施
Epic Games 对堡垒之夜要求安全启动和 TPM 2.0 直接与 DMA 威胁相关。安全启动确保只有签名的引导加载程序运行,这可以防止可能禁用 IOMMU 或安装固件级作弊的启动时攻击。TPM 2.0 启用度量启动(每个启动阶段的哈希记录在 TPM 的 PCR 寄存器中),提供一个证明链,证明系统以已知良好状态启动。使用 TPM 的远程证明可以允许服务器验证客户端系统没有在固件级别被篡改。
这并不直接解决 DMA 问题(物理连接到 PCIe 插槽的 DMA 攻击设备绕过所有这些),但它关闭了一些软件辅助的 DMA 攻击路径。
行为检测与遥测
没有静态保护方案是足够的。在游戏遥测上运行的行为检测是补充层,解决内核保护无法解决的问题。
鼠标和输入分析
内核反作弊驱动运行在可以拦截原始输入在它到达游戏之前的级别。HID(人机接口设备)输入的驱动程序,特别是鼠标和键盘的驱动程序,位于输入驱动栈中。通过在 mouclass.sys 或 kbdclass.sys 之上安装过滤器驱动,反作弊可以以系统时钟(微秒分辨率)精确的时间戳观察所有输入事件。
自瞄检测针对鼠标移动的统计特性。人类瞄准表现出特定属性:Fitts 定律支配接近轨迹,当光标接近目标时有特征性的减速,速度曲线有特定的加速和减速曲线,以及测量噪声。对目标执行完美线性插值的自瞄产生的移动违反这些属性。
触发机器人(当准星在目标上时自动开火但不操纵鼠标移动)通过反应时间分析检测:人类对目标穿过准星的反应时间有最小生理下限(约 150-200ms),具有特征分布。低于此下限且高度一致的反应时间表明自动化。
机器学习检测
Collins 等人(CheckMATE 2024)的论文记录了 CNN 在触发机器人检测中的应用,在标记数据集上达到约 99.2% 的准确率。输入网络的特征包括鼠标位置时间序列、相对于目标位置的点击时序和速度曲线。
AntiCheatPT 论文(2025)将 transformer 架构应用于自瞄检测。使用 256 tick 窗口,每个 tick 44 个数据点(包括位置、速度、加速度、视角速率和点击事件),模型在区分合法玩家和自瞄用户方面达到 89.17% 的准确率。Transformer 架构非常适合这个问题,因为自瞄通常在输入数据中引入时间相关性(平滑跟踪、周期性修正),注意力机制可以利用这些。
反虚拟机和环境检查
基于 CPUID 的 VM 检测
最可靠的 VM 检测是基于 CPUID 的。当使用 EAX=1 执行 CPUID 时,如果存在 hypervisor,则设置 ECX 的第 31 位(这是”Hypervisor Present”位)。使用 EAX=0x40000000,hypervisor 供应商字符串在 EBX、ECX、EDX 中返回。
基于特征的 VM 检测
每个 VM 平台在注册表和设备枚举中留下特征性的痕迹:
-
VMware:注册表键
HKLM\SOFTWARE\VMware, Inc.\VMware Tools;PCI 设备\Device\VMwareHGFS;在Win32_PnPEntity中出现名称包含 “VMware” 的虚拟设备。 -
VirtualBox:
HKLM\SOFTWARE\Oracle\VirtualBox Guest Additions;VBoxMiniRdDN驱动;注册表键HKLM\HARDWARE\ACPI\DSDT\VBOX__。 -
Hyper-V:
HKLM\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters;存在vmbus和storvsc驱动对象。
反作弊从内核模式查询这些特征,在那里它们不能被用户态 hooking 拦截。呈现任何这些特征的系统可能在 VM 中运行,反作弊可以拒绝操作或标记会话。
检测嵌套 Hypervisor
外挂开发者有时使用嵌套 hypervisor 来创建透明分析环境:他们在 VM 中运行游戏,外挂在 VM 的主机中运行。嵌套 hypervisor 的检测依赖于时序异常:在嵌套 VM 内执行的 CPUID 由两个 hypervisor 依次处理,引入双倍开销。RDMSR 和 WRMSR 指令同样有放大的延迟。数百次时序测量的统计分析可以可靠地区分本机执行、单级虚拟化和嵌套虚拟化。
硬件指纹识别与封禁执行
收集的标识符
反作弊收集多个硬件标识符来创建唯一的指纹,以在账号封禁后持久存在:
-
SMBIOS 数据:制造商、产品名称、序列号、UUID。通过
NtQuerySystemInformation(SystemFirmwareTableInformation, ...)或直接通过固件表访问。 -
磁盘序列号:通过 IOCTL
IOCTL_STORAGE_QUERY_PROPERTY的物理磁盘序列号。这些是稳定的标识符,在操作系统重装后仍然存在。 -
GPU 标识符:设备实例 ID、适配器 LUID。
-
MAC 地址:通过 NDIS 或注册表的 NIC MAC 地址。这些在软件级别可欺骗,但非技术用户通常不会更改。
-
启动 GUID:
HKLM\SOFTWARE\Microsoft\Cryptography中的MachineGuid,或更持久地,通过 SMBIOS 可访问的 UEFI 固件平台 UUID。
HWID 欺骗与检测
HWID 欺骗涉及修改反作弊读取的标识符来逃避硬件封禁。欺骗方法包括:
-
基于注册表的欺骗:修补报告磁盘序列号、MAC 地址和 SMBIOS 数据的注册表条目。这对通过注册表路径查询这些数据的反作弊有效,但对直接查询硬件的反作弊无效。
-
驱动级欺骗:一个拦截硬件标识符 IOCTL 请求并返回欺骗值的内核驱动。这对使用标准 IOCTL 路径的反作弊有效,但对直接查询硬件的反作弊无效。
-
物理欺骗:将不同的 MAC 地址编程到 NIC 固件中,刷写新的磁盘序列号(一些驱动器支持)。这很少见,有时是永久的。
反作弊通过交叉引用多个标识符来源检测欺骗。如果 SMBIOS UUID 是 FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF(常见的欺骗值),那是立即的标记。如果报告的磁盘型号是”Samsung 970 EVO”但磁盘序列号格式不匹配三星的格式,那是欺骗指示。如果 UEFI 固件表报告一个 UUID 而注册表报告不同的,注册表值已被篡改。
军备竞赛:当前趋势与未来方向
升级层次结构
过去十年我们看到的升级遵循一个清晰的模式:
- 用户态外挂被用户态反作弊对抗。
- 内核外挂被内核反作弊对抗。
- 带 BYOVD 的内核外挂被驱动阻止列表和更严格的 DSE 执行对抗。
- 基于 Hypervisor 的外挂被 hypervisor 检测对抗。
- DMA 作弊是当前前沿,部分被 IOMMU、安全启动和 TPM 证明对抗。
- 下一级是基于固件的攻击,其中外挂嵌入在 SSD 固件、GPU 固件或 NIC 固件中。
固件攻击特别令人担忧,因为它们在操作系统重装后仍然存在,对所有内核级检查不可见,并且在没有物理访问设备进行固件验证的情况下极难检测。今天没有广泛部署的反作弊防御对抗固件作弊。
AI 驱动的外挂
下一代威胁是由在 GPU 或辅助计算机上运行的计算机视觉模型驱动的自瞄。这些系统使用摄像头或屏幕捕获来分析游戏帧、识别目标,并通过硬件(一个 USB HID 设备,完全绕过软件输入检查)移动鼠标。它们产生的鼠标移动可以配置为模仿人类运动模式,使统计检测更困难。
通过硬件 HID 操作的 AI 自瞄,从游戏机器的角度来看,与使用鼠标的人类完全无法区分。所有输入都通过合法的硬件通道。游戏进程中没有运行代码。内核完全干净。唯一的检测面是行为特征:AI 产生的准确性、反应时间和移动模式。
这就是为什么第 10 节讨论的行为 ML 方法不是可选的,而是越来越成为有效反作弊的核心。
隐私辩论
内核级反作弊在隐私倡导者中非常不受欢迎。批评是有实质性的:
运行在 ring 0 并具有启动时加载的驱动可以访问系统上的一切。虽然 BattlEye、EAC 和 Vanguard 没有被记录滥用此访问权限进行监视,但技术能力存在。ARES 2024 论文的分析强调,信任模型与我们用于安全关键软件的相同,这意味着这些组件中的任何漏洞都是到 ring 0 的本地特权升级。
游戏需要安装启动时内核驱动作为游戏条件也是一个重大的攻击面问题。vgk.sys 中的漏洞是到 ring 0 的本地特权升级。反作弊软件本身成为攻击目标。
基于证明的方法
反作弊最有技术前途的方向是远程证明。不是运行主动对抗外挂的 ring-0 驱动,系统向游戏服务器证明它运行在已知良好状态。基于 TPM 的度量启动,结合 UEFI 安全启动,可以生成加密签名的证明,证明特定引导加载程序、内核和驱动被加载。服务器拒绝无法提供有效证明的系统连接。
这不是完整的解决方案(足够复杂的攻击者可能操纵证明),但它显著提高了门槛。证明可以与传统扫描共存以提供纵深防御。
云游戏作为反作弊
云游戏(GeForce Now、Xbox Cloud Gaming)在架构上是某些游戏类别的终极反作弊。如果游戏在数据中心运行,只有视频流传输到客户端,就没有游戏客户端代码可利用,没有游戏内存可读取,没有本地环境可操纵。外挂攻击面减少到输入操纵和视频分析,两者都有相对简单的检测方法。
限制是延迟:云游戏不适合单位数毫秒反应时间重要的竞技游戏。对于休闲和半竞技游戏,云交付可能越来越多地成为答案。
结语
现代内核反作弊系统代表了跨越 Windows 特权模型每个可用级别的分层防御架构:
-
内核回调(
ObRegisterCallbacks、PsSetCreateProcessNotifyRoutineEx、PsSetLoadImageNotifyRoutine)提供对系统事件的实时可见性,并具有主动阻止恶意操作的能力。 -
内存扫描(VAD 遍历、大池枚举、代码段哈希)提供周期性验证,证明游戏内存没有被篡改且不存在注入代码。
-
行为遥测(输入分析、统计画像、ML 推理)捕获在架构上对内核扫描不可见的外挂。
-
硬件指纹识别跨账号重置执行封禁决定。
-
反调试和反虚拟机保护使逆向工程和开发显著更困难。
没有单一技术是足够的。内核回调可以被 DMA 攻击绕过。内存扫描可以被拦截内存读取的基于 hypervisor 的外挂规避。行为检测可以被足够模仿人类的 AI 欺骗。硬件指纹识别可以被硬件欺骗器击败。是所有这些层的组合,持续更新以响应新的规避技术,提供了有意义的保护。
这场军备竞赛的轨迹指向硬件证明和服务器端验证作为可信游戏安全的最终基础。仅软件的客户端保护将永远是不对称的:防御者必须检查一切,攻击者只需要找到一个缺口。硬件证明通过使在操作修改后的系统时极难展示可信状态来改变这种不对称性。
在该基础普遍可用和强制执行之前,内核反作弊仍然是可用的最佳实际防御,伴随着所有相关的复杂性、隐私影响和攻击面。
个人感想
这篇文章是我近期读过的最详尽的内核反作弊系统技术分析。作者不仅涵盖了理论知识,还提供了大量实际代码示例和 WinDbg 调试截图,让读者能够真正理解这些机制如何工作。
对于国内的游戏开发者来说,理解这些底层安全机制有助于:
- 安全意识提升:了解攻击者的技术手段,才能更好地设计防御策略
- 技术视野拓展:Windows 内核编程是一个小众但重要的领域
- 职业发展:游戏安全是一个有前景的专业方向
值得注意的是,文章也提到了隐私争议。作为玩家,我们需要在游戏体验和系统安全之间做出权衡。作为开发者,我们应该思考如何在保护游戏公平性的同时,尽可能减少对用户系统的侵入。