Before reading this article, I highly recommend reading the previous parts.
- .NET Binary Reverse Engineering: Part 1
- .NET Reverse Engineering: Part 2
- .NET Reverse Engineering: Part 3
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 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 bypass the 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 several 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 expires after completion of a specific grace period and we can no longer use them. Finally, we can change numerous stipulated conditions such as 15-day 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 leaves subtle run-time bugs in the final software version inadvertently. The reasons for software failure might be numerous such as not conducting unit testing properly at the development site or developers being 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 purposes 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 professionals 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. However, the programmer doesn't have an idea that this application can lead to failure if they don't apply the proper precaution of operation logic 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 // -------------Here------------
IL_0014: ldloc.1 // --------------is the-----------
IL_0015: add // ----------------Bug------------
IL_0016: conv.u1 // ---------In the code-----------
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
// ---------------------- Code Fixing ----------------------------------
IL_0016: conv.ovf.u1 // add ovf here in order to show overflow alert
// ---------------------- Code Fixing Ends ----------------------------------
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 initializing each element 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
// **************************Infected Code********************************
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 of 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.
//----------------------------------Bug Fixing---------------------------------
IL_0036: conv.i4
IL_0037: clt
IL_0039: stloc.2
IL_003a: ldloc.2
IL_003b: brtrue.s IL_0024
IL_003c: ret
//----------------------------------Fixing ends--------------------------------------
Finally, save this file again and compile it using ILASM which 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 follows.
.assembly extern mscorlib
{
.publickeytoken = (B77A5C561934E089)
.ver 4:0:0:0
}
.assembly BugFix
{}
.module BugFix
// =============== CLASS MEMBERS DECLARATION ===================
.class private auto ansi beforefieldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 39 (0x27)
.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)
// --------------------Here the Vulnerable code----------------------------------//
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)
//----------------------------------Till then------------------------------------------//
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
} // end .try
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
} // end handler
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 as 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 authentication mechanism, bypassing serial key conditions.