Before reading this article, I highly recommend reading the previous parts:
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. But 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() that simply displays a passed string over the screen and another method Addition() that 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 = (B7 7A 5C 56 19 34 E0 89 )
- .ver 4:0:0:0
- }
-
- .assembly TestLib
- {
- }
- .module TestLib.dll
-
- .imagebase 0x00400000
- .file alignment 0x00000200
- .stackreserve 0x00100000
- .subsystem 0x0003
- .corflags 0x00000001
-
- .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_000d: br.s IL_000f
-
- 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_0005: br.s IL_0007
-
- IL_0007: ldloc.0
- IL_0008: ret
- }
- }
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 the 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
- {
- .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
- .ver 4:0:0:0
- }
- .assembly extern TestLib
- {
- .ver 1:0:0:0
- }
- .assembly TestLibClient
- {
- .ver 1:0:0:0
- }
- .module main.exe
-
- .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)
- IL_0000: nop
- IL_0001: newobj instance void [TestLib]TestLib.Magic::.ctor()
-
- IL_0006: stloc.0
- IL_0007: ldloc.0
- IL_0008: ldstr "Ajay"
- IL_000d: callvirt instance string [TestLib]TestLib.Magic::Hello(string)
- IL_0012: call void [mscorlib]System.Console::WriteLine(string)
- IL_0017: nop
- IL_0018: ldstr "Addition is:: {0}"
- IL_001d: ldloc.0
- IL_001e: ldc.i4.s 10
- IL_0020: ldc.i4.s 20
- IL_0022: callvirt instance int32 [TestLib]TestLib.Magic::Addition(int32, int32)
- 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 its capacity. So it is obvious that this program throws an exception related to overflow as in the following:
- .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
- }
- .module ExcepTest.exe
-
- .imagebase 0x00400000
- .file alignment 0x00000200
- .stackreserve 0x00100000
- .subsystem 0x0003
- .corflags 0x00000003
-
-
-
- .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)
- IL_0000: nop
-
-
- IL_0001: ldc.i4 2000
- IL_0006: stloc.0
- IL_0007: ldloc.0
-
-
- 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)
-
-
- IL_001e: call void [mscorlib]System.Console::WriteLine(string)
-
- IL_0023: nop
- IL_0024: ret
- }
- }
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 occur 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 the 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
-
- }
- 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
-
- }
- 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 a 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;
- }
But a prominent developer Mike Stall has made a tool called inlineIL, 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 and is ignored 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 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, thorough an understanding of IL grammar, what 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.