Udostępnij za pośrednictwem


MSIL Injection: Rewrite a non dynamic method at runtime

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

TestRuntimeMethodReplace.zip

Comments

  • Anonymous
    March 29, 2009
    PingBack from http://blog.a-foton.ru/index.php/2009/03/30/msil-injection-rewrite-a-non-dynamic-method-at-runtime/

  • Anonymous
    March 29, 2009
    You've been kicked (a good thing) - Trackback from DotNetKicks.com

  • Anonymous
    March 29, 2009
    Thank you for submitting this cool story - Trackback from DotNetShoutout

  • Anonymous
    April 07, 2010
    Hi what about method that have Exception and input variables?

  • Anonymous
    June 25, 2010
    You are right this code not support either of those.  Changing the signature cannot change because you are invoking the method you are replacing.  The SEH info is not copied in this code i don't think. Check this out on codeproject.  It replaces the dispatch stub address in the method table instead of the IL and is a little bit more flexible.   www.codeproject.com/.../CLRMethodInjection.aspx Please not Microsoft does not support any of this hacky stuff.  You should look at another means to accomplish your goal if possible.