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.

Want to learn more?? The InfoSec Institute Reverse Engineering course teaches you everything from reverse engineering malware to discovering vulnerabilities in binaries. These skills are required in order to properly secure an organization from today's ever evolving threats. In this 5 day hands-on course, you will gain the necessary binary analysis skills to discover the true nature of any Windows binary. You will learn how to recognize the high level language constructs (such as branching statements, looping functions and network socket code) critical to performing a thorough and professional reverse engineering analysis of a binary. Some features of this course include:

  • CREA Certification
  • 5 days of Intensive Hands-On Labs
  • Hostile Code & Malware analysis, including: Worms, Viruses, Trojans, Rootkits and Bots
  • Binary obfuscation schemes, used by: Hackers, Trojan writers and copy protection algorithms
  • Learn the methodologies, tools, and manual reversing techniques used real world situations in our reversing lab.

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.

.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, 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:

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:

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

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

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

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:

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