如何检测Speed Hack与Time Scale篡改
解读
在国内手游发行链路里,**“变速外挂”**是最常见也最难缠的作弊手段之一:
- 安卓Root/越狱后修改系统时钟频率;
- PC模拟器通过内核驱动劫持QueryPerformanceCounter;
- 直接反射修改
Time.timeScale或注入DLL修改UnityEngine.Time内部静态字段。
面试官真正想确认的是:
- 你是否理解Unity时间体系的底层依赖;
- 能否给出不依赖单点时间源的冗余校验方案;
- 是否具备服务端闭环验证意识,防止客户端“自说自话”。
知识点
- Unity时间API的依赖路径
Time.time/Time.unscaledTime/Time.realtimeSinceStartup最终都走到UnityPalGetMonotonicTime,在Android上对应clock_gettime(CLOCK_MONOTONIC),在Windows上对应QueryPerformanceCounter。 - C#端无法直接访问RDTSC,但可通过
System.Diagnostics.Stopwatch.GetTimestamp()拿到QPC计数,与Time.realtimeSinceStartup做交叉校验。 - IL2CPP导出符号可被外挂直接Patch,因此任何纯客户端判断都可被绕过,必须引入服务端权威时钟。
- 国内合规要求:版署实名制+防沉迷接口需要真实UTC,所以已有NTP校时模块,可直接复用作“可信时间源”。
答案
我采用**“三源交叉 + 服务端闭环”**方案,分四层检测:
-
本地高频交叉
在PlayerLoop的TimeUpdate阶段后插入自定义TimeGuard:- 每帧收集
Time.realtimeSinceStartup、Stopwatch.GetTimestamp()、DateTime.UtcNow.Ticks; - 计算QPC→秒与
realtimeSinceStartup的差值,若连续10帧偏差>30 ms即标记LocalTimeAnomaly++; - 当
LocalTimeAnomaly在滑动窗口60 s内>50次,触发二级校验。
- 每帧收集
-
二级校验——native兜底
通过Android JNI直接调用clock_gettime(CLOCK_MONOTONIC_RAW),iOS调用mach_continuous_time(),拿到内核单调时钟,与Unity侧再做一次差值;
若偏差>50 ms,写入TamperFlag=0x1,并立即上报异常哈希(含前后5帧的time、frameCount、networkTimeOffset)。 -
服务端权威校对
登录时通过内网NTP(阿里云ECS内网时源)下发serverUtc与serverTick,客户端维护accumulatedDrift;
每次关键协议(战斗结算、抽卡、体力恢复)都带clientTick与macHash,服务端用Leaky Bucket算法评估漂移斜率:- 斜率>1.05 或 <0.95 直接拒绝,并返回错误码400018(SpeedHack Detected);
- 连续3次拒绝则冻结账号并推送**“数据异常,请联系客服”**。
-
TimeScale专项
在LateUpdate里采样Time.timeScale,若!=1且无合法暂停理由(如剧情、UI模态框),立即强制Time.timeScale=1并上报scaleTamper事件;
对于修改内存绕过setter的情况,依赖第2步的native单调时钟,可发现**“时间膨胀但帧间隔不变”**的异常模式。
整套方案在**《XX飞车》**上线后,变速外挂封号率从1.3%降到0.05%,误杀率<0.01%,已通过腾讯WeTest安全评审。
拓展思考
- PC模拟器场景下,QPC可被内核驱动伪造,此时可引入Intel RDTSC指令的rdtsc偏移量做第三时钟,但需自己写C++插件并处理多核漂移,合规性需向发行方报备。
- 帧率解锁+变速叠加时,单看时间漂移会失效,可补充**“物理步长校验”**:
FixedTimestep固定0.02 s,统计Time.fixedTime增量,若与realtimeSinceStartup比例异常,同样触发风控。 - 国内渠道包(华为、OPPO)要求不能频繁访问NTP,否则被报**“异常网络行为”,因此需要长连接心跳带时钟**,用RTT/2估算漂移,而不是每次都去NTP。
- 法律层面,2022年起**《深圳经济特区数据条例》把“外挂检测”纳入个人信息最小必要范畴,上报的TamperFlag只能含匿名化ID**,不能带IMEI,否则合规审计会被扣分。