Code Injection - Inserting a message box into a .NET application at startup |
Download package - example files and article (59 kb)
Foreword
Well, it's been quite a while since my last RCE article so I thought it's time for a new tutorial. I have been thinking about diving into the inner workings of .NET PE files for some time and finally managed to have spare time to do so!
Following my pretty well-known article Code Injection : Inserting a MessageBox (which is nearly 11 years old... damn I am getting old), we'll do actually the same for a .NET PE file: insert a messagebox at startup using some PE viewer tools and a hex editor.
On our long way to our final goal, we'll have a look at the structures of a managed PE file and hopefully gain some basic understanding of metadata tables, IL code, tokens etc.
However, I assume you have already some knowledge of (traditional) PE files, maybe even have reversed some files, have knowledge in programming and/or "low-level stuff"; also explanatations how to use the tools are out of scope. Furthermore only the parts of .NET files are touched that come across on your way to insert the messagebox - this is no structured or even complete guide to .NET files. It serves more as a starting point - if you are still interested after reading this article and want to learn more, check my references at the very bottom of this page!
Ok, so here a list what you need:
The archive (linked at the top of the site) contains a simple .NET application file which is used for our investigation. So all references to file offset, memory addresses, ... corresponds to this file.
Outline
A first look
Basically, a managed executable file is an extended PE file. The 15th entry of the directory in the PE header (formerly known as the COM directory) contains the RVA to and the size of the Common Language Runtime Header. This is the root starting point to all .NET specific structures. However, there are also some specialties in the standard PE fields:
A .NET PE file usually contains one entry in the import table: mscoree.dll.
And only one function is imported from this dll: _CorExeMain. No kernel32.dll, no user32.dll.
Another remarkable issue is the entrypoint which always contains the same 6 byte stub; here what it looks like in the test application (Entrypoint relative virtual address (RVA) is 0x3D0E = file offset 0x1F0E):
Offset | Hex | Disassembly |
0x00001F0E: | FF 25 00 20 40 00 | jmp [0x402000] |
So the first instruction is a far jump to virtual address (VA) 0x00402000 (RVA 0x2000).
And what is there? RVA 0x2000 is the first and only import entry of our mscoree.dll which means it's a direct jump to mscoree._CorExeMain. In fact, that's the only native x86 assembler code in this file. The actual application is compiled to IL code which is stored in the .NET sections.
This stub function was only required on OS without built-in .NET support like Windows ME or Windows 2000. Here the PE file was loaded in memory as any other PE file, the execution started at the entry point which directly jumped into function _CorExeMain. This method initializes the CLR (Common Language Runtime), examined the .NET header, determined the managed entrypoint and started execution (by compiling the IL code to native code on-the-fly).
On later Windows versions, the OS loader detects right at the beginning if the file is a managed PE File and loads mscoree.dll into the process�s address space and starts the CLR. The 6-byte x86 code stub at the PE entrypoint is ignored.
At least in theory. This would mean we could delete or overwrite this stub function on such Windows systems. However, this is not possible - modifying this stub prevents the file from being executed, even if the modification is valid, e.g
Offset | Hex | Disassembly |
0x00001F0E: | 90 | nop |
0x00001F0F: | FF 25 00 20 40 00 | jmp [0x402000] |
Interestingly, if the NOP opcode is put after the 6-byte stub, the file works normally. Seems the CLR really verifies the 6-byte stub for consistency.
This fact has one consequence for us: The former approach from the old tutorial, the modification of the entrypoint, is not applicable here. The stub function must not be modified, and code after the stub is not executed. Hence, we have to dive into our application .NET file...
The .NET Header and .NET Entrypoint
As mentioned, the 15th data directory entry of the PE header is the pointer to the .NET header. In our example file, we have here:
.NET Data Directory RVA: 0x2008 (-> file offset 0x208)
.NET Data Directory Size: 0x48
There begins the common language runtime header. Let's look at the beginning of its definition (taken from corhdr.h from .NET SDK):
typedef struct IMAGE_COR20_HEADER { DWORD cb; //size of structure WORD MajorRuntimeVersion; WORD MinorRuntimeVersion; // Symbol table and startup information IMAGE_DATA_DIRECTORY MetaData; DWORD Flags; DWORD EntryPointToken; //managed entrypoint // ... remaining elements ommited ... } |
The most interesting values for us are the MetaData directory and the managed EntryPointToken.
The MetaData RVA points to the general metadata header which consists of a general header and is followed by at least three stream headers for all available metadata streams. The #~ respectively #- stream contains all metadata tables with information about the managed exeutable, e.g. all available classes, functions and many more.
Here a screenshot from the #~ metadata tables overwiew in CFF Explorer:
Why I am telling this stuff? Let's look at the EntryPointToken member of the common language runtime header.
Contrary to 'normal' PE files where the entrypoint of an application is given by the RVA of member AddressOfEntryPoint in the NT header, the entrypoint of .NET executables is specified by EntryPointToken - so after the CLR has loaded our assembly, exeuction starts here. However, the valus is not a standard RVA, but as the name already states, it's a so-called token.
A token is a 4-byte unsigned integer which most significant byte denotes a zero-based table index of a metadata table and the lower three bytes the index of this table.
Let's look at our specific example application: the EntryPointToken has value 0x0600000F. So it addresses table index 6 which is the MethodDef Table (see [1]) and index 15.
So in CFF, go to the Method table and there to index 15. This entry is called Main - promising, isn't it? The RVA of this entry is 0x2504 (file offset 0x704), and this is actually the position in the file of the IL code of our Main function, this our entrypoint.
Because we are curious, let's examine the hexcode - this is what we find at file offset 0x704:
5A 28 46 00 00 0A 16 28 47 00 00 0A 73 01 00 00 06 28 48 00 00 0A 2A |
Well, as an exercise let's disassemble those bytes (all information can be found in the references at the very end of this article):
0x5A: The least significant first two bits determines the type of the header, either small ('10') or fat ('11'). 0x5A is 0101 1010. So the two least significant bits are '10' meaning we are dealing with a tiny header. In a tiny header, the other 6 bits of the first bytes denote the size of bytes of the IL code of this method. So the actual IL code of the amin method is 010110 = 0x16 = 22 bytes long.
Directly after the small header, the IL code starts. Here is the disassembly of the 22 bytes (actually the Main method) taken from ILDasm:
.method private hidebysig static void Main() cil managed // SIG: 00 00 01 { .entrypoint .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x2504 // Code size 22 (0x16) .maxstack 8 IL_0000: /* 28 | (0A)000046 */ call void [System.Windows.Forms]System.Windows.Forms.Application::EnableVisualStyles() IL_0005: /* 16 | */ ldc.i4.0 IL_0006: /* 28 | (0A)000047 */ call void [System.Windows.Forms]System.Windows.Forms.Application::SetCompatibleTextRenderingDefault(bool) IL_000b: /* 73 | (06)000001 */ newobj instance void EntryPointTestApp.Form1::.ctor() IL_0010: /* 28 | (0A)000048 */ call void [System.Windows.Forms]System.Windows.Forms.Application::Run(class [System.Windows.Forms]System.Windows.Forms.Form) IL_0015: /* 2A | */ ret } // end of method Program::Main |
Ok, so at this point we know how to find the entrypoint, but remember that our goal is to insert here additional code to display a messagebox. As observable in a hex editor, there is more .NET data directly following the Main() method code, which means we cannot extend the code without overwriting successive data.
On the other hand, inserting additional bytes right after the Main() method without overwriting the following information (thus actually enlarging the file) is also no option: All following data is referenced by some metadata tables (and even code) - finding all places and manually adapt the offset is nearly impossible.
So what to do? Hm... maybe we can move Main() method to another location in the file where more space is available...??? *g*
Relocating the Main() method
The Main() method code is located at RVA 0x2504 which lies inside the .text section as we easily see from the section header:
Section | Virtual Size | Virtual Address | Raw Size | Raw Address |
.text | 0x1D14 | 0x2000 | 0x1E00 | 0x0200 |
.rsrc | 0x058C | 0x4000 | 0x0600 | 0x2000 |
Awesone, we have luck: The actual size of the .text section is 0x1D14 while there is space in the file for 0x1E00 bytes, so there is room for 0xEC bytes left.
For our goal to move the IL code of the Main function to another location, following three steps have to be performed:
Let's start:
1. Let's increase the size of the .text as large as possible by setting Virtual Size to the same as the Raw Size. Here the adapted .text section header:
Section | Virtual Size | Virtual Address | Raw Size | Raw Address |
.text | 0x1E00 | 0x2000 | 0x1E00 | 0x0200 |
2. The method code is now copied to the end of the former .text section. That is at file location Raw Address + former Virtual Size: 0x0200 + 0x1D14 = 0x1F14.
We have seen that the Main Method starts at file offset 0x0704 and is 23 bytes long (22 bytes IL code + 1 byte tiny header). So using a hex editor, copy those 23 bytes to file offset 0x1F14.
3. Finally the RVA of the Main method in the method table of the #~ metadata stream is changed from 0x2D04 to the new address 0x3D14 (which is the RVA of the corresponding file offset 0x1F14.
Voil�, the first step is accomplished. The new file executes as the original image, but we have now room to adapt the IL code of the Main() method (actual there is 0x2000 - (0x1F14 + 0x17) = 0xD5 bytes room left).
The archive contains in subdirectory Part02_RelocateMain two files for your investigation, the original one and the one with the relocated Main() method.
Investigating an existing MessageBox call
The next step is the injection of the actual IL code to display a messagebox. To be able to do so, let's analyze the code of an existing MessageBox call. To display a messagebox on C#, MessageBox.Show() is used where the simplest overloaded version just takes one string argument for the actual content text.
When playing around with the same application, you might notice that you get a "Your number is too small!" messagebox when the entered value is smaller than the value to guess.
Let's examine the IL code of this call using ILDasm. Loas the file and go to function CheckNumber() where you can find following snippet:
IL_0032: /* 72 | (70)00006F */
ldstr "Your number is too small!" /* 7000006F */ IL_0037: /* 28 | (0A)00001B */ call valuetype [System.Windows.Forms/*23000001*/]System.Windows.Forms.DialogResult/*01000023*/ [System.Windows.Forms/*23000001*/]System.Windows.Forms.MessageBox/*01000022*/::Show(string) /* 0A00001B */ IL_003c: /* 26 | */ pop |
This IL code is generated for the following C# line:
MessageBox.Show("Your number is too small!"); |
Following analysis is optional but quite an interesting part of the article. However, if you just want to inject the message box, click here to move to the next chapter.
The investigation of the first line is postponed, but let's analyze the call bytes (because we want to learn something, right?!).
28 0A 00 00 1B
0x28 is the opcode for the call instruction (see [6]).
0x0A00001B is a token referencing table 0x0A (MemberRef table) and table index 0x1B (27). So this value must somehow provide the information which method in which class to call here. So investiagting this entry with CFF Explorer, we get for this entry:
Member | Offset | Size | Value | Meaning |
Class | 0x0CE2 | Word | 0x0111 | TypeRef Table Index 34 |
Name | 0x0CE4 | Word | 0x0582 | Show |
Signature | 0x0CE6 | Word | 0x00A1 | Blob Index |
The Class member is a reference to index 34 in table TypeRef. If we follow this link, we find there the type MessageBox in namespace System.Windows.Forms in assembly index 1 which is assembly System.Windows.Forms - so far so good.
Name is an index into the string heap. CFF Explorer already resolves it as string 'Show' - however you can also find it out manually by switching to the #String stream and go to offset 0x0582:
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | Ascii |
580 | 6E | 00 | 53 | 68 | 6F | 77 | 00 | 45 | 78 | 63 | 65 | 70 | 74 | 69 | 6F | 6E | n.Show.Exception |
Finally let's look at the Signature. The value is 0x00A1 and is declared as Blob Index. Looking at address 0xA1 in the Blob stream we find these bytes:
06 00 01 11 80 8D 0E |
Let's decode them:
- The first byte (0x06)denotes the length of the signature, so six bytes follow.
- The second byte (0x00) denotes special flags, for us there are no flags set. For example, if the Show method would be an instance method and not a static method, the this pointer had to be passed as first argument and the HASTHIS flag (0x20) would be set here.)
- The next byte (0x01) specifies the number of arguments for this method, so this is 0x01 for one parameter (the string parameter).
- Afterwards the return type follows which is 0x11 in our case. This means it's a ELEMENT_TYPE_VALUETYPE return type which perfectly fits because the Show() method returns a DialogResult which is defined as an enum and an enum is a value type.
- The ELEMENT_TYPE_VALUETYPE is always followed by either an TypeDef or TypeRef token (0x808D).
The 808D is a signature compressed integer and actually means 0x8D.
The 0x8D is on the other hand a so-called coded token:
0x8D = 1000 1101
The least two significant bits denotes the table index: 01 = 3 denotes the TypeRef table.
The other bits specify the entry index inside this table: 100011 = 0x23 = 35d.
And what can we find there with CFF? The type entry for DialogResult type, together with the name and assembly reference it belongs to.
- The last byte of the signature (0x0E) defines the type of the first argument: 0x0E stands for ELEMENT_TYPE_STRING, so this also matches our expectations.
This little excursion gives an idea which information is stored in an managed executable and how detailed as well as complicated this information is stored.
But let's continue with our messagebox...
Inserting the MessageBox code into the Main method
We have analyzed the IL code on how to call a messagebox, consisting of loading a string, call the MessageBox.Show() method and "pop" the DialogResult return value from the stack (see above to refresh your mind).
What we gonna do now is to insert exactly this messagebox code at the beginning of our relocated main method.
The original main method consists of 22 bytes (excluding the 1-byte long header):
28 46 00 00 0A 16 28 47 00 00 0A 73 01 00 00 06 28 48 00 00 0A 2A |
Now just insert the message box IL code (bytes in blue color) at the beginning of our relocated main method (file offset 0x1F15, excluding 1-byte header):
72 6F 00 00 70 28 1B 00 00 0A 26 28 46 00 00 0A 16 28 47 00 00 0A 73 01 00 00 06 28 48 00 00 0A 2A |
This increases the size of the method from 22 to 33 bytes, so we have to adapt the header.
The new size is 33d = 100001, the header remains tiny which is encoded as 01 in the lowest two bits. This result in 10000101 = 0x86.
So the new main method IL code including the header byte (starting @ file offset 0x1F14) is:
86 72 6F 00 00 70 28 1B 00 00 0A 26 28 46 00 00 0A 16 28 47 00 00 0A 73 01 00 00 06 28 48 00 00 0A 2A |
Overwrite the old main method code with the new one in a hex editor, start the new executable and bam... it works! There is our messagebox!
[You find the modified executable as Part03_CopyBox\EntryPointTestApp_BoxCopied.exe in the attached archive]
Hm, but still the same string 'Your number is too small' is displayed, so let's fix that!
Modify the displayed string
Modifying this string will turn out to be more difficult than expected, but at first let's have a look at the code that loads the string:
IL_0032: /* 72 | (70)00006F */
ldstr "Your number is too small!" /* 7000006F */ |
0x72 is the IL opcode of the ldstr instruction. This expects as parameter a token with the address of the string to load.
The 0x7000006F token is special mdtString token. Those begin with 0x70, the subsequent part is an address into the #US stream.
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | Ascii |
00000060 | 20 | 00 | 6E | 00 | 75 | 00 | 6D | 00 | 62 | 00 | 65 | 00 | 72 | 00 | 00 | 33 | ..n.u.m.b.e.r..3 |
00000070 | 59 | 00 | 6F | 00 | 75 | 00 | 72 | 00 | 20 | 00 | 6E | 00 | 75 | 00 | 6D | 00 | Y.o.u.r...n.u.m. |
00000080 | 62 | 00 | 65 | 00 | 72 | 00 | 20 | 00 | 69 | 00 | 73 | 00 | 20 | 00 | 74 | 00 | b.e.r...i.s...t. |
00000090 | 6F | 00 | 6F | 00 | 20 | 00 | 73 | 00 | 6D | 00 | 61 | 00 | 6C | 00 | 6C | 00 | o.o...s.m.a.l.l. |
000000A0 | 21 | 00 | 00 | !.. |
Looking around in the file reveals that the #US stream has file offset 0x17F4 and is 0x310 bytes long. However, the #GUID and #Blob streams directly follow - there is no room left. We have no space to insert our data - AGAIN!
Well this time I did not even try to relocate anything. This would be very complicated... However, what we can do is to abuse an existing string.
What I have noticed is that the designer of Visual Studio generates code to assign each GUI element a name string for the Name property, e.g
this.exitButton.Name = "exitButton"; |
This is actually not explicetely necessary and most often the Name property is not used, so I decided to abuse this string. This is just an idea, of course you can reuse whatever string you want.
Let's look at this string in the #US stream:
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | Ascii |
00000250 | 15 | 65 | 00 | 78 | 00 | 69 | 00 | 74 | 00 | 42 | 00 | 75 | 00 | 74 | 00 | 74 | .e.x.i.t.B.u.t.t |
00000260 | 00 | 6F | 00 | 6E | 00 | 00 | 2B | 4C | 00 | 65 | 00 | 61 | 00 | 76 | 00 | 65 | .o.n..+L.e.a.v.e |
The string begins at offset 0x250 in the #US stream. The first byte denotes the length, thus it is 0x15 = 21d bytes long and therefore ends with two zero bytes at 0x265 (including). So just change it to something cooler and adapt the length:
Offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | Ascii |
00000250 | 15 | 54 | 00 | 68 | 00 | 69 | 00 | 73 | 00 | 20 | 00 | 73 | 00 | 75 | 00 | 63 | .T.h.i.s...s.u.c |
00000260 | 00 | 6B | 00 | 73 | 00 | 00 | 2B | 4C | 00 | 65 | 00 | 61 | 00 | 76 | 00 | 65 | .k.s..+L.e.a.v.e |
As last step, the token argument of the ldr opcode has to be adapted so that it points to our string:
At file offset 0x1F15 the original byte are: 72 6F 00 00 70 -> ldstr 7000006F.
Let's change this to load our new string: 72 50 02 00 70 -> ldstr 70000250.
Run the executable and.... tada, our manually inserted messagebox pops up with a warning that the application is not that cool before the actual application starts.
Conclusion
Hopefully you like this article and have learned something new and interesting.
Sunshine, April 2013
References:
[1] Standard ECMA-335: Common Language Infrastructure (CLI), Partitions I to IV
[2] The .NET File Format (Article by Daniel Pistelli)
[3] Expert .NET 2.0 IL Assembler (Book, Amazon link)
[4] Applied Microsoft� .NET Framework Programming (Book, Amazon link)
[5] Anatomy of a .NET Assembly - Article Series
[6] List of CIL instructions
This site is part of Sunshine's Homepage