The .Net Framework contains classes to create dynamic methods at runtime but what about rewriting existing methods at runtime?  Is it even possible?  It turns out yes, but with some stringent constraints.  We are going to create a sample program where we do just this.  In our example we are going to create two static methods.  The signatures for these methods are below.  We will talk more about these later.

ReplaceMethod ( MethodBase source, MethodBase dest );

ReplaceMethod ( byte[] source, MethodBase dest );

Before any code is executed in a program it must be loaded into memory by the loader.  Our first step will be to find where in a memory a particular method exists.  Lucky for us CLR assemblies are filled with gobs of metadata that has from referenced types and attributes to the names of fields in a type.  The CLR metadata can also tell us where we can find the body of a particular method either in memory or on disk.

CLR assemblies are in the Portable Executable or PE format.  Besides program code PE files contain other data used by the OS to execute a program.  We are just interested in the CLR metadata but an understanding of the PE format is needed for us to get there.  All of the structures we need to parse a PE header are defined in windows.h.  Unfortunately we do not have time to go in to PE in depth in this article.  We will just be going over the parts that are relevant to us getting to the CLR metadata.  If you want to learn more about the PE format you should check out the links at the bottom of this article.

Many of the address in a PE file are relative but not to the start of the file, but relative to the layout of the file when it is loaded in memory.  When the file is loaded in memory the OS will align the various sections of the file to pages in memory.  The addresses are called relative virtual addresses or RVA.  These sections are not aligned on the disk though.  The PE file Image section can be used to convert an RVA to a disk offset.  In order to find our CLR method body we will need to convert a RVA in to an actual address.

How can we identify a CLR assembly?  The first thing we should check if the PE header’s DataDirectories field.   The field contains a list of IMAGE_DATA_DIRECTORY’s that contain the size and start address for various sections of the assembly.  The 15th entry should tell us the address of the CLR header.   If this address is zero then we know right away this is not a CLR assembly.  Another thing we look at is the Import Address Tables.  If we look at a CLR assembly in dependency walker or a similar tool we will always see one entry in the import table to mscoree.dll for either “_CorDllMain” or “_CorExeMain”.  The OS does not know how to execute MSIL, so one of these functions must be imported so the program can yield execution to the CLR vm.

Below is the CLR header structure.  As you can see there is lots of interesting data here.  We have the CLR version, EntryPointToken, Managed Resources location, etc.  We are interested in just the MetaData location.  The metadata IMAGE_DATA_DIRECTORY contains the starting location of metadata in the assembly.  We are going to jump to that address.

typedef struct IMAGE_COR20_HEADER

{

    // Header versioning

    DWORD                   cb;

    WORD                    MajorRuntimeVersion;

    WORD                    MinorRuntimeVersion;

 

    // Symbol table and startup information

    IMAGE_DATA_DIRECTORY    MetaData;

    DWORD                   Flags;

    DWORD                   EntryPointToken;

 

    // Binding information

    IMAGE_DATA_DIRECTORY    Resources;

    IMAGE_DATA_DIRECTORY    StrongNameSignature;

 

    // Regular fixup and binding information

    IMAGE_DATA_DIRECTORY    CodeManagerTable;

    IMAGE_DATA_DIRECTORY    VTableFixups;

    IMAGE_DATA_DIRECTORY    ExportAddressTableJumps;

 

    // Precompiled image info (internal use only - set to zero)

    IMAGE_DATA_DIRECTORY    ManagedNativeHeader;

 

} IMAGE_COR20_HEADER, *PIMAGE_COR20_HEADER;

 

The CLR metadata starts with the header below.  This header contains a signature and some version information that we can use to verify we are in the correct spot.  The metadata header is followed by a storage header.  The storage header contains the number of metadata streams in a file.  The storage header is immediately followed by a list of stream headers that contain the stream name, offset from start of metadata section, and size.

 

typedef struct COR_METADATA_HEADER {

      char Signature[4];  // BSJB

      WORD MajorVersion;

      WORD MinorVersion;

      DWORD ExtraData;

      DWORD VersionLength;

      byte Version[1];

} *PCOR_METADATA_HEADER;

 

CLR metadata is defined in six streams.  These streams are listed below.

1.       #~                           Optimized Metadata Stream

2.       #-                            Unoptimized - Metadata Stream

3.       #US                        User defined strings

4.       #Strings                Internal strings (type names, namespaces, etc). 

5.       #GUID                  Internal Guids

6.       #Blob                    Internal Blob

The #GUID, #String, and a metadata stream (#~ or #-) will always be present the rest of the streams are optional.  The most important of these streams are the optimized #~ and unoptimized #- metadata streams.   These are mutually exclusive, only one can exist at a time.  These streams contain all the CLR type information like class names, class namespace, method names, class members, etc.  CLR metadata is composed in tables very similar to a relational database.  The unoptimized version contains several tables that are not available in the optimized version (FieldPtr, ParameterPtr, MethodPtr, etc).  

There are 45 different metadata tables in total.  These tables are listed below.  A CLR assembly will also contain schema information for each table.  It will tell us the name of a table, the size of a row, the index column in the table, the number of columns, the names of the columns, etc.  Unfortunately I could not find too much information online about these tables.  I recommend you check out Serge Lidin’s .NET IL Assembler book which goes over these tables in detail. 

Module

TypeRef

TypeDef

FieldPtr

Field

MethodPtr

Method

ParamPtr

Param

InterfaceImpl

MemberRef

Constant

CustomAttribute

FieldMarshal

DeclSecurity

ClassLayout

FieldLayout

StandAloneSig

EventMap

EventPtr

Event

PropertyMap

PropertyPtr

Property

MethodSemantics

MethodImpl

ModuleRef

TypeSpec

ImplMap

FieldRVA

ENCLog

ENCMap

Assembly

AssemblyProcessor

AssemblyOS

AssemblyRef

AssemblyRefProcessor

AssemblyRefOS

File

ExportedType

ManifestResource

NestedClass

GenericParam

MethodSpec

GenericParamConstraint

   

At first I was going to manually parse the assembly metadata but that was too much work.  I stumbled across the unmanaged metadata api.  You can find more information about the metadata api here Metadata (Unmanaged API Reference).  Here are going to be using the IMetadataTables interface to find out where a method exists in an assembly.  The method below takes a MethodBase object and returns the starting address in memory of the method body.  We use the MetadataToken property of the MethodBase to get the row number of the method in the method table.  Then we read the RVA column and add it to the base address to get starting location.

static byte* GetMethodStart(MethodBase^ method)

{

      PIMAGE_NT_HEADERS header;

      LPVOID imageSectionStart;

      CComPtr<IMetaDataDispenserEx> metaDataDispenser;

      CComPtr<IMetaDataTables> metaDataTables;

      CComPtr<IUnknown> unknown;

      COR_METADATA_TABLE_INFO tableInfo;

      HRESULT hr;

     

      // Get the module base address in memory.

      byte* moduleStart = GetModuleAddress(method->DeclaringType->Assembly);

      if ( moduleStart == 0 )

      {

            throw gcnew Exception("Module not found!");

      }

      // Read the nt hewaders.

      ReadNtHeader(IntPtr((void*)moduleStart),&header,&imageSectionStart);

 

      // Create the metadata dispenser

      hr = CoCreateInstance(

            CLSID_CorMetaDataDispenser,

            NULL, CLSCTX_INPROC_SERVER,

            IID_IMetaDataDispenserEx,

            (void**)&metaDataDispenser

      );

      if ( FAILED(hr) )

      {

            throw gcnew Exception("Failed to create metadata dispenser");

      }

     

      // Read the metadata start location.

      byte* metaDataStart;

      int metadataSize;

      ReadMetaDataStart(header,moduleStart,&metaDataStart,&metadataSize);

 

      // Validate the start location.

      PCOR_METADATA_HEADER metaDataHeader = (PCOR_METADATA_HEADER)metaDataStart;

      if ( memcmp(metaDataHeader->Signature,"BSJB",4) != 0 )

      {

            throw gcnew Exception("Invalid metadata header");

      }

 

      // Open memeory scope and get the IMetadataTables interface

      hr = metaDataDispenser->OpenScopeOnMemory(

            metaDataStart,

            metadataSize,

            CorOpenFlags::ofReadWriteMask,

            IID_IMetaDataTables,

            &unknown

      );

      if ( FAILED(hr) )

      {

            throw gcnew Exception("Open scope memory failed");

      }

      if ( FAILED(unknown->QueryInterface(IID_IMetaDataTables, (void**)&metaDataTables)) )

      {

            throw gcnew Exception("failed to get the metadata table");

      }

 

      // Get the row in the metadata table

      PCOR_METADATA_METHOD_ROW row;

      int rowIndex = 0xFFFFFF & method->MetadataToken;     

      tableInfo = ReadTableInfo(metaDataTables,CorTable::CorTable_Method);

      hr = metaDataTables->GetRow(tableInfo.ixTbl,rowIndex,(void**)&row);

      if ( FAILED(hr) )

      {

            throw gcnew Exception("failed to read metadata row");

      }

      if ( row->Rva == 0 )

      {

            throw gcnew Exception("method has no body.");

      }

 

      return moduleStart + row->Rva;

     

};

 

 

Before the start of the method body there is a method header.  The method header tells us how big the method is in bytes, max stack size, a signature for local vars, and some flags to tell us if we are going to have a SEH table following the method body, etc.  There are two types of method headers, a tiny and fat header.  A tiny header is only 1 byte and can be used when a method does not have any local variables and the methody body byte size is less than 64 bytes.  The first two bits in the first byte of the header will tell us of it is fat or tiny.  Below are the tiny and fat structures from Cor.h.

 

/* Used when the method is tiny (< 64 bytes), and there are no local vars */

typedef struct IMAGE_COR_ILMETHOD_TINY

{

    BYTE Flags_CodeSize;

} IMAGE_COR_ILMETHOD_TINY;

 

/************************************/

// This strucuture is the 'fat' layout, where no compression is attempted.

// Note that this structure can be added on at the end, thus making it extensible

typedef struct IMAGE_COR_ILMETHOD_FAT

{

    unsigned Flags    : 12;     // Flags

    unsigned Size     :  4;     // size in DWords of this structure (currently 3)

    unsigned MaxStack : 16;     // maximum number of items (I4, I, I8, obj ...), on the operand stack

    DWORD   CodeSize;           // size of the code

    mdSignature   LocalVarSigTok;     // token that indicates the signature of the local vars (0 means none)

 

} IMAGE_COR_ILMETHOD_FAT;

 

typedef union IMAGE_COR_ILMETHOD

{

    IMAGE_COR_ILMETHOD_TINY       Tiny;

    IMAGE_COR_ILMETHOD_FAT        Fat;

} IMAGE_COR_ILMETHOD;

 

 

 

Our replace method is listed below.  The first we will need to do is get a process handle with the PROCESS_VM_WRITE and PROCESS_VM_OPERATION access so we can write to program memory.  You can learn more about these permissions here Process Security and Access Rights.  Next we run into our first constraint.  The source method must be smaller or equal in bytes to the destination method.  We will talk more about this later and maybe see if we can fix in a future article.  At first I just tried to write the source method over the destination method without worrying about size difference but this did not seem to work.  Addig some nop instructions to the start of the method seemed to resolve the issue.

 

static void ReplaceMethod(byte* source, byte* dest)

{

      HANDLE processHandle;

      try

      {

            // Get token with write process memory access

            processHandle = GetElevatedProcessHandle();

           

            // Get the code size.

            int sourceCodeSize = GetMethodCodeSize(source), destCodeSize = GetMethodCodeSize(dest);

            bool isSourceFat = HasFatHeader(source), isDestFat = HasFatHeader(dest);

           

            // Get the headers.

            IMAGE_COR_ILMETHOD* sourceMethodHeader = (IMAGE_COR_ILMETHOD*)source;

            IMAGE_COR_ILMETHOD* destMethodHeader = (IMAGE_COR_ILMETHOD*)dest;

           

            // See if we have enough space

            if ( sourceCodeSize + (isDestFat || isSourceFat? sizeof(IMAGE_COR_ILMETHOD_FAT):sizeof(IMAGE_COR_ILMETHOD_TINY)) >

                  destCodeSize + (destCodeSize? sizeof(IMAGE_COR_ILMETHOD_FAT):sizeof(IMAGE_COR_ILMETHOD_TINY)))

            {

                  throw gcnew Exception("Cannot replace a method if the destination is less than the source !");

            }

 

           

            SIZE_T bytesWrote = 0;

            int size = 0, sizeDiff = 0;;

            bool useFat, result;

            byte* buffer;

           

            // Get size diff

            if ( destCodeSize > sourceCodeSize )

            {

                  destCodeSize - sourceCodeSize;

            }

            useFat = isDestFat || isSourceFat || ((sizeDiff + sourceCodeSize) > MAX_TINY_METHOD_SIZE);

 

            // Write the header

            try

            {

                  // If source or dest are fat then use fat.

                  if ( useFat)

                  {

                        IMAGE_COR_ILMETHOD_FAT* fat;

                        size = sizeof(IMAGE_COR_ILMETHOD_FAT);

                        buffer = new byte[size];

                        fat = (IMAGE_COR_ILMETHOD_FAT*) buffer;

                        FillHeader(fat,   sourceMethodHeader);

                        fat->CodeSize += sizeDiff;

                  }

                  else

                  {

                        size = 1;

                        buffer = new byte[size];

                        *buffer = (byte)(sourceMethodHeader->Tiny.Flags_CodeSize);

                        IMAGE_COR_ILMETHOD_TINY* tiny = (IMAGE_COR_ILMETHOD_TINY*)buffer;

                        tiny->Flags_CodeSize = (tiny->Flags_CodeSize & 0x3) |

                              (((tiny->Flags_CodeSize >> 2) + sizeDiff) << 2);

                       

                  }

                 

                  result = WriteProcessMemory(

                        processHandle,

                        dest,

                        (LPCVOID)buffer,

                        size,

                        &bytesWrote

                  );

 

                  // Move to start of il.

                  dest += bytesWrote;

                  source += isSourceFat ? sizeof(IMAGE_COR_ILMETHOD_FAT) : 1;

 

            }

            finally

            {

                  if ( buffer != 0 )

                  {

                        delete buffer;

                  }

            }

 

            // Add padding

            if ( sizeDiff > 0 )

            {

                  try

                  {

                        // Create buffer filled with nop instruction and write.

                        buffer = new byte[sizeDiff];

                        ZeroMemory(buffer,sizeDiff);

 

                        result = WriteProcessMemory(

                              processHandle,

                              dest,

                              (LPCVOID)buffer,

                              sizeDiff,

                              &bytesWrote

                        );

                        dest+=sizeDiff;

 

           

                       

                  }

                  finally

                  {

                        if ( buffer != NULL )

                        {

                              delete buffer;

                        }

                  }

            }

 

            //replace the method il.

            result = WriteProcessMemory(

                  processHandle,

                  dest,

                  (LPCVOID)source,

                  sourceCodeSize,

                  &bytesWrote

            );

      }

      finally

      {

            // Close the process handle.

            if ( processHandle != 0 )

            {

                  CloseHandle(processHandle);

            }

      }

};

 

 

Lets test our replace method now.  Each of these test methods will just print write a string to the console.  The first method shows the JIT in action running the method and keeping a cached compiled copy.  Even after we replace the IL the method gives us the same results as the first time we ran it.  Our call to TestFour works correctly and we see “TestThree” printed to the console.

 

// Get method handles for the test methods.

MethodBase[] methods = new MethodBase[]

{

    typeof(TestClass).GetMethod("TestOne",BindingFlags.Static|BindingFlags.Public),

    typeof(TestClass).GetMethod("TestTwo",BindingFlags.Static|BindingFlags.Public),

    typeof(TestClass).GetMethod("TestThree",BindingFlags.Static|BindingFlags.Public),

    typeof(TestClass).GetMethod("TestFour",BindingFlags.Static|BindingFlags.Public),

    typeof(TestClass).GetMethod("TestFive",BindingFlags.Static|BindingFlags.Public),

    typeof(TestClass).GetMethod("TestSix",BindingFlags.Static|BindingFlags.Public),

    typeof(TestClass).GetMethod("TestSeven",BindingFlags.Static|BindingFlags.Public)

};

 

// Call TestOne

TestClass.TestOne();

 

// Replace test one with test two.

MethodReplacer.ReplaceMethod(methods[1], methods[0]);

 

// Call test one again.  Same result.  JIT already compiled.

TestClass.TestOne();

 

// Replace TestFour with TestThree.

MethodReplacer.ReplaceMethod(methods[2], methods[3]);

 

// Call TestFour.  JIT has not yet cached this method, we get expected results.

TestClass.TestFour();

 

Why not just allocate some new memory, write some IL, and the update the RVA?  This way we don’t have to worry about any size constraints.  This was my first approach but it did not work.  I suspect the issue is with the IMetadataTables->GetRow() function.  The address returned for the row does not appear to be in the same address space as the assembly in memory.  Maybe in the future I will try to manually locate the row, update the RVA, and see if I get different results.  If we can write to the metadata tables and the VM recognizes our changes then we can do all kinds of interested things, like maybe trick the vm into running some x86 assembly code instead of IL.

Rewriting methods at runtime does not seem to work to well. We have to deal with the size issues and JIT keeping a compiled copy of the method after it invoked for the first time.  Let's not forget about NGEN too.

Cecil is a library developed by Mono that allows one to inspect and rewrite assemblies before they are loaded them into memory.  I have not had a chance to place with Cecil yet but I hear nothing but good things about it.  If you want to play around with metadata and rewrite an assembly before it is loaded I suggest you give Cecil a try.

Peace

 

An In-Depth Look into the Win32 Portable Executable File Format

Microsoft Portable Executable and Common Object File Format Specification

Metadata (Unmanaged API Reference)

Cecil


Share/Save/Bookmark