本篇文章已上传至 bilibili up主:水番正文-【第玖期 REVERSE 分享会】直播录屏,空降50分钟可以观看讲解视频,题目讲解过程就不放在博客文章中了
【第玖期 REVERSE 分享会】拨云见日之浅谈flutter逆向 & LLVM的超简易入门 & 从出题到做题——探Unity游戏逆向_哔哩哔哩_bilibili
前言——unity相关基础
(有过相关基础的师傅可以移步正篇内容)
unity引擎与游戏文件
unity是一款游戏引擎,可以开发多平台,多系统下的2D 3D游戏,游戏平台上的一些小游戏,或者像我们熟悉的手游王者荣耀,原神。。。都是基于unity开发的游戏。
我们去打开一个unity游戏的文件夹时,我们可能会遇到下面这两种形式的文件形式,.exe便是我们的游戏启动器,无论是左边还是右边,都有一个后缀为_Data的文件夹
继续跟进目录后 我们就会发现两者有点细微的差别,左边名为il2cpp,右边名为管理的managed文件,想要进一步了解unity内部的原理,了解这两种的区别,就要接着往下去认识这两种语言。
C#和Mono
C sharp以.net框架作为基础一种面向对象的语言。我们知道unity有强大的多平台部署能力,而支撑这一能力的关键,就是包含了c#编译器等其他工具的开源项目Mono
Mono:一个由 Xamarin公司主持的自由开放源代码项目,目标是创建一系列符合ECMA标准(Ecma- 334和Ecma-335)的.NET工具包括C#编译器和通用语言架构。与微软的.NET Framework(共通语言运行平台)不同Mono项目不仅可以运行于Windows系统上,还可以运行于 Linux,FreeBSD,Unix,OS X和Solaris,甚至一些游戏平台。Mono使得C#这门语言有了很好的跨平台能力。
上右文件夹中的MonoBleedingEdge文件夹便是包含运行Unity服务器所需的所有Mono运行时库和依赖项。
在2014年,Unity3D官方博客上发布了“The future of scripting in unity”的文章,引入了比mono更为安全的il2cpp技术
IL语言
上文的il,全称Intermediate Language (中间语言),如何更好地理解他呢,我们可以把它理解为.NET框架下的“汇编”(低阶的人类可读编程语言)。我们实际在dnspy中去看一看他的结构
1 2 3 4 5 6 7 8 9 10 11 | public class PinHead : MonoBehaviour { // Token: 0x06000014 RID: 20 RVA: 0x0000221E File Offset: 0x0000041E private void OnTriggerEnter2D(Collider2D collision) { if (collision.tag != "PinHead") { GameObject.Find("GameManager").GetComponent<GameManager>().GameOver(); } } } |
可以看出一段Csharp代码的对应IL中,出现了call,ldstr等类似与汇编语言中的命令,
通过一张图片我们可以更加清楚的理解上文提到的C#,高级语言如C# vb unityscript等语言经过编译,会先转为IL,IL再通过后续的编译,转为机器码使计算机去执行相应的指令。
正篇——逆向中的unity
出题
借着一次招新赛出题的机会,尝试去自己开发一下unity游戏,经过自己的实际开发,我发现一款游戏产生就只有简单的三步,设计构思-素材设计-代码编写,在强大的unity引擎帮助下,很多类似物理效果,动画效果的实现都是有unity中自带的一些模块库就可以轻松实现(Boxcollider 2D就可以实现一个实体 Rigitbody 2D就能实现2的游戏的物理效果)
而我们需要去做的就是编写出结合游戏设计思路的关键脚本(比如游戏玩法,规则,人物的移动,关卡的转换逻辑之类的)
在这一部分主要想和大家分享一下在写c#脚本时的关键点,我们了解了正向的过程,能更好的去理解逆向的过程。我们在unity中新建一个C#脚本时,他会默认生成两个方法名Start和update
start叫做生命周期函数,就是他会在启动脚本时第一个去执行他,我们比如在这里就是实现了,在每次执行玩家c#代码时,先定义一个animator变量,再去执行下面的update代码。
update顾名思义就是更新,他是游戏画面逻辑更新的关键代码,很多玩家的行为,都写在这个方法之中,比如这里写到的就是一个简单的控制玩家行走跳跃的代码,以及按键的设置功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | void Start() { //获取animator组件 animator = GetComponent<Animator>(); } void Update() { //获取玩家的左右键按键 -1,0,1 (a和d) float h = Input.GetAxis("Horizontal"); if (h != 0) { //移动.方向 * 上一帧花费的时间*玩家按键 transform.Translate(transform.right*Time.deltaTime*h); //这里实现的是人物的方向,为了让人物行走更加真实,方向判断根据上面的h if (h<0) { GetComponent<SpriteRenderer>().flipX = false; } if(h>0) { GetComponent<SpriteRenderer>().flipX = true; } animator.SetBool("run",true); } else { animator.SetBool("run",false); } if(isOnGround && Input.GetKeyDown(KeyCode.W)) { GetComponent<Rigidbody2D>().AddForce(Vector2.up*180); } } } |
那么作为一名逆向的思路,我们在寻找函数时,如果遇到的类似的2d游戏,在想去查找一些关键的执行逻辑在哪些方法中实现,可以直接去寻找update方法名的关键词,从中去获取到一些信息。
做题经历
在市面上普遍有两种unity逆向,他们分别使用不同的编译方式,解决的思路也不同。
Mono编译
这是原始的编译方式,也是最容易攻破,不太安全的一种编译方式,生成的游戏文件夹中,游戏的关键代码文件会直接生成在 _Data\Managed\Assembly-CSharp.dll的文件之中,出题人一般会想让我们通过破解游戏逻辑,或者在c#中进行解密来获取到最后的flag。
JEEK大挑战2023-是男人就来扎针
安洵ctf2023-牢大我想你了
(题目讲解请参考文章开头视频)
il2cpp编译
这串怎么去读呢? IL二cpp吗?按照英文的读法,应该是 IL two(to)cpp,也就是从IL中间语言转换为cpp的过程,这一过程是如何实现的呢。
在上文的基础中我们已经知道,unity的代码在执行时,会先将高级语言转换为相应的IL中间语言,中间语言再通过Mono VM编译为机器码。刚才提到了The future of scripting in unity,便是使用了IL2CPP技术将原先的过程变为了,在代码转换为IL之后,不会通过Mono编译,而是通过IL2cpp转变为c++代码,然后再使用本地的c++编译器编译为ASM汇编代码,通过IL2cpp VM转为机器码执行。
图片可以很直观的看到改变的步骤
AOT(Ahead Of Time)编译而非JIT(Just In Time)编译Unity之IL2CPP解析-腾讯游戏学堂
而通过出题我们也很明显的看到了这一过程,将编译选择为IL2cpp后,生成项目时编译会出现“编译为c”“编译为ASM”这样的提示
我们的文件夹中也是提示不要伴随你的游戏文件的文件夹中,有il2cpp相应的输出文件,看到里面便是.cpp代码。也就是说,我们发布游戏的话,只用打包data中的文件,而不用把c#的dll直接像mono一样暴露在外面,所以说这种方式也就更为安全。
打开data文件夹中,我们发现只有少的可怜的global-metadata(元数据)但是这其中就包含着C#中的类名、方法名、属性名、字符串等地址信息,程序启动时会按需从中读取。可以说是il2cpp的关键逆向入口点。
XYCTF babyunity
使用工具
Il2CppDumper
1 | Il2CppDumper.exe GameAssembly.dll global-metadata.dat output-out |
将游戏文件中的函数名恢复正常 进行解题
结语——后期的项目
这几次的unity经历更多还是从基础的原理或者简单的题目中去学习的,还没有真正去实战的角度去分析,在整理资料的时候发现了一位师傅实战某手游的文章,弄完下一个项目之后计划再去搞一下类似的项目。
才疏学浅,希望本篇能够起到抛砖引玉的效果,所讲内容中有误的地方也希望师傅们可以批评指正,同时也希望能与更多感兴趣的师傅们一起继续讨论unity逆向相关的内容。
本文参考
[原创] XYCTF两道Unity IL2CPP题的出题思路与题解-CTF对抗-看雪-安全社区|安全招聘|kanxue.com