查看原文
其他

一个Unity游戏保护方案的分析和还原符号信息,偷学对global-metadata保护的思路

Mengluu 看雪学院 2021-03-07

本文为看雪论坛优秀文章

看雪论坛作者ID:Mengluu





前言


我发现,似乎每当我因为手残玩不过游戏的时候,似乎都会遇到倒霉的事情。
 
上一个帖子是因为手残打不过音游,试图作弊走捷径,结果遇到一个超麻烦的dll保护方案。
 
这一个帖子是因为手残打不过魂类游戏,同样试图作弊,结果又遇到了一个新的保护方案。
 
游戏是新的某帕姓魂类手游,TapTap上就有卖,推荐大家玩一下,质量还是不错的,顺便支持一下国产,反正也不贵。
 
接下来是正题,来说说我分析这个游戏的时候都遇到了些什么。





初见


首先当然是使用我们万能的GG修改器上去试一下了,结果发现这游戏并没有做内存保护,甚至没有检测GG,直接搜索数值改就成功了,顿时觉得索然无味。
 
但是怎么可以就这么简单地就结束呢?那多无聊,于是我把目标瞄准了游戏存档。
 
到数据目录下查看,发现了感兴趣的文件夹:

 
一个Save文件夹,一个android文件夹。

Save文件夹里面为我们的目标:存档文件,而android文件夹里面是热更新的资源,一个典型的tolua框架。

 
首先把lua资源拷贝出来,尝试用AssetStudio读取,发现读取失败。

这里是一个Unity3D读取资源打包资源的一个设置,它是可以设置偏移的。
这里在前面塞了一个tipsworks,这个好像是公司标识?

 
写个脚本,把前面的字节去掉就可读取了,直接把lua资源解压出来,尝试用文本编辑器打开:

 
熟悉的LJ标志,这里开发者将所有lua脚本全部编译成了luajit框架的二进制代码。
 
在这里感谢 NightNord 大佬开发的ljd反编译框架以及后续的各位参与维护的大佬,让luajit编译之后的二进制代码依旧可以被还原为可读代码。
 

ljd框架 github地址https://github.com/NightNord/ljd

 
把所有脚本用ljd框架转为可读的脚本之后,我们可以快速定位到Save相关部分,找到保存相关的代码:

 
这里可以看到,它将存档路径和存档相关内容传入到了Recorder.write函数里面。也就是说我们找到这个Recorder类就行了。
 
然而事与愿违,这玩意不存在在Lua脚本之中,估计是使用了Wrap类在C#代码中实现了这个类。
 
那么就回到了我们熟悉的节奏了,逆向Unity3D游戏,不就是抱住 Prefare 大佬的大腿当一个脚本小子吗。





global-metadata的保护与还原


在正式开始之前,先简单地说一下global-metadata文件(下称gm文件)的用处。
 
il2cpp技术是将C#转为C++代码的一种技术,然而和C++代码不同,C#之间的函数调用很多时候不是直接跳转,而是需要先通过符号查找函数地址,再进入函数。
 
因此C#在转为C++代码时,需要保留C#中的符号信息,比如函数定义,函数名称,类名称等等。
 
而Il2CppDumper的作用就是将gm文件里的信息提取出来,和il2cpp文件对应起来。
 
直接上Il2CppDumper,结果理所当然的出错了:

 
从错误提示中可以看到,它没能识别出gm文件,用hex打开,发现连gm文件头的标识都没了:

 
正常的gm文件都是以AF 1B B1 FA字节开头的,这里没有,很明显游戏对gm文件进行了加密。
 
把ilbil2cpp.so文件拖入IDA,然后找到加载gm文件的函数,我们把它和原函数代码做一个对比:


 
可以看到,两者之间非常相似,但是存在一定的区别。
 
对比着分析,大概知道了gm文件的解密流程。
 
首先读入gm文件,并且让一个指针指向它的头部。
 
再读取0x110大小字节数组,进行解密:

 
再将之后的内容进行解压缩解密,完毕。
 
在使用gm文件信息的时候,一般是通过gm文件指针加上gm头部结构的偏移值来指向需要的部分。

在原函数中,gm文件指针和gm文件头部实际上指向的是同一个地址,因此直接使用一个指针就行了。
 
而在这里,gm文件头部和gm文件分开进行了解密,存在两个不同的位置,因此在使用gm文件信息时,会出现两个指针:

 
指向这两个部分的是全局变量,因此直接靠偏移就可以在内存中找到这两个部分,dump下来之后,将头部信息的0x110个字节覆盖到解密之后的主体文件中,就获得到了解密之后的gm文件:

 
现在,再使用Il2CppDumper来尝试提取符号信息:

 
dump成功,但是创建dll失败,原因是不明字符串,这让我有了不祥的预感。
 
打开dump.cs文件,结果一片乱码……

 
很明显,部分字符串被加密混淆了,dump出来的信息基本没用……
 
本来到这里我都想放弃了,毕竟如果没有这些符号信息,il2cpp的逆向将会比直接cpp的逆向复杂无数倍,让人心态爆炸。
 
但是细心的我发现了一个问题,这个加密混淆的系统将一些关键词也混淆掉了,比如Start、Update、Awake……
 
这里就涉及到一个Unity3D引擎的原理问题,U3D引擎通过Start,Update之类的关键词函数来调用用户写的代码,实现诸如初始化,帧更新等功能。
 
如果它连这些关键词都给加密混淆处理了的话,那么U3D引擎将无法执行用户的代码。
 
所以,为了让程序正常运行,它必定在内存中解密了这些字符串。
 
那么这个解密的时机选取在哪里比较好呢?我们先来分析一下。
 
第一个时机在读取gm文件时,这里我们已经分析过了,并没有解密相关的部分。
 
第二个时机在函数初始化的时候。在il2cpp技术转化出来的Cpp函数开头会有这么一部分:

 
这里就是函数信息初始化的部分,在函数第一次被调用的时候,执行初始化函数。
 
追踪下去,可以看到最主要的部分:

 
分别对函数信息和类信息初始化的部分继续跟踪分析,借助原代码进行对比,很快就可找到解密字符串的部分。
 
这里为了保护一下开发者,不全部公开了,提示是将字符串每个字节分别与某个值进行异或处理,即可解密。
 
至于这个值是怎么获取的,大家有兴趣就自己找找吧。
 
接下来根据解密方式改造一下Il2CppDumper工具,再次解密:

 
成功将信息dump出来,获取到了明文信息:

 
接下来就是正常的分析过程了,最后得知存档文件的加密方式是通过AES加密,密钥为tiencikpncoanvsnauewjxzogtrdfkes,再base64编码即可。
 
解密得到的存档:

 
到这里,我们的目标就完全实现了。





总结


接下来就是神装走起,满级出门,然后被BOSS血虐。
 
这个故事说明了一个道理:在魂类游戏中手残不是靠装备和等级可以弥补的。
 
说说这次逆向分析的感谢吧,首先就是这个gm文件加密还是挺少见的。

最常见的就是给so文件加壳,然后被动态dump,防御力只有5(说的就是那些卖保护机制十几万块每年还做的没啥防御力的公司)。
 
加密gm文件感觉其实比给so文件加壳更加有效,至少能够挡住大部分只依靠工具的脚本党(比如我,只会抱大佬大腿)。
 
我一直以来的观点是与其想着怎么挡住那些抱着恶意的攻击者,倒不如想着怎么提高他们的破解成本,给他们制造麻烦,延长他们的破解时间,更加符合游戏保护的目的。
 
这次的保护方式还挺对我胃口的,算是从U3D引擎原理的层面来进行保护,需要对引擎的机制有一定了解才能更好地进行分析。可惜最后的混淆不是对非关键函数进行随机字符串替换,只是简单地加密,被发现了破绽。
 
分析这些保护方式真的是层层递进,像是剥洋葱一样,流着泪分析。在一堆的代码中间找和原版程序不一样的地方,再进行分析,算是个体力活吧,所幸最终成功了,还是挺有的成就感的。




- End -




看雪ID:Mengluu

https://bbs.pediy.com/user-848784.htm

  *本文由看雪论坛 Mengluu 原创,转载请注明来自看雪社区。



推荐文章++++

* HW前期之分析一款远控木马

* 熊猫烧香病毒逆向过程及其分析思路

* Galgame汉化中的逆向 (一):文本加密(压缩)与解密

* 恶意样本分析学习—GlobeImposter3.0勒索病毒分析

* 逆向学习sgavmp篇



好书推荐















公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存