前几天有个做小游戏的朋友跑来找我,一脸崩溃:游戏上线才一周,排行榜就被一群金币上亿的号刷穿了。我让他把客户端发给服务器的请求日志拉出来看了一眼,问题一目了然——客户端说“给我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签名包结构设计
协议层防篡改的基础,是给每个数据包加一个带密钥的校验值,业内最常用的是HMAC(Hash-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, digestmod=hashlib.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, digestmod=hashlib.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甚至8;MOBA和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 为什么实时对战里更容易出问题
前面讲的多是回合制、卡牌、弱联网玩法。实时PVP(MOBA、格斗、射击类)的防篡改难度要高一个量级,原因有两个:延迟极度敏感,客户端不可能每次都等服务器回包再表现;网络环境复杂,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的服务器权威和协议签名做好、把监控跑起来,绝大多数攻击者就头也不回去找更软的柿子了。