字节暗面

游戏数据上报校验:从值域校验到服务端重演


方案评审里,经常会听到一句话:“这块我们走服务端判定。”这话本身没问题,问题是继续往下问:服务端拿什么判?很多项目最后绕了一圈,判断依据还是客户端报上来的那份 JSON。

战斗结算这类接口尤其典型。包签名对得上、ts 新鲜、nonce 没重复,协议层能做的都做了,一路绿灯进库。可这些只能证明包没在路上被动过、没被原样重发,跟里面那串数字是不是真打出来的,完全两码事。

服务端手里根本没有 ground truth。它验不了真假,只能验合不合理。我们能做的,只有抬高“伪造数据”的成本。

值域校验能拦住的,比想象中少

第一层一定是值域。拿服务端存的面板快照算个理论上限,maxDamage = atk × critMulCap × hitCap,超了丢掉。必须有,但也就是个地板。

现在没人往 score 里塞 999999 了。改成上限的 95%,每局多拿一点,阈值一次都不响。更尴尬的是阈值本身。伤害系数、暴击上限常常就写在随包下发的数值表里,解个包就知道及格线画在哪。见过更离谱的:服务端算上限用的系数,是从客户端上报的字段里读的。那不叫校验,叫让对方自己出题自己判卷。

还有个反直觉的:校验没过,别告诉客户端。返回“数据异常”等于白送一个 oracle,二分几十次就能把阈值量得一清二楚。照常返回成功,异步打标。

单个数都在范围里,凑一起就不一定

值域横着看每个字段,自洽竖着看字段之间的物理约束。总伤除以时长超过理论 DPS,某个技能报了 8 次释放、可它 CD 8 秒、整局才打 30 秒。单看都合法,凑一起就露馅。

// 同一技能释放 n 次,至少要跨 (n-1) 个 CD
foreach (var g in req.SkillCasts.GroupBy(c => c.Id)) {
    long need = (g.Count() - 1) * SkillTable[g.Key].CooldownMs;
    if (need > req.DurationMs) Flag(uid, "cd_overflow");  // 只打标,照常返回成功
}

工程上让客户端多报几个冗余字段,但别公开哪几个之间存在约束。改一个数容易,改到整份自洽,得先把战斗公式吃透。

问题是有一类情况根本不用改包。内存改攻击力、变速齿轮拉帧、开无敌,然后老老实实打完一局,客户端正常上报。签名对、值域过、字段全自洽——因为那场战斗在客户端确实发生过。你收到的数据是真实产生的,只是产生它的客户端已经不是你发出去的那个了。

想验真假,只能自己再算一遍

客户端别报结果,报输入。seed 加一串操作传上来,服务端拿同一份战斗逻辑跑完,比对结果 hash。以服务端为准,说的其实是这个:服务端手里得有自己能算的东西。

var sim = new BattleSim(snapshot, req.Seed);   // 与客户端同一份战斗核
foreach (var act in req.Actions)               // 每回合 4 字节
    sim.Step(act);

if (sim.ResultHash != req.ResultHash)
    Flag(uid, "replay_mismatch");
Settle(sim.Result);                            // 一律按服务端跑出来的发奖

代价是双端必须算出一模一样的结果:浮点换定点数,随机别用 UnityEngine.Random、换成自己带 seed 的 xorshift,战斗逻辑抽出来两边共用一份。早期做不难,中途改伤筋动骨。

第一个坑也在这儿。上线头几天 replay_mismatch 会多到吓人,扒开看基本都是自己两边逻辑没对齐,不是真作弊。先只打标观察,别急着接处置。

决定的是全量还是抽样,要看你游戏品类。卡牌、回合制、放置这类开销小。输入就是每回合出哪张牌、打哪个目标,(cardId u16, targetId u8) 这么几个字段,整局加起来几十字节;服务端重跑一遍是纯逻辑运算,没渲染没物理。这种直接全量,每局结算都跑,不用抽,抽了也省不下什么。

实时动作、MOBA、射击正相反,每一帧都是输入,一局下来几千帧,重演等于把整场战斗重新跑一遍,存储和 CPU 都不是一个量级。这类只能抽样。再叠一层触发式全量:榜单 Top、掉了高价值道具的、已经被打标的账号,不管抽没抽中都跑。

抽样率不用高,1% 就够——作弊要变现必须反复刷,逃掉单局没有任何意义。刷 300 局,至少被抽中一次的概率是 1 - 0.99^300 ≈ 95%。抽的不是某一局,是持续作弊的期望收益。

绕法也直接。知道你会重演,他就不改数据了,改成脚本:自动出牌、自动刷本、按键宏。输入序列货真价实,重跑当然对得上,因为他确实是这么打的。到这一步假的已经不是数据,是操作的人,再往后就不是校验的事了,归风控管。

这里提一个容易踩的坑:不少项目上报的是“本局平均反应时间”,一个聚合过的数,分布在客户端就被抹平了,等于没报。要报就报原始采样的时间戳序列,一局 50 个点足够。

封号之前先想想误报

弱网重传、断线补包、低端机掉帧、自家客户端的 bug,都能让数据看着不合理。分级处置稳妥些:先打标,再限制变现路径(交易、提现、榜单结算),多维信号攒够了再上人工。

校验堆到第几层不是技术问题,而是这份数据背后的成本问题。换皮小游戏的周榜,值域加自洽就到头了,再往上纯属自我感动;带交易、能提现的,重演那笔工程账再难看也得认,中间那一大片,得自己掂量。

← 返回列表