基于战斗重演的全校验---- 塔防大师PVP反外挂设计

发表于2015-10-28
评论2 2.3k浏览

原创作者:鹿宝生

1 背景介绍

    《塔防大师》是一款以英雄对战为主的塔防手游,其中最具特色并最受玩家喜爱的玩法为以塔防为核心,玩家自编关卡的PVP抢夺。《塔防大师》PVP塔防战斗中战斗属性计算、攻击伤血等都是在客户端实现,会带来外挂作弊的风险,同时在PVP战斗中,进攻方可以通过抢劫玩家获取资源和奖杯,从而争夺天梯排名,并通过排行榜领取极品装备,因此,PVP战斗过程的外挂作弊,动力也非常大。

    为了保证了整个游戏生态的健康和正常玩家的游戏体验,PVP战斗的反外挂校验,成为整个游戏成败的重要基础,并在开发中给予了极大重视。

2 反外挂现有技术分析

2.1 常见外挂

    目前塔防类游戏的外挂类型主要有:

  •           修改塔防元素的属性。比如士兵和塔的攻击力、怪物的防御力等。
  •           修改金币变化。比如升级塔可以加金币或者不扣除金币。
  •           改变技能释放CD。比如可以无CD释放技能。
  •           修改游戏速度。比如降低编辑防线中的怪物序列通过塔防的移动速度。

    除了以上比较常见的外挂类型,目前非常难预防的外挂是注入式外挂。采用类似反汇编的方式,改变程序执行的流程。下面举一个例子:

    1)外挂通过反汇编代码分析,找到造塔消耗金币的函数地址getBuildTowerCost:

    2)游戏中启动PVP后,会自动启动外挂调试器,并在getBuildTowerCost返回处下断点。

    3)在游戏中点击建造任一建筑物时,会停在断点处。

    4)外挂程序修改函数返回值为-0xfffff,然后去掉断点,继续运行游戏;此时金币已被修改:

    由于此类注入式外挂可以通过反汇编代码分析,找到任何函数的地址,并更改函数返回值,因此此类外挂是非常难以防范的。

2.2 常见反外挂技术分析

    前面介绍了塔防游戏的几种常见外挂,接下来描述一下公司内部游戏工作室和安全部门的一些反外挂方法,这些反外挂方法往往已经比较成熟,具体可以参见参考文章中的相关内容,在这里主要分析不同反外挂方法在具体实施过程中的局限性。

2.2.1 客户端常用反外挂策略及其局限性

      客户端的反外挂,主要为防止内存修改,通常采用关键内存数据偏移或者mask模式存储、资源和协议加密等方式。其局限性为:

  •           客户端所采用的任何反外挂手段,都只能提高破解的门槛,以通过数值偏移存储来防止关键内存数据被修改的反外挂手段为例,这样的处理的确可以有效防止使用数值搜索修改的外挂程序来保证安全,但是无法防范外挂作弊器使用反汇编注入程序的方式,直接修改已经从内存读取并且还原后的数值。
  •           由于反外挂代码改动频繁,客户端侧的校验,势必会增加客户端的二进制版本更新次数,然而手游的更新会受到如渠道发行方的多方限制。

2.2.2 服务器常用反外挂策略及其局限性

    服务器端的反外挂校验,通常是服务器对关键的流程进行正确性验证。客户端以日志方式记录战场产生的关键过程数据,在战斗结束后上报服务器校验。服务器在校验时,又大致有精确计算校验和粗略计算校验两种方式:

    1)在精确计算校验中,服务器需要完全按照客户端计算逻辑,重构相应的战斗数据,与客户端上报内容进行对比校验。通常类似《塔防大师》这种重度手游的战斗系统非常庞大,由此带来这种精确校验的缺陷是:

  •           服务器需要维护客户端的全量战斗资源,导致游戏资源开销大幅度增加。
  •           服务器需要维护与客户端完全一致的计算逻辑,否则会出现误报,导致服务器复杂度增加,并且依赖于客户端代码逻辑。
  •           上报数据校验正常,无法代表游戏中战斗正常,因为有可能异常数据在上报服务器时又被改为正常数据。
  •           上报数据由于是抽样数据,无法代表整个游戏过程中数据的正确,而整个游戏过程中的数据由于数量巨大,无法以日志方式上报服务器。

    2)在粗略计算校验中,服务器会设置一定范围的容差数据,超过容差值则视为作弊。这种粗略校验的缺陷是:

  •           精确度不高。
  •           容差范围很难控制,容差过大容易漏检,容差过小容易误检。

    考虑到PVP战斗对整体游戏的重要性,以及目前常用反外挂策略的局限,《塔防大师》创新性的采用了基于服务器战斗过程重演的全校验,从根本上解决了当前常用反外挂策略的局限,保证了PVP战斗的公正性。

3 全校验系统设计原则

    《塔防大师》反外挂的全校验系统,基于服务器对于PVP战斗的完全重演,一些设计原则如下:

  •           考虑到反外挂校验的根本目的是为了保证游戏公平性,基于塔防战斗重演的全校验,只对整体战斗的合理性做校验,不纠结于特定数值的校验。
  •           考虑到校验系统改动比较频繁,并且战斗完全重演开销较大,会影响正常游戏效率,全校验系统需要与正常game server分离,作为异步旁路系统运行。
  •           为了保证校验的准确性,并减少服务器对客户端代码依赖度,PVP重演中的塔防战斗逻辑,服务器使用的代码与客户端完全保持一致。
  •           考虑到客户端的多版本特性,服务器需要支持不同PVP战斗塔防逻辑的战斗重演。
  •           减少客户端与服务器交互数据量,客户端仅上传手动操作的记录(造塔、升级塔、卖塔、使用技能),服务器基于这些操作,进行PVP战斗的重演。
  •           减少客户端数据依赖性,服务器端不信任任何客户端数据,对客户端上传的操作记录,需要进行合理性验证。
  •           校验的结果输出,需要能够作为后续反外挂运营的证据和支撑。

4 全校验系统整体架构

 

    全校验系统整体架构如上图所示:

    1)进攻方客户端在PVP战斗结束后,向游戏服务器game server发送战斗结束请求,请求中包含战斗中进攻方的手动操作记录,以及当前战斗结束信息(剩余血量、剩余金币)。

    2)如果进攻方胜利,game server将进攻方的操作记录、以及双方玩家的相关属性数值,发送到旁路的全校验服务器check server。

    3)check server基于进攻方操作记录和双方的属性,进行PVP塔防战斗的完全重演,并将重演后的结果记入DB,供运营处理。

    4)check server对于PVP战斗的完全重演,与客户端基于同一份svn的塔防逻辑代码。

5 系统难点处理

5.1 客户端塔防代码在服务器的移植

    全校验系统开发之初,客户端塔防代码的情况是:

  •           基于自研引擎,C++实现
  •           仅支持win32,android和ios平台
  •           仅支持32位系统

    服务器的塔防战斗重演为了和客户端使用同一份svn代码,需要将客户端塔防代码移植到服务器的linux平台。

5.1.1 编译项目的移植

    客户端代码项目基于vs2008构建。项目组一共尝试了两种方式将基于vs2008的客户端塔防代码的项目移植到linux。

    最初项目组试图使用sln2mak,关于sln2mak的使用,可参考【5】。由于sln2mak直接生成的makefile文件在项目较大时不宜维护,并且有目标源文件损失的问题。最后项目组使用cmake,完成了vs2008客户端代码到linux的编译项目移植。

    由于服务器只需要共享客户端塔防逻辑代码,具体在定义CMakeLists时,需要将客户端相关目录包含的无关源文件移除,如:

5.1.2 64位系统的移植

    由于客户端的塔防代码最初只为32位系统设计,为了适应服务器端64位应用,项目组对客户端代码进行了64位移植,主要修改了以下几类代码:

  •           指针赋值给int会出现截断,如:

  •           long提升为64位,long与int的混合运算会引起潜在符号转换,如:

  •           Log格式化时使用%ld和%lx防止64位整数或者地址输出截断

5.1.3 去外设化

    客户端塔防代码外设操作采用插件式框架,实现win32、android和ios不同平台的外设操作。在移植到linux时,项目组新增linux平台外设操作的插件代码,并在插件代码中屏蔽了图形渲染、声音播放等外设操作。

5.2 塔防逻辑在客户端与服务器端运行一致性保证

    客户端塔防代码完全基于C++实现,实现服务器linux移植后,为保证塔防战斗重演完全一致,项目组在测试阶段,以游戏帧为单位,对同一场PVP战斗在客户端和服务器的运行结果,进行了严格比对,保证塔防逻辑在客户端与服务器端运行一致性。

    过程中修复了很多同一份代码跨平台运行结果不一致的问题,主要包括:

5.2.1 消除随机数序列跨平台下的不一致

    客户端塔防逻辑中,每一局PVP战斗,均使用一个确定种子的随机数序列,用来生成攻击、路径、动作等随机行为。

    客户端代码基于C++实现,使用了rand()来生成随机数序列,由于RAND_MAX在不同平台下并不相同(如,win32下为2^15-1,linux下为2^32-1),所以即使是相同种子,使用rand生成的随机数序列在不同平台的差别也非常大,从而会导致整个塔防过程跨平台不一致。

    一种解决方式是引入BOOST中的随机数生成器,但是这样会引入较多外部依赖,增加客户端复杂性。

    具体项目组的解决方式是,基于线性同余数发生器算法(参见【6】),实现了一个比较简单但是跨平台运行结果一致的随机数生成器,消除了塔防战斗过程因为随机数序列导致的不一致问题。

5.2.2 解决float类型计算跨平台下的不一致

    客户端代码使用float类型坐标体系,同时在具体战斗逻辑中包含很多float和int数据类型的混合操作,此类操作在不同平台下,会出现不一致的情况,如:

    以上代码_totalGameTimeForPVP的值,linux平台下为20,win32客户端平台下为19。

    对于此类问题,项目组采用了以下处理方式,基本解决了浮点数混合运算跨平台不一致的问题:

  •           float与int类型混合运算,尽量将float类型数据转换为double,再进行混合运算。
  •           为常量浮点数据增加精度,如将0.02改为使用0.0200001。
  •           减少浮点数系统函数调用,如ceilf、rintf。

5.3 服务器塔防逻辑的优化

    解决了客户端代码服务器linux的移植、以及跨平台一致性问题后,项目组发现塔防战斗在服务器端的重演存在比较大的性能问题,比如一局4分钟的PVP战斗,在服务器端重演需要大致1分钟左右。使用gprof对服务器重演校验性能做了一个初步分析:

    依据gprof输出并分析客户端代码逻辑,塔防逻辑中的动画演算、图形更新,占用了相当多的cpu执行时间。由于客户端代码中,塔防战斗逻辑与动画演算、图形更新代码耦合比较严重,并且存在战斗逻辑依赖动画演算的情况,项目组采用了预定义宏的方式,在服务器端的塔防回放逻辑中,去除了与战斗逻辑无关的动画演算和图形更新。以下例子是在服务器逻辑中去除怪物行走时的水波动画的情况:

    经过优化,服务器端重演单局塔防战斗的时间控制在了ms级别。

5.4 客户端塔防多版本的应对

    由于手游的多二进制版本特性,外部可能存在多个版本的塔防战斗逻辑,服务器需要支持不同PVP战斗塔防逻辑的战斗重演。具体方式如下图所示:

    多版本处理的核心要点为:

  •           客户端每次修改塔防逻辑,发布新的二进制版本,会对当前代码打svn tag,服务器与客户端共用所有的trunk和tag代码,并在服务器端分别编译生成动态库so,由check server统一加载维护。
  •           客户端PVP战斗结束后,会在战斗结束请求中带上当前客户端塔防代码版本号,check server依据版本号,选择相应塔防代码动态库so,所有塔防逻辑动态库,都以统一的api接口供check server使用。
  •           客户端修改塔防逻辑发布新版本时,服务器端同步制作对应的动态库so,并通过信号方式,通知check server动态加载新的动态库,check server不停机服务。
  •           客户端较老版本会被强制升级到最新版本,该老版本对应的塔防代码动态库,也会在服务器端以信号的方式,通知check server卸载。

5.5 扩展

    由于check server对于PVP塔防战斗的重演全校验,存在执行开销不确定性(客户端玩法的不断更新可能会带来额外的重演性能开销),因此check server需要具备并行扩展能力。

    在具体使用中,check server以集群的方式提供服务,game server通过以下方式对check server进行访问:

    目标checkserver地址 = hash(玩家uid) % 当前checkserver数目

    checkserver在并行扩展时,game server只需要重新加载配置,无需重启。

5.6 运营支撑

    实际运营中,check server基于战斗重演的校验,判定玩家外挂作弊的原则为:

  •           对玩家上传的战斗过程操作记录,在战斗重演过程中做合理性验证,如果出现异常,如金币变为负值、同一个位置造多个塔、同一个塔卖了多次等,判定当前战斗作弊。
  •           Checkserver只对进攻胜利的PVP战斗进行校验,如果服务器端战斗重演的结果为进攻方失败,则判定当前战斗作弊。
  •           服务器端战斗重演的结果如果为进攻方胜利,则将战斗重演结束时的剩余血量和剩余金币,与客户端上传的实际战斗结束时的剩余血量和剩余金币进行比对,如果不一致,则判定当前战斗作弊。

    由以上原则可以看出,基于塔防战斗重演的全校验,只对整体战斗的合理性做校验,并没有像传统客户端上报服务器日志的校验方式,去进行特定战斗数值的校验。因为在《塔防大师》项目组看来,校验的根本目的是为了保证游戏公平性,服务器端战斗重演结束后的剩余血量和金币,如果与客户端实际战斗中一直,即认为当前战斗是合理的。这种情况下,即使客户端使用外挂作弊造成一些局部属性异常或者伤害异常,该作弊也不会影响战斗合理性,因为不使用外挂作弊,整个战斗的结果也是一样的。对整个游戏的公平性和生态,没有任何影响。

    check server全校验的结果,会以异步的方式,落地到DB作为后期运营操作的支撑,DB存储包括客户端实际战斗结果、服务器战斗重演结果、判定结果、时间信息、用户信息等:

    具体的运营策略,《塔防大师》还是沿用了现有成熟的一些策略,如:

  •           公告通知。
  •           首次外挂作弊,只警告。
  •           多次作弊进行3天封号,并回滚资源。
  •           解封后重新发现作弊,永久封号。

6 总结

    本文描述了《塔防大师》在反外挂作弊中遇到的挑战,创新性的实现了基于战斗重演的全校验系统,并详细描述了整个开发过程中遇到的难点和实际工程上的解决方法,最后,分享一下项目组对此类系统的反思:

  •           整个全校验系统开发期间,客户端代码的移植、跨平台运行结果一致保证,占有了绝大多数开发时间,根本是因为客户端代码完全基于C++实现。如果客户端代码中的塔防战斗逻辑,采用嵌入跨平台脚本语言如LUA来实现,则服务器全校验,只需要公用LUA脚本实现战斗逻辑重演即可。类似有全校验需求的游戏,可以考虑将校验部分需要公用的逻辑,使用LUA此类跨平台脚本语言来实现。
  •           考虑到服务器自身工作特性,即便客户端与服务器公用同一份代码,服务器端进一步的精雕细琢也必不可少,如:通过宏定义剔除逻辑无关并且影响性能代码分支、确保内存使用安全无泄漏、保证客户端代码使用的封装和独立性等。

    本文到此为止。《塔防大师》在反外挂的道路上才刚刚起步,整个项目组所面临外挂作弊的挑战也会越来越严峻。在整个反外挂系统不断完善的同时,对于本文所述现有反外挂技术缺陷、基于战斗重演的全校验技术和系统构架、跨平台移植和优化的相关思路,希望可以有机会进一步与各位探讨。

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引