Bug Fixing: .NET Reverse Engineering: Part 4

Before reading this article, I highly recommend reading the previous parts:


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 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 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 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 leaves subtle run time bugs in the final software version inadvertently. The reason for 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 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 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 logic related to the Byte data type.
  1. .assembly extern mscorlib  
  2. {  
  3. }  
  4. .assembly BugFix  
  5. {  
  6. }  
  7. .module BugFix.exe  
  9. .class private auto ansi beforefieldinit Program  extends [mscorlib]System.Object  
  10. {  
  11.   .method private hidebysig static void  Main(string[] args) cil managed  
  12.   {  
  13.     .entrypoint  
  14.     .maxstack  2  
  15.     .locals init ([0] uint8 b1,[1] uint8 b2,[2] uint8 total)  
  16.     IL_0000:  nop  
  17.     IL_0001:  ldarg.0  
  18.     IL_0002:  ldc.i4.0  
  19.     IL_0003:  ldelem.ref  
  20.     IL_0004:  call       uint8 [mscorlib]System.Byte::Parse(string)  
  21.     IL_0009:  stloc.0  
  22.     IL_000a:  ldarg.0  
  23.     IL_000b:  ldc.i4.1  
  24.     IL_000c:  ldelem.ref  
  25.     IL_000d:  call       uint8 [mscorlib]System.Byte::Parse(string)  
  26.     IL_0012:  stloc.1  
  27.     IL_0013:  ldloc.0                             //-------------Here------------  
  28.     IL_0014:  ldloc.1                             //--------------is the-----------  
  29.     IL_0015:  add                                 //----------------Bug------------  
  30.     IL_0016:  conv.u1                             //---------In the code-----------  
  31.     IL_0017:  stloc.2  
  32.     IL_0018:  ldloc.2  
  33.     IL_0019:  call       void [mscorlib]System.Console::WriteLine(int32)  
  34.     IL_001e:  nop  
  35.     IL_001f:  ret  
  36.   }   
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:
  1. IL_0012:  stloc.1  
  2. IL_0013:  ldloc.0  
  3. IL_0014:  ldloc.1  
  4. IL_0015:  add  
  5. //    ---------------------- Code Fixing----------------------------------  
  6. IL_0016:  conv.ovf.u1                     // add ovf here in order to show overflow alert  
  7. //    ---------------------- 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 initializes 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:
  1. .assembly extern mscorlib  
  2. {  
  3. }  
  4. .assembly BugFix  
  5. {  
  6. }  
  7. .module BugFix.exe  
  9. .class private auto ansi beforefieldinit Program  extends [mscorlib]System.Object  
  10. {  
  11.   .method private hidebysig static void  Main(string[] args) cil managed  
  12.   {  
  13.     .entrypoint  
  14.     .maxstack  3  
  15.     .locals init ([0] string[] arry,[1] int32 i,[2] bool CS$4$0000)  
  16.     IL_0000:  nop  
  17.     IL_0001:  ldc.i4.3  
  18.     IL_0002:  newarr     [mscorlib]System.String  
  19.     IL_0007:  stloc.0  
  21.     IL_0008:  ldloc.0  
  22.     IL_0009:  ldc.i4.0  
  23.     IL_000a:  ldstr      "India"  
  24.     IL_000f:  stelem.ref  
  26.     IL_0010:  ldloc.0  
  27.     IL_0011:  ldc.i4.1  
  28.     IL_0012:  ldstr      "USA"  
  29.     IL_0017:  stelem.ref  
  31.     IL_0018:  ldloc.0  
  32.     IL_0019:  ldc.i4.2  
  33.     IL_001a:  ldstr      "Italy"  
  34.     IL_001f:  stelem.ref  
  36.     IL_0020:  ldc.i4.0  
  37.     IL_0021:  stloc.1     
  38.     IL_0022:  br.s       IL_0033  
  40.     IL_0024:  nop  
  41.     IL_0025:  ldloc.0  
  42.     IL_0026:  ldloc.1  
  43.     IL_0027:  ldelem.ref  
  44.     IL_0028:  call       void [mscorlib]System.Console::WriteLine(string)  
  45.     IL_002d:  nop  
  46.     IL_002e:  nop  
  47.     IL_002f:  ldloc.1  
  48.     IL_0030:  ldc.i4.1  
  49.     IL_0031:  add  
  50.     IL_0032:  stloc.1  
  51.     IL_0033:  ldloc.1  
  52.     IL_0034:  ldloc.0  
  53.     IL_0035:  ldlen  
  54.     IL_0036:  conv.i4  
  56. // **************************Infected Code********************************  
  57.     IL_0037:  cgt  
  58.     IL_0039:  ldc.i4.0  
  59.     IL_003a:  ceq  
  61.     IL_003c:  stloc.2  
  62.     IL_003d:  ldloc.2  
  63.     IL_003e:  brtrue.s   IL_0024  
  65.     IL_0040:  ret  
  66.   }  
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:
  1. //----------------------------------Bug Fixing---------------------------------  
  2. IL_0036:  conv.i4  
  3. IL_0037:  clt  
  4. IL_0039:  stloc.2  
  5. IL_003a:  ldloc.2  
  6. IL_003b:  brtrue.s   IL_0024  
  7. IL_003c:  ret  
  8. //----------------------------------Fixing ends-------------------------------------- 
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.
  1. .assembly extern mscorlib  
  2. {  
  3.   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                        
  4.   .ver 4:0:0:0  
  5. }  
  6. .assembly BugFix  
  7. {}  
  8. .module BugFix  
  10. // =============== CLASS MEMBERS DECLARATION ===================  
  12. .class private auto ansi beforefieldinit Program  
  13.        extends [mscorlib]System.Object  
  14. {  
  15.   .method private hidebysig static void  Main(string[] args) cil managed  
  16.   {  
  17.     .entrypoint  
  18.     // Code size       39 (0x27)  
  19.     .maxstack  2  
  20.     .locals init ([0] int32 x,[1] int32 y,[2] int32 Result)  
  21.     IL_0000:  nop  
  22.     IL_0001:  ldc.i4.s   10  
  23.     IL_0003:  stloc.0  
  24.     IL_0004:  call       string [mscorlib]System.Console::ReadLine()  
  25.     IL_0009:  call       int32 [mscorlib]System.Int32::Parse(string)  
  27. //--------------------Here the Vulnerable code----------------------------------//  
  28.     IL_000e:  stloc.1  
  29.     IL_000f:  ldloc.0  
  30.     IL_0010:  ldloc.1  
  31.     IL_0011:  div  
  32.     IL_0012:  stloc.2  
  33.     IL_0013:  ldloca.s   Result  
  34.     IL_0015:  call       instance string [mscorlib]System.Int32::ToString()  
  35.     IL_001a:  call       void [mscorlib]System.Console::WriteLine(string)  
  36. //----------------------------------Till then------------------------------------------//  
  37.     IL_001f:  nop  
  38.     IL_0020:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()  
  39.     IL_0025:  pop  
  40.     IL_0026:  ret  
  41.   }   
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.
  1. .method private hidebysig static void  Main(string[] args) cil managed  
  2.   {  
  3.     .entrypoint  
  4.     .maxstack  2  
  5.     .locals init ([0] int32 x,[1] int32 y,[2] int32 Result)  
  6.     IL_0000:  nop  
  7.     IL_0001:  ldc.i4.s   10  
  8.     IL_0003:  stloc.0  
  9.     IL_0004:  call       string [mscorlib]System.Console::ReadLine()  
  10.     IL_0009:  call       int32 [mscorlib]System.Int32::Parse(string)  
  11.     IL_000e:  stloc.1  
  12.     .try  
  13.     {  
  14.       IL_000f:  nop  
  15.       IL_0010:  ldloc.0  
  16.       IL_0011:  ldloc.1  
  17.       IL_0012:  div  
  18.       IL_0013:  stloc.2  
  19.       IL_0014:  ldloca.s   Result  
  20.       IL_0016:  call       instance string [mscorlib]System.Int32::ToString()  
  21.       IL_001b:  call       void [mscorlib]System.Console::WriteLine(string)  
  22.       IL_0020:  nop  
  23.       IL_0021:  nop  
  24.       IL_0022:  leave.s    IL_0034  
  26.     }  // end .try  
  27.     catch [mscorlib]System.DivideByZeroException   
  28.     {  
  29.       IL_0024:  pop  
  30.       IL_0025:  nop  
  31.       IL_0026:  ldstr      "Denominator must not be Zero"  
  32.       IL_002b:  call       void [mscorlib]System.Console::WriteLine(string)  
  33.       IL_0030:  nop  
  34.       IL_0031:  nop  
  35.       IL_0032:  leave.s    IL_0034  
  37.     }  // end handler  
  38.     IL_0034:  nop  
  39.     IL_0035:  call     valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()  
  40.     IL_003a:  pop  
  41.     IL_003b:  ret  
  42.   } 
After running this program, if the user inputs 0 as a denominator value then again, the compiler echoes an alert as in the following:


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