.NET Reverse Engineering – 3
Introduction
We have taken tour of the syntax and semantics of raw CIL up till now. In this article, we shall be confronted with the rest of implementation in the context of CIL programming such as how to build and consume *.dll file components using MSIL programming opcodes instruction set. Apart from that, we will see how to integrate exception handling related opcode instruction into IL code in order to handle unwanted thrown exception. Finally, we'll come across with some unconventional methods of inline IL programming by integrating its opcodes into existing high level language source code.
Building and Consuming *.DLLs files
DLLs (Dynamic Linking Library) files are deemed to library components of business logics for future reusability. We have seen creation of DLL file components in numerous examples using Visual Studio IDE earlier, which isn't rocket science at all. But it is very cumbersome to build dll's through CIL grammar.
Here the following code, defines two methods Hello() which simply displays a passed string over the screen and second method Addition() takes two integer values in order to calculate their sum as following:
Building DLLs File
[plain]
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4:0:0:0
.assembly TestLib
{
}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003
.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
.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_000f: ldloc.0
IL_0010: ret
.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_0007: ldloc.0
IL_0008: ret
} // end of method Magic::Addition
}
After you finish coding, compile this TestLib.il file using ILASM in order to generate its corresponding *.dll file as the following:
ILASM.exe /dll TestLib.il
And later, it is recommended you verify the generated CIL using the peverify.exe as the following:
Consume DLLs File
It's time to consume the previously generated TestLib.dll file into a client executable Main.exe file. So create a new file as main.il and define appropriate external reference of mscorlib.dll and TestLib.dll file. Don't forget to place TestLib.dll copy into the client project solution directory as the following:
[plain]
.assembly extern mscorlib // Define the Reference of mscorlib.dll
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.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
}
.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_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
}
[/plain]
Main.il
Finally, compile this program using ILASM.exe and you will notice that a main.exe file is created under the solution directory. It's also recommended to verify the generated CIL code using peverify.exe utility.
Now test the executable by running it directly from the command prompt. It will produce the desired output as the following:
Exception Handling
Sometimes during conversion between different data type, 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 variable and assigning some value beyond its capacity. So it obvious that this program throws an exception related to over size as the following:
[plain]
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4:0:0:0
}
.assembly ExcepTest
.hash algorithm 0x00008004
.ver 1:0:0:0
}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003
// =============== 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
.locals init ([0] int32 x,[1] uint8 bVar) // init two variable x and bVar
IL_0000: nop
IL_0001: ldc.i4 2000 // assign x= 2000
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: call uint8 [mscorlib]System.Convert::ToByte(int32) // convert integer to byte type (bVar=x)
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)
IL_001e: call void [mscorlib]System.Console::WriteLine(string) // print bVal
IL_0023: nop
IL_0024: ret
} // end of method Program::Main
}
Now compile this code and run the executable file, the code is unable to handle the overflow size because the Byte data type can handle the size of data up to 255 and here, we are manipulating greater than 255 so our code throws the exception as the following:
The previous program was not able to handle unexpected occurring errors during the program execution. In order to run the program in the appropriate order, we must have to include try/catch block. The suspicious code that might cause some irregularities should be placed in a try block and the thrown exception handled in the catch block as the following:
[plain]
.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
} // 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
} // end handler
IL_0038: nop
IL_0039: ret
}
After you applied the exception handling implementations in the code, now you need to compile it using ILASM and run the generated exe file. This time the try/catch block handle the thrown exception related to size overflow as following:
Inline MSIL Code
Typically, there isn't a provision for IL Inline coding in .NET CLR. We can't execute IL opcode instruction with high level language coding in parallel. In the following sample, we are creating a method which takes two integer type of arguments and later defines the addition functionality using IL coding instruction as:
[c language="#"]
public static int Add(int n, int n2)
#if IL
ldarg n
ldarg n2
add
ret
#endif
return 0; // place holder so method compiles
}
But a prominent developer, Mike Stall has made a tool called inlineIL which can execute IL code side by side with the existing C# code. In this process, we first compile our C# code using regular csc or vbc compiler in debug mode and generate a *.pdb file. The compiler won't confuse with instruction defined in #if block and skipped by the compiler.
csc %inputfile% /debug+ /out:%outputfile*.pdb%
The original source code is diassembled, and the ILASM opcodes are extracted and injected into the disassembly code. The line number information for the injection comes from the PDB file which produced from first step as
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 ILAsm inserts as following
ilasm %il_output% /output=%output_file *.exe% /optimize /debug
Although, it does not make sense to integrate IL code into C# code file. This experiment is done just for a knowledge point of view. We must download the tool Mike Stall developed in order to see this implementation.
Summary
Become a certified reverse engineer!
As you can see, IL opcode has directly opened various ways of new possibilities. We can drill down the opcode in order to manipulate it as per our requirements. In this article, we have learned how to build our own dll file component in order to consume it into a front end clients program, and protected code by applying exception handling. So up till now, we have obtained thorough understanding of IL grammar which is substantially required for .NET reverse engineering. Now it's time to mess with hard core reverse engineering and as you will see in the forthcoming articles, how to manipulate .NET code in order to crack passwords, reveal serial keys and lots of other significant possibilities.