【逆向】.NET程序逆向基础

概述

.NET Framework是微软开发的一个平台无关性的软件开发平台,与java类似,无论机器运行的是什么操作系统,只要该系统安装了.net框架,就可以运行.net可执行程序。

实例代码

 1 // 引用System空间的类和方法
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Text;
 6 using System.Threading.Tasks;
 7 
 8 namespace net测试
 9 {
10     class Program
11     {
12         static void Main(string[] args)
13         {
14             // 程序从Main函数入口开始执行
15             Console.WriteLine("Hello .NET");
16             Console.ReadKey();
17         }
18     }
19 }

名词解释

MSIL:微软中间语言,简称"IL"。无论程序使用什么高级语言(C#、C++/CLI、VB.NET)编写,最终都会被编译为IL中间语言。IL是.net唯一能读懂的语言,也是唯一可以执行的语言。IL相当于Win32下的汇编语言。
CLR:通用语言运行时,CLR类似Java Jre,它是IL语言的运行环境,在Windows中CLR系统其实就是几个Ring3层中以"mscor"开头的DLL文件,例如:"mscorwks.dll"、"mscorjit.dll"。
JIT:即使编译,这是.net运行可执行程序的基本方式,在程序运行时JIT引擎会将IL代码即时编译为本地汇编代码后执行。
Metadata:元数据,它被用来描述一个可执行文件的所有信息:"版本"和"类型"的各个成员("方法"、"字段"、"属性"、"事件")等。一个有效的.net可执行程序必须包含正确的元数据定义。
Token:Token是元数据的唯一标识,它被用来在同一程序中区分和定位不同的元数据。IL语言通过Token来引用和定位元数据。
Assembly:程序集,它是构成.net程序的基本元素,类似于winddows下的线程。
Module: 一个包含可执行代码并以.netmodule结尾的文件,一个或多个包含执行代码的模块加上一些必要的控制信息就构成了一个程序集(Assembly)。
Type:类型,它是面向对象编程中的概念。类型是.net程序构成的基本元素,常见的类型有:类(Class),结构(Struct),枚举(Enum)。
Method:方法,类型(Type)可以有很多成员(Member),而最重要的成员就是方法(Method)。方法是代码的基本单元,类似面向过程编程中的函数。
AppDomain:应用程序域,.net程序运行的基本单位是Assembly,几个Assembly可以构成一个AppDomain。它类似于windows下的进程。

PE文件扩展

在传统win32平台上.text段通常只存储ASM汇编指令,而在.net中所有的元数据和IL代码都被存储在了该区段中。本节将介绍如何定位文件中的IL代码和元数据内容,并对重要字段进行讲解。(以下内容建议使用010Editor+CFF Explorer一起配合学习)


.text段的第2项数据"Common Language Runtime"(CLR头),由数据目录表的第15项数据指定。其中CLR头中的"MetaData"字段指定了元数据头的RVA和大小。

CLR头:

Flags字段定义了该文件的最基本性质:

1 COMIMAGE_FLAGS_ILONLY            =0x00000001//此程序由纯IL代码组成
2 COMIMAGE_FLAGS_32BITREQUIRED     =0X00000002//此程序仅在32位系统上运行
3 COMIMAGE_FLAGS_IL_LIBRARY        =0x00000004//此程序仅作为IL代码库(很少用)
4 COMIMAGE_FLAGS_STRONGNAMESIGNED  =0x00000008//此程序有强名称(重要)
5 COMIMAGE_FLAGS_NATIVE_ENTRYPOINT =0×00000008//此程序入口方法为非托管
6 COMIMAGE_FLAGS_TRACKDEBUGDATA    =0x00010000//1oader和JIT需要追踪调试信息

 MetaData字段指定元数据头的RVA和大小:

MetaData头:

 iStreams字段指定流数据的数量,紧跟元数据头的就是流数据头

 流数据头:

流数据头结构指向不同的流数据,流数据按存储结构的不同可分为:"堆"和"表"
以下是.net中的几种流:
#-:#~的未压缩(或称为未优化)存储,不常见。
#~:元数据表流,也是最重要的流,几乎所有的元数据信息都以表的形式保存于此。每个.NET程序中必须包含此流。
#GUID:存储所有的全局唯一标识(Global Unique Identifier)。
#US:以Unicode格式存放的IL代码中使用的用户字符串(User String),例如ldstr调用的字符串。
#Strings:UTF-8格式的字符串堆,包含各种元数据的名称(例如类名、方法名、成员名、参数名等)。流的首部总有一个0作为空字符串,各字符串也以0表示结尾。在CLR中,这些名称的最大长度是1024字节。
#Blob:二进制数据堆,存储程序中的非字符串信息,例如常量值、方法的Signature、PublicKey等。每个数据的长度由该数据的前1~3位决定,0表示长度为1字节,10表示长度为2字节,110表示长度为4字节。

接下来紧跟流数据头结构的就是#~(元数据表流)的内容。#~流是最重要的元数据存储区域,不同的元数据按表进行分类,然后保存在#~流中。

 #~元数据表流:

Mask Valid字段指定#~中有多少种代表元数据的表被使用,.net中最多可以定义8*8=64个表。而实际上.net已定义的表只有45个。另外这45个表中,只有23个表可以用Token表示,剩余的22个只用于内部。

表含义:

 紧跟#~(元数据表流头)的是一个4字节数组,如果#~中有n个表,那从#~结尾(n*4字节)之后就是各表的数据了。

 注:如果对以上讲解的各数据结构还有疑问的地方,可以使用CFF Explorer等工具,打开.net程序进行对比学习。

MSIL汇编基础

前面说过.net平台下的程序,无论开发时使用的是那种高级语言,最终都会被编译为微软的中间语言MSIL。而IL与其他高级语言相比显得更为底层,所以也被称为"IL汇编"。
IL最大的特点就是不直接和内存地址打交道,而是以栈为基础进行操作。在IL代码中必须保证栈的平衡,如果一个方法开始时栈为空,那么在方法结束时栈也必须为空。
IL中除了br和br.s(直接跳转),其它以字母b开头的条件跳转均会从栈顶取出1-2个元素进行比较,比较结束后栈已经被改变。所以喜欢爆破的同学,修改完跳转后还需要注意栈平衡

// 下面对IL语言的基本要素进行说明:
1. IL源文件的扩展名为".il"2. 在IL源文件中,用"/""/* */"进行代码注释。
3. EXE文件必须有入口,在IL源文件中入口方法不一定是Main,也可以用.entrypoint来表示。
4. .assembly定义本程序集,.assembly extern则定义被引用的程序集,两者分别对应于元数据表中的Assembly与AssemblyRef。mscorlib是所有NET程序的基础,每个程序都会引用它。
5. 对于本地变量(由locals定义),可以用名称引用,也可以用序号表示。如果代码中只有一个本地变量val,那么在取val的值时,既可以用"ldloc val",也可以用"ldloc.0"6. 读取常数值时,对于0~8,可以直接使用简短指令,形如"ldc.i4.1"。而对大于8的数值,例如10,则必须使用完整指令,形如"ldc.i4 10"7. 在调用某个方法时,必须完整地写出方法的返回值、空间名、类名,最后才是方法名及方法的参数。
8. IL中也有空指令nop,不过它的十六进制编码是00h,而不是Win32ASM中的90h。

IL代码操作栈流程示意图:

说完IL的栈操作原理,对IL语言的学习已经完成了一半,剩下的一半只要记忆各类指令助记符、关键字、代码格式就可以了。
IL指令英文缩写及意义:

 IL与元数据

元数据定义了IL语言,而IL语言又对元数据进行操作。IL中通过Token来引用和定位元数据
下面我们用"IL DASM",对文章开头的实例代码进行解码,看看元数据在IL中的表示。查看时请打开"View"菜单中的"Show byes"、"Show token values"选项。

.method /*06000001*/ private hidebysig static 
        void  Main(string[] args) cil managed
// SIG: 00 01 01 1D 0E
{
  .entrypoint
  // Method begins at RVA 0x2050
  // Code size       19 (0x13)
  .maxstack  8
  IL_0000:  /* 00 |             */ nop
  IL_0001:  /* 72 | (70)000001  */ ldstr  "Hello .NET" /* 70000001 */
  IL_0006:  /* 28 | (0A)000011  */ call   void [mscorlib/*23000001*/]System.Console/*01000013*/::WriteLine(string) /* 0A000011 */
  IL_000b:  /* 00 |             */ nop
  IL_000c:  /* 28 | (0A)000012  */ call   valuetype [mscorlib/*23000001*/]System.ConsoleKeyInfo/*01000014*/ [mscorlib/*23000001*/]System.Console/*01000013*/::ReadKey() /* 0A000012 */
  IL_0011:  /* 26 |             */ pop
  IL_0012:  /* 2A |             */ ret
} // end of method Program::Main

以上代码中出现的所有Token值都被包含在了"/* */"中,Token值实际上就是一个UINT32值: AABBBBBB,其中字节"AA"指出了它所对应的表,字节"BB"指出了它在表中的位置,也就是索引记录(RID,RecordIndex)。
Token值的解析可以参考下图,关于Token值其它表类型的含义,可以参考之前列出的45个表含义图

签名

介绍完Token的概念,我们来介绍下签名(Signature)
在上面的IL代码中,方法名下方有这样一行注释:
// SIG: 00 01 01 1D 0E 这行注释表示Main方法的签名是:"00 01 01"。
从PE结构上看,签名就是存储在#Blob中的一段二进制数据,它的作用是描述特定元数据的性质
在.NET中共有6种表引用了签名(Signature),分别是Field、MethodDef、Property、MemberRef、StandAlongSig和TypeSpec。
关于Main方法的签名"/SIG:000001"可以按如下方法解码: 

// 第1个00:任何Signature的第1个字节都代表calling conventions,它定义了该签名的类型是method、field还是
property。00h代表IMAGE_CEE_CS_CALLCONV_DEFAULT,意思就是普通(默认)的方法,含定长的参数列表。
// 第2个01:代表方法中的参数个数。Main方法有一个参数,因此为01。 // 第3个01:代表方法的返回值类型,其中ELEMENT_TYPE_VOID=01h,表示没有返回值。

这样一个方法(method)便被Token和签名(Sig)完全确定了:
可以在相应的表中根据Token值查找方法的代码,而方法的调用方式、参数个数和返回值类型被其相应的签名限定
所有的sig数据保存在#Blob流中,在面对一般的保护时,并不需要用到sig解码,而一旦深入.net核心(例如脱壳后的文件修复)就会遇到自行解码sig的情况。

 

未完待续

参考:《加密与解密》

posted @ 2019-12-10 01:33  SunsetR  阅读(1354)  评论(0编辑  收藏  举报