我发现,似乎每当我因为手残玩不过游戏的时候,似乎都会遇到倒霉的事情。
上一个帖子是因为手残打不过音游,试图作弊走捷径,结果遇到一个超麻烦的dll保护方案
这一个帖子是因为手残打不过魂类游戏,同样试图作弊,结果又遇到了一个新的保护方案
游戏是新的某帕姓魂类手游,TapTap上就有卖,推荐大家玩一下,质量还是不错的,顺便支持一下国产,反正也不贵,25块钱。
PS:不是广告
接下来是正题,来说说我分析这个游戏的时候都遇到了些什么,过于啰嗦请慎入。
首先当然是使用我们万能的GG修改器上去试一下了,结果发现这游戏并没有做内存保护,甚至没有检测GG,直接搜索数值改就成功了,顿时觉得索然无味。
但是怎么可以就这么简单地就结束呢?那多无聊,于是我把目标瞄准了游戏存档。
到数据目录下查看,发现了感兴趣的文件夹
一个Save文件夹,一个android文件夹。Save文件夹里面为我们的目标:存档文件,而android文件夹里面是热更新的资源,一个典型的tolua框架。
首先把lua资源拷贝出来,尝试用AssetStudio读取,发现读取失败。
这里是一个Unity3D读取资源打包资源的一个设置,它是可以设置偏移的。
这里在前面塞了一个tipsworks,这个好像是公司标识?
写个脚本,把前面的字节去掉就可读取了,直接把lua资源解压出来,尝试用文本编辑器打开:
熟悉的LJ标志,这里开发者将所有lua脚本全部编译成了luajit框架的二进制代码。
在这里感谢 NightNord 大佬开发的ljd反编译框架以及后续的各位参与维护的大佬,让luajit编译之后的二进制代码依旧可以被还原为可读代码。
ljd框架 github地址
把所有脚本用ljd框架转为可读的脚本之后,我们可以快速定位到Save相关部分,找到保存相关的代码:
这里可以看到,它将存档路径和存档相关内容传入到了Recorder.write函数里面。也就是说我们找到这个Recorder类就行了。
然而事与愿违,这玩意不存在在Lua脚本之中,估计是使用了Wrap类在C#代码中实现了这个类。
那么就回到了我们熟悉的节奏了,逆向Unity3D游戏,不就是抱住 Prefare 大佬的大腿当一个脚本小子吗(雾)
在正式开始之前,先简单地说一下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引擎原理的层面来进行保护,需要对引擎的机制有一定了解才能更好地进行分析。可惜最后的混淆不是对非关键函数进行随机字符串替换,只是简单地加密,被发现了破绽。
分析这些保护方式真的是层层递进,像是剥洋葱一样,流着泪分析。在一堆的代码中间找和原版程序不一样的地方,再进行分析,算是个体力活吧,所幸最终成功了,还是挺有的成就感的。
最近我就要搞大三的生产实习了,找的工作是某公司的客户端安全,现在心情坎坷,不知道去了之后会不会拖团队的后腿,或者说某些方面不了解而犯错。
有没有那个大佬有兴趣说说实习都要干些啥啊,我就大二出去实习过一次,还不是搞安全,只算是长见识。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课