分析背景
NGame游戏海外版出现了破解版,该版本在dump出游戏的dll中不能直接通过反编译工具查看修改后的游戏代码,导致无法确定外挂修改的直接逻辑点。本文主要针对AssemblyCSharp.dll模版,分析其dll保护的方法。
分析过程
1、拿到Encrypt_Assembly-CSharp.dll,拖进reflector中查看,发现文件不能被解析,提示说无效的COFF 头。
2、用010Editor打开Encrypt_Assembly-CSharp.dll查看文件,如下:
熟悉的MZ头,后面的“This program cannot be run in DOS mode”还被改成了黑客 网址,再看看后面的数据,基本确定是PE格式了。再看看这个网址,应该是这个组织把游戏给hack了再加上壳的。我们用010Editor来解析下这个PE文件,得到如下图
可以得出DOS头通过e_lfanew解析出了NT头,但是奇怪的是NT头却没有解析出Section,让我们查看下NT头是否有不符合COFF格式的项。展开NT头,发现下面的Signature值有误,应该只是“PE”即 0x4550 两个字符,修改之。
修改后,重新运行下PE模板,就可以看到被解析的Section了。再次拖进reflector去解析dll,发现有如下错误:
我们知道,DataDirectory是PE文件很重要的一个结构,在 IMAGE_OPTIONAL_HEADER下的最后的位置,一般在NT头偏移+0x60的位置,像导入表,导出表,重定位表的RVA和size都放在这里。正常的DataDirectory有16项,分别如下:
以前的第15项是装 COM_DESCRIPTOR Directory 的RVA和size的,后来MS没有用这个,就把这个项装CLR信息了,也就是 .Net的所有信息。如果出现了reflector上面的DataDirectory数目不对的问题,那应该就是反编译器读取DataDirectory项数的时候出现了问题,也就是IMAGE_OPTIONAL_HEADER中的NumberOfRvaAndSizes错了,实质上应该是0x10才对,修复之,保存。
顺便查看一下我们要的CLR数据的RVA和size信息:
RVA为0x2008,size为 0x48
5、拖到reflector看看解析结果,如下:
出现了不允许非负数的情况,在这个时候,首先应该考虑到ILSpy的异常栈回溯查看问题,如下:
然而笔者觉得打log应该也是看不清的,但是唯一可以马上得出的两点结论是,metadata数量相关的索引被改成了负数,导致越界。
6、一般笔者分析metadata会利用CFF Explorer,把修复了部分的Encrypt_Assembly-CSharp.dll拖进CFF Explorer 中,发现了如下异常:
经验告诉我这个是Metadata Header的NumberOfStream 被改大了,改回 5就行了,正常的CLR数据一般只有5个流,分别为#~,#Strings,#US,#Blob,#GUID。
7、下面我们找到CLR的头去修复NumberOfStream,首先已知CLR头在0x2008位置,这个是虚拟地址,现在SectionHeader里面看看我们的这个虚拟地址落在哪个段上面:
原来是在.text段(其实一般也是在这个段中),根据虚拟地址0x2000和文件偏移的基址0x200转换下虚拟地址0x2008的文件地址,得到其文件偏移为 0x208。查看下这个位置的数据,如下:
这72个字节的结构是这样的:
NumberOfStream在MetaData里面,IMAGE_DATA_DIRECTORY结构就是RVA和size组成的。所以,在CLR头也就是.net目录结构头偏移 +8的位置可以得到MetaData的RVA为0x2F4E6C,转换成文件地址为 0x2F4E6C – 0x2000 + 0x200 = 0x2F306C。我们去0x2F306C的位置看看:
熟悉的BSJB出现在眼前,这个就是Metadata Header。红圈部分就是NumberOfStreams,修改成5即可,保存。
8、拖进CFF Explorer正常解析了,然后结果如下图:
9、这个时候用反编译器还是不能解析的,因为游戏逻辑是保存在#中的,那么我们进入#里面的Tables看看是否解析成功,发现CFF Explorer卡死了,和预期一样。正常的dll文件应该在点击Tables的时候会显示出每一个Table及其项数。
这里说一下MaskValid的意义,把这个值变成二进制表示为:
上面的最右边(最低位)为索引00,该值为 1,表示存在该表(为0即表示不存在),该表名为Module,如下:
其余类推。统计了一下,总共有24个1。是不是理论上会有24个表存在呢?其实不是的,细心的读者会发现第43个1没有对应的表,所以0x1E093FB7BF57值只能代表23个表。
10、我们看看#~保存的表的位置:
可以看到,在MetaData头偏移 +0x6C 的位置保存着我们的#~的header,也就是0x2F306C + 0x6C = 0x2F30D8 的位置。如下蓝色覆盖的地方为我们的#~的header:
这个时候很容易就想到了为什么reflector会出现这样的情景:
其实很容易能够想到只要把这三个数据的高位字节(小端存储)patch成00就可以了,然而抱着严谨的态度我们还是去看看NGame破解版的libmono.so到底是怎么解析这一块数据的,查看开源的mono代码找到相关的解析位置load_tables,然后反汇编libmono.so即可,如下:
这个函数破解版和原版的解析方式是一致的,可以看出load_tables只取了低位24个字节。而到了这里我们可以知道reflector和ILSpy解析了32个字节所以导致出错了,我们把这三个数据的高位字节patch成00,保存下。
11、用reflector或者ILspy查看修复后的Encrypt_Assembly-CSharp.dll,如下:
原理总结
外挂针对dll的保护处理主要包括如下四个方面:
1、修改NT头的Signature。
2、修改IMAGE_OPTIONAL_HEADER中的NumberOfRvaAndSizes。
3、修改了CLR头指向的MetaData中的NumberOfStream。
4、修改了MetaData指向的#~堆中的tables数据项数。
外挂保护dll做的工作本质还是对抗主流的反编译器,防止其他人对外挂进行分析,实质上对mono源码基本没有进行大的修改。只要掌握一定的PE和.Net文件结构格式知识,就可以直接修复被修改的数据。
外网也存在一些修改elf文件的so保护机制,针对的是链接视图甚至是执行视图的修改,也是防止静态分析工具如IDA等进行分析,但是本质也是没有动linker所需要用到的数据。