一、写在前面
1.1 游戏安全评审是什么
随着各类游戏在国内和国际越发火热,外挂和打金工作室等黑产业务也愈演愈烈;
阻止外挂和黑产的主要手段,从攻击产生的时间上可以分为:
- 事先防御:通过游戏安全评审,发现游戏漏洞和可能存在被黑产攻击的问题并反馈给游戏业务方完成修复。
- 事中防御:通过代码保护、协议加密、内存保护等手段,在游戏Apk上部署防御措施,提升黑产活动的攻击门槛与攻击代价
- 事后防御:通过外挂云查进行外挂分析和打击,对黑产账户和违规玩家进行账号封禁或其他惩处措施
一个优秀的游戏厂商,会全面发展上述三种技术手段来对抗外挂黑产。在这三者中,事先防御手段,对于游戏业务的代价较低,可以在造成实际损失之前从服务器侧解决漏洞问题。
1.2 游戏安全评审可以解决什么问题
一款游戏在开发完成上线测试前,可能存在一些日后导致严重危害的安全漏洞如:协议漏洞、服务器宕机、内存变速、隐私合规等。
游戏安全评审就是为了防止游戏在线上运营阶段因以上问题产生安全事件而进行的工作,旨在提前暴露游戏安全风险,完成漏洞修复,不给外挂黑产以可乘之机。
二、游戏安全评审技术演进
2.1 协议重建&fuzzing方案
2.1.1 核心模型
提到协议安全,大家肯定会首先想到协议fuzzing,这也和我们最初的测试思路一致
由于游戏服务器和客户端一般都采用了特殊的格式进行通讯,并且报文内容和报文外部的封装都进行了复杂的序列化与加密,所以直接向游戏服务器发送大量的fuzz二进制数据流绝大部分会被服务器的网关过滤掉;因此我们需要对游戏协议进行重建。
协议重建即在摆脱游戏操作场景的情况下,对游戏的数据流进行完全模拟和重新封装,构建一个合法的游戏链接,发送经过fuzz字段覆盖的测试协议,通过返回包或者游戏表现来确定是否有安全漏洞。测试流程如图所示:
2.1.2 登录态与心跳
游戏业务对于登录态和存活链接有校验,所以我们需要根据游戏校验逻辑的不同,来执行针对性的登录态和心跳模块。
大部分游戏要求的登录态都是唯一的,也就是说,在开启测试工具的同时,原本客户端上的游戏登录态会被挤下线,所以对于测试工具新建立的登录态,我们要保证:
- 登录态的获取逻辑完全脱离客户端
- 完整的实现密钥协商流程
- 按照要求的频率进行心跳包发送(是的,心跳包的频率有时也会影响新建登录态的活性)
虽然登录态、密钥交换、心跳包的维持比较费时费力,但是在这个过程中可以发现很多安全漏洞,例如:不合理的密钥交换甚至固定密钥、固定的登录口令、多登录态等严重问题。
2.1.3 正确的调用链
由于游戏业务在通信上有别于常规业务的特殊性:很多特殊的协议及其生效判定只有在特定场景和环境下才会生效,可以理解为只有正确的协议链条,才会得到服务器正常的反馈和数据;于是我们在进行如下改进。
- 先将游戏客户端挂代理整体运行一遍
- 记录下游戏正常运行过程中的协议调用链
- 根据调用链对协议进行重建和自动读取
虽然这个过程较为耗时,但是可以发现很多在游戏里不会触发的隐藏BUG, 这类问题主要是使游戏进程不按照预定的调用链来进行通信,从而产生的多协议组合漏洞。例如在战斗中进行跳关和在商店中开启战斗等;
2.1.4 游戏自研DSL的处理
对于重建协议的方案来说,大部分的模块都是通用的,尤其是协议解析和序列化的部分;但是在个别情况下,除了采用常见的protobuf、json等数据系列化格式外,游戏还会使用自研私有DSL协议。
针对自研协议格式的游戏,我们进行了一些研究,发现大部分私有协议格式本根同源,对于将明文数据序列化成二进制流的关键点上的差异不大,于是我们对这些通用的部分进行整合,开发可以通用的序列化与反序列化接口,设置几个可选参数来适应不同的格式,这样就可以适配大部分的私有协议了。
OK,到这里为止,我们就可以对绝大部分接入安全评审的游戏进行协议安全测试了,但是该方案还存在一些问题,看到这里,相信有的同学已经发现了这个方案中的一些不足。
2.1.5 方案局限性
根据评审工作的发展,发现本方案存在的几个问题:
- 不能直接在游戏中看到漏洞的展示效果,在我们发送一条构造了特殊字段的测试协议后,预期得到服务器负面(否定)的反馈,但是不同的游戏设计的反馈不同,有的游戏不反馈,有的游戏只反馈一个错误码,这对于判断测试字段是否有效(有效是指服务器没有拒绝我的错误字段而将他直接执行了,比如我告诉服务器,我出生了,我满级了)是一个很大的障碍(如下图中展示)
- 协议接入的时间较久,需要对协议链条的各类细节掌握透彻,才可以将工具完美的适配游戏,而对于一些急于上线的游戏,这个评审周期可能会影响到发行进度;
- 对游戏更新适配性较弱,在我们对一款游戏完成首次测试后,游戏的登录态、登录流程、密钥交换的逻辑会有很大的改变,而协议重建又是个费时的操作,所以这会影响后续的增量测试效率;
这些问题都是由于本方案的局限性导致,那么,我们就要考虑在后续的方案中进行升级;
2.2 基于中间人劫持的工具方案
2.2.1 核心模型
我们在上一段提到了,新方案主要是为了提升漏洞发现的效率,所以我们首先需要解决的问题是不能直观的看到漏洞在游戏中的表现效果。为了达到这个目的,就需要一个可以不脱离客户端的测试方案:将数据流的起点和终点定位到客户端和服务器,而测试工具通过中间人的思想,在信道中进行测试操作,不关心客户端上的实现和协议链。
设计的大体模型如下:
为了实现tcp中间人的操作,我们调研了使用 iptables&pfctl 进行端口转发和使用代理进行流量转发等几种方式,经过一段时间的摸索和验证,最终采用了socks5代理服务器的方式来完成数据流的转发和中间人操作;
选择socks5代理的原因是它很简单,由于其不关心协议种类,只是简单的进行数据包的传递,所以socks5速度较快,不会影响到游戏的正常运行,另外socks5代理对测试客户端的环境没有特殊要求,相比端口转发的方式更加轻便和快捷。socks5代理方式的工作流程示意如下:
我们使用了一个socks5的代理客户端,并在测试端上搭建一个代理服务器,测试逻辑主要运行在代理服务器中;预期本次设计的方案,其核心架构图设计如下:
2.2.2 数据修改与重发
现在我们已经可以通过新的工具方案用代理的模式正常的进行游戏了,并且还可以在工具中看到游戏的上下行数据流,但是为了达到测试目的,我们还需要将数据管道中的数据拿出来,经过修改后再写回数据管道,这里的解析和封装模块与上一个方案一致,不再详细展开;
我们通过对本地测试文件进行改写,来控制测试的流程和目标,现在我们的新方案已经初步成型了:
2.2.3 协议劫持与主动发起
但是我们目前还有一些问题存在:
中间人方案能进行测试的协议都是由游戏客户端发起的,例如购买商品,所以无法绕过客户端的限制比如金币不足时无法发起购买,但实际上游戏服务器中可能存在金币不足购买不扣费的漏洞;简而言之,就是只依赖中间人方案测试覆盖面不够:虽然可以覆盖到全部的游戏协议,但是不能覆盖游戏在特殊情况下存在的问题;
我们希望,可以在任何情况下都可以主动的发起对一条协议的测试,从而绕过客户端上的逻辑限制,深度挖掘游戏服务器中的问题(还是那个原则,客户端不可信)。这里就有一个很巧妙的办法:协议劫持。
而劫持的具体做法是:寻找一条简单的,不会受到客户端逻辑限制的协议,比如游戏里的动作、表情等。因为这类协议对于游戏安全来说是冗余的,安全可靠的。
使用中间人工具进行测试的工作流程如图所示
2.2.4 方案局限性
基于中间人的测试方案可以在游戏客户端上直接看到bug的展示效果,这很大程度上提升了漏洞的发现效率;但相对于上一代方案来说,还是有两点问题没有解决:
- 协议接入时间较久,因为协议包的解析还涉及到粘包和拆包等问题,较为复杂, 不仅需要对数据层面进行解析,还要对协议层面和数据流层面进行解析。工作量较大。
- 对于新游戏的二次适配消耗较高,在进行增量测试时,游戏往往已经根据业务需求和首轮安全评估结果修改了加密和协议方案,该方案很可能和当前接入的协议流程不一致,所以需要对协议流程进行重新的解析,较为耗时。
以上问题的产生,都是由于协议解析类方案的局限性而导致的,想要进一步发展,就需要从根本上抛弃原有的思路。
2.3 基于客户端注入的劫持和伪造工具(GameDancer)
为了解决以往方案中的不足,我们准备对测试方案进行重新设计,结合以往的测试经验,重新设计的方案的目标是:
1、不在数据流层面进行操作,较少的在协议层面进行操作,直接操作可以看到的明文数据内容。
2、测试协议的组件过程尽量在加解密之前,因为加解密方案是多变的、难以固定适配的。
3、可以直接在客户端上看到漏洞的展示效果,不需要脱机运行。
4、可以方便的主动发起测试协议,用非劫持的方法来向游戏服务器发起测试协议,并且不需要对游戏本身的包序号进行管理。
5、测试工具在前端展示完整解析的协议格式和数据,进行测试数据推荐和fuzz自动覆盖。
2.3.1 核心模型
为了达到以上的设计目标,我们放弃了原有的协议方案,通过代码注入的方式来开发一款新的游戏安全漏洞挖掘工具——GameDancer。
设计的主要思路是通过hook游戏逻辑中协议交互的接口,拿到游戏的交互明文数据,对数据进行解析后,通过主动调用游戏接口,将测试协议通过游戏客户端发往游戏服务器。
说的简单一点,对于这套测试方案来讲,游戏客户端就是一个给我们工具提供协议解析和协议发送服务的模块,将这部分复杂的工作通过游戏本身来自动化的完成。其整体的设计模型如下:
2.3.2 前端UI
工具前端是与安全评审人员主要的交互场景,也是决定使用效率的地方。为了提高漏洞的发现效率,我们需要完成以下目标:
- 针对不同类型的协议字段进行可能导致BUG的字段值推荐,减少评估人员填入数据的环节
- 可以对某条或者某几条协议进行快速自动fuzz的高级发送功能,一键覆盖可能的问题
- 针对测试环境的不同,提供PC版和网页版前端界面,PC截图如下:
前端主要和核心框架中的hook引擎进行数据交互,数据交互流程较为简洁;
2.3.3 加载器和hook引擎
加载模块主要提供注入游戏的功能,需要将包含游戏脚本hook引擎功能的库文件加载到游戏同一个进程和命名空间下,使我们自定义的游戏测试逻辑可以注入到游戏上下文当中,从而对游戏原生逻辑进行修改和操控;
加载器有两种技术选型,一种是针对root环境,使用附加Zygote的方式来完成注入;另一种是针对非root环境使用虚拟多开技术进行注入,root模式对测试终端的环境有一定要求,但是其适配的游戏范围广;非root模式不额外需要root环境,但是对于有些游戏和机型需要特殊的适配。
注入Hook引擎的流程如下:
hook引擎中包含了针对各类游戏引擎及其技术选型的hook支持,可以对游戏脚本进行注入和替换,是该套技术方案中的重点部分。
以Unity3d引擎mono选型为例,主要方案是借助libmono.so本身的接口和Mono的JIT机制;
- 首先加载我们自己逻辑的模块,这里是我们用C#开发的dll文件。
- 通过mono_class_get_methods 定位到我们要替换的目标方法、替换方法,以及代替原方法的空指针。
- 接下来借助libmono.so 的mono_compile_method 进行JIT编译,得到native code 返回地址。
- 完成方法替换,即hook了目标逻辑。
2.3.4 逻辑层
逻辑层是控制游戏运行逻辑和游戏协议管理的核心,在逻辑层我们需要完成以下预定功能:
- 版本适配
- 因为游戏开发商很多,其所使用的Unity的.Net FrameWork版本也不同,高版本的.NET特性无法在低版本运行。
- 由于需要引入游戏的逻辑,所以想要解析游戏的脚本作为动态库,那么需要适当的版本来支持解析。
- 协议解析&数据上报
- 因为这里hook的游戏接口不同,需要根据不同的情况,将数据解析成明文数据,一般是针对proto和json的解析
- 数据上报即将解析完成的数据以Json的形式传递给UI前端,这里最大的问题在于Json的构建,由于游戏Unity版本的问题,有的.net版本不支持通用的Json库,所以需要根据游戏的运行环境来选择对应的Json代码
- 热更脚本交互(Lua)
- 除了在C#层进行hook之外,还需要对游戏的热更脚本进行操作,因为一部分游戏的主要逻辑存在于热更脚本中,例如C#与lua的交互流程如下
- 线程监控,持续注入;在一些自定义脚本引擎中,一段时间后会清空注册过的脚本方法,所以需要利用MonoBehaviour等自动创建实例按帧调用的方式来监控脚本方法的注册情况。
- 回调收集;
- 由于游戏的协议设计机制不同,有的游戏在收到返回包后根据返回包的内容进行页面渲染,有的游戏需要通过发包时的回调来完成对应的状态改变,所以需要对发包时的回调进行收集并保存。
- 需要额外注意公开枚举器IEnumerable与yeild的调用流程,这里的流程不可打断,不可单独调用。
2.3.5 未来展望
GameDancer的开发,并不只是提供了一款快捷、高效游戏安全评审工具,更重要的,它提供了一个游戏注入平台,根据一定的文档指引,相关人员可以开发自己的游戏插件来控制游戏的逻辑走向、观察游戏实时性能数据、了解游戏BUG产生的详细情况;这些也是我们对工具未来发展的期望;
2.4 小结
各个方案虽然是一代代的演进过程,但是旧方案并不会被淘汰,每一个技术方案在漏洞挖掘的过程中都有其独特的用处:基于客户端注入的方案虽然高效。但是无法发现游戏加密方案和登录流程在设计上的安全问题,这类问题仍然需要通过协议重建来发现。
在安全评审工作中,我们会采用多类型的工具对游戏进行多角度的安全评估,保证在评审覆盖的过程中不留遗漏。
三、结束语
“工欲善其事必先利其器”,如你所见,通过技术方案的不断演进,游戏安全漏洞的挖掘效率和漏洞质量得到了提升;从最开始为了快速开展安全评估工作的协议重建方案,到后来可以直观看到漏洞效果的中间人方案,再到GameDancer方案,每一次的方案升级都是为了更好的解决游戏安全评审业务眼下所存在的问题和局限。
游戏安全评审是一项繁琐的工作,漏洞的种类及其产生条件、原因不一而足,想要尽可能全面的覆盖一款游戏中的安全问题,需要评审人员耐心与细致的检查,更加需要技术能力和高效工具的支持。在游戏安全评审技术方案的进阶之路上,“追求极致”是激励我们不断前进的动力。
如果你也想参与到我们的游戏安全工作建设中来,体会在各类游戏中发现漏洞的乐趣、享受和黑产外挂斗的其乐无穷,欢迎加入无恒实验室,简历投递:https://job.toutiao.com/s/ee1qpHt ;或者通过二维码直接投递: