CF中的外挂

游戏中的作弊问题一直是开发者面临的一个严峻的问题,各种类型的外挂层出不穷,在笔者小时候就有许多修改器存在,这些外挂俨然成了影响游戏公平性的罪魁祸首,甚至能在网络上直接搜到某些猖狂的外挂公司。目前大多游戏公司都采用专业的游戏安全公司提供的防外挂服务,例如腾讯的游戏安全ACE等。

虽然安全公司提供了很多服务,但一个不争的事实是:正所谓道高一尺魔高一丈,各种游戏外挂依然层出不穷,射击游戏更是成了外挂的重灾区。防外挂是一项长期工作,需要开发者们与外挂分子斗智斗勇,本文将介绍一些作为开发者的我们可以采取的几种反外挂方案。

防外挂措施

非法输入

服务端防外挂的关键点是——不能信任客户端。因为任何客户端传来的数据都有可能是经过外挂篡改的数据。举个例子:假设一个玩家想要购买某个价值648金币的商品,客户端在玩家点击确认时会向服务端发送一个协议,例如:

1
2
3
4
{
"item_id" : 1, // 购买的商品id
"need_money" : 648 // 花费的金币
}

这时按正常的逻辑,服务端会检查玩家剩余金币是否足够支付,如果足够则扣除玩家金币并添加商品到玩家背包。可是如果玩家利用外挂将协议篡改了:

1
2
3
4
{
"item_id" : 1,
"need_money" : -648
}

这时协议里的所需金币变成了“-648”,服务器会判定玩家所持有金币数(至少是一个≥0的数)大于所需金币,玩家的金币数变为 x-(-648),也就是 x + 648,同时将商品加入玩家背包。如此一来,玩家通过外挂实现了刷金币的操作。

帧同步和分布式状态同步中部分计算是放在客户端上完成的,例如文章一开始提到的射击游戏通常就是采用分布式状态同步实现的,设计者会将一些弱一致性的计算放在客户端本地,这就给了外挂可乘之机。服务端不能信任客户端传来的数据,必须对其做校验,例如检查数据是否合法。在前面的案例中,就有必要检查所需金币与商品是否对应、数据是否在正常范围。


服务端不仅不能信任客户端的数据,还不能信任客户端发包的时机。例如当玩家开启了“无限连招”、“无限技能”或其他类型的“无限(无cd)”类外挂时,就代表玩家可能无视正常的cd冷却向客户端发送了非常相关的包。

因此,有必要在服务端加上对技能冷却时间的判定,下面是包含了技能cd检查的服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
function onAttack(player, skillid)
-- 冷却时间判定
local cd = getCDTime(skillid) -- 获取技能冷却时间
-- 是否还在冷却中
if Time.now() < player.last_skill_time + cd then
return
end
-- last_skill_time代表上次使用技能的时间
player.last_skill_time = Time.now()
-- 其他判定和处理
-- ...
end

无限技能也可能是外挂修改了游戏的内存(直接改动了技能的cd参数),导致客户端认为数据“合法”。面对这种情况,我们需要对客户的进行保护

  • 采用腾讯ACE等工具检测游戏进程内存是否被篡改、防止dll注入、检测第三方工具
  • 我们可以对客户端的内存信息进行加密,或者使用内存地址偏移混淆、陷阱数据(诱导性的假数据)等方式增加作弊难度

广播信息

在状态同步中,服务端计算指令的结果后需要向客户端广播新的状态以实现同步。然而在这一步,我们需要谨慎控制服务端广播的信息,因为发送的信息越多外挂能利用的资源就越多。比如棋牌游戏中,服务端向客户端广播所有手牌的信息,如果协议被破解,那么外挂玩家就可以看到对手的手牌。相应的改进措施也很简单,服务端只向玩家同步其自身卡片的状态变化即可。服务端应最大限度地控制信息发送范围,同时也能降低服务器网络IO负载。

但对于射击游戏来说,由于AOI的存在,需要同步AOI范围内玩家的信息。客户端拿到这些信息之后,就可以拆除其中的模型信息(例如包围盒),然后将其绘制到客户端上实现作弊。通过拿取其他玩家的位置信息,还能实现自瞄等作弊方式。外挂在获取敌人坐标后,发送模型鼠标信号,使游戏程序执行后直接将准心移动到目标位置,当玩家开枪时就会命中对手。

输入在驱动层面很难检测到这样的外挂,但正常玩家与自瞄玩家的行为上有很大区别,即行为数据层面才是解决外挂问题的关键。这也是为什么射击游戏外挂多,因为行为层面不好分析,需要综合大量测试数据进行校验和判定,还经常出现误封的情况。

正常玩家和作弊玩家的行为数据区别

变速齿轮

变速器是最常见的外挂之一,它可以改变客户端的运行速度,从而获取速度上优势。例如:客户端真实运行了1分钟,Time.time 的值理应是60(秒),但由于加速器把游戏速度调高了 1 倍,现在 Time.time 的值变成了 120,程序从每 0.2 秒发送一次协议变成每 0.1 秒发送一次,从而取得速度上的优势。正常每秒执行 50 次的操作将会变成每秒执行 100 次,这对攻击等操作的影响是很大的。

而我们采用的校验方式就是心跳包。客户端每隔一定间隔的时间会朝服务端发送一个用于校验的包,因为其发送时间固定而有规律就像心跳一样,所以我们称其为心跳包(非常形象吧)。因此正常的客户端可能每隔 0.2s 发送一个心跳包,而受到变速齿轮影响的客户端必然发送的更快,所以当服务器监测到某一客户端发送心跳包的时间间隔明显小于 0.2s 时就可以判定其作弊。

心跳包的功能不止用于校验变速,还可以让服务器了解客户端是否掉线。

防封包

外挂通常会利用WPE(Winsock Packet Editor,网络数据包编辑器)等封包工具,这类工具可以截取和修改网络数据包,进而向服务端发送任意数据。一般这种情况,我们会对通信协议的消息体进行加密,同时也会在通信协议中取一个段内容用来判断数据包的合法性,比如下面协议的“密码段(校验码)”。

服务端会要求客户端按特定的格式计算校验码(譬如服务端发送一串数字,客户端在这串数字的基础上根据当前发送协议的次数对数字进行处理),这样虽然简单却可以有效防止封包录制(复制相同的包发送)。

帧同步投票

帧同步的操作是完全依赖客户端运算,很容易作弊,但服务端可以通过投票机制找出作弊的玩家。

1
2
3
4
5
{
"_cmd" : "check",
"frameid" : 10,
"status_code" : 14566455
}

服务端可以要求每个客户端每隔一定的帧数就发送一次状态协议,协议中包含客户端当前的帧数及状态码。如果没有作弊,那么在同一帧时,各客户端应处于同样的状态,状态码也应相同。服务端需要收集所有客户端的状态码,如果某个客户端的状态码不一样,则该客户端的玩家很有可能是在作弊(也有可能是游戏本身的Bug造成的)。状态码是反映客户端当前状态的数值,例如角色的生命值、体力值、位置、攻击力,金币数、道具数等都是游戏的某一项状态值,组合这些状态值便能反映游戏的整体状态。


防外挂的核心要点,就是要尽可能多地让服务端做逻辑运算、尽 可能多地校验客户端的运算结果,不要相信客户端的一切输入。

参考资料

百万在线:大型游戏服务端开发(罗培羽)
https://www.bilibili.com/video/BV1sJ4m1G7Jz/
https://gitlib.com/pages/586420/
https://gwb.tencent.com/community/detail/114770
https://segmentfault.com/a/1190000044035543