内容简介:本人虽然是个手残,却非常喜欢尝试各种音游,即使被虐到爆炸也停不下来。最近看上一款某渊的音游,它的判定线移动打拍的玩法挺不错的,于是乎又手贱买了,然后被虐到体无完肤。本着至少要给自己爽一下的原则,就准备对它下毒手了。
本人虽然是个手残,却非常喜欢尝试各种音游,即使被虐到爆炸也停不下来。
最近看上一款某渊的音游,它的判定线移动打拍的玩法挺不错的,于是乎又手贱买了,然后被虐到体无完肤。
本着至少要给自己爽一下的原则,就准备对它下毒手了。
首先拆一下压缩包,发现是unity引擎,而且还是 Assembly-CSharp.dll 的方式,感觉也许会轻松不少(并不是
拖出来,上dnSpy,理所当然地出错。
拖进UltraEditor,发现可以明显地看出PE文件结构,就是有点怪而已。所以这应该不是那种简单地把整个dll文件套个加密算法,然后加载的时候解密那种方法。
大概是加密了关键部分,然后在解析的时候再解密吧。
试一下能不能Dump,ida附加,结果游戏直接闪退,估计是有反调试之类的,咱一个逆向萌新,对安卓方面的反调和反反调完全不懂,瞬间懵逼。
于是咱打算换一个歪门邪道,用GG修改器的Dump功能,这个游戏似乎没有针对GG修改器做什么。
Dump下整个游戏进程,然后在里面搜索DLL,把文件提取出来,但是发现,其他什么 UnityEngine 什么的都有,唯独缺了 Assembly-CSharp.dll。
抱着不祥的预感在内存中搜索 6D 7A 90 00 03 00 00 00 04, 发现dll文件本身在内存中没有半点变化……
估计是魔改mono文件,然后用自己的一套来解析的吧……
拖libmono.so到ida里面,结果发现libmono.so也被搞了……
看一下Hex,被大量无意义字节填充了,估计是在载入so的时候再将原函数填回去……
还真是到处搞事情啊,不过so方面不比dll,到了内存里面你总得把这些函数给我还回来吧。
还是上GG(真好用),把 libmono.so给Dump 出来,记得选范围时要完整,宁可范围搞大一点,也别少选
修复一下Dump出来的文件,可以参考这里: 内存dump 获得基于Unity3d的游戏相关代码
再拖进IDA,现在就没问题了
通过之前的试探,我们大概可以猜测一下dll被加载的过程,首先是原文件读入内存,再通过魔改后的mono使用自己的一套方法来解析文件。
看其他dll都没有事,所以在这个魔改后的mono中应该分开了两种不同的加载方式。
mono加载dll的时候,首先是进 mono_image_open_from_data_with_name这个函数(这个应该就不用细说了),然后走到 do_mono_image_load 对PE文件进行分析。
static MonoImage *
do_mono_image_load (MonoImage *image, MonoImageOpenStatus *status,
gboolean care_about_cli, gboolean care_about_pecoff)
{
MonoCLIImageInfo *iinfo;
MonoDotNetHeader *header;
mono_profiler_module_event (image, MONO_PROFILE_START_LOAD);
/* if MONO_SECURITY_MODE_CORE_CLR is set then determine if this image is platform code */
image->core_clr_platform_code = mono_security_core_clr_determine_platform_image (image);
mono_image_init (image);
iinfo = image->image_info;
header = &iinfo->cli_header;
if (status)
*status = MONO_IMAGE_IMAGE_INVALID;
if (care_about_pecoff == FALSE)
goto done;
if (!mono_verifier_verify_pe_data (image, NULL))
goto invalid_image;
if (!mono_image_load_pe_data (image))
goto invalid_image;
if (care_about_cli == FALSE) {
goto done;
}
if (!mono_verifier_verify_cli_data (image, NULL))
goto invalid_image;
if (!mono_image_load_cli_data (image))
goto invalid_image;
if (!mono_verifier_verify_table_data (image, NULL))
goto invalid_image;
#ifndef USE_COREE
/* if the last bit is not set, then the image is mixed mode with native code */
if (!(iinfo->cli_cli_header.ch_flags & 1))
goto invalid_image;
#endif
mono_image_load_names (image);
load_modules (image);
done:
mono_profiler_module_loaded (image, MONO_PROFILE_OK);
if (status)
*status = MONO_IMAGE_OK;
return image;
invalid_image:
mono_profiler_module_loaded (image, MONO_PROFILE_FAILED);
mono_image_close (image);
return NULL;
}
首先是 mono_image_load_pe_data 函数对PE文件进行解析,这里有一个检验MZ头的步骤,而这个dll的头部是mz,所以这里应该有问题
不仅仅认正常的MZ,也认修改后的mz,没错了。
顺着加载过程摸下去,我们还可以找到魔改后同时认可PE和pe的部分
修改这里两处标识后之后,继续尝试读取dll文件,仍旧失败,根据失败原因继续走,发现时dll文件的段头被加密了,在mono中对应部分找到解密方法
段头的每一项都被单独处理,加密过程中密钥会随着改变的一个流加密,有伪代码的情况下还原解密方法没什么难度。
继续读取,发现PE结构解析部分没有问题,但还是出错了,继续往后跟,发现是.Net部分读取时出了问题。
在#~读取的时候,其中的各个项都出现读取失败的情况,但是奇怪的是,最开始的几个项读取正常。在load_tables函数中发现,读取MaskValid时做了处理,将MaskValid和MaskSorted异或后再保存。
但是在分析这个函数的时候,发现它和原版有很大的区别,它对一个全局变量进行赋值,将Reserved和Maskvalid拼接后保存了。先记下来,估计之后会用到。
修复MaskValid后,发现可以正常读取#~中的内容了,Method项中的函数名也可以被正常读取,但是在反编译函数时提示错误
所以游戏应该还在函数上单独加了料
Method中保存了所有函数的RVA,转化为Offset后可以直接定位到函数在文件中的位置。
找过去,在函数头里发现了猫腻,dll文件中的函数头按照正常的解析方式得出来的函数大小全部都是0,在ida中寻找解析函数头的对应位置,发现确实动了一点小手脚
都被或2了,同时,在这个函数中,还发现了对函数体解密的部分,正好一起搞了
这里利用了之前保存的 Reserved和MaskValid 进行解密,还有……这算啥?低配虚拟化?
然后,采用这种方法解密函数体后,反编译仍旧出错,排查了好久,发现在使用完解密完 header 和函数体后,它还在 mono_method_get_header 对函数体每一个字节异或0x30,真是麻烦,到处下坑。
解密完全部函数体,将它拖到dnSpy中,依旧爆炸……
虽然绝大部分函数炸了,但是却还有一些极小的函数可以被正常反编译
看到这里,我大概猜到它做了什么……
它应该是打乱了Opcode表,重构了读取Opcode的方法……
我自己也干过这事……在加密dll的时候打乱Opcode……
没辙,Opcode被打乱,我就只能对照着正常dll加载的函数和魔改版来分析修改后的Opcode表
分析Opcode的函数快两万行,我的电脑配置不够,要是用伪代码的方式一下就直接卡死,只能扣掉F5,一点点看汇编……
花了两个多星期,我才把新的Opcode分析完。它不仅仅打乱顺序,一些相邻的Opcode码,比如ldarg0,ldarg1这些,原版是采用n-ldarg0的方式来一次性分析这一个系列的Opcode,但是魔改版直接将n-ldarg0这个值固定死,然后把这些必须相邻的Opcode也拆开来分析了,给我造成了巨大的麻烦……
还原Opcode之后,再用dnSpy打开
代码被还原成功,可以被直接查看和修改了
之所以想写一篇分析记录,不仅仅是花了不小的时间来逆向这个游戏,也是因为这个游戏保护dll文件的方法较为全面,常用的手段基本都用上了,现在来总结一下。
①篡改PE文件中的标识符。最基本的就是“ MZ”,“PE”两处,在.Net中,还可以对“BSJB”,“#~”标识下手
②改变PE文件结构。在这个游戏中,它加密了段头部分,除此之外,还可以修改各个值的偏移量,数值等等,如果有那个精力和实力的话,你甚至可以重构mono对PE文件结构的解析部分,自创一个结构
③对.Net中特有的元数据结构下手。以前将将整个元数据部分用垃圾数据填充,在读取时再填回来的方法,这个游戏只是单纯地篡改了MaskValid来干扰我们读取#~表。事实上可以做的事情还有很多,比如改变#~中项的顺序。在这里注意一点,Reserved是一个闲置的项,可以被利用来存一些标志或密钥之类的,不管是保护还是逆向都应该多注意一下。
④针对函数的加密。这其实不是什么新鲜玩意了,很久以前就有用hook掉mscorre的方法来加密函数体,在mono中只是实现起来更加方便一点。对函数体加密的话,可以保证使用Dump的方法几乎没啥用,毕竟内存中至始至终都没有出现过被解密的dll文件,攻击者一次最多Dump一个函数……
⑤最后,是最恶心的Opcode替换了。攻击者必须忍受恶心来对一个巨大的函数进行分析,这已经不是技术手段了,这是赤果果的精神攻击!由于在mono中对Opcode分析并没有封装,所以保护者必须对Opcode有一定的了解,否则一不下心就会出错。没有时间精力来了解这个的话,也有一个办法,之前举办过一个比赛,上面就有一个替换Opcode的例子。它对解析Opcode的函数没有更改,只改了Opcode.def文件中的值而已,算是利用了一个小巧合吧。比赛题目链接: Gslab 2017游戏安全技术竞赛
这一次对游戏dll的加密分析到这里就结束了,我也就将Note判定稍微改松了一点,来慰藉我这个手残受伤的心灵……
这个游戏有内购功能,所以为了厂商着想,就不放任何成品以及解密代码了,感兴趣的人可以根据什么的过程自行走一便,知道答案的解题过程不会很难……除了还原Opcode部分(不能让我一个人恶心!!!)
在网上查dll加密,铺天盖地的是XXTEA加密文件再在读取时解密,毫无新意,保护力度为5,。代码保护这种东西,怎么能用别人的成品?那不就相当于所有门用同一把锁,那把锁的钥匙都还满大街人手一份。
所以,保护厂商在做代码保护时,可以借鉴思路,但是绝对不能套用!否则就是对自己产品的不负责!
希望这里写的一点浅薄的东西能够给没有技术大牛支持的厂商们带来一点点思路。
- End -
看雪ID: Mengluu
https://bbs.pediy.com/user-848784.htm
本文由看雪论坛 Mengluu 原创
转载请注明来自看雪社区
热门图书推荐
戳 立即购买!
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com
戳原文,查看更多干货!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。