之前曾用微软的Phoenix RDK写过一个简单的流程反混淆工具,功能尚不完善。随着Phoenix库的不断升级,功能也随之增强,但与新版本的.NET有个兼容性问题始终无法解决。于是下决心将平台换到Cecil上来。同时还需要用到一个Cecil的子项目FlowAnalysis。
本文就是一篇Cecil与Cecil.FlowAnalysis的学习笔记,参考的对象主要是利用Cecil编写的工具及两个库本身的源代码。
    利用Cecil编写的工具已经有不少,其中Smart Assembly Explorer(以下简称SAE)算是非常经典的一个,最后更新的版本中还包括了FlowAnalysis库的使用;另一个很有名的是Reflector的插件ReflexIL。本文中使用的代码示例主要来自于SAE,在这里感谢一下其作者WiCKY HU无私地将这些代码分享出来。
1.  Cecil学习
1.1.  Cecil简介
    要说Cecil则不能不提MONO,MONO是一个致力于在 Linux, Solaris, Mac OS X, Windows, and Unix等多个系统上运行.NET程序的平台,而Cecil则是MONO下的一个子项目。简单地说,Cecil库用来修改符合ECMA CIL规范的.NET可执行文件。
    Cecil使用简单却功能强大,提供比.NET本身Reflection更多的操作功能,更重要的是它是开源的,于是深得许多大牛的喜爱。(数年前就有大牛推荐偶学这个库了,可惜当时没有实现。)MONO虽然没有提供Cecil的tut,但是Cecil库本身的代码是self pxplanation的,所以学习起来并不困难,再加上有许多开源的工具,学起来更是方便。Cecil开源项目的网址是:http://groups.google.com/group/mono-cecil/web/projects-using-cecil。
1.2.  Cecil总体结构
    用Reflector载入Cecil.dll并查看其结构,可以发现Cecil主要由七个名称空间构成(有时在Reflector中查看比直接阅读源代码还轻松些,特别是对于这种整体结构的把握),如图1所示。

图1  Reflector中查看Cecil的整体结构

    分别浏览各名称空间,便可了解各空间中代码的主要功能。
1.2.1.  Mono.Cecil空间
主要包含一些:
(1)一些.NET关键概念(或者说元数据表)的定义,如AssemblyDefinition、ModuleDefinition、MethodDefinition、EventDefinition等。这些定义的名称直接代表了相应的元数据概念,都是很好理解的,代表了这些概念的MONO实现(或都说Cecil实现)。
(2)一些关键标志的定义,如AssemblyKind:

代码:
public enum AssemblyKind
{
    Dll,
    Console,
    Windows
}
(3)一个操作Assembly的总体方法定义,主要是AssemblyFactory类,其间提供了打开、保存Assembly的相关方法,代码示例如下:
AssemblyDefinition assembly = AssemblyFactory.GetAssembly(assemblyFullPath);
//取得AssemblyDefinition后,可以获取程序集的相关信息,如程序集全名,如下
string fullName = assembly.Name.FullName;

1.2.2.  Mono.Cecil.Binary空间
    从名字就可以看出来,该名称空间主要定义了二进制操作相关的代码和概念。这其中包括:
(1)基本PE结构及其操作,如DOSHeader、PEFileHeader、PEOptionalHeader,以及.NET对PE结构的扩展,如CLIHeader。同时还将一些PE中的概念扩展为类,比如RVA,更加易于编程。
(2)提供了资源和文件(Image)读写的相关类,如ImageReader、ResourceWriter等,但这几个类都是internal属性,一般用于被Cecil(当前程序集)中别的类调用,而不是由用户直接调用。
    利用Reflector的分析功能可以清楚得出哪些代码调用这几个读写类,如图2所示。

图2  利用分析功能查看读写类的调用情况

1.2.3.  Mono.Cecil.Cil空间
    这是非常关键,也是相对较复杂的一个空间,因为其中的大部分代码都和IL与方法的操作有关。
(1)首先是几个关键的enum。包括定义所有IL代码的Code:
代码:
public enum Code
{
    Nop,
    Break,
    Ldarg_0,
    Ldarg_1,
    Ldarg_2,
    Ldarg_3,
    Ldloc_0,
    //略
    其它几个enum均是与方法和IL代码流程相关的,比如ExceptionHandlerType、FlowControl(直接跳转?条件跳转?)、OpCodeType和OprandType等。
还有几个enum用于Cecil内部使用,比如MethodHeader和MethodDataSection,

(2)将一些关键的.NET概念定义为类,比如GuidAttribute、ExceptionHandler、OpCode,便于编程使用。比较重要的是Instruction类,该类将IL指令定义为类,提供了许多非常有用的属性,用以获取指令的相关信息:
代码:
public sealed class Instruction : ICodeVisitable
{
    // Fields
    private Instruction m_next;
    private int m_offset;
    private OpCode m_opCode;
    private object m_operand;
    private Instruction m_previous;
    private SequencePoint m_sequencePoint;

    // Methods
    internal Instruction(OpCode opCode);
    internal Instruction(OpCode opCode, object operand);
    internal Instruction(int offset, OpCode opCode);
    internal Instruction(int offset, OpCode opCode, object operand);
    public void Accept(ICodeVisitor visitor);
    public int GetSize();

    // Properties
    public Instruction Next { get; set; }
    public int Offset { get; set; }
    public OpCode OpCode { get; set; }
    public object Operand { get; set; }
    public Instruction Previous { get; set; }
    public SequencePoint SequencePoint { get; set; }
}
(3)定义了几个功能强大的类,用于操作方法和IL。方法层次的为MethodBody类,IL层次的为CilWorker类。后者提供了几种对IL指令的最基本操作,如Append、InsertAfter、InsertBefore、Remove和Replace,而这五种操作又是建立在更底层的Create与Emit两大类方法上。因此,实践过程中,这两个类的使用应该是比较多的。
比如,下面的代码将一个现有的IL指令替换为nop:
代码:
//代替换指令为ins,worker为CilWorker
    int size = ins.GetSize();
    ins.OpCode = OpCodes.Nop;
    ins.Operand = null;
    for (int i = 1; i < size; i++)
    {
        Instruction instr = worker.Create(OpCodes.Nop);
        worker.InsertAfter(ins, instr);
    }
1.2.4.  Mono.Cecil.Metadata空间
    看名称就可以知道,该空间主要与元数据的操作相关。其实对.NET文件或是方法、IL代码的任务操作,归根结底都是对底层的元数据进行操作。因此,其它空间中的许多方法最后会引用到Metadata空间中的有关代码。
    同样,还是让我们简单将该空间中的类型进行分类。
(1)将元数据的物理结构定义为类。其中包括元数据堆,如BlobHeap、GuidHeap、StringHeap、UserStringsHeap等;此外是将所有支持的元数据表以Row和Table的形式进行定义,比如ModuleRow、ModuleTable等。
(2)将一些元数据底层的关键概念进行了定义,比如MetadataToken类,枚举包括CodedIndex、ElementType和TokenType。
    大多数情况下,该空间中的类和方法,编程时不会直接使用,用户通常只会用到别的名称空间中的代码,并在内部调用Metadata空间的代码。以保存修改后的程序集为例,我们所需编写的代码仅仅就是下面几行:

代码:
CilWorker _worker;
...
_worker.InsertBefore(target_instruction, some_instruction);  //这里对代码进行修改
...
AssemblyFactory.SaveAssembly(asm_def, saveFileName);
    所有的工作都由SaveAssembly方法在内部完成了。不妨用Reflector的分析功能看看从SaveAssembly方法到Metadata空间内部方法的调用过程。这次的分析,我们寻找一个中间点,既Cecil空间的ReflectorWriter类的VisitMethodDefiniton方法,从该方法到SaveAssembly的调用过程如图3所示:

图3  SaveAssembly方法到VisitMethodDefinition的调用过程

    初步可以这样理解,SaveAssembly保存程序集时需要向文件中写入修改后的元数据,其中方法的元数据是通过VisitMethodDefinition实现的。下面是VisitMethodDefinition的代码:
代码:
public override void VisitMethodDefinition(MethodDefinition method)
{
    MethodTable methodTable = this.m_tableWriter.GetMethodTable();
    MethodRow row = this.m_rowWriter.CreateMethodRow(RVA.Zero, method.ImplAttributes, method.Attributes, this.m_mdWriter.AddString(method.Name), this.m_sigWriter.AddMethodDefSig(this.GetMethodDefSig(method)), this.m_paramIndex);
    methodTable.Rows.Add(row);
    this.m_methodStack.Add(method);
    method.MetadataToken = new MetadataToken(TokenType.Method, (uint) methodTable.Rows.Count);
    this.m_methodIndex++;
    if (RequiresParameterRow(method.ReturnType))
    {
        this.InsertParameter(this.m_tableWriter.GetParamTable(), method.ReturnType.Parameter, 0);
    }
    this.VisitParameterDefinitionCollection(method.Parameters);
}
    可以清楚地看到,上面的方法中有多处调用了元数据空间中的代码,比如CreateMethodRow创建了一个方法的Row,AddString用于在#Strings流中添加方法名称,并且为方法新建了一个MetadataToken。
    从这里看出,Cecil库的设计非常巧妙而复杂。幸运的是作为编程者无需和如此复杂的内部调用打交道,会使用SaveAssembly就足够了。
1.2.5.  Mono.Cecil.Signatures空间和Mono.Cecil.Text空间和Mono.Xml
    这三个空间就不多说了。Signatures全部都是关于签名的类,并且都是internal,是无法提供外部程序集直接使用的;Text提供了关于Unicode字符串的一些方法;最后的Xml是一些关于xml的内部操作定义。这三个空间的重要性和对我们的意义不大。

1.3.  Cecil的基本操作
1.3.1.  读取IL代码
    Cecil对IL代码的操作(包括读取、修改与保存)是Cecil是基本功能,也是入门最好示例。下面的代码部分参考http://www.rainsts.net。
    先看sample.exe的代码,后面将利用Cecil对此sample进行各种操作。代码很简单,没什么好解释的。
代码:
using System;

class program
{
  public static void Main()
  {
    Console.WriteLine("Cecil is fun");
  }
}
    在编写操作sample.exe的代码之前,先要熟悉一下Instruction类的成员,因为以后的诸多操作都是在此基础上进行的。Instruction类的定义如下:
代码:
public sealed class Instruction : ICodeVisitable
{
   // Fields
   private Instruction m_next;
   private int m_offset;
   private Mono.Cecil.Cil.OpCode m_opCode;
   private object m_operand;
   private Instruction m_previous;
   private Mono.Cecil.Cil.SequencePoint m_sequencePoint;

   // Methods
   internal Instruction(Mono.Cecil.Cil.OpCode opCode);
   internal Instruction(Mono.Cecil.Cil.OpCode opCode, object operand);
   internal Instruction(int offset, Mono.Cecil.Cil.OpCode opCode);
   internal Instruction(int offset, Mono.Cecil.Cil.OpCode opCode, object operand);
   public void Accept(ICodeVisitor visitor);
   public int GetSize();

   // Properties
   public Instruction Next { get; set; }
   public int Offset { get; set; }
   public Mono.Cecil.Cil.OpCode OpCode { get; set; }
   public object Operand { get; set; }
   public Instruction Previous { get; set; }
   public Mono.Cecil.Cil.SequencePoint SequencePoint { get; set; }
}
      其中Offset代表该指令在本方法中的偏移(还记得ildasm反编译代码中的行号吗?),OpCode和Operand分别代码操作码和操作数,而Next和Previous则对反混淆非常有用,分别指向后一个和前一个指令。
    首先来看看如何利用Cecil读取并显示sample的所有方法和每个方法的IL代码。
代码:
using System;
using Mono.Cecil;
using Mono.Cecil.Cil;

class Program
{
  public static void Main()
  {
    AssemblyDefinition asm = AssemblyFactory.GetAssembly("sample.exe");
    foreach (TypeDefinition tp in asm.MainModule.Types)
    {
      Console.WriteLine(tp.Name);
      foreach (MethodDefinition method in tp.Methods)
      {
        Console.WriteLine("  "+method.Name);
        Console.WriteLine("  .maxstack {0}", method.Body.MaxStack);
        foreach (Instruction ins in method.Body.Instructions)
        {
          Console.WriteLine("  L_{0}: {1} {2}", ins.Offset.ToString("x4"), 
          ins.OpCode.Name, 
          ins.Operand is String ? String.Format("\"{0}\"", ins.Operand) : ins.Operand);
        }
      }
    }
  }
}
    编译时,只需要利用/reference:Mono.Cecil.dll选项添加对Cecil的引用即可。最终disp运行的结果如下:
I:\tmp>disp
<Module>
program
  Main
  .maxstack 8
  L_0000: nop
  L_0001: ldstr "Cecil is fun"
  L_0006: call System.Void System.Console::WriteLine(System.String)
  L_000b: nop
  L_000c: ret

1.3.2.  修改IL代码
    下面在上面示例的基础上将难度提升一点点,主要是修改IL指令。最简单的,将ldstr的指令修改为别的字符串。这里,ldstr便是指令的OpCode,而其后接的String便是Operand。要修改ldstr指令,需要定位到该指令所在的方法。一般方法是通过“程序集-->模块-->类型-->方法”顺序循环查找的方法实现,而这里由于指令位于入口方法中,因此可以直接采用程序集的EntryPoint属性得到。
    下面在disp.cs基础上进行修改(这里只列出Main方法):
代码:
  public static void Main()
  {
    AssemblyDefinition asm = AssemblyFactory.GetAssembly("sample.exe");
    MethodDefinition method=asm.EntryPoint;
    foreach(Instruction ins in method.Body.Instructions)
    {
      if(ins.OpCode.Name == "ldstr" && (string)ins.Operand == "Cecil is fun")
      {
        Console.WriteLine("Find target instruction, start modify..");
        ins.Operand="Cecil is SO fun";
      }
    }
    AssemblyFactory.SaveAssembly(asm, "_sample.exe");
    Console.WriteLine("Save complete");
  }
    编译并运行后,生成目标文件_sample.exe,用Reflector查看,目标指令已经被修改了。
1.3.3.  添加新的方法和IL代码
    下面我们来演示第三步:添加新的指令。这种添加指令的操作很像静态地代码注入。我们的目标是给sample示例添加最简单的字符串编码保护:既Main方法中的ldstr指令不再直接读入“Cecil is fun”字符串,而是一段乱码(这里采用Base64编码),并新建一个方法用于运行时的解码。
    直接看代码吧,添加了注释,应该非常容易理解:
代码:
using System;
using System.Text;


using Mono.Cecil;
using Mono.Cecil.Cil;

class Program
{
  public static void Main()
  {
    AssemblyDefinition asm = AssemblyFactory.GetAssembly("sample.exe");
    MethodDefinition method=asm.EntryPoint;
    TypeDefinition tp=(TypeDefinition)method.DeclaringType;
    Instruction insertPoint=null;
    
    //定义新方法
    MethodAttributes attr = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static;    
    AssemblyNameReference asmNameRef = new AssemblyNameReference("mscorlib", "",new Version("2.0.0.0"));
    TypeReference ret = new TypeReference("String","System",asmNameRef,false);
    MethodDefinition new_method=new MethodDefinition("StringDecode",attr,ret);
    
    ParameterDefinition para=new ParameterDefinition(ret);
    new_method.Parameters.Add(para);    
    tp.Methods.Add(new_method);
    
    //给新方法定义代码
    new_method.Body.MaxStack=8;
    MethodReference mr;
    CilWorker worker=new_method.Body.CilWorker;
    
    //插入call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8()
    mr= asm.MainModule.Import(typeof(Encoding).GetMethod("get_UTF8"));
    worker.Append(worker.Create(OpCodes.Call,mr));
    
    //插入ldarg.0
    worker.Append(worker.Create(OpCodes.Ldarg_0));
    
    //插入call uint8[] [mscorlib]System.Convert::FromBase64String(string)
    mr= asm.MainModule.Import(typeof(Convert).GetMethod("FromBase64String"));
    worker.Append(worker.Create(OpCodes.Call,mr));
    
    //插入callvirt instance string [mscorlib]System.Text.Encoding::GetString(uint8[])
    mr=asm.MainModule.Import(typeof(Encoding).GetMethod("GetString",new Type[]{typeof(Byte[])}));    
    worker.Append(worker.Create(OpCodes.Callvirt,mr));
    worker.Append(worker.Create(OpCodes.Ret));
    
    
    //修改老方法
    foreach(Instruction ins in method.Body.Instructions)
    {
      if(ins.OpCode.Name == "ldstr" && (string)ins.Operand == "Cecil is fun")
      {        
        Console.WriteLine("Find target instruction, start modify..");
        ins.Operand="Q2VjaWwgaXMgZnVu";
        insertPoint=ins;
        break;
      }
    }
    //插入对新方法的调用
    mr=asm.MainModule.Import(new_method);        
    worker=method.Body.CilWorker;
    worker.InsertAfter(insertPoint,worker.Create(OpCodes.Call,mr));    
    
    //保存程序集
    Console.WriteLine("Save assembly...");
    AssemblyFactory.SaveAssembly(asm,"_sample2.exe");
    
  }
}
    代码中值得关注的就是新方法与新代码的插入,难点是如何插入call指令。多看一些示例,多调试几次就可以掌握了。编译并运行,程序会修改sample.exe并生成_sample2.exe。用Reflector查看_sample2,可以发现Main方法已经被修改:
代码:
public static void Main()
{
    Console.WriteLine(StringDecode("Q2VjaWwgaXMgZnVu"));
}
    同时,program类中多出了我们新定义的方法StringDecode,代码如下:
代码:
public static string StringDecode(string text1)
{
    return Encoding.UTF8.GetString(Convert.FromBase64String(text1));
}
    到这里,Cecil的学习告一段落。短短数页文字,无法尽述Cecil强大的功能,更多细节留待我们在日后慢慢发掘。下面的文章会介绍另一个库FlowAnalysis的基础知识。

(配套代码见附件)
to be continued...
上传的附件 1.rar