Mono.Cecil (Part II): Basic Operations

INTRO

In this second article I´ll try to ilustrate some of the basic operations that can be done with Mono.Cecil. As we commented in previous Mono.Cecil post, with mono we are able to perform assembly manipulations without need to load assembly inside AppDomain. With Cecil we can do code-injection and code-removing, this way we can easily create our custom post-processors that work over the IL code inside the assembly instead of C# or VB.Net code files.

In this Basic Mono.Cecil article we are going to show following actions:

  • Read Target Assemblies
  • Scan for Types, Methods, Properties, etc….
  • Injecting or Removing ILCode (Reweaving) in Types, Methods, Properties, etc…
  • Saving the Assemblies

In later articles we will build a more elaborated injector framework for allowing assembly manipulations in a more general fashion

SAmple Solution Overview

In order to show such operations we will introduce a dummy solution with traditional three-layer architecture (IUL, BLL and DAL) with a ASP.NET MVC front-end: www.cecilsample.swat4net.com .

In addition, a primitive console application (ConsoleReweaver) will be introduced here in order to perform the basic Mono.Cecil operations previously commented. In later articles we will create a proper-agnostic IL-injector framework that will be also part of this solution with their corresponding samples.

Mono.Cecil.Lessor2.Overview
Mono.Cecil.Lessor2.Overview

As we commented earlier, we will create a new ASP.NET MVC 5 web application project with a little database in SQLServer over which we will perform the most of samples related with reweaving (for including some variations over typical samples related canonical reweaving for  Console.WriteLine methods 🙂 ). This application will initially have Persons link, enabling a CRUD set of functionalities for person entities in addition to Search and Listing. This structure will be used in the following articles as the operational basis for coming samples

Mono.Cecil.Lessor2.Web
Mono.Cecil.Lessor2.Web
Mono.Cecil.Lessor2.Web2
Mono.Cecil.Lessor2.Web2

In order to check the updates done by our dummy reweaver we will use ILSpy to inspect WebForSamples and referenced assemblies (Interestingly, ILSpy is also using Mono.Cecil behind the scenes), this way we will be able to see all IL-modifications in C# (and IL of course). ILSpy is an open-source .NET assembly browser and decompiler. In addition you can access to ILSpy code at GitHub: https://github.com/icsharpcode/ILSpy

Mono.Cecil.Lessor2.ILSpy
Mono.Cecil.Lessor2.ILSpy

The goal

Well, our first (and simple) goal will be, hack pre-existing binary code from web application in order to include the literal “Code Injected!” as a prefix for each person name in person-list view. To that end, we need to reweave the BLL assembly (ClientLibrary.dll) and more specifically we will look for the following method:

public List<Person> GetPersons(bool oleDb = false)
{
    try
    {
       List<Person> results = new List<Person>();
       DataTable dataTable = new DataTable();
 
       if (oleDb)
           dataTable = _oleDbPersonDAL.GetAll();
       else
           dataTable = _personDAL.GetAll();
        if (null != dataTable && null != dataTable.Rows && dataTable.Rows.Count > 0)
        {
           foreach (DataRow dr in dataTable.Rows)
           {
               results.Add(
                   new Person
                   {
                      Id = Guid.Parse(dr["Id"].ToString()),
                      Name = dr["Name"].ToString(),
                      Surname = dr["Surname"].ToString(),
                      Email = dr["Email"].ToString(),
                      DateOfBirth = System.Convert.ToDateTime(dr["DateOfBirth"]),
                      Details = dr["Details"].ToString(),
                      Gender = (dr["Gender"].ToString().TrimEnd() == "Male" ? Gender.Male : Gender.Female),
                      FlattedAddress = dr["FlattedAddress"].ToString(),
                      Mobile = dr["Mobile"].ToString(),
                      Phone = dr["Phone"].ToString(),
                      PostalCode = dr["PostalCode"].ToString(),
                      NIF = dr["NIF"].ToString(),
                      Married = System.Convert.ToBoolean(dr["Married"].ToString()),
                      ImagePath = dr["ImagePath"].ToString()
                     });
 
               }
               return results;
         }
         return null;
     }
     catch (Exception ex)
     {
       // implement your exception handing here
          throw;
      }
 }
Mono.Cecil.Lessor2.4
Mono.Cecil.Lessor2.4

As can be seen, this method implement two data-access fashions: With SqlCommand or OleDbCommand depending on the boolean argument, once we have the datatable, the method iterates through each row for populating final Person list. The console app will locale the IL corresponding to this method and will update it for including the prefix. Let´s see how step by step

CODE FOR STATIC MAIN CONSOLE METHOD

Here we will include logic for key stages: Read the assemblies we want to reweave, look for target methods (in this case we want only one, which that is located at PersonHelper component inside BLL project (ClientLibrary.dll), Reweave the code for target methods (in this case we want to add a prefix “CodeInjected!”  to each person in Person List page, and finally save the updated assemblies

static void Main(string[] args)
{
    try
    {
        // #1. Read the assemblies
        var targetAssemblies = ReadAssemblies();
 
        // #2. Scan for Specific Types or methods, etc...
        var targetMethods = ScanForMethods(targetAssemblies.Select(o => o.Assembly).ToList());
 
        // #3. Reweaving target code
        ReweaveMethods(targetMethods);
 
        // #4. Saving the Assemblies
        SaveAssemblies(targetAssemblies);
    }
    catch(Exception ex)
    {
        // implement exception handling here!
        throw;
    }
}

READING TARGET ASSEMBLIES

Let´s star by creating new wrapper over native Cecil AssemblyDefinition class for including current local path for each assembly, this will we useful for later saving operations,  something like

public class TargetAssemblyDefinition
{
    public AssemblyDefinition Assembly { get; set; }
    public string LocalPath { get; set; }
}

In addition we will include one simple settings file for defining DLLs root path and Dll names that are going to be reweaved (dummy values)

namespace ConsoleReweaver {
 
 
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "14.0.0.0")]
    internal sealed partial class Console : global::System.Configuration.ApplicationSettingsBase {
 
        private static Console defaultInstance = ((Console)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Console())));
 
        public static Console Default {
            get {
                return defaultInstance;
            }
        }
 
        [global::System.Configuration.ApplicationScopedSettingAttribute()]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        [global::System.Configuration.DefaultSettingValueAttribute("")]
        public string DLLPath {
            get {
                return ((string)(this["DLLPath"]));
            }
        }
 
        [global::System.Configuration.UserScopedSettingAttribute()]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        [global::System.Configuration.DefaultSettingValueAttribute("ClientLibrary.dll;ClientLibrary_DAL.dll;WebForSamples.dll")]
        public string DLLNames {
            get {
                return ((string)(this["DLLNames"]));
            }
            set {
                this["DLLNames"] = value;
            }
        }
 
    }
}

From here we are ready for implementing our ReadAssemblies() method, you can code something similar to:

public static List<TargetAssemblyDefinition> ReadAssemblies()
        {
            List<TargetAssemblyDefinition> result = new List<TargetAssemblyDefinition>();
 
            var _rootPath = Console.Default.DLLPath;
            var _clientdlls = Console.Default.DLLNames;
 
            List<string> assemblyPaths = new List<string>();
            List<string> targets = _clientdlls.Split(';').ToList();
 
            foreach (var target in targets)
                assemblyPaths.Add(_rootPath + target);
 
            try
            {
                foreach (string assemblyPath in assemblyPaths)
                {
                    result.Add(new TargetAssemblyDefinition
                    {
                        Assembly = AssemblyDefinition.ReadAssembly(assemblyPath),
                        LocalPath = assemblyPath
                    });
                }
 
                return result;
            }
            catch (Exception ex)
            {
                // implement exception handling here!
                return result;
            }

This way we will rescue the TargetAssemblyDefinition instance for each dll we want to reweave; these are the inputs for the next method to be invoked in our specific execution pipeline

Scanning for Types, Methods, Properties, etc…

Next step in this lab is to rescue specific method (or methods) from target assemblies over which we want to perform reweaving, so we implement ScanForMethods() method, in which we will provide specific method firm for rescuing. In this dummy sample we include the method name hardcoded (from here it´s very easy to add new argument for making development more general) something like the following:

public static List<MethodDefinition> ScanForMethods(List<AssemblyDefinition> targetAssemblies)
{
    List<MethodDefinition> result = new List<MethodDefinition>();
    string methodName = "GetPersons(System.Boolean)";
    try
    {
        foreach (AssemblyDefinition assembly in targetAssemblies)
        {
            var methods = assembly.MainModule.Types.Where(o => o.IsClass == true)
                                                   .SelectMany(type => type.Methods)
                                                   .Where(o =>o.FullName.Contains(methodName));
 
            if (methods.Count() >0)
                result.AddRange(methods);
        }
 
        return result;
    }
    catch(Exception ex)
    {
        // implement exception handling here!
        return result;
    }
}

Reweaving Types, Method, Properties, etc…

Now the cool thing: we are going to update the target method by updating the current MSIL code. So we will create proper IL instructions for adding the logic we are looking for

public static void ReweaveMethods(List<MethodDefinition> targetMethods)
 {
  // we need to rescue the string.concat method which lives in mscorlib.dll
  var assemblyMSCorlib = AssemblyDefinition.ReadAssembly(Console.Default.ILMsCorlibPath);
 
  foreach (var method in targetMethods)
  {
      try
      {
          var methodToInject = assemblyMSCorlib.MainModule.Types.Where(o => o.IsClass == true)
                                                                .SelectMany(type => type.Methods)
                                                                .Where(o => o.FullName.Contains("Concat(System.String,System.String)")).FirstOrDefault();
 
          var InstructionPattern = new Func<Instruction, bool>(x => (x.OpCode == OpCodes.Ldstr) && (x.Operand.ToString().Contains("Name")));
          var InstructionPatternSet = new Func<Instruction, bool>(x => (x.OpCode == OpCodes.Callvirt) && (x.Operand.ToString().Contains("ClientLibrary.Entities.Person::set_Name(System.String)")));
 
          if (null != methodToInject)
          {
              // importing aspect method 
              var methodReferenced = method.DeclaringType.Module.ImportReference(methodToInject);
              method.DeclaringType.Module.ImportReference(methodToInject.DeclaringType);
 
              // defining ILProcesor
              var processor = method.Body.GetILProcessor();
 
              method.Body.SimplifyMacros();
 
              // first update
              var writeLines = processor.Body.Instructions.Where(InstructionPattern).ToArray();
              foreach (var instruction in writeLines)
              {
                  var newInstruction = processor.Create(OpCodes.Ldstr, "Code Injected !");
 
                  Instruction targetReferenceInstruction = processor.Create(OpCodes.Nop);
                  targetReferenceInstruction = instruction.Previous;
                  processor.InsertBefore(targetReferenceInstruction, newInstruction);
              }
 
              // second update
              var writeLinesSet = processor.Body.Instructions.Where(InstructionPatternSet).ToArray();
              foreach (var instruction in writeLinesSet)
              {
                  var concatInstruction = processor.Create(OpCodes.Call, methodReferenced);
                  processor.InsertBefore(instruction, concatInstruction);
              }
 
              method.Body.OptimizeMacros();
 
          }
      }
      catch (Exception ex)
      {
          //throw;
      }
  }
 }

Some remarks to highlight here: in order to use the String.Concat method, we need to read first the mscorlib.dll library, and later we commonly need to call to ImportReference method for importing the method to current assembly (similar to c# using). In this case (mscorlib and string.concat) is automatic but this syntax becomes mandatory for general methods and types from external assemblies.

In this method we manually create the instructions to be included and locale existing ones that will be used as reference points for later insertion

Saving Assemblies

Finally we need to save the updated assemblies (ClientLibrary.dll in our case). Here we will use the LocalPath property pf TargetAssemblyDefinition type in a pretty simple syntax:

public static void SaveAssemblies(List<TargetAssemblyDefinition> assemblies)
{
    try
    {
        foreach(var targetAssembly in assemblies)
            targetAssembly.Assembly.Write(targetAssembly.LocalPath);
    }
    catch (Exception ex)
    {
        // implement exception handling here !
        throw;
    }
}

Note: It´s possible to omit LocalPath property by using Assembly.MainModule.FullyQualifiedName property for saving the assembly

Running the Console

From here we are ready to run the console app in order to reweave our web application:

Mono.Cecil.Lessor2.5
Mono.Cecil.Lessor2.5
Mono.Cecil.Lessor2.6
Mono.Cecil.Lessor2.6
Mono.Cecil.Lessor2.7
Mono.Cecil.Lessor2.7
Mono.Cecil.Lessor2.8
Mono.Cecil.Lessor2.8

Checking IL Updates with ILSpy

We check the code updates with ILSpy

Mono.Cecil.Lessor2.11
Mono.Cecil.Lessor2.11

Running the Web App

Finally we run the Web Application from IIS and see the “injected code” at runtime:

Mono.Cecil.Lessor2.10
Mono.Cecil.Lessor2.10