艾莉希雅 发表于 2024-2-17 22:36

增强游戏跨平台应用鲁棒性的设计与实现

本帖最后由 艾莉希雅 于 2024-2-18 00:45 编辑

根据版规"求助帖、讨论帖、笔记帖和学习记录请发布至『编程语言讨论求助区』"
本篇内容原文是一篇学习性质的设计,因此发至本区
成文时间为2019年,因此本文内容实际已经过时许久
因答辩需要,按老师要求改稿时删去了大量实际的代码与理论部分,并充入了大量的水分
如果你发现前面有引述但后面只字未提的,就是被删掉了,包括但不限于动态修改对抗等部分

关于mono的部分更详细的思路在这篇文章说了一下
https://www.52pojie.cn/thread-985157-1-1.html

发出来的原因纯粹就是缅怀自己逝去的青春,以及毕业后找不到对应专业的工作而终止的技术梦
以下内容可以让各位见识一下,学生是怎么进行注水的
以下内容包括但不限于这些要素:强行升华、指鹿为马、丢车保卒、信口开河、画饼充饥
因为附件只能传txt,jpg,png,所以会找个网盘丢pdf还有查重的链接
我太蠢了,markdown都不会用
至于为什么这么多车轱辘废话,因为当时要降重啊!

艾莉希雅 发表于 2024-2-17 22:37

本帖最后由 艾莉希雅 于 2024-2-18 00:03 编辑


# 摘要


当今社会科技日新月异,文化百花齐放,人民生活日益富足,社会的主要矛盾也从“人民日益增长的物质文化需要同落后的社会生产之间的矛盾”转化为“人民日益增长的美好生活需要和不平衡不充分的发展之间的矛盾”。新时代的人们早已不满足于滚铁圈,跳大绳,踩房子等旧时代的娱乐方式。运行在手机、计算机、家用游戏主机等设备上的电子游戏才是新时代的新宠。电子游戏行业依托于科技的发展逐渐壮大,逐渐扩展,与此同时,以谋利为目的破坏游戏规则的“骇客程序(Hack Tool)”也依托于电子游戏行业的发展逐渐浮现。没有一款游戏能逃的掉“骇客程序”的荼毒, “骇客程序”对游戏的破坏是直接且致命的,在网络游戏中一个需要充值上万才能得到的体验使用“外挂”可以轻松得到甚至更强,在单机游戏中一个“修改器”便可让玩家化身为创世神而不需要“内购充值”,在主机游戏中“金手指”的作用与单机游戏相仿但更致命。因此游戏厂商必须拥有捍卫自己成果、保障自身营收的能力。本篇论文主要以unity引擎游戏为样本,分析了当前游戏市场上实际存在的“骇客行为”,同时提供一套可行的跨平台unity游戏的保护方案。本研究有一定实际意义:采用本文讨论的有针对性的提高游戏鲁棒性的方法,游戏开发者可以寻找到一个适合与“黑色产业链”对抗的方式,可以保护自身权益免遭侵害,可以将更多的时间与精力投入到游戏开发以及后续的迭代中。游戏产品与策划在制定运营活动时可以减少对抗作弊者的精力,花费更多精力在设计新玩法上。玩家能享受到更公平的游戏环境,能避免因为“外挂”的存在丧失游戏体验。



关键词: unity3d;应用加固;软件安全

艾莉希雅 发表于 2024-2-17 22:39

本帖最后由 艾莉希雅 于 2024-2-17 23:42 编辑

# ABSTRACT
Today's social sciences and technology are changing with each passing day, cultures are blossoming, people's lives are becoming richer, and the main social contradictions have also changed from "contradictions between people's increasing material and cultural needs and backward social production" to "people's increasing needs and imbalances for a better life The contradiction between insufficient development ". People in the new era have long been dissatisfied with the entertainment methods of the old era such as rolling iron circles, jumping big ropes, and stepping on houses. Video games running on mobile phones, computers, home game consoles and other devices are the new darlings of the new era.
The video game industry has grown stronger and expanded by relying on the development of science and technology. At the same time, the "Hack Tool" that breaks the rules of the game for profit is gradually emerging from the development of the video game industry. No game can escape the poisoning of "hacker programs". The damage of "hacker programs" to the game is direct and fatal. In an online game, an experience that requires tens of thousands of recharges can be easily used. Get even stronger, a "modifier" in a stand-alone game can allow players to become a creator without the need for "in-app purchase recharge", the role of "golden finger" in the console game is similar to the stand-alone game but more deadly. Therefore, game manufacturers must have the ability to defend their achievements and guarantee their own revenue.
This paper mainly takes unity engine games as samples, analyzes the actual "hacking behavior" in the current game market, and provides a set of feasible cross-platform unity game protection schemes. This research has some practical significance: using the targeted methods to improve the robustness of the game discussed in this article, game developers can find a way to fight against the "black industry chain", which can protect their rights from infringement. Put more time and energy into game development and subsequent iterations. Game products and planning can reduce the effort to fight against cheaters when formulating operational activities, and spend more energy on designing new gameplay. Players can enjoy a fairer gaming environment and can avoid losing game experience due to the existence of "plug-ins".
Key words: Unity3d; application reinforcement; software security

艾莉希雅 发表于 2024-2-17 22:42

# 1 绪论

## 1.1 研究背景
如今的电子游戏市场正伴随着科技的进步的脚步蓬勃发展,新时代的人们也寻找到了新的优雅的娱乐方式。从前的一杯茶一副牌逍遥快活一整天变成了现在的一瓶可乐一台电脑逍遥快活两三天。而那怕是如此轻松优雅的娱乐方式,也有投机分子不怀好意。牌局上有“作牌”,电子游戏自然也逃不过“外挂”这种新型作弊方式的荼毒。
与牌局不同,传承千年的牌局早已有了各种方式应对作弊者,无论是现代的“记牌器”还是古代的“牌谱”均能很好的发现阻止作弊者,在牌局中“出千”十有八九会被抓出去然后打断腿。而在发展了仅仅几十年的电子游戏中,情况便与此不同了。电子游戏玩家通常都有这样子的经历,在FPS类游戏中你的角色突然间死于非命而你连人影都没看见,在竞技游戏中你所在的队伍被对方如同割草一般一刷倒下一片而你打对方如同打在空气上,甚至在线上斗地主中对方使用满手的“炸弹”来赢遍全场。
与传统的现实娱乐方式不同,在现实娱乐方式中作弊的下场可能会被拖出去打断腿,而在线上游戏中对作弊者最大的惩罚也仅仅是封禁账号而已,只要花上几分钟重新注册一个新的账号又便可体验“神挡杀神佛挡杀佛”的作弊快乐。而这些行为这些作弊者,正常玩家对他们无疑是十分痛恨的,游戏使人快乐而你被外挂虐待可就不快乐了。
电子游戏的发展速度,与计算机软件安全的发展速度是存在矛盾的,日新月异的硬件发展作为电子游戏发展温床促使其如雨后春笋般飞速拔高,而需要时间沉淀的计算机软件安全仍停留在二十世纪后期的水平,就拿我们使用的各式常用的信息安全加固方式为例:1999年诞生的AES加密算法、1973年诞生的RSA加密算法、1993年诞生的SHA摘要算法等,均为古旧的加密方法,而新型加固方法迟迟未能问世。计算机安全的发展远远跟不上时代的变化。
作弊行为肆虐的游戏,最终的下场往往极其地惨淡,开发商血本无归,游戏寿命早早终结。许多游戏开发商都在寻求一种可以预防作弊的方法,而不是陷入封禁作弊者这种无止境的无用功之中。

## 1.2 研究现状
目前,游戏保护正在处于起步阶段,欧美安逸的游戏环境与严格的版权保护使得游戏保护并非如此重要,更多的厂商更愿意将资金投入至技术研发之中,更高性能的引擎,更新更强的表现方式才是他们侧重的目标。哪怕是偶现的作弊者也很快会被告上法庭送入大牢。而最早接触家用计算机电子游戏韩国,在引进电子游戏后同时也催生了大量的作弊程序,韩国的家用机游戏安全体系在与作弊者进行对抗中积累了大量的经验,是当之无愧的家用计算机游戏安全领域的第一强国。
而中国在早期接触电子游戏后本有与韩国一般的腾飞之势,但是《关于开展电子游戏经营场所专项治理的意见》等文件的出台使得刚萌生的游戏行业胎死腹中。很长一段时间游戏行业一蹶不振,国内对电子游戏研究近乎停滞。加入WTO后电子游戏系列禁令被取消,国内游戏行业终于迎来了春天。但是行业迎来了春天不代表技术迎来了春天,过度依赖引进而忽视发展自身技术,我国目前游戏业水平仅落后国外约1年,但游戏保护水平也与欧美国家相仿,仅有一间名为“腾讯”的厂商通过仿制韩国的计算机游戏保护技术制作出了“TenProtect”获得了在世界领先的防护水平。
而在新兴的移动端游戏市场,各国防护水平均基本接近于无,仅有少数公司通过在个人计算机领域积累的对抗经验应用在移动端上获得了一定程度的保护能力,而真正属于移动端的保护目前还未出现。
但是国外的部分科研机构与高等学院已经着手将研发精力转向嵌入式设备与物联网,真正属于移动端的防护方式与方法近几年可能会诞生,但是专门针对性用于保护电子游戏的项目目前仍未有相关信息

## 1.3 设计目标
本篇的目标是通过分析目前网路空间中存在的各式游戏外挂,针对目前市面上最常见且占有率最高的跨平台游戏引擎unity针对性研究并实现一套可行的保护方案。以缓解目前游戏市场外挂横行的现象,延长游戏寿命,创造公平的游戏环境。

艾莉希雅 发表于 2024-2-17 22:49

# 2 开发技术与环境
## 2.1 系统使用的技术

### 2.1.1 CSharp
  CSharp是一个面向对象的高级编程语言,unity引擎使用最多的语言,也是微软目前主推的编程语言。该语言具备的跨平台生命力也是unity引擎能够运行在多种平台上的基石。没有CSharp便不存在unity的跨平台特性。虽然CSharp具备一次编译到处运行的特性但是其依赖于一个类似虚拟机的解释器,微软开发的闭源解释器.NET Framework与微软开源的解释器.NET Core以及开源的mono等均可作为执行CSharp的载体,而unity便是选择mono作为跨平台的解释器。可以说mono是unity运行的基石。
  通过分析其运行时虚拟机,我们可以一窥其运行方式。


<div align='center' >图2.1 三种不同的CSharp运行时</div>
### 2.1.2 C++
&ensp;&ensp;C++是一门老牌语言,它既可以被应用于面向对向设计也可以用于面向过程设计。从1983年至今无数的设备与平台都在使用这门语言。由于它是编译型语言,且经过多年编译优化发展,其性能在各种计算机语言中是最为高效的。unity中使用的mono解释器便是C++编写的。在unity5中unity引入的新特性il2cpp更是能将il字节码转化为C++后直接编译为原生代码,摆脱解释器的臃肿,并享受针对C++的编译优化。
### 2.1.3 unity
&ensp;&ensp;unity是由Unity技术公司开发的一款专业的全面整合的游戏引擎。因其3D模拟能力出众,甚至有部分技术公司用其进行虚拟现实开发与高精度基础物理模拟。它不仅功能强大,且简单易用,独有的资源包与商店模式可以让个人开发者在数小时之内组装出一款游戏。一套代码全平台运行更是让开发者们趋之若鹜,unity引擎制作的游戏可以在Mac、Windows、PS4、PS3、Wii、网页浏览器、iPhone、Windows Phone和Android等平台运行,只需要适配各平台的控制方式即可。
&ensp;&ensp;但是该引擎是闭源的商业引擎因此对其进行扩展的难度极高,敢碰的都是不要头发的真勇士真英雄。
<div align='center' ></div>
<div align='center' >图2.2 Unity引擎</div>
### 2.1.4 Windows
&ensp;&ensp;Windows是诞生于1985年的老牌操作系统,也是当今世界上家用机占有率最高的操作系统。凡是家用计算机基本上都逃不过windows系统,因此游戏行业绝不会忽视针对该平台开发游戏。如今,windows系统并不仅应用于传统的大型设备中,各式嵌入式设备中均可一窥windows的宏图伟愿。windows的生态圈更是独立的与其他平台格格不入,其独树一帜的PE可执行文件格式自dos问世以来便使用至今。
&ensp;&ensp;windows平台拥有良好的生态环境但内部实现机制不明确如同黑箱,索幸同样使用NT内核的reactOS开源系统使我们可以一窥windows内部的实现机制,ReactOS将大量的Windows库甚至操作系统重写还原堪称奇迹。
### 2.1.4 Android
&ensp;&ensp;Android操作系统是目前全球使用率最高的嵌入式操作系统,当前手机市场中份额最大的操作系统便是Android,其他嵌入式设备中也有大量使用Android系统的存在,例如:地铁刷卡机,工地刷脸机,考勤机,中控平台等。甚至有游戏机厂商直接使用Android作为基础系统再集成PPSSPP、droid等模拟器制作号称什么游戏都能跑的全能游戏机。
## 2.2 系统使用的指令集平台

### 2.2.1 X86
&ensp;&ensp;X86是因特尔于1978年推出的Intel8086中首次亮相,并在此后的几十年间大放异彩,时至今日X86仍是主流的指令集平台。X86具有很强的向下兼容能力,其版本并没有明确的名称,因此在起步阶段版本名称一般根据出现的产品命名,我们在中古编译器中常见的的8086、80286、80386便是这种命名方式的,根据不同指令集版本编译出最适合该平台运行的可执行程序。而如今X86平台不仅限于Intel公司所有命名方式也由对应指令集出现在的CPU名称改变成为新加入的扩展指令集集合名称,例如MMX、SSE等。而其中也存在着异类,诸如AES-NI等指令集合虽然也被引入至X86家族中,但是并不以其命名。现代编译器中指令集选项-m32(i386)指的便是MMX指令集合出现时的全部X86指令,而SSE则是SSE指令集合出现时全部X86指令。
### 2.2.2 arm
&ensp;&ensp;arm指令集则是近年新秀,作为RISC微处理器指令集的一员,它保持着低功耗的优良传统,因而在移动平台与嵌入式平台中应用甚广。而ARM指令集和它的子集Thumb指令集混合使用极大的优化了代码空间提升了运行效率,但是在逆向分析中我们区分可变长度的指令是较为困难的,如何静态区分Thumb指令集是目前软件安全行业的挑战之一。arm命名方式分为家族与架构,一般开发者仅需要区分其架构便可,同一家族的不同架构差异极大。常见的架构有ARMv5、ARMv7、ARMv7s,ARM64,其余架构虽然繁多但是并不多见。

艾莉希雅 发表于 2024-2-17 22:56

# 3 unity游戏遇到的安全问题的分析
## 3.1 unity生成的mono运行时游戏
&ensp;&ensp;Unity引擎十分强大,其中很重要的一点便是一处编译到处运行。而这一点是依托于CSharp语言的特性实现的。CSharp如同Java等语言一样,虽然它们均非解释型语言而是编译型语言,其源代码虽然不会如python与Ruby一般直接暴露。但是因为其均为托管运行的模式,极易被反编译分析,正常编译的可执行文件反编译后生成的伪代码可读性与源码基本一致,甚至可以直接通过反编译后的文件直接重新构建游戏。如图3.1与图3.2所示,源代码与反编译的代码的相似度极高,基本完美还原了源代码,甚至部分地方代码质量比源代码更胜一筹。
&ensp;&ensp;如图可见,编译前的代码在局部变量作用域是未作限定的,animationHandler没有使用new修饰符,数据是没有精度表示的。但编译后的代码明显略胜一筹,new修饰符的使用使得本处内存管理更为科学,变量作用域的限定使得jit性能得到提升,数据的精度优化确保了数值的准确性。

图 3.1 原始代码

图 3.2 反编译后的代码

### 3.1.1 直接反编译修改
&ensp;&ensp;Unity游戏生成的mono运行时游戏,其特点便是在可执行文件的目录下有一个“Data”文件夹,而其中的“Managed”内存存在着诸如“Assembly-CSharp.dll”、“UnityEngine.dll”等许多扩展名为“dll”的库文件,其扩展名虽然与动态链接库扩展名一致,但实际上只是托管库。
&ensp;&ensp;将Unity引擎生成的“Assembly-CSharp”拖入任意的一个流行的反编译器中,例如“Dnspy”、“ILSPY”等工具均可直接查看到与源代码高度一致的伪代码。大多数情况直接修改并重新编译替换就可以达到各种神奇的操作。诸如简单修改数值达到一刀秒杀、不灭不死的效果又或是直接修改逻辑达到不用动手便可得到诸如时间减慢、敌人自杀、一键最强等效果。而许许多多的线上游戏也避免不了被直接修改。
&ensp;&ensp;如图3.3所示,图3.2的代码中的“while (GameAttribute.instance.life > 0)”经过直接的简单修改为“while (GameAttribute.instance.life > 0||true)”后直接生成游戏库文件实现了无限生命的作弊功能。且此行为成熟度极高并非新鲜事物,修改后的代码可见其甚至在经过编译优化后大量无效语句直接被优化掉,作弊玩家性能反而得到了提升。

图3.3 修改后再次反编译的代码
### 3.1.2 直接内存修改
&ensp;&ensp;Unity游戏生成的mono运行时游戏,与其他应用程序一样,均会在内存中存储重要数据,而只要获得了系统最高权限后任何应用程序的内存均可被读取与修改。
&ensp;&ensp;在windows下这种最高权限便是ring0,存在着操作系统内核、驱动程序、硬件抽象层,而驱动程序便是可以由第三方开发人员自行开发的部分,只要利用驱动程序便可以系统最高权限对任何进程进行修改。
&ensp;&ensp;在Android系统中,最高权限便是root权限,拥有root权限的进程可以对任何一个进程动手,读取修改内存不在话下,哪怕是有诸如SELinux(Security-Enhanced Linux)等防护模块阻拦,在root权限下都是纸老虎。
&ensp;&ensp;在最高权限下,无论各app如何加强外功均是无用功,设置内存读写权限等方式只能自欺欺人,使用各种“修改器”直接从内存中寻找到诸如“金币”、“血量”、“经验”等数值,修改成任意自己想要的数值便可。
## 3.2 unity生成的il2cpp运行时游戏
&ensp;&ensp;Unity尽管拥有一处编译到处运行的能力,但是某些平台对mono的JIT(Just-In-Time)这种能动态修改自身的东西深恶痛绝,诸如苹果公司的App Store在就不允许这种存在,其《App Store Review Guidelines》的2.5.2条款与《Apple Developer Program Information》的3.2.2条款均明文禁止应用程序使用动态修改自身的技术。其他平台也存在着类似苹果公司这种禁止动态执行技术的规定,为了应对这种诡异的限制mono中的AOT(Ahead-Of-Time)技术便被应用了起来。而mono的AOT只是一个妥协产物,它不仅没有预编译的相关性能优化加成还带来了严重的体积增大的问题,在iPhone5的时代许多unity开发者都遇见TEXT段的大小超过了80M这种无法通过苹果公司审核的情况。

图 3.4 苹果对应用的大小要求
&ensp;&ensp;而针对这个问题,在Unity5版本中引用了il2cpp这种运行时,它在CSharp代码编译完成生成dll库文件后,将其从il转换为C++语言代码,再通过将要发布的平台提供的工具链再次编译成目标平台的native code,这种方式既能规避某些平台的规定,也能使代码享受编译器的各种优化,还能摆脱mono的运行环境。唯一的缺点大概就是失去了一次编译多处运行的优势。

图 3.5 il2cpp转换后的CPP文件
### 3.2.1 直接二进制修改
&ensp;&ensp;Unity游戏生成的il2cpp运行时游戏,将代码最终编译成native code后虽然避免了mono运行时下的被直接反编译出接近源代码的伪代码的问题,但正是因为被编译成了native code,因此它也会被传统的二进制修改行为困扰。直接修改反汇编代码也可以达到一刀秒杀、不灭不死的效果,但由于汇编的难度较大实现复杂功能的代价往往极大。
&ensp;&ensp;但因大多数CSharp程序员的编程习惯,代码中普遍存在形如get_value与set_value的字段属性,而这些字段往往被编译成独立的一个函数,照成il2cpp转换后的二进制程序比传统的二进制程序更易被修改数值上的东西。
&ensp;&ensp;而il2cpp转化的程序除了上述编码转换带来的问题,还存在着符号容易被还原的问题,因CSharp拥有反射等其他极依赖名称的特性,il2cpp为了维护这些高级特性会将函数名称、变量名称、以及其他地址等存放在一个叫global-metadata.dat的文件中,而这些信息被称作元数据,可以根据这些数据还原所有的名称。
结合这两点,一般静态修改il2cpp运行时的unity游戏的手法便是先根据元数据还原出全部的命名空间、函数名、变量名、字符串等信息,再根据这些信息找到对应的get\set关键数值的位置进行修改。

图 3.6 反编译的il2cpp代码

图 3.7 对应原文CSharp代码
### 3.2.2 HOOK或patch相关函数修改
&ensp;&ensp;Unity游戏生成的il2cpp运行时游戏,因为经过一次代码转换后再经过目标平台编译器的优化与编译,其内存中存储的各式变量均已面目全非,直接通过搜索与修改内存数值难度较大,但仍有优化完后变量仍保持一致的情况存在。一般而言,只要在编译前的变量加入一些简单的无用运算便可确保它们一定会被编译器优化的面目全非。
&ensp;&ensp;静态修改il2cpp生成的库在某些频繁更新的游戏成本可能较大,每一次的修改意味着需要重新安装一次修改后的游戏,而hook或patch方式很好的解决了这个问题,甚至还能解决很多区分版本的单机游戏。
&ensp;&ensp;所谓hook,便是在某个函数执行前或执行后插入一些自己想要的操作,例如某个公共变量同时存储敌人与我方的血量,简单的对其进行修改可能导致双方进入无敌状态,而hook可以实现更详细的操作,实现判断敌人血量维持原状不做修改而只修改我方血量是十分简单的。前插方式可以让某个函数被传入的变量修改成自己想要传入的变量,后插入方式可以让其他函数调用这个函数生成某个变量后被替换。甚至通过hook我们可以实现敌人攻击加血,我方攻击维持原逻辑的做法。
&ensp;&ensp;而patch则与静态修改相仿,但它是在游戏运行时进行修改,通过替换某个函数的汇编代码达到修改的目的,其优势在于维护成本较低,每一次的更新只需要更新要替换的地址与需要替换上的代码便可。

艾莉希雅 发表于 2024-2-17 23:01

# 4 针对unity游戏弱点增加安全性的方法
## 4.1破坏游戏的符号
&ensp;&ensp;因unity引擎为满足CSharp的“反射”等各种高级特性,保留了命名空间、函数名称、变量名称等诸多信息,使得攻击者极易获得大量开发者并不想暴露的私有信息,利用这些信息很简单的就可以分析出游戏的关键弱点并对其修改。针对该弱点,减少游戏分析者获得的信息加大其分析的难度是增强游戏鲁棒性的基础解决方案。元数据信息一旦被获得,就如同迷宫的地图被泄露,再复杂的迷宫也可以按图索骥轻松抵达终点。
&ensp;&ensp;破坏掉我们希望破坏掉的符号不仅不影响游戏运行,还可以缩短某些占用空间较大的长名称节约存储空间一举两得。
## 4.2改变mono运行时的运行方式
&ensp;&ensp;CSharp库的先天弱点使其极易被反编译修改的根本原因,是因其开放程度过高,其他能被轻易反编译生成如图所示的高质量伪代码的语言如Java,无一例外都是高度开放的语言。
通过修改mono运行时的运作方式,我们可以将大众所知的CSharp语言变为一种运行细节完全不同的“新私有语言”,防止mono运行时的游戏库文件被轻易的反编译成接近源代码的伪代码。
&ensp;&ensp;在改变mono运行时的游戏,尽可能让其性能不会因此发生较大降低。
## 4.3加入“花指令”废代码
&ensp;&ensp;上世纪出现的保护方式“花指令”,时至今日我们仍能在各种软件中看见,“花指令”的本质就是一段加入到代码中也不会影响运行逻辑的部分,反之亦然,将其从代码中剔除也不会对代码的运行逻辑进行任何影响。这种代码在对抗逆向分析的方案中成熟可靠。对于unity游戏易于分析的弱点,持续几十年的老方案带来了新希望。

图 4.2 简单花指令原理示意图
&ensp;&ensp;通过在代码中插入大量结果恒定的运算式、加入不影响流程运行的代码、加入恒定的case-switch等花指令,可以极大的增强游戏的鲁棒性。
&ensp;&ensp;但花指令加大分析难度的同时,也会降低游戏的运行性能,控制加入的点与加入的量及其重要。

图 4.3 花指令的实际使用
## 4.4向函数加入“代{过}{滤}理函数”
&ensp;&ensp;因unity运行时依赖的名称不能被破坏殆尽,因此“代{过}{滤}理函数”的存在便是为了方便我们破坏掉全部函数名称。将元函数的名称安排给代{过}{滤}理函数,再将元函数的名称破坏,所有对元函数的请求均由代{过}{滤}理函数进行转发通信,这种方式可以让诸如Xrefs、Flow等许多静态分析的分析方法直接失效。
&ensp;&ensp;如图4.4,示例结构中的取tag,与“==”比较,因为分别使用了unity引擎运行库的UnityEngine.Component::get_tag与系统库的System.String::op_Equality,因此元数据不能被破坏,否则游戏将会崩溃。该C#代码编译后使用IL反编译查看,可以如图4.6中清晰看见两个库函数的使用情况。

图 4.4 代{过}{滤}理函数处理前

图 4.5 代{过}{滤}理函数处理前IL
&ensp;&ensp;为了既不影响游戏运行,也不妥协降低鲁棒性。图4.6所示,在进行代{过}{滤}理了处理后可见,取other的Tag与==操作被放置到了smethod_11与smethod_12中。如图4.7清晰可见,两个调用第三方库的操作被直接提取出来,以单独的method存在调用系统或引擎的函数,再自定义代{过}{滤}理函数的名称便可达到变相破坏名称的目的。

图 4.6 代{过}{滤}理函数处理后

图4.7 代{过}{滤}理函数的代{过}{滤}理部分IL
&ensp;&ensp;而代{过}{滤}理函数照成的性能损失在现代处理器中可以忽略不计,但是在流水线不成熟的处理器上将会造成极大影响。二次代{过}{滤}理甚至三次代{过}{滤}理都是可行方案,甚至可以在代{过}{滤}理函数中插入“花指令”。
## 4.5更改元数据的数据结构
&ensp;&ensp;Unity的il2cpp运行时所需求的元数据,我们不能直接对其进行破坏,但可以通过修改它的数据结构使现有的分析方式全部失效,增强其鲁棒性。在修改元数据后破解者便不能简单的通过现有的分析方法直接分析元数据获得命名空间、函数名称、字符串等。
&ensp;&ensp;修改元数据的同时修改unity游戏读取元数据的方式,并对读取方式进行一定的保护便可。元数据读取操作仅在游戏初始化时进行,如无极大安全需求甚至可以仅做修改数据结构而不保护读取方式,对运行时性能影响为0。
&ensp;&ensp;如图4.6与图4.7所示,unity引擎的il2cpp使用codeRegistration信息时会先将其存入s_Il2CppCodeRegistration中,此后codeRegistration就不再被使用转而使用s_Il2CppCodeRegistration。虽然不知道为何引擎会如此设计,但是此处为我们对codeRegistration信息进行加密创造了便利的条件,我们只需要重写unity的s_Il2CppCodeRegistration = codeRegistration操作,便可实现对codeRegistration的加密。
&ensp;&ensp;如对启动性能需求高,我们甚至可以不修改il2cpp::vm::MetadataCache::Register的代码,而去重写Il2CppCodeRegistration也可以达到使现有分析工具失效的目的,但是对手工分析的影响可能会小很多。

图4.8 生成的codeRegistration

图4.9 codeRegistration的使用

艾莉希雅 发表于 2024-2-17 23:21

# 5 针对不同运行时的unity游戏鲁棒性加强实现方案
## 5.1 unity生成库文件后插桩操作
&ensp;&ensp;unity引擎库文件破坏名称的最佳时机本应是代码编译前直接针对源代码进行批量名称替换,但由于每次替换均需要人力参与,因此如此实现所需的人力成本过大,极不适用于目前国内环境的自动化打包。
&ensp;&ensp;而当游戏整体已经生成完成后再进行元数据破坏,会造成关系分析困难,且因为“木已成舟”大量元数据存在强依赖而无法破坏大部分名称,照成元数据破坏效果大打折扣。
为了平衡成本与效益,将元数据破坏的时机延后到库文件生成完成时,且资源依赖尚未形成时为佳,且此阶段易于与自动化打包结合,使得质量检测部门可以尽早发现因库混淆出现的问题。

图 5.1 国内高度自动化的打包推送

图 5.2 机械化流水打包构建环境

### 5.1.1 分析unity生成过程寻找插桩点
&ensp;&ensp;Unity游戏生成主要有“Build Scene”、“Build Resource”、“Compiling shader variants”、“Postprocessing Player”等,根据平台或配置不同还有“Stripping assemblies” 、“Mesh data optimization”等优化类配置。
因5.1中提及的原因,我们寻找的插桩点在库生成完毕且资源尚未生成时便可。其中的“Build Scene”之后便是库文件已经生成完成资源即将开始生成的时间节点。根据最小化修改的原则,在添加我们需要的功能时我们最好使用现有的实现避免直接通过逆向工程修改unity引擎。而“UnityEditor.Callbacks”内部所提供的回调之一“PostProcessSceneAttribute”恰好可以满足我们的需要。
&ensp;&ensp;通过添加于插桩函数头部我们便可在第一个Scene生成完毕后触发我们的代码,将unity生成的库文件内部的元数据尽可能的破坏掉。
插桩函数局部代码:
```
//触发第一个PostProcessScene后进行混淆操作
public static void OnPostProcessScene(){
    if (EditorUtility.DisplayDialog("库生成已完成", "Unity相关库已经编译完成是否进行下一步操作", "确定","不了")){
      string targetDir = @"Library\PlayerScriptAssemblies\";
      string targetFile = "Assembly-CSharp.dll";
      string targetBakFile = "Assembly-CSharp.bakup.dll";
      if (File.Exists(targetDir + targetFile)) {...}
      if (EditorUtility.DisplayDialog("选择元数据破坏模式", "使用内置简易破坏还是使用第三方破坏", "内置", "第三方")){...}
      else {...}
      EditorUtility.RevealInFinder(targetDir + targetFile);//显示相关文件
    }}
```
### 5.1.2 破坏库文件元数据
&ensp;&ensp;Unity库文件的元数据,主要分为“Namespaces”、“Members”、“Properties”、“Methods”、“Types”,而常规CSharp存在的元数据“Resource”在unity中并无作用可以完全无视之。
&ensp;&ensp;由于Unity内部实现混乱,为了做到最大兼容我们可以简单粗暴的将BaseType为“UnityEngine”的所有元数据都列入白名单不作处理,但这种简单粗暴的方式实属掩耳盗铃,因国内unity开发的习惯BaseType不为“UnityEngine”的元数据基本寥寥无几。
&ensp;&ensp;为了使得元数据破坏的效果得到保证,使用一刀切的方式明显是不合适的。经过逆向工程的分析与测试,影响Unity运行的关键部分仅仅是BaseType为“UnityEngine”的部分Methods,其影响范围主要集中在BaseType为 “UnityEngine.MonoBehaviour”的"Awake","FixedUpdate","LateUpdate",OnAnimatorIK","OnAnimatorMove","OnApplicationFocus","OnApplicationPause","OnApplicationQuit","OnAudioFilterRead","OnBecameInvisible"等Methods与“UnityEngine.StateMachineBehaviour”的"OnStateMachineEnter","OnStateMachineExit","OnStateEnter","OnStateExit","OnStateIK","OnStateMove","OnStateUpdate"。&ensp;&ensp;将上述的Methods加入到白名单中,即可避免绝大多数的崩溃情况,如有其它崩溃情况,发现后再加上去就完事了。
&ensp;&ensp;在针对unity的特殊性建立了白名单后,剩余元数据破坏规则就可以直接按普通CSharp库的方式处理即可。

图 5.3 元数据破坏对比
## 5.2 il2cpp的插桩操作
&ensp;&ensp;Unity游戏编译方式之一便是il2cpp,将易于被反编译逆向的CSharp转换为C++在提升性能的同时也无意中提升了游戏安全性。而为了维护CSharp语言的特性,在转换过程中它会将大多数CSharp元数据填入global-metadata.dat中。而其格式因Unity的编译需要直接暴露公开,因此针对il2cpp的名称还原方案在互联网上数不胜数。在经过5.1中的库元数据破坏后,编译成il2cpp后的global-metadata包含的数据也是我们破坏后的,但针对il2cpp编译方式我们仍有其他方式继续增强其安全性。
### 5.2.1 修复生成il2cpp的恶性BUG
&ensp;&ensp;Unity在生成il2cpp中存在一个至今未有其他人发现的恶性BUG,在转换破坏元数据后的CSharp库到CPP时会引发崩溃。且此错误在搜索引擎与其他私有文献中均未提及。

图 5.4 il2cpp崩溃
&ensp;&ensp;经过逆向工程的分析,位于“Unity.IL2CPP.Metadata.VTableBuilder”中的“private static Dictionary<MethodReference, MethodDefinition> CollectOverrides(TypeDefinition typeDefinition)”会引发il2cpp生成错误,原因是其进行“dictionary.Add(key, methodDefinition);”操作前未判断key是否存在于dictionary中导致il2cpp.exe运行出现崩溃现象,该bug触发条件是存在相同Methods时便会触发。根据分析多个版本的il2cpp发现,该BUG存在时间极长。推测该BUG目前尚未被人发现的原因是因正常的CSharp编译器在同一个namespace中不允许出现同名的method、type。
&ensp;&ensp;在Add操作前增加“if (!dictionary.ContainsKey(key))”进行判断即可解决该BUG。经过逆向工程的修复后,可以编译出能正常运行il2cpp运行时的游戏。

图5.5 修复后的CollectOverrides
### 5.2.2还原游戏符号信息需要的资讯
&ensp;&ensp;通过分析各还原工具,我们可知还原游戏符号信息需要的文件有两个,一个是经由il2cpp转换编译后的游戏库文件,另一个是global-metadata.dat。而还原工具通过其算法寻找Il2CppCodeRegistration与Il2CppMetadataRegistration两个结构体的位置,解析留存的元数据生成地址函数名称等一一对应的关系信息。

图 5.6 工具还原的地址与函数名称对应关系
### 5.2.3隐藏文件对抗分析
&ensp;&ensp;通过将global-metadata.dat文件直接嵌入il2cpp中,实现将其隐藏。同时可以通过极低性能损失的异或等简单加密方式或更为复杂的AES、DES甚至是性能消耗丧心病狂的RSA来加密global-metadata.dat。
&ensp;&ensp;但是整体加密global-metadata.dat的意义并不大,在如图5.2中我们可以看到,Unity引擎在读取global-metadata.dat后会将其整体放入内存中,此时整个文件都是处于解密状态下,直接对设备内存进行dump便可导出明文的global-metadata.dat。且由于如图5.3中的“sanity”存在,在内存中寻找到该元数据文件是十分简单的。
&ensp;&ensp;因此对其进行加密的意义并不大,且会严重影响游戏的启动性能。

图5.7 global-metadata.dat加载代码片段

图5.8 global-metadata.dat中的sanity
### 5.2.4更改数据结构对抗自动分析
&ensp;&ensp;由于Il2CppCodeRegistration与Il2CppMetadataRegistration的寻找难度较小,即使通过其他手段隐藏或变造了指向此处的指针地址也可以通过手工分析的方式快速寻找到两个结构体的位置,尝试隐藏两个结构体的效果并无太大作用。
&ensp;&ensp;但通过修改Il2CppCodeRegistration与Il2CppMetadataRegistration我们可以达到使相关的工具直接报废的效果,重新定义一个结构即可。例如简单调换Il2CppCodeRegistration的reversePInvokeWrapperCount与invokerPointersCount。在修改struct Il2CppCodeRegistration或struct Il2CppMetadataRegistration的同时,我们需要使用逆向工程技术,修改unity转化il2cpp的过程,使得生成的代码符合我们修改的结构体。
表5.1 Il2CppCodeRegistration数据结构


| 名称 | 类型 |
| -------- | -------- |
| reversePInvokeWrapperCount   | uint32_t   |


表 5.2 Il2CppMetadataRegistration数据结构

### 5.2.5加密数据对抗静态手工分析
&ensp;&ensp;上一节的处理方式可以很好的对抗自动化工具的分析,使游戏鲁棒性有一定的提升。但由于Il2CppCodeRegistration与Il2CppMetadataRegistration的数据实际上仍明文暴露,手工分析虽然成本高,但实际上配合自动化工具仍可以寻找到两个数据存在的位置,只要对反汇编进行一定分析便可找到结构体对应的数据。
&ensp;&ensp;因此,对关键数据进行处理是十分有必要的,通过分析调用堆栈,发现两个数据结构的终点“il2cpp::vm::MetadataCache::Register”是最佳的修改点,但为了简化本文的书写与分析,本次的修改位点为两个数据结构的起点也就是 “Il2CppCodeRegistration.cpp”,在其中新建“解密”算法。如图所示,被加密的CodeRegistration不再直接展示,而是在运行时解密再传入“il2cpp_codegen_register”最终被使用。

图 5.9 CodeRegistration或MetadataRegistration相关堆栈
&ensp;&ensp;为了方便说明,本次的“加密”方式为将真实数据乘以十,而解密是将加密数据除以十,在实际使用中建议选用更强大的算法,解密的位置也应结合到“il2cpp::vm::MetadataCache::Register”甚至是每一个调用点处,确保关键数据不集中解密形成致命弱点。

图 5.10 “加密”后的CodeRegistration对比
解密CodeRegistration局部代码:
```
Il2CppCodeRegistration D_CodeRegistration = { 0 };//初始化结构体
Il2CppCodeRegistration decrypt_g_CodeRegistration(Il2CppCodeRegistration E_CodeRegistration)//解密操作,应结合在实际读写处,此示例仅作演示{
        D_CodeRegistration.reversePInvokeWrapperCount = DecryptUint32(E_CodeRegistration.reversePInvokeWrapperCount);
……//略去相似的解密过程
        D_CodeRegistration.codeGenModules = E_CodeRegistration.codeGenModules;
        return D_CodeRegistration;//返回解密后的CodeRegistration
}
void s_Il2CppCodegenRegistration(){
D_CodeRegistration= decrypt_g_CodeRegistration(g_Enc_CodeRegistration);//传入前解密
        il2cpp_codegen_register (&D_CodeRegistration, &g_MetadataRegistration, &s_Il2CppCodeGenOptions);
}
```
&ensp;&ensp;经过加密后,可见我们的CodeRegistration已经被全部“加密”,在实际使用中它甚至可以被全部打散至应用程序各处,甚至可以将该结构体的数据放置到网络文件中获取。

图 5.11 “加密”后的CodeRegistration
## 5.3 unity生成mono运行时游戏
&ensp;&ensp;作为Unity运行时之一的mono,伴随了unity不知多少个版本,时至今日仍在使用mono运行时的游戏仍然占据着半壁江山。在经过元数据破坏后,根据函数名称直接进行爆破式修改的难度增加了不少,但仍可以针对其运行时特点进一步增加逆向难度。

### 5.3.1 破坏反编译CSharp库的必要信息
&ensp;&ensp;Unity生成的mono运行时库,其本质与正常CSharp并无区别,但其运行时mono与微软的.net framework又或是.net core截然不同。因其实现不同,因此有些在其他运行时下必须的部分信息在mono运行时下并无作用,将其抹除并不会影响其运行。但是将部分信息抹除后,反编译工具便不能识别CSharp库。
### 5.3.2 整体加密游戏的CSharp库
&ensp;&ensp;与il2cpp运行时的global-metadata.dat相似,mono运行时所必要的CSharp库也可以进行整体加密。但进行整体加密的意义并不大,因为最终mono运行时会将解密的CSharp库整体的放入内存中,只需要简单的完整dump内存便可轻松还原此类整体加密。
        如图所示,在“mono_image_open_from_data_internal”加入解密CSharp库的代码即可实现在加载时解密CSharp库,因为解密过程性能损失极大,因此仅加密核心的“Assembly-CSharp.dll”库是最具有性价比的选择,根据需求也可以加密其他库,甚至可以加密全部库文件。
&ensp;&ensp;但本方案的缺陷如代码所示,被加密的库在解密时会整体明文出现在运行内存中,因此在游戏运行后只需要对内存进行导出操作即可获取到全部解密后的库文件,且因解密后的文件必然与静态存储的文件内容不一致,因此便不能使用内存映射的方式读取库文件,而只能全部读入内存中操作。因此该方案对内存的需求也极大,运行效率极低,保护效果极差。

图 5.12 解密加密的CSharp库
### 5.3.3 修改mono运行时OPCode
&ensp;&ensp;Unity游戏的mono运行时为开源软件,其代码可于github寻找到源代码自行编译。拥有mono运行时源代码我们便可在任何一阶段改变其运行方式以增强其安全性。我们可以整体加密CSharp库文件,也可以加密每个Method名称,运行时解密。更可以通过重新定义OPCode的映射关系,在最大程度上加强防护。例如将CEE_CALL(0x28)与CEE_JMP(0x27)映射进行对换,CEE_POP(0x26)与CEE_DUP(0x25)映射进行对换,CEE_BRFALSE_S(0x2C)与CEE_BRTRUE_S(0x2D)进行对换。在目前版本的mono中OPCode的数量有332个,假设将OPCode全部两两对换将可以创建出繁复多变的映射关系。
&ensp;&ensp;理论上,直接简单暴力的重新映射全部OPCode确实可以形成极大的保护强度,但由于国内开发习惯,游戏通常都会引入大量诸如bugly、umeng等第三方SDK或插件,由于第三方插件使用的是mono运行时的标准OPCode映射关系,这将会造成他们无法在我们的定制版mono上运行,通过逆向工程替换第三方插件或SDK使用的OPCode虽然可以解决该问题,但有极大的法律风险,为了最大程度提高兼容性同时降低法律风险,简单暴力的对OPCode映射关系进行重定义是不可取的。
&ensp;&ensp;根据分析开源的mono运行模式,我们可以寻找到在jit运行时下用于处理opcode映射关系的“mono_method_to_ir”该函数位于“mono\mini\method-to-ir.c”中,该函数以“switch (*ip)”开始,通过上百个case实现各个OPCode的操作。只要在其中添加我们自定义的OPCode使其跳入对应的case中,便可等效替换OPCode。
加入opcode.def文件的新OPCode:
```
OPDEF(CEE_LDARG_0_0_YYY, "ldarg.0.0", Pop0, Push1, InlineNone, X, 1, 0xFF, 0xB1, NEXT)
OPDEF(CEE_CALLYYY, "call", VarPop, VarPush, InlineMethod, X, 1, 0xFF, 0xB2, CALL)
```
&ensp;&ensp;以call为例进行自定义,于“mono\cil\opcode.def”内添加我们的自定义映射CEE_CALLYYY(0xB2),同时在“mono\mini\method-to-ir.c”的CEE_CALL(0x28)附近添加我们的等效OPCode,在这里我们还可以发现部分不同的OPCode的实际实现是一样的,例如图中的CEE_CALL与CEE_CALLVIRT两个case最终执行的代码是一致的。

图 5.13 mono_method_to_ir新增CEE_CALL等效的CEE_CALLYYY
&ensp;&ensp;同时我们可以批量替换游戏CSharp库中的全部CEE_CALL为CEE_CALLYYY,再重新尝试进行反编译,如图所示反编译工具无法还原代码,OPCode为UNKNOWN1。

图5.14 批量替换后

图 5.15 批量替换前
&ensp;&ensp;被替换为自定义OPCode后通用的反编译程序因为不能解析我们的自定义OPCode,因此不能解析应用程序的IL码,更不能将代码直接反编译为高可见性的CSharp代码。

图 5.16 被识别为UNKOWN1的自定义OPCode

图5.17 反编译因未知OPCode已不能识别代码
### 5.3.4 新增mono运行时OPCode
&ensp;&ensp;Unity游戏的mono运行时为开源软件,因此即便是重新映射OPCode后仍旧有可能花费极大的代价,人工动态调试通过每个OPCode行为来猜测出原始的OPCode。为了防患未然,在重定义OPCode的方法上我们还可以在此处进行更强的操作,那便是新增OPCode。
&ensp;&ensp;OPCode的新增需要对整个mono十分熟悉,分析mono运行时并非本文的主要内容,为了降低说明门槛,可以尝试以组合OPCode的形式新增一个具有两个OPCode功能效果的新OPCode。本实例以两个CEE_LDARG家族的指令为例,如图所示的源代码片段所示,从CEE_LDARG_0至CEE_LDARG_3的实现本质上是相同的指令,理论上把全部CEE_LDARG家族的指令替换成同一个并不会影响运行,但就理论而言我们要注意他们MSIL的语义是不同。

图 5.18 CEE_LDARG家族指令
&ensp;&ensp;下面的例子是以两个“ldarg.0”为例重新组合为一个“ldarg.0.0”,实质上该指令等价任意两个CEE_LDARG家族指令。将CEE_LDARG的实现简单的复制两次,并把重复CHECK与EMIT_NEW_ARGLOAD全部注释以避免访问到错误的内存造成崩溃,一个OPCode只能进行一次EMIT_NEW_ARGLOAD。

图 5.19 新定义的OPCode“ldarg.0.0”
新增的OPCode“CEE_LDARG_0_0_YYY”的实现代码:
```
case CEE_LDARG_0_0_YYY:
        CHECK_STACK_OVF (1);//栈检查
        n = (*ip) - CEE_LDARG_0_0_YYY;//IP确立
        CHECK_ARG (n);//参数检查
        EMIT_NEW_ARGLOAD (cfg, ins, n);//值运算
        ip++;//IP自增
        *sp++ = ins;//sp修改
        n = (*ip) - CEE_LDARG_0_0_YYY;//IP第二次确立
        ip++;//IP第二次自增
        *sp++ = ins;//SP第二次修改
        break; //中断case
```
&ensp;&ensp;在新的指令诞生后,我们便可在Unity的CSharp库中尝试替换使用,通过修改现存的指令替换为新增的指令,我们便可以达到让攻击者无法仅通过人肉方式观察特征猜测出对应关系,因为该指令在原始的OPCode中根本不存在。我们甚至可以使用单个OPCode实现变种的MD5摘要算法或修改过的AES加密算法,给攻击者造成极大的困扰。

图 5.20 替换两个CEE_LDARG家族指令为新指令CEE_LDARG_0_0_YYY
&ensp;&ensp;在修改本处时要格外注意由于Unity是闭源游戏引擎,mono库仅为外部引用因此我们无法进行动态调试,出现问题只能依赖日志进行排查。因此在修改OPCode实现时我们要尽可能的打印更多日志,方便我们定位问题所在。


图 5.21 访问无效地址错误

艾莉希雅 发表于 2024-2-17 23:27

# 6 unity游戏鲁棒性加强后性能测试
## 6.1 unity游戏鲁棒性加强后性能测试原因
&ensp;&ensp;unity游戏鲁棒性加强后,代码与数据结构均有较大变化,因此有必要对经过鲁棒性加强操作后生成游戏进行性能测试。性能测试的目的是为了证明本设计方案的对性能损失较小,可以直接被应用到生产环境中。
由于mono运行时与il2cpp运行时区别极大,将会分开测试。
### 6.1.1 进行测试环境的平台介绍
&ensp;&ensp;众所周知,测试只有在无关变量保持一致时才有意义,而测试环境是实验数据诞生的平台,脱离平台的数据是毫无意义的。因此对实验平台进行说明是十分有必要的,避免因平台问题导致结果出现偏差。
&ensp;&ensp;本次实验平台为其一为惠普笔记本,型号为“14q-aj105tx”。CPU为英特尔的i5-6200U,内存为“SK hynix HMT41GS6BFR8A-PB”,将使用CPU核心显卡进行图形运算并关闭处理器睿频技术,主频锁定至0.8Ghz,系统为windows10 19041.153。实验平台二为三星手机,型号为“SM9250”,系统版本号MMB29K.G9250ZCS2DQG3,电源策略“performance”。
实验环境使用热空调将室温加热保持到30摄氏度,每次实验前满载静置设备30分钟,实验过程中均外接AC供电。
## 6.2 unity游戏mono运行时对比测试
&ensp;&ensp;本测试的以同样的代码片段,编译选项均保持相同。Backend为mono,color space为Gamma,Lightmap Encoding为High Quality,Quality为Ultra。
&ensp;&ensp;运行时间计算方式为:代码初始化开始计时,直至第一次UpdateAction计时结束,记录结果为三次测试均值。包体积大小计算方式为直接计算unity引擎打包后的包大小。
### 6.2.1 Windows平台mono运行时性能测试
&ensp;&ensp;于Windows下,unity输出的日志保存在“C:\Users\%username%\AppData\LocalLow\Unity\%packet%”下,分别使用直接打包,元数据破坏,花指令,代{过}{滤}理函数,OPCode自定义,整体加密六种方式进行打包。记录打包后各包大小后,运行性能测试软件满载计算机30分钟后运行游戏,记录代码初始化至第一次UpdateAction所需的时间。
&ensp;&ensp;如表所示进行整体加密后,性能损失较为严重,执行时间大于未处理库44%,符合预期结果,原因在于解密过程消耗大量算力,如果被加密的CSharp库体积继续增加性能损失将会更为严重,而生产环境的包体积往往数倍大于本次测试的包体积,因此如无必要整体加密不应被应用到实际应用。
&ensp;&ensp;而花指令在性能损失上虽然仅次于整体加密,但其损失率远低于整体加密,执行时间仅仅大于未处理库6%。如果将花指令插入的函数范围进一步缩小,那么性能影响将会更小。花指令的加入也略微加大了游戏的体积,但对于动辄数千兆的游戏来说可以忽略不计,由于花指令强度与执行效率高度相关,可以考虑在不同函数使用不同强度的花指令平衡性能与鲁棒性。
&ensp;&ensp;代{过}{滤}理函数在性能表现上与花指令相似,在实际测试的时候性能损失约为0.5%,可以算作为误差,体积增加也小的可以忽略不计。因此可以认为,大规模使用代{过}{滤}理函数处理CSharp是没有过大的性能开销的,该鲁棒性增强方式可以在生产环境尝试使用。
&ensp;&ensp;元数据破坏在性能表现是比未处理略胜一筹,但由于差距过小可认为是实验误差。就理论而言,保守的元数据破坏对性能并无影响。但由于CSharp诸如反射等高级特性均依赖元数据,在生产环境中使用时应注意避免使用高级特性,即便是不使用il2cpp也应养成良好编程习惯。
&ensp;&ensp;OPCode自定义的性能比未处理时略胜一筹,经过简单分析推测可能是自定义的mono使用的编译器优化水平比Unity官方使用的编译器更优。
表 6.1 Windows X64下的测试数据

### 6.2.2 Android 平台mono运行时性能测试
&ensp;&ensp;于Android 平台下,我们可以使用ADB工具的“adb logcat -s Unity”命令实时查看输出的日志。Android系统是一个跨平台系统,可以于MIPS、X86、arm平台上运行,本次实验受条件限制,仅于arm平台进行测试。
如表所示,整体加密仍是损失性能最高的加密方式,如无必要于Android平台中也不宜使用。其性能损耗与windows平台相似,在Android平台上也无较大区别,因此在生成环境中如无必要该加密方式不应被应用。
而代{过}{滤}理函数在本次测试中对性能影响出乎意料的高,但它确确实实就是本次测试的结果。造成这个现象的可能原因是三星猎户座处理器的特殊性,大量的跳转破坏了处理器的流水执行导致性能降低,且该处理器的分支执行性能较低。在实际使用时应根据覆盖的处理器先行调研后再作出决定。
&ensp;&ensp;花指令在本次测试中性能表现如预期一般,在保护后执行时间略微延长,在实际使用中可以考虑按需使用。
元数据破坏于本次测试中执行时间比未处理更低,但执行时间偏差较小极有可能是误差,但不排除是因破坏元数据后减少了调用长度优化了性能的可能。在生产环境中避免使用反射等高级特性即可。
&ensp;&ensp;OPCode自定义在本次执行中大放异彩,执行时间缩短了许多,只能说Unity官方提供的mono版本使用的编译器可能是有问题的。即使是不对应用进行任何鲁棒性增强操作,建议也替换掉Unity自带的mono以提升游戏性能。
表 6.2 安卓平台mono运行时测试结果

## 6.3 unity游戏il2cpp运行时对比测试
&ensp;&ensp;本测试的以同样的代码片段,编译选项均保持相同。Backend为il2cpp,优化级别为Master,color space为Gamma,Lightmap Encoding为High Quality,Quality为Ultra。
&ensp;&ensp;运行时间计算方式为:代码初始化开始计时,直至第一次UpdateAction计时结束,记录结果为三次测试均值。包体积大小计算方式为直接计算unity引擎打包后的包大小。
### 6.3.1 Windows平台il2cpp运行时性能测试
&ensp;&ensp;于Windows下,使用il2cpp运行时的unity游戏输出的日志也保存在“C:\Users\%username%\AppData\LocalLow\Unity\%packet%”下,分别使用直接打包,元数据破坏,花指令,代{过}{滤}理函数,数据结构重定义,整体加密六种方式进行打包。记录打包后各包大小后,运行性能测试软件满载计算机30分钟后运行游戏,记录代码初始化至第一次UpdateAction所需的时间。
&ensp;&ensp;整体加密的执行时间不负众望果然是性能最低下的,解密global-metadata.dat消耗的性能不容小觑,在实际使用时如无必要不建议使用。
&ensp;&ensp;花指令在本次测试中性能损失极小, 经过观察编译器优化将大量的跳转进行switch分发器优化,for循环展开,性能损失减小了许多。因此花指令在il2cpp运行时可以大规模使用而无需过于担心性能问题。
&ensp;&ensp;代{过}{滤}理函数性能测试中性能损失极小,经过反汇编观察,代{过}{滤}理函数又被编译器重新内联(inline)进原始函数中。&ensp;&ensp;因此在实际应用中il2cpp运行时并没有使用代{过}{滤}理函数的必要,因为编译优化会把它全部内联回去。
&ensp;&ensp;重定义数据结构在本次测试中与未处理性能相似,微小区别可以视作误差。因此该方案在实际应用可以放心大胆安心用。
表6.3 il2cpp运行时Windows平台测试结果


### 6.3.2 Android 平台il2cpp运行时性能测试
&ensp;&ensp;于Android 下,unity使用il2cpp运行时日志仍可以使用ADB工具的“adb logcat -s Unity”查看。分别使用直接打包,元数据破坏,花指令,代{过}{滤}理函数,数据结构重定义,整体加密六种方式进行打包。记录打包后各包大小后,运行性能测试软件满载计算机30分钟后运行游戏,记录代码初始化至第一次UpdateAction所需的时间。
&ensp;&ensp;如表所示,整体加密执行时间仍位居首位,即使是在Android平台上也是如此结果,因此在Android平台的il2cpp运行时不建议使用。
&ensp;&ensp;花指令在Android平台表现优秀,编译器优化有效的展开了循环,优化了switch分发器,此方案在il2cpp运行时的安卓平台可以使用。
&ensp;&ensp;重定义数据结构表现也仍然优秀,性能损耗基本与未处理一致,因此在Android 平台上改方案可以使用。
&ensp;&ensp;代{过}{滤}理函数经过反汇编发现聪慧的编译器已经将全部代{过}{滤}理函数又内联了回去,此方案在编译器优化下无任何效果。
&ensp;&ensp;元数据破坏在此表现也与未处理一致,由于编译为il2cpp后CSharp已经失去了高级特性,因此可以任意使用。
表6.4 il2cpp运行时在Android系统测试结果

## 6.4 unity游戏鲁棒性增强方案测试结论
&ensp;&ensp;本测试的结果已经反映了本文设计的几种鲁棒性增强方案性能与空间的对比,由此我们可以根据测试结果得出结论。
&ensp;&ensp;在Windows平台上整体加密方案因其性能问题无论在何种运行时上都不推荐使用。OPCode自定义在重编译mono后性能得到了极大的提升即便是不对OPCode进行自定义也推荐替换掉Unity官方的mono运行库。代{过}{滤}理函数在mono上运行效率较高可以使用而在il2cpp编译时代{过}{滤}理函数被内联进原始函数中对运行没有影响因此可以使用。花指令使用后会降低游戏的运行效率,插入的花指令越多运行速度越慢,适度使用可以平衡性能与鲁棒性。元数据破坏在mono的使用需要注意要求程序员不得使用CSharp的高级特性,而在il2cpp中无需注意。
&ensp;&ensp;在Android平台上,整体加密性能损耗过大因此在何种运行时都不宜使用。OPCode自定义在重编译mono后性能也如Windows平台上一般得到了极大提升,因此在安卓平台上也推荐替换掉Unity。代{过}{滤}理函数在mono运行时上由于频繁破坏ARM处理器执行流水线以及ARM处理器没有分支预测功能导致性能损失较大,而在il2cpp运行时由于编译器优化降低了性能影响,因此在mono运行时下如无必要可以避免使用代{过}{滤}理函数而在il2cpp运行时下可以使用。花指令在安卓平台上的表现与Windows上相似,只需要平衡加入的花指令数量与性能要求即可。元数据破坏在任意运行时均可使用,对性能无影响。而重打包后的apk包体积对比Unity直接输出的apk包减小了体积,对体积高度敏感可以尝试重新打包。

艾莉希雅 发表于 2024-2-17 23:28

# 总结
## 7.1 总结
&ensp;&ensp;unity游戏鲁棒性加强后,游戏的鲁棒性得到了显著的提升。在选择合适的鲁棒性加强方法后,游戏得以使用较少的性能损失换取更高的鲁棒性。
&ensp;&ensp;在本次毕业设计前,我便已经调查了解到了当前黑色产业对我国茁壮成长的游戏产业造成了极大的困扰。黑色产业的作为甚至改变了我国游戏产业从业人员的思维模式,当今从业人员普遍认为游戏的出路只有赚快钱。
&ensp;&ensp;黑色产业链缩短了游戏寿命,破坏了游戏平衡,削减了游戏收入。于是厂商便只能选择在黑色产业链进军自家游戏之前“捞回本”,在黑色产业进军自家游戏后采取极端运营策略,逼迫所有人进行“充值”、“消费”。在游戏寿命后期,厂商更是无所不用其极,甚至连现实生活中的各种经典骗术都运用了起来,诸如“充50送100”的空头支票、诸如“分享邀请码,填写邀请码的用户充值返10%”的传销手段。
&ensp;&ensp;在以往,厂商与黑色产业的对抗往往是处于下风的,黑色产业的技术能力往往比任何一间游戏厂商都要高出许多。游戏厂商无法预知未来,仅能根据现有条件兵来将挡水来土掩。绝大多数厂商又没有设立单独的安全部门,往往他们认为这种部门及无意义也无必要。
&ensp;&ensp;而有安全意识的游戏厂商,选择与安全厂商进行合作,购买现成的通用解决方案。在彼此均不信任的背景下,安全厂商端出的解决方案都是通用性较高的“万金油”解决方案,并不能根据每个游戏的引擎专门推出一种解决方案。而从事黑色产业可以从通用的解决方案中摸索出解决掉通用解决方案的方法,这使得这类“万金油”方案时效性极差,需要经常更新,而发布出去的游戏使用的则会变成“过时的”解决方案。
&ensp;&ensp;本设计中的方案,为打包时生成,使得每一次打包的“解决方案”均略有不同,极大的增加了游戏的鲁棒性。即便是一款使用本设计方案的游戏遭到荼毒,也不会变成使用本设计方案的作品全部敞开大门的结局。
&ensp;&ensp;相信我国的游戏产业终有一日会回归正轨,游戏业界的浮躁之风能消退,中国人也能产出优秀的游戏作品。我相信会有那么一天,大家不会谈国产游戏如闻虎色变。
页: [1] 2 3
查看完整版本: 增强游戏跨平台应用鲁棒性的设计与实现