1.NET的介绍
.NET并不是一种编程语言,而是一个平台,一种运行环境。无论是什么操作系统,只要安装了.NET框架,便可以运行.NET可执行程序。目前,.NET平台支持20多种编程语言,包括Visual Basic.NET(VB7.0之后的版本)、C++、C#等。 为了支持多种操作系统,.NET程序不能直接存储x86、arm等汇编指令,而是存储“.NET汇编指令”,在.NET程序运行的时候,.NET环境将“.NET汇编指令”即时编译(JIT)成x86、arm等CPU能直接识别的汇编指令(而不是翻译),然后再执行。这里的“.NET汇编指令”被称为中间语言(IL,Intermediate Language),又是也叫MSIL,Microsoft Intermediate Language。这些IL代码,被直接保存在.NET程序中。而编译的过程就是CLR,Common Language Runtime,通用语言运行时,CLR是.NET的核心,是IL语言的运行环境。
在Windows下,.NET程序仍然是以PE文件的形式存在,但是Windows不再直接负责程序的运行。一个普通的PE程序,从启动到终止,完全受到Windows控制,Windows的Loader加载器会负责该程序的内存分配,线程管理等工作。而在.NET程序中,Windows只是作为一个入口,作为进入CLR的跳板,windows只负责跳转到CLR的执行引擎(EE)中,将控制权交由CLR,由CLR进行分配内存,线程管理,异常处理等。可以看到,一个经典的.NET程序中,只有一条x86指令:jmp _CorExeMain: 进入_CorExeMain,就是进入.NET环境了,接下来.NET环境执行IL代码,直接由.NET环境负责程序的运行。 除了通过_CorExeMain进入.NET环境,还可以通过_CorDllMain进入。_CorExeMain和_CorDllMain都是MSCOREE.DLL中的导出函数,MSCOREE.DLL就是.NET环境的载体。这种方式类似VB 使用MSVBVM60.DLL的方式。
2.文件解析
在正式分析前,我们首先要弄明白几个名词:中间语言、元数据、Token、流数据、表和堆。 中间语言,IL,在前文中以及提到过。IL是.NET唯一能读懂的语言,也是唯一可执行的语言,运行完全受.NET监控。 元数据,Metadata,描述.NET程序运行时必需的一切信息的数据,包括版本、类型的各个成员(方法、字段、属性、事件)等,元数据中每个项的数据被称为一个流。流按存储结构的不同分为堆(Heap)和表(Table), Token,是为了区分各项元数据,而设置的单独的标识。 .NET文件结构的解析工具是CFF Explore,.NET PE文件结构整体如下,接下来我们进行详细介绍。
2.1. .NET文件格式分析
.NET环境下的PE文件,结构上和传统的PE文件相同,但是使用了数据目录表中的IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR条目保存.NET的信息结构,该条目指向IMAGE_COR20_HEADER结构。 IMAGE_COR20_HEADER的RVA是0x2008,计算得到offset是0x208,在.text节中。 IMAGE_COR20_HEADER一共0x48字节,记录了元数据(Metadata)的RVA(offset是0x26C)和大小,也记录入口IL代码的Token: IMAGE_COR20_HEADER结构大小是0x48,所以结束位置是0x250,Metadata开始位置是0x26C,两者中间的0x1C字节的数据就是IL代码,但我们还不清楚这段IL代码是从那里开始执行的: Metadata结构开始的地方是元数据头,记录了流的数量: 紧跟着元数据头的就是,流数据头,流数据头的结构: 本例中一共5个流数据: 第一个指向的流是#~流,也就是元数据表流,其结构如下: 通过计算得到元数据表流的偏移是0x2D8: 其中最重要的Mask Valid值是0x0000000900001447,表示该程序使用了哪些表,本例中使用的表有Module、Method等8个表: 紧跟元数据表流头的是一串4字节数组,每个元素代表该表中有多少项记录,8个表共32字节: 接下来就是每个表的内容了,每个表又都有自己结构: 其中最重要的是Method表结构,它指明了IL代码的位置,RVA是0x2050,计算offset是0x250,和前面判断的内容相符。 至此我们完全掌握了IL代码的位置,关于其他表和流数据的结构就不详细介绍了。
2.2.解析IL代码
通过前面的分析,我们发现Main的IL代码内容如下: 使用ILDASM分析,可以将其反汇编为IL代码: 可以看到IL的“机器码”中,只有操作码和操作数,而操作数是以Token的形式存在的。 每个Token的值是AABBBBBB的形式存在的,AA表示对应的表(其中0x70对应的是用户字符串流(#US)),BBBBBB表示偏移:
3.分析技巧
3.1.dnSpy
.NET程序反编译的工具很多,但功能上都大同小异。笔者推荐使用的是dnSpy,因为他不仅能将.NET程序反编译为C#代码,还支持动态调试。 该软件的使用很简单,直接将.NET程序拖进工具中,你就能看到程序的入口点: 点击进入,分析Main函数: 你还可以进行动态调试:
3.2.de4dot
在某些情况下,你可能看到入口点名称是不可读的,那说明该程序可能被加壳了: 这种时候你就可以祭出de4dot,它支持十几种.NET壳的识别与处理(Agile.NET (aka CliSecure)、Babel.NET、CodeFort、CodeVeil、CodeWall、CryptoObfuscator、DeepSea Obfuscator、Dotfuscator、.NET Reactor、Eazfuscator.NET、Goliath.NET、ILProtector、MaxtoCode、MPRESS、Rummage、Skater.NET、SmartAssembly、Spices.Net、Xenocode )。 使用-d参数 识别壳: 直接脱壳: 脱壳后效果:
3.3.恶意代码隐藏
.NET支持内存中的Assembly载入,然后由Invoke调用该Assembly的方法(在某种程度上,类似于java反射执行): 所以有一些程序会将恶意代码存放在资源中,通过ResourceManager、ResourceReader等方式将资源加载进内存,并经过一系列的处理释放恶意代码: 最后执行恶意代码中的关键函数: 在分析使用此类技术的样本时,使用dnSpy动态调试分析非常方便。
参考资料:
https://www.cnblogs.com/dwlsxj/p/MSIL.html https://www.cnblogs.com/Mikhail/p/6134647.html https://www.codeguru.com/csharp/.net/net_general/il/article.php/c4635/MSIL-Tutorial.htm 《加密与解密 第四版》