游戏开发安全问题

这几年开发Unity手游时,陆续会针对安全问题做一些处理,在这里分享一下我们的一些做法。我将安全问题分为四大块,每一块都有很多可以深入挖掘的点,也有专门做安全的部门或第三方支持。这里更多介绍的是一些项目组会用到的安全策略。

—— 数据安全

数据安全首先需要处理的是内存安全,面对大量的内存扫描,内存修改工具,如果项目内的核心数据是裸放在内存中的话,肯定避免不了被改属性或者锁血这些问题。 内存安全我们的具体做法是把关键数据int或float的4字节随机映射到一块36字节的空间内。

这36字节的最后4字节是一个float,用来存一个随机生成的整数。根据这个整数我们设计一些规则换算出4个值范围在0至31的下标来标识前32个字节中,哪4个字节存的是真正的有效数据。读的时候我们就通过key换算出v1、v2、v3、v4的位置,取出这4字节后用一些简单规则还原出原本的int或float。写的时候我们先把前32个字节都以随机数填充,然后生成一个合法的float来标识这4个位置,把我们需要存的4字节数据混淆后分别放入4个格子中。这样做的好处是读的算法简单高效,写的时候整片空间都会发生修改,无法判断哪里存的是有效数据,有效数据也不以最初的二进制写到内存中,从而避免被扫描修改的问题。还有一个细节问题是,C# 中byte与int/float互转的接口是会有内存分配的,频繁调用的话会有GC的问题。针对这种情况我们可以做一个临时中间值,用中间值来保存结果,然后给中间值一个有效时间(1、2秒),在有效时间内的读可以直接读中间值,超过有效时间后丢弃原来的中间值,重新生成新的中间值并利用上述的读方法从36字节中解码出中间值。中间值应该每次从堆上重新分配,避免在固定位置被扫描锁定的情况。如果是用C++实现的话就没有这个问题,原本的算法就足够高效并且不会有额外的GC问题。

数据安全的第二点是配置表安全,明文的配置表应该单独放在项目工程以外的目录,通过编表工具编译成密文后再放入到项目工程中。这样能避免被解包后看到明文配置表的问题。配置表编译我们应该做到明文表中任何一个字节发生变化,都会影响密文表全部发生变化,这样做有两个好处:一是配置表更新前后变化大,不容易分析;二是很多配置表明文的前几个字节会相同,采用这样的处理可以让每一张密文表都不会有相同的前部分,增加分析难度。具体的算法分享一个简单的思路,从头到尾遍历明文表,通过加和或连续异或得出一个值,再利用这个值从头到尾处理整张表。这样做就是通过整张明文表计算一个加密key,再利用该key对整张表进行加密。另外,针对协议表等特别重要的文件,可以采用分割改名等方法再额外处理一层,增强安全性。

第三点是lua代码的安全,项目中的lua代码也不应该明文放到包内,处理方式可以先编译,然后采用上述配置表加密的类似方式进行加密后再放入客户端内。

—— 包体安全

Unity打包容易被破解是一直以来困扰开发者的大问题。最初如果采用mono编译的话,连代码的注释都能逆向出来,跟明文是没什么区别的。采用il2cpp的编译方式会稍微好一点,但还是很容易逆向出绝大部分明文代码。后面有很多第三方公司会做这方面的服务,采用各种各样的技术来保护代码及资源。Unity从2018.4.23开始,对于中国区的用户额外提供了一个Security Build的功能,能够提供加密打AssetBundle以及加密编译代码的功能,安全性会提高很多。具体的技术细节在2019的Unite大会上有一场专门的介绍 《一步解决 Unity 游戏新安全风险》,感兴趣的可以找相关资料看看。值得一提的是,即使抛开安全性的考虑,il2cpp的编译在性能上也会比mono更有优势。大家应该尽量选择il2cpp的编译方式。

在大公司内,项目组打的都是子包,真正对外的包还会经过统一的加壳处理,添加各种渠道信息和进行一些安全保障。最终对外的包上通常会附带一些检测程序,检测当前是否有运行黑名单内的修改器,如果有会不允许应用继续使用。这样的机制也能起到一定的保护作用。

还有一项处理就是代码混淆,针对C#的代码混淆推荐微软的Roslyn库,VS的搜索,分析,重命名等功能就是基于这个库实现的。不过一些特殊的写法会使得查找引用不完全,从而导致混淆后编译不过。项目组内要针对这些问题逐一做特殊处理。另外代码混淆怎么接入到项目的开发和出包流程中也会有很多工作流上的问题,在不同的项目组中会有不同的情况。关于代码混淆这里只作简单介绍,不详细展开。

—— 通信安全

通信安全涉及到前后端通信机制以及通信协议的设计。首先在网络上传输的数据肯定不可以是明文的,一定要做一些加密处理。在做这些加密处理的时候需要考虑几个点:第一个是同客户端的包重放问题。简单说就是具体某一条协议被抓取下来后,重复发一样的内容到服务器上,服务器是否会认。这就要求我们在协议设计时需要有序号,服务器判断小于等于的序号的包就不认。有了协议号以后,前后内容一样的协议在加密后网络传输的数据也会变得不一样。第二个需要考虑的点是不同客户端的包重放问题。不同客户端即使从登录开始进行一模一样的行为,发送的协议序号及内容都是一样的,在传输上传输的密文也应该不一样。这样做是为了避免外挂把协议抓下来后进行重放自动进行游戏。具体的做法是在登录成功后由服务器为每个客户端安排一段特殊的CheckCode,每个客户端利用自己的CheckCode去加密。这样把A客户端的密文抓取下来给B客户端发也是无效的。除了考虑序号以及每个客户端专属的CheckCode以外,我们还可以在协议的加密中加入随机的因子,这样做的好处是可以让同序号同CheckCode的协议内容也有不一样的密文表现,通信安全可以更有保障。如果序号安排在一个较小的范围内重复使用的话,就需要考虑序号相同的情况,如果序号安排在一个非常大的范围则不需要做这些处理。

在上面我们说了一条协议应该要有序号,随机因子以及协议号。如何将这三个整数保护传输,我们采用的方式是通过一些计算将三个整数变成一个浮点数存放在4字节大小的头中。这里就要考虑浮点数精度的问题,如果不注意的话有可能从浮点数转换回来的三个整数会与原来的不一样。前面有一篇文章是专门讲这个的,有兴趣的可以看看《浮点数二进制表示》

最后服务端针对客户端发的协议应该要有一些拦截策略,第一个是针对单个客户端发送频率的拦截。例如单客户端通信频率不应该超过1秒10条,如果超过了就认为该客户端异常,直接断开与该客户端的连接。具体拦截频率要看需求,客户端对短时间内产生的大量重复协议也应该做合并,节省流量以及避免触发断线。第二个是针对所有客户端的某些特定协议拦截,这个更多的是作为一种应急机制来设计。例如服务端发现某个流程出现问题,存在被刷的漏洞时,可以直接设置对所有客户端都不响应相关的协议 ,减少损失的同时不影响其他玩法。修复完成后再将这些拦截去掉。

—— 防作弊策略

最后介绍一些通用的防作弊策略,第一个是客户端加速的检测。针对客户端加速的检测很简单,我们在自己的逻辑中会有一些定时上报的协议,服务器判收到这些协议的频率是否在合理范围内,就可以判断出客户端现在的速率是否正常。第二个是数值校验,客户端应该定时上报角色的数值,服务端进行校验,如果有被修改的情况服务器可以及时处理。第三个是战斗数据收集,这些数据包括操作数据以及战斗数据,例如攻击,跳跃,闪避的按键次数,顺闪,技能的释放频率,最大DPS,最大承伤,Buff数据等等。收集这些数据后可以通过策划设计的一些规则来做合法性判断,也可以通过机器学习来训练判断模型,自动检测出不正常的数据。第四个是产出控制,这涉及到玩法相关,需要策划全局把控。因为按键精灵,模拟器内操作录制这些是很难完全避免的, 一定要避免玩家一直打能一直有产出。产出控制是最后也是最有效的防御手段。第五点是针对帧同步战斗玩法做的验证服机制,如果一局游戏中各个客户端数据出现不一致,战斗服务器会在这一局结束后将玩家的操作以及随机事件等全部发往验证服务器,由验证服务器重放一次整个战斗过程,得出正确的结果来做裁决。这个具体做法在讲解帧同步的文章中再详细介绍。

以上就是近几年关于Unity游戏开发遇到的一些安全问题和相应的策略方案的总结,希望能对大家有所帮助。

Tagged . Bookmark the permalink.

Comments are closed.