插件式反外挂的设计
前言
本文分为三部分:第一部分介绍我对反外挂的认识;第二部分是上线游戏反外挂的介绍,包括反外挂的架构、反外挂运营与反外挂的痛点;最后介绍插件式的反外挂(新反外挂架构)
反外挂的认识
我对反外挂的认识可以用下图来展示:
图1反外挂组成
(1) 反外挂由客户端、服务器以及安全组三部分组成。
客户端负责本地数据的保护、接入安全SDK上报手机参数、单局结束时上报统计数据供服务器和安全组校验。
服务器制定策略校验客户端统计数据、惩罚作弊玩家、记录作弊日志方便后续查询统计、将客户端统计数据以安全日志形式输送给安全组。服务器是在版本发布前,根据逻辑功能,制定数据统计结构,预埋策略。一旦发现外网某种作弊方式,通过查询是否有预埋策略可以应对;若有则根据统计日志分析策略是否可以在外网打开,拦截外挂。若无则需要依靠安全组根据外网数据实时制定应付策略。
安全组能够在出现外挂时,根据上报的安全日志以及分析大量外网玩家单局数据,实时制定应对策略。通过调用idip等接口惩罚作弊玩家,清除作弊获利。
上述的服务器策略和安全组策略的区别:服务器的策略是预埋性,也就是预先分析哪些会被作弊,针对这些作弊点制定数据统计结构体,预埋校验策略。特点:预埋性,工作量大,起作用策略百分比低,更新是静态的[需要灰度重启服务器];安全组的策略都是实时性的,是在外网出现了某种外挂才开始做的。特点:实时性,工作量少,起作用策略百分比高,更新是动态的[不影响业务]。
(2) 单条业务策略来说,它又是由产品[70%]、实现[20%]、运营[10%]三部分组成。
产品在确定一个单局内的逻辑功能时,已经确定了该逻辑功能校验策略的70%。比如给定或不给定天花板数值,按规律或者随机产生金币或者道具等。这很大程度上已经决定了业务策略的逻辑。
程序在产品已经决定了逻辑功能行为的情况下,只有20%的空间去制作对应的策略。比如设计通用的统计结构体,通用的校验策略(适应各种模式)。
在完成了70%的逻辑功能确定和20%的校验策略指定后,现在策略已经到了运营的阶段了。运营需要根据外网玩家投诉、论坛反馈以及外挂情况,运营各种校验策略,决定策略的开放。这个时候的工作量只有10%。
反外挂介绍
这里主要介绍反外挂的架构、配置设计和运营工具。
1、 反外挂架构
图2中就是反外挂架构图。
图2 反外挂架构图
有一个AntiCheatMgr管理这所有的策略,所有的策略都继承自同一个虚基类。在AntiCheatMgr中的LoadCfg和CheckOnGameEnd都是遍历的调用对应策略的LoadCfg和CheckOnGameEnd。
该架构的好处是:统一管理,方便扩展
2、反外挂配置
图3 反外挂配置
(1)基本控制字段
ShowCheckReason:用于控制是否在客户端显示策略详情。true显示 false不显示
DisableKick:用于控制反外挂是否生效。 true 不生效 false生效
AutoForbidPeriod和AutoForbidWeight用于控制封号:AutoForbidPeriod为封号时间(s), AutoForbidWeight为作弊权重累积达到该值时封号。
(2)Error ID
ID:错误ID
Priority%:是指对应模式的权重(比如Priority1对应GM_Main)。对应模式不想检查该错误ID,可以直接不配置对应的Priority%,默认为-1
值:-1表示不记录日志 不踢人 不加权重(不配的情况)
0 表示记录日志 不踢人 不加权重
1 表示记录日志 踢人 不加权重
>1 表示记录日志 踢人 累加权重(累加值为Priority%的值)
(3)各个子模块的配置(Score|GameTime|……)
GameMode%=1 对应 GM_Main ,不需要检查该策略的,可以滤过不配,比如GameMode=3[GM_Daily]不需要校验分数,上面也就没有配置GameMode%=3
(4)Kick和Slient是开踢和静默模式;Enable用于控制对应模式开启。Slient的Enable为true的话,相当于整个策略涉及到的所有错误ID都不会踢人,只会记录日志
注意:错误ID上面配置不同模式下面的权重主要针对的该错误ID对应的单个策略在各种模式下生效情况。子模块配置前面的GameMode用于控制的是某种模式是否进入该校验子模块。Kick与Slient可以一次性让该子模块所有的校验策略进入静默模式[不踢人,只记录日志]。
3、反外挂的运营
反外挂会记录反外挂日志。如下图
图4 反外挂日志
有了反外挂日志,配上一套运营脚本可以很方便进行数据的统计分析,从而决定策略的开放,权重设置等。
图5 反外挂扫描工具
cut_idx_log.sh 和cut_idx.sh都是用来裁剪列的,cut_idx_log.sh是以“|”作为列的分割符,适用于直接从日志文件中裁剪数据。cut_idx.sh是以空格 tab键作为列的分隔符,适用于已经从日志文件中裁剪出来的数据。
filt_cond.sh 用于寻找满足对应条件的列表。
stat_idx_section.sh 适用于对某个列进行范围统计。比如对第2列,进行间隔值为100统计。stat_idx_section.sh 2 100 1 。
stat_idx.sh适用于对某个列值进行统计个数。
具体扫描实例:
(1) 扫描2014年10月14日,策略12221被踢的数量(未去重)
(2) 扫描2014年10月14日,策略12221被踢的数量(去重)
当需要去重时,可以直接使用玩家OpenID这列来去重
(3) 扫描2014年10月14日,策略12221被踢 XXX模式的数量(去重)
4、反外挂的一些痛点
该反外挂系统已经很完善了,从架构、配置到运营工具的设计都很不错,预埋的策略也非常多。但是该反外挂系统还有不足之处:(1)每个版本反外挂工作量非常大,因为需要预埋很多的策略。(2)当出现新的作弊方式,同时预埋策略里面没有可阻止的话,如果需要写新的策略阻止这种作弊,那么服务器就需要灰度。
插件式反外挂
插件式反外挂主要是指反外挂代码可以在不灰度服务器的情况下更新;在每个逻辑版本的时候不需要预埋很多策略,只需要预埋一些重要的策略以及尽量多的统计数据。当外网出现某种外挂时,可以实时根据统计数据编写策略,更新到外网。
1、插件式反外挂的方式
(1) 使用配置来实现反外挂
后台校验策略由两部分组成:配置策略 + 后台C++逻辑校验;基本配置如下:
后台有一套根据配置的东西生成对应校验公式。优点:可以根据外网外挂出现情况 实时配置策略 应对外挂。缺点:配置的策略必须比较简单,参数也是有限的,不灵活。
(2) 使用脚本语言(lua|python等语言)实现(参考文献2和3)
整个后台反外挂策略使用脚本语言(lua|python)来编写,这样策略可以动态变化,不需要灰度服务器。优点:可以动态应对外网外挂,随时调整策略,不需要灰度服务器。缺点:脚本语言对复杂结构体的支持比较差,在数据传递方面会非常复杂(比如python需要ctypes lua是靠棧相互传,非常繁琐)。(使用脚本来开发反外挂逻辑还有一个要求就是项目组内的后台成员需要都去学习某种脚本语言,成本会有点大)
(3) 使用so实现反外挂
将反外挂逻辑代码编译成一个so,主进程通过dlopen来加载so,dlsym获取so里面的函数指针,从而调用so里面的方法。优点:so可以动态更新(必须采用mv覆盖,不能够cp.参考文献3),都是c++实现,所以设计会方便很多。
2、新项目项目组的插件式反外挂
图6 插件式反外挂架构
新采用so来实现插件式反外挂;从上图可以看出反外挂的管理类是CAntiCheatMgr,通过定义该类的一个虚基类来将所有的接口暴露给主进程调用。
虚基类CAntiCheatMgrInterface拥有CAntiCheatMgr的所有接口,可以通过dlopen加载so 利用dlsym获取so里面的全局函数地址。比如生成CAntiCheatMgr实例的函数和销毁CAntiCheatMgr的函数。通过这两个函数地址在dlopen时创建so里面的实例,在dlclose时销毁so里面的实例。
当有新的反外挂策略需要更新时,只需要确保协议一致的情况下build so;替换so reload svr;新的反外挂策略就生效了。
SO遇到的一些坑
1、替换so时不能够使用cp,必须是mv。具体可以查看文献3
2、假设加载so的可执行程序是A;如果A和so都使用到同一个结构体,那么更新更新A或者是更新so都是同时更新A和so。否则容易造成两边结构体不一致,导致数据值不对。
3、调试so:直接gdb attach进程 然后打断点进so,是没法调试进so的,需要在dlopen的地方断点;这个时候才会将so的符号加载进来,然后再打so里面的断点。
参考文献
[1] Lua和C++交互详细总结http://www.cocos.com/doc/tutorial/show?id=1474
[2] Python:使用ctypes库调用外部DLL http://www.cnblogs.com/wuchang/archive/2010/04/04/1704456.html
[3] Linux共享库(so)动态加载和升级http://www.piao2010.com/linux%E5%85%B1%E4%BA%AB%E5%BA%93so%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BD%E5%92%8C%E5%8D%87%E7%BA%A7