近几年,移动端游戏发展迅速。在碎片化的时间争夺战中,手游因其好玩、易玩的优势,收获了大量玩家。
在此之上,部分硬核玩家为了追求更好的游戏体验,会在PC上使用模拟器来操作手游,虽然相比于原生的PC端游戏,模拟器的流畅度还是差强人意。
为了回应这一类玩家,许多游戏厂商开始尝试将手游和PC绑定,实现两者的互联互通。如《阴阳师》、《第五人格》、《荒野行动》等手游都支持在PC上运行,无论是游戏的操作体验还是沉浸感均高于手机与模拟器。
不过,外挂也以此为转折点,不断繁衍开来。手游一旦上到PC端,许多弊端就暴露出来,例如应用权限混乱、取证场景复杂等,这让游戏厂商束手无策,也给外挂党可乘之机。
一般而言,反外挂由两个部分组成,即静态代码保护与动态运行时对抗。本文将聚焦静态代码保护,从实际的保护效果出发,先阐述通用的PE代码保护在易盾端游反外挂代码保护中的应用,后介绍面向游戏引擎的代码保护策略,并在此基础上,探索一种通用的游戏逻辑代码保护方案。
1.通用代码保护
1.1 PE代码保护介绍
对于PC端来说,原生代码保护指的就是对x86架构的二进制文件做的保护,即保护Windows上的PE文件。
PE代码保护已逾20多年的历史,从21世纪初期,PE代码保护已经有了雏形,且诞生了许多直至今日仍具有不小影响力的加固思路与加密算法,如大名鼎鼎的“UPX”壳,便是利用其最核心的UCL这一效率较高的压缩算法来实现的。
根据代码保护的实现效果,PE加固用到的技术可以分为以下几类:
○ 整体加密,最常见的即压缩/加密壳及其附属功能,附属功能包括IAT加密、反调试、完整性校验等
○ 混淆,包括花指令、指令变形、代码乱序、字符串加密等
○ 虚拟机保护
1)整体加密
整体加密用到了SMC(self-modifying code)的思想,加密原有代码,并通过patch PE文件的entrypoint,再插入壳代码,利用壳代码添加解压缩/解密功能的代码,即被加密的原有代码将在运行时还原;同样地,上面提到的附属功能也会被添加在壳代码中。
整体加密的效果,更多是体现在对抗静态分析上,而大部分这类加密壳,在运行时就会被直接释放到内存中,因此对于性能开销来说,整体加密更多影响的是进程的启动时间,对运行时性能开销的影响极小;转到攻击者角度,在分析时,无需关注加密壳的加密算法,用正确的姿势Dump内存,再使用一些技巧修复附属功能即可,比如恢复可能被加密的导入表与数据段。
针对这个问题,易盾独创了一种指令抽取的方法,可以针对需要被保护的关键函数,将其核心汇编指令抽取至外部,在运行时,该部分关键函数将在外部被执行,攻击者无法在内存中找到连续且完整的代码段,大大提高其分析成本。
指令抽取效果如下:
抽取前,可以完整看到函数CFG,可以使用Decompiler看到伪代码
抽取后,由于核心指令已不在该二进制文件中,函数CFG已被破坏,不再能用Decompiler看到伪代码,如下图(图中的函数符号是加固后手动修改的):
2)混淆
与整体加密不同,混淆对代码段、数据段都做了直接的变换,因此对静态分析或者是动态调试来说,都能起到一定的防御作用。
根据混淆方式不同,这里可以分为无源码混淆与有源码混淆两种。
无源码混淆:易盾在上一小节中提到的指令抽取的基础上,对抽取的指令通过私有的花指令引擎处理,使在不影响性能的前提下,对原指令进行变形与膨胀,并插入与条件分支相关的花指令,即使攻击者绕过反调试,剔除了层层加密,也难以去掉耦合度较高的花指令,进而无法恢复原指令。
加花前:
加花后:
有源码混淆:易盾使用自研的安全编译器作为解决方案,支持控制流混淆、指令变形、字符串混淆、防反编译等功能。同样处理“无源码混淆”中被保护的函数,开启控制流混淆与指令变形后,效果如下:
3)虚拟机保护
其中强度最高的自然是虚拟机保护,因其引入了一种私有的CPU指令集,将真实的原生汇编指令转化为私有的指令运行在虚拟出来的运行时之上;但与强度相对的,由于虚拟运行时本身也依赖真实的运行时环境,会带来极大的性能开销,而游戏与一般的商用软件不容,需要进行严格的优化,且游戏逻辑相关的代码往往会被频繁调用,在这种情景下,虚拟机保护并不会是一个好的选择。
1.2 游戏逻辑外挂原理
接下来我们换一个角度,从攻击者的视角出发,要实现一个游戏外挂(特指修改游戏数据,改变游戏设计逻辑的行为,暂不讨论模拟点击类型的外挂),要关注的点一般有这两个:
○ 与游戏逻辑相关的关键数值,如攻击力、血量、倒计时等
○ 关键函数,如伤害判定、战斗结算、技能释放等
对于关键数值,攻击者往往会选择使用一些内存修改工具,定位到目标地址进行修改。
最常见的工具有Cheat Engine,该工具可以实现扫描目标进程内存,并添加监视,最终使得攻击者找到关键数值的地址实现篡改。
一个简单的例子:使用CE修改某单机游戏中的金币与钻石,可通过反复出售道具并回购,定位金币与钻石在内存中的地址并修改,如下图:
那么传统代码保护可以防御这一类外挂吗?无论是整体加密、混淆甚至是强度看起来最高的虚拟机,似乎都不能满足。
对于关键函数的定位,这里又分两种情况:
第一种情况是游戏逻辑相关的代码就是原生的机器码,如Unity3D IL2CPP及UE4。攻击者往往会选择静态分析结合动态调试的形式来分析游戏逻辑,此时传统代码保护可以起到比较好的防御效果,大大提高攻击者分析游戏逻辑的时间成本,而且对于大部分攻击者而言,这个时间成本是不可估量的。
第二种情况是部分游戏逻辑相关的代码是通过游戏引擎来加载的,如Unity3D Mono。这类游戏的引擎往往包含一个运行时解释器,来解释执行存放于外部的游戏逻辑,此时游戏逻辑所在文件自然就不符合传统的PE代码保护方案了。
由上面两节的介绍不难看出,传统的代码保护方案在对抗游戏逻辑篡改类的外挂时,只适用于部分场景。
而对于符合要求的这类游戏来说,需要考虑的也不仅仅是代码保护的强度,还有游戏的流畅度、稳定性。大量的混淆、虚拟化,势必会对这两点造成影响。
2.游戏引擎保护
由于传统代码保护方案并不能覆盖到所有游戏代码保护的场景,因此我们需要使用一些特定的保护方案,在兼顾性能的同时,起到保护游戏逻辑难以被分析的目的。这里以Unity3D引擎为例,介绍易盾端游代码保护对于这一游戏引擎的保护方案。
2.1 Unity3D端游的代码保护
1)Mono DLL整体加密
未被保护的Mono DLL符合常规的PE文件格式,以“MZ”开头,可以被一些工具直接解析。
且可以通过反编译工具直接看到逻辑:
针对这个问题,易盾结合传统PE代码保护中SMC的思想,设计了一种整体加密的保护方案。加密后的DLL不再符合PE文件格式:
且使用反编译工具无法打开:
与上文传统代码保护中的指令抽取类似,将Mono方法中关键的IL指令抽离到外部,在运行前才解密,使得攻击者无法在内存中找到包含完整逻辑的DLL。下面是加密效果。
方法加密前,代码逻辑清晰可见:
方法加密后,指令已被抽离,逻辑不再可见:
3)Mono DLL格式私有化
用一种私有化的文件格式存储加固前DLL中的关键加密信息,即使是游戏运行过程中,DLL也不会恢复。
4)IL2CPP global-metadata 加密
大部分场景中,IL2CPP游戏代码分析的第一步,就是用一个开源工具IL2CppDumper来解析游戏代码的符号信息。而这些符号信息的来源就是global-metadata.dat这个文件。
保护原理即通过一种自定义的加密算法来保护落地的global-metadata.dat文件,使得符号解析失效。
加密前,工具运行正常:
加密后,工具解析失败:
5)IL2CPP 指令抽取
IL2CPP的游戏逻辑在GameAssembly.dll中,通过加固,将游戏核心代码抽离至外部,配合私有的乱序变形引擎,攻击者将无法在内存中找到完整的GameAssembly.dll。
2.2一种通用的游戏引擎保护方案
正如上一节中提到的,要保护某一种游戏引擎的代码逻辑,势必要对整个引擎有一定的了解,再针对其中的代码逻辑的运行时特性,结合加密、混淆等技术实现一种或多种特化的保护方案。
对游戏开发者来说,在接入游戏代码保护方案后,若需要升级甚至更换引擎,相应的成本势必会增加;
对安全开发团队来说,游戏引擎的更新,也需要耗费大量成本去升级保护方案。那么有没有一种相对通用且性能较好安全性较高的保护方案呢?也即需要关注三点:
○通用性,与游戏引擎种类相对应,游戏开发的语言也是五花八门,要做到针对不同语言或者编译产物都能实现保护功能。
○性能,保护后的游戏代码在执行时的运行效率应当与保护前接近。
○安全性,需要支持各类混淆,支持虚拟机保护。
先看安全性,众所周知,设计虚拟机保护方案的解释器时,有三个角度,分别是针对源码、中间语言和汇编指令。其中,中间语言和汇编指令的解释器是被应用最广的,而源码级虚拟机由于要考虑到编译环境,且安全性较另外两者稍低,一般只在脚本语言(如JavaScript、Lua和Python等)中被使用。
再看通用性,因为部分脚本语言并不会编译成原生的汇编指令,所以这种通用保护方案在设计上,就无法采用针对汇编指令进行加固的形式;而对于中间语言来说,虽然兼顾了通用性,但要在现有的中间语言基础上为多种游戏开发语言适配编译器前端,或重新设计一种支持多语言的中间语言,工作量无疑是巨大的。最后还剩下一个选择,这里采用定制AST引擎来解析源码。
最后回到性能上,对于游戏代码的保护来说,大部分情况下不会大范围地开启代码虚拟化,更多时候是选择混淆加上少量核心代码虚拟化的形式,此时只需一个轻量的虚拟机,穿插一些混淆技术,就可以起到很好的保护效果,也由于虚拟机设计上的轻量化,性能和稳定性都可以在掌控之中。如下图:
3.端游代码保护总结
代码保护是端游反外挂战斗中处于最前线的第一道墙,代码保护方案的优秀与否,与实际的游戏体验与安全性紧密相关。
不过,这些尚不能覆盖到反外挂的所有方面。可能细心的读者也发现了,上文中在介绍关键数值类型的外挂时,还留有一个坑,本文针对代码的保护方案似乎都不能很好地覆盖到这一类型的外挂。那么如何解决呢?敬请关注后续内容,带你走进运行时反外挂的前世今生。