一个Unity游戏保护方案的分析和还原符号信息,偷学对global-metadata保护的思路
2020-06-15 23:21:10 Author: bbs.pediy.com(查看原文) 阅读量:849 收藏

我发现,似乎每当我因为手残玩不过游戏的时候,似乎都会遇到倒霉的事情。

上一个帖子是因为手残打不过音游,试图作弊走捷径,结果遇到一个超麻烦的dll保护方案

这一个帖子是因为手残打不过魂类游戏,同样试图作弊,结果又遇到了一个新的保护方案

游戏是新的某帕姓魂类手游,TapTap上就有卖,推荐大家玩一下,质量还是不错的,顺便支持一下国产,反正也不贵,25块钱。
PS:不是广告

接下来是正题,来说说我分析这个游戏的时候都遇到了些什么,过于啰嗦请慎入。

首先当然是使用我们万能的GG修改器上去试一下了,结果发现这游戏并没有做内存保护,甚至没有检测GG,直接搜索数值改就成功了,顿时觉得索然无味。

但是怎么可以就这么简单地就结束呢?那多无聊,于是我把目标瞄准了游戏存档。

到数据目录下查看,发现了感兴趣的文件夹
数据文件

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

首先把lua资源拷贝出来,尝试用AssetStudio读取,发现读取失败。
这里是一个Unity3D读取资源打包资源的一个设置,它是可以设置偏移的。
这里在前面塞了一个tipsworks,这个好像是公司标识?
头部塞入字节

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

熟悉的LJ标志,这里开发者将所有lua脚本全部编译成了luajit框架的二进制代码。

在这里感谢 NightNord 大佬开发的ljd反编译框架以及后续的各位参与维护的大佬,让luajit编译之后的二进制代码依旧可以被还原为可读代码。

ljd框架 github地址

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

这里可以看到,它将存档路径和存档相关内容传入到了Recorder.write函数里面。也就是说我们找到这个Recorder类就行了。

然而事与愿违,这玩意不存在在Lua脚本之中,估计是使用了Wrap类在C#代码中实现了这个类。

那么就回到了我们熟悉的节奏了,逆向Unity3D游戏,不就是抱住 Prefare 大佬的大腿当一个脚本小子吗(雾)

在正式开始之前,先简单地说一下global-metadata文件(下称gm文件)的用处。

il2cpp技术是将C#转为C++代码的一种技术,然而和C++代码不同,C#之间的函数调用很多时候不是直接跳转,而是需要先通过符号查找函数地址,再进入函数。

因此C#在转为C++代码时,需要保留C#中的符号信息,比如函数定义,函数名称,类名称等等。

而Il2CppDumper的作用就是将gm文件里的信息提取出来,和il2cpp文件对应起来。

直接上Il2CppDumper,结果理所当然的出错了:
gm文件加密

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

正常的gm文件都是以AF 1B B1 FA字节开头的,这里没有,很明显游戏对gm文件进行了加密。

把ilbil2cpp.so文件拖入IDA,然后找到加载gm文件的函数,我们把它和原函数代码做一个对比:
gm文件加载函数
加载gm文件源代码

可以看到,两者之间非常相似,但是存在一定的区别。

对比着分析,大概知道了gm文件的解密流程。

首先读入gm文件,并且让一个指针指向它的头部。

再读取0x110大小字节数组,进行解密:
解密gm文件头

再将之后的内容进行解压缩解密,完毕。

在使用gm文件信息的时候,一般是通过gm文件指针加上gm头部结构的偏移值来指向需要的部分。在原函数中,gm文件指针和gm文件头部实际上指向的是同一个地址,因此直接使用一个指针就行了。

而在这里,gm文件头部和gm文件分开进行了解密,存在两个不同的位置,因此在使用gm文件信息时,会出现两个指针:
使用gm文件信息

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

现在,再使用Il2CppDumper来尝试提取符号信息:
gm文件字符串加密

dump成功,但是创建dll失败,原因是不明字符串,这让我有了不祥的预感。

打开dump.cs文件,结果一片乱码……
字符串被加密

很明显,部分字符串被加密混淆了,dump出来的信息基本没用……

本来到这里我都想放弃了,毕竟如果没有这些符号信息,il2cpp的逆向将会比直接cpp的逆向复杂无数倍,让人心态爆炸。

但是细心(?)的我发现了一个问题,这个加密混淆的系统将一些关键词也混淆掉了,比如Start,Update,Awake……

这里就涉及到一个Unity3D引擎的原理问题,U3D引擎通过Start,Update之类的关键词函数来调用用户写的代码,实现诸如初始化,帧更新等功能。

如果它连这些关键词都给加密混淆处理了的话,那么U3D引擎将无法执行用户的代码。

所以,为了让程序正常运行,它必定在内存中解密了这些字符串。

那么这个解密的时机选取在哪里比较好呢?我们先来分析一下。

第一个时机在读取gm文件时,这里我们已经分析过了,并没有解密相关的部分。

第二个时机在函数初始化的时候。在il2cpp技术转化出来的Cpp函数开头会有这么一部分:
初始化函数信息

这里就是函数信息初始化的部分,在函数第一次被调用的时候,执行初始化函数。

追踪下去,可以看到最主要的部分:
初始化主体

分别对函数信息和类信息初始化的部分继续跟踪分析,借助原代码进行对比,很快就可找到解密字符串的部分。

这里为了保护一下开发者,不全部公开了,提示是将字符串每个字节分别与某个值进行异或处理,即可解密。

至于这个值是怎么获取的,大家有兴趣就自己找找吧。

接下来根据解密方式改造一下Il2CppDumper工具,再次解密:
修改的IL2CPPDUMPER

成功将信息dump出来,获取到了明文信息:
字符串被解密

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

解密得到的存档:
解密的存档

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

接下来就是神装走起,满级出门,然后被BOSS血虐。

这个故事说明了一个道理:在魂类游戏中手残不是靠装备和等级可以弥补的。

说说这次逆向分析的感谢吧,首先就是这个gm文件加密还是挺少见的。最常见的就是给so文件加壳,然后被动态dump,防御力只有5(说的就是那些卖保护机制十几万块每年还做的没啥防御力的公司)。

加密gm文件感觉其实比给so文件加壳更加有效,至少能够挡住大部分只依靠工具的脚本党(比如我,只会抱大佬大腿)

我一直以来的观点是与其想着怎么挡住那些抱着恶意的攻击者,倒不如想着怎么提高他们的破解成本,给他们制造麻烦,延长他们的破解时间,更加符合游戏保护的目的。

这次的保护方式还挺对我胃口的,算是从U3D引擎原理的层面来进行保护,需要对引擎的机制有一定了解才能更好地进行分析。可惜最后的混淆不是对非关键函数进行随机字符串替换,只是简单地加密,被发现了破绽。

分析这些保护方式真的是层层递进,像是剥洋葱一样,流着泪分析。在一堆的代码中间找和原版程序不一样的地方,再进行分析,算是个体力活吧,所幸最终成功了,还是挺有的成就感的。

最近我就要搞大三的生产实习了,找的工作是某公司的客户端安全,现在心情坎坷,不知道去了之后会不会拖团队的后腿,或者说某些方面不了解而犯错。

有没有那个大佬有兴趣说说实习都要干些啥啊,我就大二出去实习过一次,还不是搞安全,只算是长见识。

平时干啥都是自己瞎搞,现在担心我的各种不规范和外行行为会被大佬看穿,反思自己为啥找了个这么样的家伙进来……

作为当代废柴大学生代表,我现在慌得一批。

[培训]科锐逆向工程师培训(6月24日远程教学开班, 第38期)!


文章来源: https://bbs.pediy.com/thread-260071.htm
如有侵权请联系:admin#unsafe.sh