在那个只有零和一的世界里,对零的向往,终究是一的执着。

正在下载:learn.zip 腾讯游戏安全技术竞赛2017Round2B

反调试

所以调试的时候可以直接跳过该线程的创建patch流程到下面的代码。另外,除了这个地方外,程序在tls主函数执行前还有个小坑,会检测两段代码是否下了int3中断,检测原理就是通过异或运算校验代码:

检测中断

OK,前面这些小坑我是写了一小段OD脚本直接在载入后直接过掉,之后就可以放心往下跟程序流程了:


/*patch*/
mov [0040123a], #eb#
mov [0040126a], #eb#
go 401090 //main
go 4010a4
mov eip, 4010ba //pass anti thread

主流程

这个main.exe程序比较大,将近2mb,原因是静态编译进去了一个开源项目mono:

静态编译mono

网上找了些关于mono的资料,结合题目给的运行库,大概了解了引入该项目的目的是为了在c++程序里执行c#编写的程序,而且由于该项目是跨平台,同样可以让安卓环境运行微软的.net程序。偷偷瞄了一眼安卓版的题目,除了main程序是安卓平台的程序外,其他两个文件是一样的。也就是说,最终安卓环境同样会载入运行库mscorlib.dll这个pe文件,这还是挺让人兴奋的,真的做到了跨平台。

既然源码在手,先随便下载一份,慢慢跟一下主程序,碰到疑似mono库的函数就找相关的字符串,在源码里搜,然后比较代码,确认函数是否一致,最后标记出该函数名。用这种笨方法慢慢分析,最后还是能勉强理清出这个程序主要的大概流程:

标记函数流程

从上图可以看到,程序在子函数jiemi_406C27处解密出一个pe格式的文件,这个调试过程中发现的:

解密pe

不过仔细观察“PE”处,该处本来应该是“PE00”,现在被改成“PE01”,导致一般工具可能误认为该文件不是个pe文件。将该文件dump下来可以看出是个c#编译的dll程序:

c#资源

不过将该文件拖进IDA会发现解析出问题,无法正常解析代码。这里应该是个坑,先放着。解密出该dll后,根据标记出来的mono函数看,程序去获取了.net程序集encrypt,这个程序集应该是来自刚才解密出来的c#编译的dll,因为jiemi_406C27除了解密出该dll外,还将其进行了一些加载。然后再从程序集encrypt获取一个类Hello,再接着从类中获取一个方法SayHello,最后通过x_mono_runtime_try_invoke_412EF4这个函数执行了该方法打印出了“Hello GSLab!”。而实际上,从上述解密出的dll里可以找到打印的字符串和其他相关字符串:

关键字符串

至此,可以确定,我们需要的加解密函数应该是存在于这个c#编译的dll程序集里,因为上面的字符串包含着“areyouok.png”和“areyouok_encrypted”这两个与题设相关的字符串。所以解题的关键还是这个解密出来的c#dll,可能加解密函数就是其中的一个方法。但是现在遇到的问题是IDA无法正常解析该文件,并且尝试了一下用mono的一个反汇编工具monodis,该工具可以正常反汇编出用mono编译器mcs编译的c#程序:

正常的反汇编

但是反汇编dump出来的dll文件时却遇到了错误:

反汇编出错

这种情况可能就是dll加载到内存后数据产生了变化,或者最怕的是作者修改了mono的源码,使格式发生变换。没有办法,只好往回看这个dll解密出来后的加载过程,看看能不能发现有什么猫腻。

内存dll加载

内存解密dll并加载的函数在jiemi_406C27,现在需要好好的分析其内部过程。首先看解出dll的过程,其实就是简单的异或进行二次解密:

异或解密

执行完解密后g_encrypt_data指向的内存就出现内存dll,然后将该内存dll进行拷贝,并把新内存地址传给一个结构体的某个字段:

传递地址

然后用上面的土办法,对下面的函数进行识别,可以发现,上面的这个结构体v9其实就是mono里面的image结构,其具体定义的结构_MonoImage可以在源码\mono\metadata\metadata-internals.h中找到,如对应上面两个字段:

image结构定义

接着剩下的代码进行简单的识别后发现其实就是源码里面的函数mono_image_open_from_data_internal实现,对应接口函数是mono_image_open_from_data_full,从名字上就可以看出是mono从c#的dll数据里进行加载的过程:

加载数据

跟进这个加载函数可以看到内部进行了pe格式的各种数据的解析和加载:

加载过程

Ok,看到这里,发现这个解密加载函数jiemi_406C27除了进行异或解密外就是调用的mono的源码对内存dll进行装载,除此之外并没有发现对内存dll的其他修改操作。而装载完后程序又能正常调用c#程序的类方法,说明装载过程也就是这里的提到mono源码里可能被处理过,才可能对dump下来这种非正常的c#程序能够正常识别。果然是福不是祸,是祸躲不过,害怕的终究是发生了。要直接去找出,哪里被修改了,可能很困难,毕竟层层的函数调用,不过我们可以从这个“异常”的文件格式本身入手,按正常的流程来看异常点,所以只能去分析.net的内部结构了。

.Net的结构

相关的知识可以从加密解密3的相关章节去复习。先来看看dump下来的dll文件格式,发现正常工具无法将其识别为PE格式,原因是PE头被偷偷修改了一个字节:

修改pe头

将“PE01”改回“PE00”就能正常解析PE结构了:

正常解析pe

但是虽然PE格式正常了,不过.net格式还解析有误,此时拖IDA或者ILSpy等工具无法正常解析c#代码。只好用工具去浏览.net的内部目录数据看看有什么异常了,这里使用CFF Explorer,如下发现一个异常提示:

cff异常提示

大概意思是说发现元数据的streams数量是261,可能有错,这个数量一般也不会大于10,在pe里定位一下该字段,发现确实是0x105(即261):

streams数量

然而从上图也可以看出总共就5个stream,猜测这里是个坑,所以果断改回“05 00”,果然就正常不弹提示了,但是在流量目录数据时发现了两个异常,其中一个是元数据流显示不正常,不是和上面5个一样,“#String”流后面的名称无法正常显示:

strings流名称

仔细观察了一下streams头的结构才发现,正常应该是“#Strings”,而文件里偷偷少了尾部一个“s”字符,导致stream字符串名称的终止符直接在4字节对齐的该处终止了,影响了后面结构的正常解析:

名称修正

如上将“s”添加后即能正常显示元数据流名称:

正常显示流

另外一个异常是,在点击数据流“#~”时,表头部能正常显示,但是点击查看表数据时,CFF工具就崩溃了:

表数据解析异常

这说明该数据流的元数据可能也被修改过了,没办法,只好手工去解析该数据寻找异常了。这里,根据表头部的信息:

表头信息

从该64位掩码值可以确定64个表(实际上最多只有44个已定义)中哪些表有被定义和排序,过程忽略(自行查看资料手册),直接把结果贴一下:

对应表

图中,共定义了14个表,每个表名后面对应其有多少项记录,如第一个表Moudle对应了0x01000001项记录,这明显有问题,直接去掉最高字节数据则为1项记录;另外一处有问题的是第六个表Param对应0xe3000008项,也不可能,同样去掉一个最高字节数据变成8项。最后修改后的表项如下:

修正记录数

此时,表数据目录得以正常浏览:

正常浏览表数据

并且,IDA也能开始解析该c#的dll了:

IDA解析

挺激动的,虽然解析还有些问题,不过总算看到了一点苗头,确定了把图片“areyouok.png”加密成“areyouok_encrypted”是dll里面MyClass类的EncryptDataFile这个方法。测试一下,修改一下主程序调用的方法:

修改取的类方法

成功获取了该方法,事先在同目录下准备一张任意数据的图片“areyouok.png”,调用该方法执行后同目录果然生成了一个新文件“areyouok_encrypted”,并且加密后的数据多了4个字节:

加密复现

OK,至此,确定了加密的方法,第一阶段初步告捷,显然后面还有坑,因为IDA等各种还无法完全正常的解析方法函数的代码,monodis反汇编工具也再次崩溃(虽然好一点,解析出来了一些):

反汇编异常

反思

从上面的分析可以看出,无良作者明显挖了各种坑等着我们跳,对内存dll格式肆意地更改,自以为源码在手就可以唯恐天下不乱。仔细想想修改元数据表的过程,他既然敢改不怕加载出错,肯定对加载过程也动了手脚。比如我们可以跟踪一下加载元数据表的过程,看看上面我们瞎蒙更改的表项数值是否正确。源码是把双刃剑,作者可以改,我们也可以看,快速定位找到加载table的过程,很容易发现解析表项数量的位置:

解析表项数量

对应到程序的该函数位置,可以看出同样的过程多了些的异或操作,比较明显的坑:

多余的异或操作

OD调试跟踪一下读取的表项数量,发现和我们修改后的一致:

调试解析表项数量

所以能够确定,作者在加载元数据表解析数据表项数量时修改了其逻辑(添加多余的异或等运算),导致格式与正常.net发生了偏差。类似的,目前还无法完全恢复方法代码,原因估计和这个差不多,可能还是一样在后面的解析过程修改了正常逻辑。

在调试跟踪上面加载表的过程中还遇到一个现象,在加载解密出来的dll之前、函数mini_init_41A8F3调用完之后的途中奇怪地也命中了上面异或表项数量的断点,仔细深究后发现,这个过程中加载了运行库mscorlib.dll,从参数image的结构字段可以看出是在解析该运行库的表项;从这点来看,这个运行库似乎也是和内存解密出来的dll一样,使用统一的一套修改后的格式,而非正常的c#运行库。这点从其代码解析出错的现象也可以印证,出现了和上面那个dll一样的情况,具体的方法函数代码解析报错:

运行库代码解析异常

可见,作者真是用心良苦了,难怪要特地提供一个运行库给我们,原来是同一个坑,坑得我们不知不觉啊。注意上面这个解析工具解析方法代码的结果是直接出错,而在IDA里对我们解析出来的dll里面的方法,却能够解析出不少代码出来:

IDA正常解析代码的情况

当然这其中解析的对不对现在无法确认,起码不是完全正确,如上出现了奇怪的call。再换个工具ILDasm看看:

ILDASM分析

可以看到不少类似上面的token解析错误,从这里就可以推测,无良作者肯定又对IL的字节码做了手脚,才导致这么多解析工具出现的各种相同的不同的奇怪现象,加上我找了从加载dll到执行方法函数的过程,没再发现类似异或运算的可疑操作了,所以更加确认作者应该是修改了mono引擎解析的opcode。

潜行

真是逼着人去研究IL的解析细节啊,无奈再看看opcode相关的东西吧。从源码里找到了opcode的定义,其目录是\mono\cil\opcode.def,一堆opcode定义如下:

字节码定义

然后与其相关的找到源码里的两个解析函数,一个是位于\mono\mini\method-to-ir.cmono_method_to_ir函数,另一个是位于\mono\metadata\verify.cmono_method_verify函数,分别用于mono引擎动态解析和验证方法的IL。而对应到main.exe程序中,这两个函数分别位于mono_method_to_ir_4CB206mono_method_verify_45CC38。两个函数里面都是一个大的switch,一堆的opcode作为其case,简直不想活了,跟不下去了,没想出有什么捷径能快速恢复出被修改的opcode,坐等大牛题解,再见。