Extreme .NET Reverse Engineering - 4
Introduction
We shall explore round-trip engineering, which is one of the most advanced tactics to disassemble IL code in order to manipulate reverse engineering in the context of existing NET-built software applications. The .NET round-trip engineering requires a thorough understanding of MSIL grammar, which we have already confronted in the previous articles because all we have to do is to play with IL code. After getting entirely competent in round-trip engineering, we can bypass serial keys and user authentication mechanisms and fix inherent bugs that are shipped into existing applications without having source code.
Round-Trip Engineering
Round-trip engineering refers to disassembling the IL code of an existing application. This sophisticated process first re-manipulates the IL code, modifying it as per our requirement, 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; for example, sometimes we need to modify an assembly in case of bug fixing for which you no longer have access to the source code. Some trail software expires after completing their specific grass 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 we can enter into software interface without having relevant password. This 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 round-tripping process as:
The process of round-trip engineering in the case 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:
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:
ilasm /dll testNew.il /out:Final.dll
Bug Fixing
At the production site, application software won't work properly or it might produce some strange implications. The programmer typically left sort of subtle run-time bugs in the final software version inadvertently. Reasons for software failure might be numerous, such as not conducting unit testing properly at the development site or the developers are in hurry to launch the application due to the pressure of a deadline from 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 to understand for them. There could be another scenario in which the organization that developed the software no longer exists, which might cause a huge problem because now the client has no one to ask in order to fix the bugs.
Note: Reverse Engineering can be executed with both offensive and defensive purposes and this article aims to get the knowledge of reverse engineering for the defensive reading and testing point of view.
Now the question is how to fix the bugs despite not having the source code of the software. The answer is round-trip reverse engineering. The final shipped bundle includes the executable of the software with its dependent library files. If the client still insists on relying on software that is full of bugs, the client has the option of approaching some ardent reverse engineer so they can try to fix the bugs in order to produce desired result without having access to source code.
Memory Overflow Bug
The following sample illustrates the simple addition of two-byte type variables and displays the calculated output on the screen. The operation seems very simple, superficially. But the programmer doesn't have an idea that this application can lead to failure if he didn't apply the proper precaution of operation logics related to byte data types.
[plain]
.assembly extern mscorlib
{
}
.assembly 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] 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, it can be tested by passing two data as 200 and 70 at the command line to be added together. This program produces a bizarre result, 14 rather than 270.
The problem with that code is that the byte data type only goes up to 255 and we are adding variable for which the result is beyond its capacity (270). The programmer forgot to validate the memory overflow run-time exception. We can fix this bug by modifying the IL code by putting exception overflow check (ovf) without peeping into source code:
[plain]
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, which yields another fixed version of this application. This time the compiler echoes an alert in the case of adding values that have a result beyond the byte data capacity, as shown in the following:
It is good programming practice to include a try/catch block to handling run-time error occurrences, as we will see later in the article.
Array Index Out of Range Bug
The following sample demystifies arrays in which the index out of range exception normally occurs. Here we are declaring a string type array with length of 3 and we initialize each of its elements with some hard-coded string values. Later, we enumerate array elements using a for loop construct in order to display them as following:
[plain]
.assembly extern mscorlib
{
}
.assembly BugFix
{
}
.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_0008: ldloc.0
IL_0009: ldc.i4.0
IL_000a: ldstr "India"
IL_0010: ldloc.0
IL_0011: ldc.i4.1
IL_0012: ldstr "USA"
IL_0018: ldloc.0
IL_0019: ldc.i4.2
IL_001a: ldstr "Italy"
IL_0020: ldc.i4.0
IL_0021: stloc.1
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
// **************************Infected Code********************************
IL_0037: cgt
IL_0039: ldc.i4.0
IL_003c: stloc.2
IL_003d: ldloc.2
IL_0040: ret
}
}
After running this program, we notice that the application encounters an exception of index out of range after displaying three elements. This happens because the for loop iterates one time extra by placing the equal sign in the condition block and compiler throws an exception as follows:
We can fix this bug by manipulating IL code implicitly. The ceg opcode is responsible for specifying the equal sign, so all we have to do is to replace clt opcode with ceg, which stipulates the less then condition and eradicate the ldc opcode value. Now the loop construct will iterate three times rather than four times, as shown here:
[plain]
//----------------------------------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 by using ILASM, which produces bug-free executable file, as follows;
Divide by Zero Exception Bug
The following program simply divides a number by another value; the logic implementation is very easy but the programmer must not forget to validate the denominator value, which should not be zero. Our application will crash and throw a DivideByZeroExcpetion alert. Here is the IL code implementation:
[plain]
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4:0:0:0
}
.assembly 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()
//--------------------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. Unfortunately, we entered it as 0 and 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 the application won't interrupt the execution and throw an alert to user if they enter wrong values. We put the try/catch block here, as follows:
[plain]
.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
} // 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
} // 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 the denominator value again, the compiler echoes an alert:
Summary
Become a certified reverse engineer!
I hope you have enjoyed this article a lot. We have learned couple of advance operations related to round-trip engineering by modifying 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, etc., by altering the corresponding IL opcodes. In the next article, we shall explorer how to crack the user authentications mechanism, bypassing serial keys conditions.