Reverse engineering

Extreme .NET Reverse Engineering - 4

Ajay Yadav
August 2, 2013 by
Ajay Yadav

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.

Become a certified reverse engineer!

Become a certified reverse engineer!

Get live, hands-on malware analysis training from anywhere, and become a Certified Reverse Engineering Analyst.

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

{

}

.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

}

}

[/plain]

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----------------------------------

[/plain]

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

{

}

.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

}

}

[/plain]

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--------------------------------------

[/plain]

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

{}

.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

}

}

[/plain]

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

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

}

[/plain]

After running this program, if the user inputs 0 as the denominator value again, the compiler echoes an alert:


Summary

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.

Ajay Yadav
Ajay Yadav

Ajay Yadav is an author, Cyber Security Specialist, SME, Software Engineer, and System Programmer with more than eight years of work experience. He earned a Master and Bachelor Degree in Computer Science, along with abundant premier professional certifications. For several years, he has been researching Reverse Engineering, Secure Source Coding, Advance Software Debugging, Vulnerability Assessment, System Programming and Exploit Development.

He is a regular contributor to programming journal and assistance developer community with blogs, research articles, tutorials, training material and books on sophisticated technology. His spare time activity includes tourism, movies and meditation. He can be reached at om.ajay007[at]gmail[dot]com