|
在刚刚举行的MDCC活动上,乐元素CTO凌聪分享了一个非常有意思的话题:移动游戏如何进行防作弊的攻防战。
凌聪分析了作弊(主要是弱联网休闲游戏的作弊)的影响,比如改变排行榜中的全局排行与好友排行,还会影响广告投放,带来经济损失。他从弱联网游戏可能存在的多种安全问题来分析玩家或破解者可能采用的手段以及研发商可以采取的对策。最后他总结了对各种问题的解决方案。
存档被篡改:AES、限制专一、限制降级
协议被破解:AES、SSL/TLS、nonce防重放
盗号或用户伪装:用户标识符、社交账号绑定
工程被逆向破译:符号隐藏、标识符混淆、逻辑混淆
函数被Hook:隐藏和混淆、阻止跟踪调试、组织外挂启动
二进制程序被修改:验证校验码
内存被修改:内存加密
大招:上传用户操作和随机种子,数据监控+回放+人工审核
以下是对凌聪演讲内容的整理。
一、为什么要防作弊
(一)经济损失
据我们调查,在电商网站上有很多卖作弊手段的道具,搜索网页也能发现非常多人在卖作弊手段,所以经济损失肯定是第一位的,《开心消消乐》的作弊使我们损失几百万。
(二)作弊的其他影响
今天我们主要讲弱联网的休闲游戏,就是可联网但是也支持离线玩。这是休闲游戏在当今市场环境里获得地位的重要手段,因为不联网的话相当于不能PK,不能跟好友对战和每日登陆等功能,这是很坏的。
弱联网游戏影响最大的是排行榜,包括全局排名、地区排名和好友排名,如果你作弊了,这三个榜上用户投诉很多,对我们影响很大,有些用户发现有人作弊会流失掉,你就要用很多手段封杀那些作弊用户。
另外是重度游戏,很多重度游戏需要本地计算 ,凡是客户端做的游戏都有可能作弊,所以重度游戏也需要防作弊 。
第三个是广告,因为广告可以直接骗你钱,我们曾经买过一次广告,当中居然有75%的量是假的。
攻防战只要有逻辑放在前端本身就是不安全设计,但是人家的手段是进化的,而且当一个作弊手段出来的时候就会在淘宝上卖,这个传播是很严重的。有一种手段没防住,它扩散了的话就没有这么简单,扩散就影响你整个系统。所以不是80%拦了就OK的。有些时候有一个手段就让你的经济系统崩溃,需要你不断的优化。比如针对游戏攻击函数钩子,这个如果真的发生,损失就是很大。
(三)弱联网游戏可能存在的安全问题
第一,存档被篡改和复制,造成你的损失。
第二,本地配置被篡改。在离线游戏、休闲游戏里面可能有些配置被篡改掉。
第三,帐号被盗和恶意修改。
第四,内存被修改。我们有三种内存,第一种是传输型的内存,传输性内存是不用加密的,因为传输型内存都是一次性的。另外一种是永久型内存 ,这肯定是要加密的,用户状态比如你的血、精力值这些都是永久状态。第三种是暂态内存,比如《开心消消乐》打完一关,获取的金币和获取的经验都会暂存的,这个暂存它有可能查就的,这要进行一个加密。
第五,网络协议被破解和重放攻击。
第六,工程被逆向破译。现在有很多技术高超的人,可以反汇编你的代码 ,然后把你的工程逆向。
第七,二进制程序可能被修改。刚才讲到的只要涉及到加解密,都会把加解密函数钩走,然后把它修改掉。
第八,函数被挂起,被Hook。
(四)防止存档篡改
方法:加密肯定是需要的,建议强加密,建议用AES 256 CBC PKCS#7。必须使用安全的对称加密算法,建议使用AES加密算法,密钥256位或以上,工业级、速度快。
密钥怎么保存?首先,密钥不要直接保存在代码里面,密钥是生成的。第二,密钥不要放在存档里面,这是很危险的。第三,密钥每次加密当场现算。第四,密钥的生产需要有随机度。
(五)存档协议设计
第一,Magic number:标记存档类型。
第二,版本号:标记加密方法类型。如果一个休闲游戏需要兼容两版本模式肯定需要版本号。
第三,源数据哈希。源数据的哈希为什么要设计?要验证你解密后的存档数据是不是一致的。
第四,初始向量。尽量每次都不要一样,否则很容易被反向。
(六)密钥的产生
密钥必须有一定的随机性,可以随机数产生但需要一些技巧,也可以哈希产生。密钥最好是生成,不要在代码里面去。比如我们看过一些破解程序,发现密钥直接写在字符串里面。
产生随机密钥的技巧:应使用工业级加密库的产生函数,这样更随机。
自己产生的随机数,如以下算法,实际上最多只有2的31次方种密钥,而加密算法要求2的256次方种。
- char key[32];
- srand(time(NULL));
- for(int i=0;i<32;++i){
- key = rand() % 256;
- }
复制代码
(七)初始向量的用法
尽量不要使用ECB,它的加密强度不够强,所以我们建议用CBC做加密比较稳妥。
每次加密,初始向量都必须不同。
(八)阻止存档转移
玩家可能将存档发给他人破解(如淘宝卖家),需阻止设备直接共享存档文件。
可通过使用UDID参与加密,来阻止存档转移。
方法1:UDID参与密钥产生
– KEY = RAWKEY ⊕ SHA256(UDID)
方法2:UDID参与IV产生
– IV = RAWIV ⊕ SHA256(UDID)
存档协议中只存储RAWIV
当你复制存档 给另外一个设备的时候设备打不开的,因为它的KEY不一样。
(九)限制存档降级
比如我们发现一个版本有漏洞,我们打了个补丁过去,结果用户发现之后赶紧把代码回滚到以前,然后再打开存档 ,这样的话就可以再次修改,这个就是存档降级。
存档降级的解决办法,旧版本的存档被新版本程序读取之后,要转换为新存档版本。而新存档不能给旧程序读取。我们还做了在服务端校验,如果设备里面生了新存档 ,不能再用旧存档。 防止一个设备上来回存档的问题。
(十)内存修改
刚才讲了内存修改分三种内存,其中第二、第三种,暂存和永久性内存是必须要加密的。
现在有两个修改器:之前有个八门神器,它就是对比内存 ,内存值变了两次之后,它找到你内存的偏移地址,可以定位到内存地址在哪里。八门神器我们当时比较好解决,用非常简单的加减法可以解决加密问题,因为它找不到原来的数据,或者不是整数,这样它也找不到。现在有一个更高级的,叫烧饼修改器,它能力挺强的,假设你修改的数加了个“038”,它一样可以找到。另外,它支持加减法,如果你的数据是加减法或者乘除法它能找到,所以内存的加密函数需要变化。防止内存修改最简单的方式是做一个加密的HashMap ,进的时候是加密的,出的时候是解密的,在函数里面加上一些较为强加密的算法就行了。
(十一)协议破解的几种可能性
协议破解一种方式是通过抓包工具,抓包完之后,如果你的协议没加密,用户可以伪造请求 ,也可以伪造服务器响应。玩家通过拦截服务器的响应做重放,客户端重放上次的请求。当然,玩家知道协议之后可以伪装成另外一个玩家,这是更恶劣的情节。
(十二)伪造请求的防范
加密肯定是需要的,加密的通讯协议最好自己写。建议AES。
(十三)伪造响应的防范
要么采用非对称签名验证方式防范 ,但是这个速度比较慢,不建议用。
另外一个是用TLS,如果我们要用TLS的话要注意校验服务器的证书在客户端那,因为服务器的证书很容易被篡改 。比如它通讯的时候可以用自己的证书,这样的话是可以拦截你的协议的。如果程序破解了,函数直接就调用了。
(十四)重放攻击
刚才讲的响应服务器主要是不让别人破解我们的协议,重放攻击是什么?重复攻击是我不需要了解你的加密协议,我只需要重放就行了。我们有一个版本的游戏是出现过这样的问题,攻击者拦截你的请求,然后多次重放,如果你没有做校验,很容易导致你的数据不断的往上增加。这个最重要的是支付,比如你从APPLE拿到一个支付响应,回传给你服务器的时候,它抓住这个请求不断的重放,这样很容易造成重放的攻击。
重放攻击最好的解决方案是中间加一个nonce。服务器的响应,服务器辨别你的时候它要看到,如果这个nonce是以前已经用过的就把它丢弃掉,为啥?因为这肯定是作假。
(十五)防范帐号伪装
首先一个过程是登陆的过程,登陆的过程拿到Session Key,其他所有的请求都要使用这个,这个是服务器端生成的,服务器端可以每次校验这个SessionKey。我们是分开的,给用户看User lD,所以我们不要在传输协议仅放一个Userld,这个用户标识是服务器产生的,安全性就没有了,客户端把这个存起来,以后都用这个服务器标识跟后端通讯。
SessionKey有两个作用,一个是防止多Session登陆,另外,SessionKey和UserlD是关联的,因为SessionKey每次都不一样,所以要伪造比较难。为什么不用用户标识?因为每次都传安全性是低的,是很危险的。所以做这两个主要是为了提高你的安全性。
(十六)程序被破解的几种情况
最关键的一个是程序被逆向了,它造成的后果 是有可能你的二进制程序被修改,有可能函数被外挂 Hook了,当然,内存被修改也是可能的,因为它知道你的加解密算法之后内存就会给修改。
(十七)防范程序被逆向破解的方法
第一个关键是符号隐藏,函数符号表隐藏住。特别是加解密函数。
第二,关键字混淆。
第三,程序逻辑混淆。
第四,内存加密。
第五,阻止动态跟踪调试。
第六,二进制程序校验。这里除了你提交的主程序之外,另外一个也要防止篡改。
第七,Lua脚本加密也很重要。
(十八)符号隐藏
尽量不用系统的函数库,因为系统的函数库你没有任何办法把它隐藏 ,它都是暴露在那的,只要你用它,它就可以挂接到钩子上去,所以尽量不要用。
另外,重要的函数在模块里面,尽量用static函数。
第三,关键加密库要设置关闭符号表导出。你把输出函数符号表隐藏掉。
第四,Xcode配置Symbols Hidden By Default。我看国外很多游戏已经这么做了。
另外,你可以通过nm命令可以查询到输出函数 。如果没有函数符号表输出就比较安全了。
(十九)标识符混淆
class-dump可读取Objc的类元数据,能看到类的方法和属性。
无法关闭类元数据的导出,因为ObjC的面向对象机制需要。
只能对类名、方法名、属性名进行字符串替换,让破解者无法看出接口的用途。
ObjC的混淆方案是,在prefix.pch中使用#define替换标识符。
工具是ios-class-guard。
标识符混淆的缺点有,当父类是第三方库时,父类的函数无法混淆,但子类同名函数却混淆,就会错误,需要对第三方库的函数进行排除。不能使用ClassFromeNSString反射。接入Lua时需要查表才能调用正确方法。
(二十)其他隐藏接口的方法
通过用C语言的static函数实现
通过ObjC-Runtime动态添加
(二十一)程序逻辑混淆
通过打乱代码次序,增加花指令等手段,可以阻止骇客对工程进一步跟踪和分析,类似加壳,但iOS项目目前没有商用的加壳程序。
或者用开源的程序逻辑混淆器obfuscator-llvm,用它替代XCode自带的llvm编译器。
(二十二)组织跟踪调试
GDB等调试工具可以跟踪程序运行,可以使用ptrace组织GDB依附APP进程。
我们可以用GDB挂去调它的函数,只要看到有一个数字函数,我就可以看到你的输入输出参数 ,我只要会一点点汇编语言,就基本能看到你的参数 。所以我们需要防止这个GDB。
(二十三)二进制文件校验
程序启动时,需要判断二进制是否被修改过,以防止代码被篡改或被植入广告。
我们在编译好包后,生成二进制文件的哈希,然后将版本号拼接起来作为校验码,用私钥生成校验码的签名,校验码和签名都存入一个文件中。由于没有私钥,玩家无法伪造校验码。
(二十四)反盗号
反盗号,如果离线的话非常容易盗号,越狱机器什么都可以改,所以我们需要防止它做这个事。
第一个,我们最好有个网络帐户,如果用它用网络帐号登陆的话,就不让它用本地帐户校验了。如果是本地校验建议不要用可变的,自己在KeyChain里面存储一个UUID,如果你做越狱了,很可能会丢掉,安卓 上面就自己建一个文件。如果是它用了网络帐户登陆的话,它如果要恢复,比如说它重装了应用的话,可以通过这个网络帐户拿回它的信息,网络帐户有可能是Facebook、QQ、微博甚至自己用户名密码都是可以的。
最后一个我们称之它为“大招”,上传用户行为数据以及随机种子以做到可重放。我们有一款游戏用了这个手段,我们把用户的行为和随机种子,因为我们所有的调入都是通过随机种子搞的,所以我们认为是个随机种子,这样通过游戏的模拟可以把现场在一个客户端重放,重放的话我们就可以看到这个用户是不是作弊了。
这里面如果做这点话需要有两个手段,第一个,我们需要在客户端让这个传输数据和随机种子做到重放,另外,需要监控到数据的异常,因为我们重放用户数据行为之后很多时候需要人工判断这个数据是不是作弊的,所以我们需要首先有个数据监控系统,我们监控到某些用户的数据突然变了异常,接下来我们会重放这些用户数据,看看这些用户数据是不是有问题,如果用户在重放的过程发现结果并不是这样的,那他肯定作弊了。
为什么说它是大招?因为有些时候是没有办法百分之百做到内存防篡改的,有些时候程序员忘了把显示在界面上的数据放到加密里面,玩家会修改你的数据,修改你的数据来作弊。
|
|