ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

游戏通讯数据防篡改实战:从协议签名到服务器权威

游戏通讯数据防篡改实战:从协议签名到服务器权威 前几天有个做小游戏的朋友跑来找我一脸崩溃游戏上线才一周排行榜就被一群金币上亿的号刷穿了。我让他把客户端发给服务器的请求日志拉出来看了一眼问题一目了然——客户端说“给我10000金币”服务器就真的给。没有签名没有防重放没有数值归属校验攻击者压根不需要逆向代码用抓包工具把请求参数改一下再放行就完成了整个篡改过程。这类问题在中小团队里太常见了。大家把大量精力花在玩法、美术、服务器稳定性上但“通讯数据防篡改”往往等到出事故才想起来补课。而等到上了线再补代价就是版本热更新、玩家赔付、声誉受损。这篇文章我就围绕“游戏通讯数据防篡改”这件事把我在实际项目里验证过的技术方案和踩坑经验整理出来覆盖协议层签名、服务器权威架构、实时对战场景的特殊处理以及加密和客户端加固的边界。适合正在做网游、弱联网单机、实时对战项目的读者无论你是客户端还是服务端出身读完至少能形成一套可以落地的防御框架。1. 攻击者到底有多少种改数据的办法1.1 上行数据篡改客户端说了算上行数据就是客户端发给服务器的请求。绝大多数被刷穿的游戏问题都出在这里服务器把客户端上报的数据当成了事实。举个例子。一个卡牌游戏里客户端在战斗结束后发一个包{ battle_result: win, damage_dealt: 999999, rewards: [legendary_card] }如果服务器直接按这个包里的字段发奖励那玩家就可以把damage_dealt改成自己想要的数值或者干脆把rewards字段改成一串传说卡。攻击者不需要懂二进制协议只要用Charles或Fiddler这类代理工具拦到请求右键编辑JSON再放行就完成了攻击。这本质上不是“通讯被篡改”的问题而是“通讯协议设计上就没防篡改”。服务器把裁决权交给了客户端那客户端自然可以为所欲为。1.2 下行数据篡改与“改显示骗逻辑”下行数据是服务器返回给客户端的数据。很多人觉得“服务器发的数据总不可能被改吧”实际也能改。典型手法是你拦截到服务器返回的{hp: 500, gold: 100}这个JSON把它改成{hp: 99999999, gold: 999999}再交给客户端解析。如果客户端的战斗逻辑里有“受到伤害时血量减少 hp 才对”这样的判断改了下行数据后客户端本地的战斗表现就会完全失真——攻击者可能在本地“无敌”“秒杀”体验上等于开挂。有些游戏更危险客户端拿到服务器下发的数据后会把里面的关键值缓存在本地后续请求时再带回来。一旦攻击者篡改了下行数据就相当于污染了客户端的状态诱导服务器做出错误判断。所以防篡改不能只盯着上行下行数据的完整性同样要保护。1.3 重放攻击和重排攻击不修改内容也能搞事还有一类攻击根本不改你的数据内容而是原封不动地重复发送。最常见的就是重放攻击。我遇到过一个小游戏签到接口只判断“客户端是否请求了签到”服务器端并不知道玩家今天签过没有。于是攻击者把签到请求截获下来写个脚本每秒钟发一次服务器每次都返回成功金币奖励反复到账。这不是通过修改包内容完成的服务器也完全没察觉异常直到运营看数据发现某个账号的金币曲线像心电图一样上蹿下跳。重排攻击则针对一些有状态流转的协议。比如新手引导关卡客户端应当按“关卡1完成→关卡2完成→领奖励”的顺序发包攻击者直接跳过关卡1和2的请求只发最后一个“领奖励”的包如果服务器没有对状态顺序做校验奖励就会被白嫖。从这些案例可以看出一条核心规律防篡改要同时解决“内容被改”“内容被重放”“请求顺序被乱排”三个问题。缺一个都会被钻空子。2. 协议层签名让每个包都带上“防伪标签”2.1 HMAC签名包结构设计协议层防篡改的基础是给每个数据包加一个带密钥的校验值业内最常用的是HMACHash-based Message Authentication Code基于哈希的消息认证码。你可以把它理解成“防伪标签”——只有持有密钥的客户端才能生成正确的标签服务器收到包后先验证标签再处理业务。任何一位攻击者哪怕只改了包里一个字节标签就对不上了包直接被丢弃。我建议的最小包结构是这样的包头(28字节) MAC(32字节) 业务数据体(变长)包头里包含session_id(4字节) timestamp(8字节) seq(8字节) nonce(4字节) body_len(4字节)。MAC就是对“包头业务数据体”整体计算出来的HMAC-SHA256值。客户端组装包的代码用Python示意一下import hmac import hashlib import struct import time import secrets HEADER_LEN 28 # session_id(4) timestamp(8) seq(8) nonce(4) body_len(4) MAC_LEN 32 # HMAC-SHA256 输出长度 def build_signed_packet(session_id: int, session_key: bytes, seq: int, body: bytes) - bytes: ts int(time.time()) nonce secrets.token_bytes(4) header struct.pack(I Q Q 4s I, session_id, ts, seq, nonce, len(body)) mac hmac.new(session_key, header body, digestmodhashlib.sha256).digest() return header mac body这里有个容易被忽视的关键点session_key不能写死在客户端代码里。通常做法是在登录或建立连接时客户端和服务器先完成一次身份验证然后动态协商出一个临时会话密钥后续所有包的HMAC都基于这个临时密钥生成。如果写死一个全局key攻击者只要逆向一次就能永久冒充客户端。2.2 服务端校验顺序服务端收到一个包后校验顺序很重要。我一直推荐按下面这个顺序来def verify_signed_packet(packet: bytes, session_key: bytes, last_seq: int) - bytes: if len(packet) HEADER_LEN MAC_LEN: raise ValueError(包长度不合法) header packet[:HEADER_LEN] mac packet[HEADER_LEN:HEADER_LEN MAC_LEN] body packet[HEADER_LEN MAC_LEN:] # 第1步HMAC校验确认内容没有被篡改 expected_mac hmac.new(session_key, header body, digestmodhashlib.sha256).digest() if not hmac.compare_digest(mac, expected_mac): raise ValueError(签名校验失败) session_id, ts, seq, nonce, body_len struct.unpack(I Q Q 4s I, header) # 第2步时间戳校验拒绝明显的延迟重放 now int(time.time()) if abs(ts - now) 90: raise ValueError(时间戳超出容忍范围) # 第3步序号校验拒绝重放和乱序 if seq last_seq: raise ValueError(重复或乱序的seq) return body为什么顺序必须是“先MAC、再时间戳、再seq”因为MAC校验是纯计算不依赖外部状态最快速时间戳校验能第一时间过滤掉绝大多数重放脚本seq校验放在最后是因为它需要服务端维护每会话的last_seq属于有状态逻辑放后面不干扰前面的快速失败路径。seq使用int64单调递增会话建立时从0开始。有人问过我用int32行不行短会话没关系但长连接游戏高强度发包时int32很容易溢出甚至绕过窗口判断不要省这4个字节。2.3 防重放窗口与序号设计只看seq last_seq并不能防住所有情况。网络游戏UDP场景下包会乱序到达你不能直接丢弃所有乱序包。更稳妥的方案是维护一个滑动窗口接收端记录已经收到的最大seq并保存一个最近N个seq的位图或集合窗口内重复的seq直接丢弃超过窗口的旧包也丢弃。我常用的参数窗口大小256也就是说允许最多256个包乱序到达。对于大部分游戏每秒钟几十个包的频率256的窗口足够覆盖正常的网络抖动又不至于给攻击者留下太大的重放空间。注意重放窗口的设计要跟业务类型匹配。回合制、卡牌这类低频请求窗口可以缩小到16甚至8MOBA和FPS这类高频状态同步窗口可能需要放大到512甚至1024否则正常网络下就会出现误杀。如果你用的是HTTPS/WebSocket这类本身就基于TCP的通道TCP的序列号机制已经帮忙解决了一部分乱序和重放问题应用层seq压力会小很多。但TCP只保证“网络传输层面”的顺序不保证“应用逻辑层面”的合法顺序所以业务状态机的校验依然不能省。3. 服务器权威防篡改的真正分水岭3.1 关键数据到底该由谁产生和决定协议签名做完了能防住“谁乱改数据包”吗能但只能防住一半。因为有一类问题签名防不住玩家在客户端内存里把攻击力从100改成10000然后客户端正常把攻击力10000放进包里用正确的session_key签好名发给服务器。服务器验签发现这是合法客户端发来的但它依然是个作弊包。这就是我要强调的观点签名保证的是“数据在传输过程中没被改”但不保证“数据本身是合法的”。要解决后者必须把裁决权从客户端收回服务器也就是服务器权威Server Authority。我用一张表来说明哪些数据应该由谁决定数据/事件权威方客户端角色服务器策略玩家血量、蓝量服务器展示与预测服务器按固定tick计算客户端预测结果可回滚攻击伤害服务器发送攻击意图服务器根据攻防属性重算伤害并广播掉落、抽卡服务器触发请求服务器跑随机算法客户端不传奖品金币、钻石服务器展示缓存每次变更由服务器做原子增减并记录流水玩家坐标服务器发送操作输入服务器按速度上限和移动逻辑模拟位置签到、领奖服务器发起请求服务器查状态重复请求直接拒绝认真看这张表你会发现一个规律越是“简单直接”的数值越不应该由客户端上报客户端最多只能上报“意图”我想攻击、我想移动、我想抽卡而不是“结果”我造成了这么多伤害、我掉落了这件装备、我增加了一千金币。3.2 状态机与频率校验除了数据归属服务器还要对“请求发生的上下文”做校验。空有数值校验不够攻击者还可以钻业务逻辑的空子。状态机校验是其中性价比很高的一环。以战斗流程为例PREPARE - READY - FIGHTING - SETTLEMENT - END服务器持有每个玩家的当前状态任何请求都必须符合状态机迁移规则。比如玩家还在PREPARE阶段就给我发来一个SETTLEMENT的结算包哪怕这个包签名合法、数值也合理也一定是恶意请求直接丢弃并告警。频率校验同样重要。一个正常玩家每秒最多发起几次攻击、每场战斗最多用几个道具、一天最多签到一次这些都是可以在服务器端统计的阈值。频率校验不需要很精确关键是能把脚本行为挡在门外。我见过最离谱的一次攻击一个脚本账号在5秒内发起了400多次攻击请求这种数据模式明显不符合真人行为服务器直接封禁即可。3.3 业务防篡改的落地难度理论说完了聊聊现实。为什么很多游戏做不成服务器权威答案通常不是技术不行而是“前期图省事后期改不动”。很多团队在原型阶段为了快速验证玩法直接让客户端把金币数值、血量数值、伤害数值传上来服务器只做存取。等游戏跑起来、玩家进来了再想改成服务器权威就发现客户端逻辑已经和这些假权威数据深度耦合改动量等于重写战斗系统。所以我的建议是项目立项时就把服务器权威当成一条基础架构约束而不是后期安全加固项。哪怕第一版做简单一点只让几个最关键的数值金币、掉落、伤害走服务器计算也比全部放客户端强。架构上先守住这几条底线后面玩法迭代再逐步收紧。4. 实时PVP场景的防篡改选择4.1 为什么实时对战里更容易出问题前面讲的多是回合制、卡牌、弱联网玩法。实时PVPMOBA、格斗、射击类的防篡改难度要高一个量级原因有两个延迟极度敏感客户端不可能每次都等服务器回包再表现网络环境复杂UDP丢包和乱序是常态。这两个原因直接导致很多标准做法在实时场景里不可用。你不能指望每个动作都做一次HMAC验签服务器裁决那会让操作延迟高到玩家直接卸载游戏但你又不能完全放手让客户端自己算数值不然就是外挂横飞。4.2 帧同步哈希校验经典做法是帧同步Lockstep。所有客户端的操作指令被广播给全场所有客户端每个客户端用完全相同的确定性逻辑跑同一场模拟。因为输入相同、逻辑相同所以每个tick结束后的游戏状态应当完全一致。安全校验点就在这里每个tick结束后客户端把当前状态比如所有单位的位置、血量的哈希值广播出去和其他客户端比对。谁的哈希对不上说明谁的输入或计算过程被篡改了服务器就能锁定作弊者。帧同步的优点是服务器不需要实时模拟整个游戏世界抗读写压力小。代价是对确定性的要求极其苛刻浮点运算结果必须一致、随机数种子必须一致、实体遍历顺序必须一致这些在真实引擎里都有无数坑。为了一个哈希校验你要付出大量工程成本。4.3 服务器权威与客户端预测另一种更稳妥但服务器成本更高的方案是“服务器权威模拟客户端预测”。服务器维护权威的模拟状态客户端为了低延迟会先本地预测——玩家按了移动键客户端立刻让角色动起来同时把操作指令发给服务器服务器在自己的模拟中跑同样的操作算出权威位置后回包如果客户端的预测结果和服务器计算结果不一致客户端回滚到权威位置。格斗游戏《Rivals of Aether》官方分享过类似的回滚方案市面上的商业对战框架也大量采用这个思路。它的安全性来自“服务器永远是对的”客户端预测再准也只是视觉上的“提前表现”。攻击者改不了服务器状态只能改自己的本地显示——而本地显示一旦和服务器冲突马上会被回滚修正作弊对玩家来说没有实际收益。这个方案最大的成本是服务器要承担实时模拟的CPU开销。但对于大多数中小体量的对战游戏一台高性能服务器同时跑几百场对局并不是不可能。4.4 UDP重放防护的细节说完了架构再补一个实时场景下最容易踩的UDP坑——重放攻击。UDP没有连接状态攻击者把一个合法的操作指令截获下来稍后重发一遍服务器很难判断这个包是新的操作还是旧包重放。解决方案是给每个UDP包加上会话令牌、序号和时间戳服务器用滑动窗口维护可见范围窗口之外的包直接丢弃。UDP payload的典型组织方式8字节会话令牌 | 4字节包序号 | 4字节时间戳 | 2字节数据长度 | 变长密文 | 16字节GCM标签注意这里我把GCM标签放在最后。AES-GCM同时提供加密和完整性认证服务器每个tick批量校验一批包的GCM标签比逐个做HMAC更高效。对实时游戏来说加解密开销能省则省。实时场景下不要为了防篡改而给每个包都套上复杂的多次握手和证书交换那只适合低频高价值请求。操作指令类数据包轻量认证GCM或HMAC加序号窗口已经能在延迟和安全之间拿到一个不错的平衡点。5. 加密和客户端加固的边界在哪里5.1 传输加密TLS的真实作用聊到通讯数据防篡改很多人第一反应是“上TLS/HTTPS”。TLS确实能防止传输链路上的中间人窃听和篡改但有一个残酷的常识攻击者根本不需要在你和服务器之间插一根网线他们直接在你的设备上、在客户端进程内部动手脚。最简单的例子Android上有大量hook框架攻击者劫持App内调用的TLS相关函数在数据加密前或解密后就能拿到明文。你加了TLS他们就把TLS Hook掉你加了证书锁定Certificate Pinning他们就用Frida脚本遍历SSL函数逐个绕过。所以我的看法是TLS和证书锁定是必须有的它挡住了最廉价的抓包改包工具但对有经验的攻击者而言它只是增加了一点点门槛不是终点。5.2 客户端加固与密钥保护的现实既然网络层能被绕过那就要回到客户端本身做文章。签名密钥不能明文硬编码这个前面说过了。进阶一点的做法是把密钥藏在so库里配合代码混淆和字符串加密让逆向者不能一眼找到。再进一步可以用白盒密码White-box Cryptography把密钥“溶解”在一大堆混乱的查表运算里让攻击者即使拿到了整个算法也难以还原出原始密钥。但不要对客户端加固抱有不切实际的幻想。Unity的C#程序集用反编译工具可以直接还原IL代码易语言和Java程序也类似。IL2CPP会好一些但也只是把C#转成了C再编译逆向成本增加了并非不可逆。客户端加固的本质是拉高攻击成本而不是做到不可破解。判断加固值不值得做的标准看你的游戏价值。一个单机弱联网小游戏的付费点可能经不起攻击者花两周时间破解但一个竞技手游如果被外挂毁掉匹配环境损失远超加固成本那就值得投入。5.3 一套可落地的防篡改实操清单说了这么多最后整理一份我实际推进项目时会用的落地清单按优先级排列优先级事项说明P0服务器权威校验关键数值、状态机、频率全部放在服务器P0协议签名和防重放HMAC/GCM 时间戳 seq窗口P1传输层加密TLS/DTLS客户端做证书锁定P1会话密钥动态化登录时协商临时key避免硬编码P2代码混淆与关键函数保护提高定位密钥和协议逻辑的成本P2反调试、反注入检测真机检测、模拟器识别、hook检测P3白盒密码高价值游戏或加密需求极高的场景再上P3数据对账与异常监控服务器定期检查数值增量、请求模式、热门外挂特征线上运营阶段我强烈建议加一套异常行为监控。不需要很复杂先记录“同一账号请求频率异常”“关键操作时间间隔异常”“胜负比异常”这几个指标有异常自动打标。很多外挂不是一次就能封完的持续监控能帮你在对抗中占得先机。最后再分享一个我个人的体会每次新项目上线前我都会让测试或QA拿三天时间假装自己是攻击者把抓包工具、内存修改器、脚本重放这三板斧对着线上接口全试一遍。只要这个“伪攻击测试”能模拟出任何一个防篡改漏洞就立刻修复再上架。这比上线后被玩家刷穿再紧急维护要省心得多。防篡改没有一劳永逸的银弹但只要把P0的服务器权威和协议签名做好、把监控跑起来绝大多数攻击者就头也不回去找更软的柿子了。
返回列表