Before reading this article, I highly recommend reading the previous parts.
- NET Binary Reverse Engineering: Part 1
- .NET Reverse Engineering: Part 2
Abstract
As yet, we have taken a tour of the syntax and semantics of raw CIL coding. In this article, we shall be confronted with the rest of the implementation in the context of CIL programming as such, how to build and consume DLL file components using the MSIL programming opcodes instruction set. Apart from that, we will see how to integrate exception-handling related opcode instructions into IL code to handle unwanted thrown exceptions. Finally, we'll explore some unconventional methods of inline IL programming by integrating its opcodes into existing high-level language source code.
Building and Consuming DLL files
Dynamic Linking Library (DLL) files are library components of business logic for reuse. We have seen the creation of DLL file components in numerous examples using the Visual Studio IDE earlier, that is in fact no rocket science at all. However, it is very cumbersome to build DLLs in the CIL grammar context.
Building DLL Files
Here in the following code, two methods are defined, Hello() which simply displays a passed string over the screen, and another method Addition() which takes two integer values to calculate their sum as in the following. These methods would be bundled in the final generated library file that shall be consumed later into the client application to expose its methods.
.assembly extern mscorlib {
.publickeytoken = (B77A5C561934E089)
.ver 4: 0: 0: 0
}
.assembly TestLib {}
.module TestLib.dll //mention the final type of file
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003
.corflags 0x00000001 // ILONLY
.class public auto ansi beforefieldinit TestLib.Magic extends [mscorlib] System.Object {
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed {
.maxstack 8
IL_0000: ldarg .0
IL_0001: call instance void [mscorlib] System.Object::.ctor()
IL_0006: nop
IL_0007: nop
IL_0008: nop
IL_0009: ret
} // end of method Magic::.ctor
.method public hidebysig instance string Hello(string str) cil managed {
.maxstack 2
.locals init ([0] string CS$1$0000)
IL_0000: nop
IL_0001: ldstr "Hello"
IL_0006: ldarg .1
IL_0007: call string [mscorlib] System.String::Concat(string, string)
IL_000c: stloc .0
IL_000d: br.s IL_000f
IL_000f: ldloc .0
IL_0010: ret
} // end of method Magic::Hello
.method public hidebysig instance int32 Addition(int32 x, int32 y) cil managed {
.maxstack 2
.locals init ([0] int32 CS$1$0000)
IL_0000: nop
IL_0001: ldarg .1
IL_0002: ldarg .2
IL_0003: add
IL_0004: stloc .0
IL_0005: br.s IL_0007
IL_0007: ldloc .0
IL_0008: ret
} // end of method Magic::Addition
}
After doing the coding, compile this TestLib.il file using the ILASM.exe utility to generate the corresponding library DLL file as in the following.
ILASM.exe /dll TestLib.il
Later, it is recommended to verify the generated CIL using peverify.exe to confirm that the generated library is compliant with the CLR, as in the following.
Consume DLL Files
The section would show how to consume the previously generated TestLib.dll file in a client executable Main.exe file. Hence, create a new file as main.il and define the external references in the form of mscorlib.dll and TestLib.dll files. Don't forget to place the TestLib.dll copy into the client project solution directory as in the following.
.assembly extern mscorlib // Define the Reference of mscorlib.dll
{
.publickeytoken = (B77A5C561934E089)
.ver 4:0:0:0
}
.assembly extern TestLib // Define the Reference of TesLib.dll
{
.ver 1:0:0:0
}
.assembly TestLibClient {
.ver 1:0:0:0
}
.module main.exe // Define the final executable name
.class private auto ansi beforefieldinit Program extends [mscorlib] System.Object {
.method private hidebysig static void Main(string[] args) cil managed {
.entrypoint
.maxstack 8
.locals init ([0] class TestLib.TestLib.Magic obj) // Init magic class obj
IL_0000: nop
IL_0001: newobj instance void TestLib.TestLib.Magic::.ctor() // initialize magic class constructor
IL_0006: stloc .0
IL_0007: ldloc .0
IL_0008: ldstr "Ajay" // Pass “Ajay” string in Hello method
IL_000d: callvirt instance string TestLib.TestLib.Magic::Hello(string)
IL_0012: call void [mscorlib] System.Console::WriteLine(string) // print Hello method
IL_0017: nop
IL_0018: ldstr "Addition is:: {0}"
IL_001d: ldloc .0
IL_001e: ldc.i4.s 10 // define x=10
IL_0020: ldc.i4.s 20 // define x=20
IL_0022: callvirt instance int32 TestLib.TestLib.Magic::Addition(int32, int32) // call Addition()
IL_0027: box [mscorlib] System.Int32
IL_002c: call void [mscorlib] System.Console::WriteLine(string, object)
IL_0038: ret
}
}
Finally, compile this program using the ILASM.exe utility and you'll notice that the main.exe file is created in the solution directory. It is also advisable to verify the generated CIL code using the peverify.exe utility.
Now test the executable by running it directly from the command prompt. It will produce the desired output as in the following.
Exception Handling
Sometimes when converting between data types, our program is unable to handle unexpected occurrences of strange errors and our program does not produce the desired result or may be terminated. The following example defines Byte type variables and assigns some value beyond their capacity. So it is obvious that this program throws an exception related to overflow as in the following.
.assembly extern mscorlib {
.publickeytoken = (B77A5C561934E089)
.ver 4:0:0:0
}
.assembly ExcepTest {
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.module ExcepTest.exe {
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003
.corflags 0x00000003
}
// =============== CLASS MEMBERS DECLARATION ===================
.class private auto ansi beforefieldinit test.Program extends [mscorlib] System.Object {
.method private hidebysig static void Main(string[] args) cil managed {
.entrypoint
.maxstack 2
// init two variable x and bVar
.locals init ([0] int32 x, [1] uint8 bVar)
IL_0000: nop
// assign x= 2000
IL_0001: ldc.i4 2000
IL_0006: stloc .0
IL_0007: ldloc .0
// convert integer to byte type (bVar=x)
IL_0008: call uint8 [mscorlib] System.Convert::ToByte(int32)
IL_000d: stloc .1
IL_000e: ldstr "Value="
IL_0013: ldloc .1
IL_0014: box [mscorlib] System.Byte
IL_0019: call string [mscorlib] System.String::Concat(object, object)
// print bVal
IL_001e: call void [mscorlib] System.Console::WriteLine(string)
IL_0023: nop
IL_0024: ret
} // end of method Program::Main
}
Now compile this code and after running the executable file, the code would be unable to handle the overflow size because the Byte data type can handle the size of the data up to 255, and here since we are manipulating a value greater than 255 our code throws the exception as in the following.
The previous program was not able to handle unexpected errors that occurred during program execution. In order to run the program in the appropriate order, we must include a try/catch block. The suspicious code that might cause some irregularities should be placed in a try block and then thrown exception handled in the catch block as in the following.
.method private hidebysig static void Main(string[] args) cil managed {
.entrypoint
.maxstack 2
.locals init ([0] int32 x, [1] uint8 bVar)
IL_0000: nop
IL_0001: ldc.i4 0x7d0
IL_0006: stloc .0
.try {
IL_0007: nop
IL_0008: ldloc .0
IL_0009: call uint8[mscorlib] System.Convert::ToByte(int32)
IL_000e: stloc .1
IL_000f: ldstr "Value="
IL_0014: ldloc .1
IL_0015: box[mscorlib] System.Byte
IL_001a: call string[mscorlib] System.String::Concat(object, object)
IL_001f: call void[mscorlib] System.Console::WriteLine(string)
IL_0024: nop
IL_0025: nop
IL_0026: leave.s IL_0038
} // end .try
catch [mscorlib] System.Exception {
IL_0028: pop
IL_0029: nop
IL_002a: ldstr "Size is overflow"
IL_002f: call void[mscorlib] System.Console::WriteLine(string)
IL_0034: nop
IL_0035: nop
IL_0036: leave.s IL_0038
} // end handler
IL_0038: nop
IL_0039: ret
}
After applying exception handling implementations in the code, now compile it using ILASM and run the generated exe file again. This time, the try/catch block handles the thrown exception related to size overflow as in the following.
Inline MSIL Code
Typically, there is no provision for IL inline coding in .NET code. We can't execute an IL opcode instruction with high-level language coding in parallel. In the following sample, we are creating a method that takes two integer types of arguments and later defines the additional functionality using IL coding instructions as in the following.
public static int Add(int n, int n2) {
#if IL
ldarg n
ldarg n2
add
ret
#endif
return 0; // placeholder so method compiles
}
But a prominent developer Mike Stall has made a tool called inline, that can execute IL code side by side with the existing C# code. In this process, we first compile our C# code using the regular CSC or vbc compiler in debug mode and generate a *.pdb file. The compiler won't be confused with the instruction defined in the #if block is ignored by the compiler.
csc %inputfile% /debug+ /out:%outputfile*.pdb%
The original source code is disassembled and the ILASM opcodes are extracted and injected into the disassembly code. The line number information for the injection comes from the PDB file that is produced from the first step as in the following.
ildasm %*.pdb % /linenum /out=%il_output%
Finally, the modified IL code is assembled using ILASM. The resulting assembly contains everything including the code defined in the inserted ILAsm as in the following.
ilasm %il_output% /output=%output_file *.exe% /optimize /debug
Although it does not make sense to integrate IL code into a C# code file. This experiment is done just for educational purposes. We must download the Mike Stall tool to see this implementation.
Summary
As you can see, an IL opcode can directly open various new possibilities. We can drill down the opcode to manipulate it as needed. In this article, we have learned how to build your own DLL file component to consume it into front-end client programs and protect code by applying exception handling. So until now, we have obtained, a thorough understanding of IL grammar, which is substantially required for .NET reverse engineering. Now it is time to mess with hard-core reverse engineering and you will see in the forthcoming articles, how to manipulate .NET code to crack passwords, reveal serial keys, and many other significant possibilities.