反作弊

Unity 加速外挂(SpeedHack)的原理与基于 Native 的检测策略

一个加速外挂就能摧毁游戏经济

在放置类 RPG 中,一名以 10 倍速运行游戏的玩家,仅一天就能扫光普通玩家数月的资源。节奏或动作游戏的排行榜顶端被异常的游玩时长与分数填满。在对战游戏里,只要一名玩家移动速度快上 1.2 倍,公平匹配便当场崩塌。

加速外挂(SpeedHack)是最常见、却最被低估的作弊手段。它不像内存篡改那样直接修改某个具体数值,而是把游戏所感知的时间流动本身整体扭曲。「校验资源获取量」「校验移动坐标」之类的事后防线,难以从根本上捕捉这种时间畸变,损害会蔓延到营收、玩家留存与排行榜可信度。

本文将从实战视角,剖析加速外挂在 Unity 引擎背后究竟如何操纵时间、常见的 C# 层防御为何容易被攻破,以及应如何在 Native(C++)层进行检测。

加速外挂操纵的是什么 — 原理

Unity 游戏的核心循环会不断计算「距上一帧过去了多少时间」。这个值就是 Time.deltaTime,移动速度、冷却、物理、动画、资源累积等几乎所有系统都依赖它。

加速外挂的原理很简单:人为地放大或缩小这个时间测量值,让引擎误以为「经过的时间比实际更多」。关键在于,Unity 并不自己生成时间,而是调用 OS 计时器 API 来获取,攻击者正是拦截这些调用。

[ 正常的时间流 ]
OS 硬件计时器 ──> OS 计时器 API ──> Unity(Time.deltaTime) ──> 游戏逻辑

[ 加速外挂生效时 ]
OS 硬件计时器 ──> OS 计时器 API ──> [加速外挂挂钩: ×10] ──> Unity ──> 游戏逻辑加速 10 倍

1) PC(Windows) Cheat Engine 等加速外挂会向游戏进程注入 DLL,然后挂钩用于计时的 API——典型的有 QueryPerformanceCountertimeGetTimeGetTickCount / GetTickCount64。被挂钩的函数会把真实的 tick 值乘以一个倍率(如 ×10)再返回,游戏只是老实地调用了 API,却感知到一帧之间流逝了 10 倍的时间。

2) 移动端(Android) GameGuardian 等工具机制相同:拦截 libcgettimeofdayclock_gettime 等函数(PLT/GOT 挂钩),或在虚拟空间(Virtual Space)内运行 App 以操纵时间基准本身。在已 root 的环境中,甚至会介入更底层。

无论哪个平台,套路都一样:扭曲信任链的最底层(OS 计时器),一次性欺骗其上叠加的整个引擎与游戏逻辑。

常见 C# 层防御的局限

遇到加速外挂的团队,通常会在 C# 层加入自研防御。方向是对的,但在实战中会撞上以下局限。

1) 比较 Time.deltaTimeTime.unscaledDeltaTime —— 毫无意义 两者最终都源自同一个 OS 计时器。计时器一旦被挂钩,两个值会按相同比例一起被扭曲,因而毫无差异。这就像拿两只被操纵的钟互相对照来判断是否正常。

2) 与 C# 自有时钟对照(DateTime.UtcNowStopwatch) —— 会被逆向移除 用独立基准对照的思路是对的。问题在于检测代码暴露在托管代码(C#)中。即便用 IL2CPP 构建,借助 global-metadata.datIl2CppDumper 也能还原大量类与方法结构,攻击者会找到检测方法并用 NOP/JMP 补丁使分支失效。

// 攻击者通过 IL2CPP 逆向找到此方法,
// 并修改二进制使该分支永不执行。
bool CheckSpeedHack() {
    if (MySystemTime != UnityTime) {
        return true; // <-- 被 JMP 补丁跳过此分支
    }
    return false;
}

3) 服务器时间戳校验 —— 强大但无法覆盖全部 将客户端动作间隔与服务器时间对照,是可靠的最后防线。但要把所有单人要素、放置进度、细微动作都拿到服务器校验,带宽与算力成本高昂,且对支持离线的游戏无法适用。

简而言之,攻击从 OS 计时器运行的 Native 层进入,而监视之眼却停留在其上一层的 C# 托管代码——等于先把主动权交给了攻击者。

在 Native(C++)层检测加速外挂的方法

要可靠检测,必须在攻击者所扭曲的同层、或更低的层面,建立独立的监视体系。核心有三个支柱。

(1) 获取抗挂钩的低层独立时钟 远离外挂主要瞄准的高层 API(如 QueryPerformanceCounter),直接读取更低的时间源以建立基准。在 Windows 上是 CPU 周期计数器(rdtsc)或用户态可读的系统时间字段;在 Android 上是绕过库接口、走 syscall 路径的单调时钟(CLOCK_MONOTONIC / CLOCK_BOOTTIME)。将该基准与游戏实际使用的时间流速做交叉校验,一旦偏差超过阈值,即判定为时间轴操纵。

不过 rdtsc 也并非万能。基于 hypervisor 的工具能够陷入/虚拟化 RDTSC 本身;若 App 运行在虚拟空间内,连直接 syscall 也可能被容器拦截。因此应把低层时钟视为「抗高层 API 挂钩的一个支柱」,并与下面的 (2)(3) 以及服务器端可信时间组合使用才稳健。

(2) 直接检测时间 API 的内存挂钩痕迹 要施加倍率,工具必须篡改时间函数的序言(prologue)使其跳转(JMP,如 0xE9),或改写函数地址表(IAT/EAT)。Native 检测会检查关键时间函数的首字节是否被篡改、地址是否指向未授权模块,从而直接捕捉工具的痕迹。

(3) 安全逻辑与 C# 解耦 这是最重要的设计原则。检测循环、时钟比较与响应逻辑都不应暴露在 C# 内存与 IL2CPP 产物中。把核心逻辑放在经过混淆的 Native C++ 二进制(.dll / .so)的独立线程中运行,即使攻击者分析整个 C# 代码库也无法定位监视者,逆向与打补丁的难度由此急剧提升。

OZero Security 的加速外挂应对

自研 Native 反作弊需要深入理解 OS 内部、覆盖多平台,并应对频繁的 OS 更新维护,对中小工作室是沉重负担。OZero Security 提供可立即接入 Unity 的 Native C++ 反作弊。

  • 基于可信时间的校验(OZero 的核心差异化): 不只依赖本地时钟,而是以轮询(round-robin)+ 法定多数(quorum)交叉比对多个 webTime 端点,建立可信的基准时间。你还可以注册自营的 webTime 端点;即便本地计时器被操纵,也能凭与外部可信时间的不一致判别时间轴操纵。
  • Native C++ 检测: 在托管代码之外进行低层时钟交叉校验与时间 API 挂钩痕迹检测。
  • 性能设计: 安全逻辑置于 Native C++ 层而非 C#,受保护数值在设计上不产生 GC 分配。
  • 灵活响应: 检测后的行为可按策略配置——记录日志、限制功能、终止进程,或将加密的检测日志发送至服务器/后台进行实时处置(Pro Add-on 遥测)。
  • 接入: 无需写代码——导入 .unitypackage,在仪表盘切换模块开关并保存 Config 即可。

实时检测演示

OZero Security 实时检测加速外挂的实际运行:

Windows(PC) — 加速外挂实时检测
Android(移动端) — 加速外挂实时检测

小结

  • 窃取时间的作弊: 加速外挂欺骗的是整个系统的时间轴而非单个数值,瘫痪经济与公平性。
  • C# 安全的局限: 即便经过 IL2CPP,暴露在 C# 中的检测逻辑在逆向工具面前也会被无力化。
  • 正道解法: 将低层独立时钟、时间 API 挂钩痕迹检测、检测逻辑的 Native 分离组合起来,并用服务器端可信时间(webTime quorum)弥补本地时钟的局限。

仅仅拦截一个加速外挂,就需要横跨 OS 架构、汇编层、逆向规避的大量知识。若你已厌倦与作弊模式无休止的追逐,不妨考虑采用经过验证的方案,把精力专注于游戏内容本身。

了解 OZero Security 如何检测并应对加速外挂。

相关阅读