Before reading this article, I highly recommend reading the previous parts:
Introduction
We shall explore round-trip engineering, one of the most advanced tactics to disassemble IL code to do Reverse Engineering in the context of existing .NET built software applications but the .NET round-tripping engineering requires a thorough understanding of MSIL grammar to which we have already confronted in the previous articles because all we need to do is to play with IL code when reversing. After getting befitting competency in round-trip engineering, we can the bypass serial key and user authentication mechanisms and fix inherent bugs that are shipped in existing .NET application software without having to access source code.
Round-trip Engineering
Round-trip engineering refers to disassembling an existing IL code of an application. This sophisticated process first re-manipulates the IL code, modifies it as needed and finally re-assembles the code without peeping into the actual source code of an application. Formally speaking, this technique can be useful under a number of circumstances such as sometimes we need to modify an assembly to fix bugs for which you no longer have access to the source code. Some trial software expire after completion of a specific grace period and we can no longer use them. Finally, we can change numerous stipulated conditions such as 15 days or 1 month trial duration by applying round-tripping or can enter into a software interface without having the relevant password. These tactics can also be useful during COM interoperability in which we can recover lost COM IDL attributes. The following image illustrates the life-cycle of the round-tripping process:
The process of round-tripping engineering of managed PE files includes two steps. The first step is to disassemble the existing PE file (assembly) into an ILASM source file and the managed and unmanaged resource files using the following:
ildasm test.dll /out:testNew.il
The second step of round-tripping is to invoke the ILASM compiler to produce a new PE file from the results of the disassembler's activities using the following:
ilasm /dll testNew.il /out:Final.dll
Fixing Bugs
At the production site, application software won't work properly or produce some strange implications. The programmer typically leave subtle run time bugs in the final software version inadvertently. The reason of software failure might be numerous such as not conducting unit testing properly at the development site or developers are in a hurry to launch the application due to the pressure of a deadline from the client side. The client typically does not have access to the actual source code of the software. They are provided only the final executable bundle of the software because most of the clients are laymen about technology; they are only proficient enough to operate from the front-end user interface. What is happening at the back-end side is entirely rocket science for them to understand. There could be another scenario in which the organization that develops the software is no longer in the market and that might cause a huge problem because now there is no one to fix the bugs.
Note: Reverse Engineering can be executed for either offensive or defensive purpose and this article's intent is to get the knowledge of Reverse Engineering for defensive reading from the testing point of view.
Now the question is how to fix the bugs that occur despite not having the source code of the software. The answer is Round-tripping Reverse Engineering. The final shipped bundle includes the executable of the software with its dependent library files even if the client still insists on relying on software full of bugs due to a fear of significant data loss. Hence, the client has another option for approaching some ardent Reverse Engineering professional so that they can endeavor to fix the bugs to produce the desired result without having access to the source code.
Memory Overflow Bug
The following sample illustrates a simple addition of two-byte types of variables and displays the calculated output over the screen. The operation seems very simple superficially. But the programmer doesn't have an idea that this application can lead to failure if they didn't apply the proper precaution of operation logics related to the Byte data type.
- .assembly extern mscorlib
- {
- }
- .assembly BugFix
- {
- }
- .module BugFix.exe
-
- .class private auto ansi beforefieldinit Program extends [mscorlib]System.Object
- {
- .method private hidebysig static void Main(string[] args) cil managed
- {
- .entrypoint
- .maxstack 2
- .locals init ([0] uint8 b1,[1] uint8 b2,[2] uint8 total)
- IL_0000: nop
- IL_0001: ldarg.0
- IL_0002: ldc.i4.0
- IL_0003: ldelem.ref
- IL_0004: call uint8 [mscorlib]System.Byte::Parse(string)
- IL_0009: stloc.0
- IL_000a: ldarg.0
- IL_000b: ldc.i4.1
- IL_000c: ldelem.ref
- IL_000d: call uint8 [mscorlib]System.Byte::Parse(string)
- IL_0012: stloc.1
- IL_0013: ldloc.0
- IL_0014: ldloc.1
- IL_0015: add
- IL_0016: conv.u1
- IL_0017: stloc.2
- IL_0018: ldloc.2
- IL_0019: call void [mscorlib]System.Console::WriteLine(int32)
- IL_001e: nop
- IL_001f: ret
- }
- }
Once this code is compiled and tested by passing two data as 200 and 70 at the command line for addition. This program produces some bizarre results such as 14 rather than 270.
The problem with precious code is that a Byte data type can contain a value up to 255 in memory and we are adding the variable yet the result (270) is beyond its capacity. The programmer forgot to validate the memory overflow runtime exception. So we can still fix this bug by modifying the IL code by putting an exception overflow check (ovf) without peeping into the source code, as in the following:
- IL_0012: stloc.1
- IL_0013: ldloc.0
- IL_0014: ldloc.1
- IL_0015: add
-
- IL_0016: conv.ovf.u1
-
Thereafter, save this file and re-compile it using the ILASM utility that yields another fixed version of this application. This time the compiler echoes an alert in the case of adding a value that results in byte data beyond the capacity as in the following.
It is a good programming practice to include a try/catch block (that we will see later in the article) to handle run time errors.
Array Index Out Of Range Bug
The following sample demystifies arrays in which normally an index out of range exception occurs. Here, we are declaring a string type array with a length of 3 and initializes its each elements with some hard-coded string values. Later, we are enumerating array elements using a for loop construct to display them as in the following:
- .assembly extern mscorlib
- {
- }
- .assembly BugFix
- {
- }
- .module BugFix.exe
-
- .class private auto ansi beforefieldinit Program extends [mscorlib]System.Object
- {
- .method private hidebysig static void Main(string[] args) cil managed
- {
- .entrypoint
- .maxstack 3
- .locals init ([0] string[] arry,[1] int32 i,[2] bool CS$4$0000)
- IL_0000: nop
- IL_0001: ldc.i4.3
- IL_0002: newarr [mscorlib]System.String
- IL_0007: stloc.0
-
- IL_0008: ldloc.0
- IL_0009: ldc.i4.0
- IL_000a: ldstr "India"
- IL_000f: stelem.ref
-
- IL_0010: ldloc.0
- IL_0011: ldc.i4.1
- IL_0012: ldstr "USA"
- IL_0017: stelem.ref
-
- IL_0018: ldloc.0
- IL_0019: ldc.i4.2
- IL_001a: ldstr "Italy"
- IL_001f: stelem.ref
-
- IL_0020: ldc.i4.0
- IL_0021: stloc.1
- IL_0022: br.s IL_0033
-
- IL_0024: nop
- IL_0025: ldloc.0
- IL_0026: ldloc.1
- IL_0027: ldelem.ref
- IL_0028: call void [mscorlib]System.Console::WriteLine(string)
- IL_002d: nop
- IL_002e: nop
- IL_002f: ldloc.1
- IL_0030: ldc.i4.1
- IL_0031: add
- IL_0032: stloc.1
- IL_0033: ldloc.1
- IL_0034: ldloc.0
- IL_0035: ldlen
- IL_0036: conv.i4
-
-
- IL_0037: cgt
- IL_0039: ldc.i4.0
- IL_003a: ceq
-
- IL_003c: stloc.2
- IL_003d: ldloc.2
- IL_003e: brtrue.s IL_0024
-
- IL_0040: ret
- }
- }
After running this program, we notice that the application encounters the exception "Index out or Range" after displaying three elements. This is happening because the for loop is iterating one extra time by placing the equal sign in the condition block and the compiler throws an exception as in the following.
So we can fix this bug by manipulating the IL code implicitly. The ceg opcode is responsible for specifying an equal sign so all we need to do is to replace the clt opcode with ceg that is stipulating the less than condition and eradicate the ldc opcode value. Now the for loop construct will iterate three times rather than four times as in the following:
-
- IL_0036: conv.i4
- IL_0037: clt
- IL_0039: stloc.2
- IL_003a: ldloc.2
- IL_003b: brtrue.s IL_0024
- IL_003c: ret
-
Finally, save this file again and compile it using ILASM that produces a bug-free executable file as in the following:
Divide by Zero Exception Bug
The following program simply divides a number with another value and the logic implementation is very easy but if the programmer forgot to validate the denominator value then that should not be zero. Our application will crash and throw a DivideByZeroExcpetion alert. Here the IL code implementation is as the following.
- .assembly extern mscorlib
- {
- .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
- .ver 4:0:0:0
- }
- .assembly BugFix
- {}
- .module BugFix
-
-
-
- .class private auto ansi beforefieldinit Program
- extends [mscorlib]System.Object
- {
- .method private hidebysig static void Main(string[] args) cil managed
- {
- .entrypoint
-
- .maxstack 2
- .locals init ([0] int32 x,[1] int32 y,[2] int32 Result)
- IL_0000: nop
- IL_0001: ldc.i4.s 10
- IL_0003: stloc.0
- IL_0004: call string [mscorlib]System.Console::ReadLine()
- IL_0009: call int32 [mscorlib]System.Int32::Parse(string)
-
-
- IL_000e: stloc.1
- IL_000f: ldloc.0
- IL_0010: ldloc.1
- IL_0011: div
- IL_0012: stloc.2
- IL_0013: ldloca.s Result
- IL_0015: call instance string [mscorlib]System.Int32::ToString()
- IL_001a: call void [mscorlib]System.Console::WriteLine(string)
-
- IL_001f: nop
- IL_0020: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
- IL_0025: pop
- IL_0026: ret
- }
- }
After running this program, it asks the user to input the denominator value and unfortunately we entered it as 0 now the application yields the following output:
Such trivial logic implementation should be handled at the time of coding by placing the sensitive code into a try/catch block so that the application won't interrupt the execution and throw an alert to the user if they enter the wrong values. However, we are putting here the try/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] int32 y,[2] int32 Result)
- IL_0000: nop
- IL_0001: ldc.i4.s 10
- IL_0003: stloc.0
- IL_0004: call string [mscorlib]System.Console::ReadLine()
- IL_0009: call int32 [mscorlib]System.Int32::Parse(string)
- IL_000e: stloc.1
- .try
- {
- IL_000f: nop
- IL_0010: ldloc.0
- IL_0011: ldloc.1
- IL_0012: div
- IL_0013: stloc.2
- IL_0014: ldloca.s Result
- IL_0016: call instance string [mscorlib]System.Int32::ToString()
- IL_001b: call void [mscorlib]System.Console::WriteLine(string)
- IL_0020: nop
- IL_0021: nop
- IL_0022: leave.s IL_0034
-
- }
- catch [mscorlib]System.DivideByZeroException
- {
- IL_0024: pop
- IL_0025: nop
- IL_0026: ldstr "Denominator must not be Zero"
- IL_002b: call void [mscorlib]System.Console::WriteLine(string)
- IL_0030: nop
- IL_0031: nop
- IL_0032: leave.s IL_0034
-
- }
- IL_0034: nop
- IL_0035: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
- IL_003a: pop
- IL_003b: ret
- }
After running this program, if the user inputs 0 as a denominator value then again, the compiler echoes an alert as in the following:
Summary
I hope you have enjoyed this article a lot. We have learned a couple of advanced operations related to Round-trip Engineering by modifying the IL opcode explicitly without manipulating the source code. We have seen how to handle run time occurrences of exceptions such divide by zero, index out of range and so on by altering the corresponding IL opcodes. In the next article, we shall explore how to crack the user authentications mechanism, bypassing serial keys conditions.