之前曾用微软的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 }
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用于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; } }
比如,下面的代码将一个现有的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); }
看名称就可以知道,该空间主要与元数据的操作相关。其实对.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);

图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); }
从这里看出,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"); } }
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; } }
首先来看看如何利用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); } } } } }
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"); }
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"); } }
public static void Main() { Console.WriteLine(StringDecode("Q2VjaWwgaXMgZnVu")); }
public static string StringDecode(string text1) { return Encoding.UTF8.GetString(Convert.FromBase64String(text1)); }
(配套代码见附件)
to be continued...