四、Unity逆向 – UNI'S ON AIR资源提取逆向全记录

  上篇文章最后说道,如果能从正面开发的过程去分析软件,那逆向的过程会容易很多。我之前已经确认了UNI'S ON AIR这个游戏是由Unity3d开发的,那么了解Unity的源代码和最终编译目标文件的对应关系就很重要了。这个过程中『unity游戏生成与修改so文件教程』这个帖子给了我非常大的帮助,参照这个教程我开始了整个过程中最核心的逆向分析。

0x01 编译过程分析

  由于之前有过Cocos的开发经验,因此对于Unity是怎么样一个流程我心里面是有数的,主要一大不同是Cocos采用JavaScript作为其脚本语言,而Unity采用C#作为其脚本语言,其他方面例如场景、预制、精灵、组件等都大同小异。

  我们首先需要对C#这个语言有一定了解,C#虽然名字里带着C,但是它与C/C++的关系远不像C和C++的关系那样亲密。C#是微软公司推出的面向对象的高级编程语言,而C/C++则是相对其较低级的编程语言。C#所对标的语言是Java,可以说在C/C++和Java中,C#更像Java。

  C#编译出来的目标文件虽然和C/C++同样是.dll动态链接库文件或.exe可执行文件,但是这和C/C++编译出来的机器码二进制文件是完全不一样的,C#编译出来的其实是.NET中间语言,一般称作IL (intermidiate language)。执行IL时需要借助.NET框架的即时编译技术(just-in-time, JIT)将IL翻译成机器码运行。借用Java的描述方式来说就是,C#编译出来的IL相当于Java中的.class字节码,.NET就相当于Java里的虚拟机,只有当系统中安装了.NET这种虚拟机,才可以运行这种IL文件,这也意味着.NET也是具有跨平台运行能力的。

  这就解释了为什么Unity采用了C#语言却可以运行在各种各样的平台上。Unity采用了Mono这个开源的.NET实现,这样只需要在不同的平台上编译一次Mono,就可以用编译好的Mono来运行各种由C#语言编写的游戏逻辑了。由于C#和Java同样采用了编译成中间语言的方式,因此这种IL与Java字节码一样能够很容易地反编译得与源代码几乎一致。

  在通常的编译设置下,Unity会生成Assembly-CSharp.dll文件来存放游戏的主逻辑,另外还有Assembly-CSharp-firstpass.dll,通常是与Unity插件有关的一些代码,在顺序上会先于Assembly-CSharp.dll加载。对于这样的Unity Android游戏,把它的apk解包后就可以在assets\bin\Data\Managed目录里看到这些dll文件,然后随便用个.NET反编译软件,比如ILSpy,打开Assembly-CSharp.dll就可以看到全部的源代码了。

一个通常的Unity游戏ILSpy反编译概览

  然而UNI'S ON AIR这个游戏相应的目录下并没有Assembly-CSharp.dll这个文件,也没有其他任何.dll的文件,这其实是因为开发者在编译游戏的时候选择了原生编译的方式。也就是说在编译的时候就将中间语言直接转换成目标平台的机器码,这样虽然失去了.NET的跨平台特性,但是却可以加快程序的运行速度,对于一些耗资源的游戏是很有帮助的,再者这样可以更好的保护代码,给反编译增加难度。

  具体的来说,Unity是通过IL2CPP实现的,IL2CPP是Unity在2014年引出的概念,即把.NET编译出的IL转换成cpp文件(Intermediate Language to cpp),然后再进行原生编译。而游戏开发者编写的脚本代码逻辑最终会放在libil2cpp.so这个动态链接库中,这个文件包含了原来Assembly-CSharp.dll以及Assembly-CSharp-firstpass.dll的内容,因此要继续的话,只能去反汇编这个ELF文件了。

0x02 初步反汇编

  反汇编最好用的就是大名鼎鼎的IDA了,用32位IDA(64位的IDA无法使用反编译C语言功能)打开libil2cpp.so然后等分析完成,要注意分析的时间可能会比较长,UNI'S ON AIR的libil2cpp.so大小有31MB,用了一个多小时才分析完。

  之前提到过,我需要在反编译后跟踪包含".usme"的字符串来找到解密函数,然而IDA分析后的结果似乎没有包含任何常量字符串,甚至连函数名也没有。这是因为Unity将字符串放在了单独的地方,即assets\bin\Data\Managed\Metadata\global-metadata.dat这个资源文件中,只有在程序动态运行时才会将这些字符串读入内存,这样一来用IDA进行静态分析就更困难了。

IDA运行script.py之前(左)和之后(右)的对比,可以看到字符串以及函数名信息已经自动加上了,极大地方便了逆向过程

  不过好在有大佬帮我们解决了这个问题,Il2CppDumper这个程序可以通过分析global-metadata.datlibil2cpp.so文件,方便地帮我们在IDA中添加上对应的字符串以及还原出C#代码中类的定义。然而我多加注意了一下这个项目,突然发现这不是写AssetStudio的那个国人大佬Perfare吗?后来又找到大佬的个人博客看了一看简直惊呆了,Perfare大佬一个人撑起了整个Unity游戏反编译的领域,真是太厉害了!

  好了话题说回来,使用Il2CppDumper的时候要注意一定要把Unity的版本输入正确,否则的话是不能成功。Unity的版本号可以通过资源文件或者UnityFS(.unity3d)文件获取,用AssetStudio加载后可以看到版本号,或者用编辑器打开在首行也会有版本号。

  成功运行Il2CppDumper后会生成dump.csscript.pystringliteral.jsonDummyDll这几个文件及文件夹。dump.cs文件包含了反编译出的C#类定义以及在so文件中对应的偏移地址,stringliteral.json包含了so文件中的字符串常量以及对应的偏移地址。另外在IDA中可以通过File -> Script file加载script.py脚本,这样就可以直接在IDA中加上引用的函数名以及使用的字符串常量了,非常方便。

stringliteral.json中搜索.usme并在IDA中进行代码定位

  现在就可以在stringliteral.json中搜索".usme"了,搜了下果然有几个字符串常量,记录下这几个字符串的地址,然后在IDA中按G跳转到这个地址,就可以看到相应的代码引用开始跟踪分析了,如果对汇编语言不是太熟悉,可以按F5反编译成C代码辅助理解。然而我发现,这种.usme文件的路径字符串好像是直接传给了CriMana.PlayerSetFile方法的moviePath参数。这说明并不是我之前想的那样,先将.usme通过某个解密函数解密成.usm,然后再将数据传给CriMana.Player,这种.usme文件很可能就是.usm文件。

  关于.usme就是.usm这一点,我后来通过编辑器直接打开.usme文件也得到了确认。CRIWARE在转换生成这种usm文件时会带有一些元数据,这些元数据保存在usm文件的开头,其中字符串是直接可读的,通过这些元数据可以看到最初生成的文件名就是usm。因此这些文件是被开发者人为改成.usme后缀的,至于为什么要加个e大概是为了区分是否是加密的吧。

  那会不会就是我的另一种猜测,密钥其实是CRIWARE SDK自带的呢?顺着这个思路,我又对汇编代码以及dump.cs文件做了进一步地分析,下篇文章就该讲到核心的汇编分析了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注