Extreme .NET Reverse Engineering: Part 5

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

Introduction

From the previous articles we have done lots of IL grammar so far. As I warned you earlier, the motivation for Reverse Engineering could be for either offensive or defensive purposes. It is now time to crack some real things with the association of IL opcode grammar knowledge. Ideally, this article taught us how to reveal sensitive information from the source code in order to bypass security constraints such as user credentials validation, extending software trial evaluation period and bypassing serial keys limitations without actually having access to the real source code. We are not going to do binary or byte code patching in the context of reversing the code since we have typically employed tools such as a hex editor, IDA Pro or ollydb software tools for playing with real bytes. In this article however, we will be confronted only with IL opcodes instead in order to divert the actual program logic flows as needed to achieve those objectives.

Cracking Serial Keys

Software is usually developed from financial points of views in this commercial world. That is, the vendor who developed the software won't allow it to be used for free. Apart from that they don't expose the source code to the client because the source code is their intellectual property. But they launch a beta version or a flexible to run version of the software for a special trial period prior to deployment of their product into the market. So, it is necessary to buy the license key of that specific software otherwise that software will stop working after completion of the specific evaluation duration.

But some skillful professionals devise a way to use the software without actually purchasing the software license key. They actually diagnose the entire software working life cycle and eventually find some vulnerability in the mechanism. They ultimately exploit such loopholes to bypass the serial key security obstacle. In this context, they can recover the actually serial key, or they can divert the serial key checking program flow or they can inject custom serial keys.

Suppose a renowned company developed an application that requires a 7 digit license key (hard-coded 1111111) in order to unlock the software and proceed. Fortunately some individuals somehow got this software to be executable (maybe the beta version) by any conduit. But they don't have the license key to unlock the software.

  1. using System;  
  2.   
  3. namespace CILComplexTest  
  4. {  
  5.      
  6.     static class LicenseKeyAuthentication  
  7.     {  
  8.         private static int Authentic_Key = 1111111;  
  9.   
  10.         public static bool VerifyKey(int key)  
  11.         {  
  12.             return key == Authentic_Key;  
  13.         }  
  14.     }  
  15.      
  16.     class Program  
  17.     {  
  18.         static void Main(string[] args)  
  19.         {  
  20.             Console.Write("Enter License key to unlock Software (7 digit):");  
  21.             var Keys = Int32.Parse(Console.ReadLine());  
  22.   
  23.             if (LicenseKeyAuthentication.VerifyKey(Keys))  
  24.             {  
  25.                 Console.WriteLine("Thank you!");  
  26.             }  
  27.             else  
  28.             {  
  29.                 Console.WriteLine("Invalid license key; Continue evaluation.");  
  30.             }  
  31.   
  32.             Console.ReadKey();  
  33.         }  
  34.     }  

After executing this software, it prompts to enter a seven-digit license key. Otherwise it could not let you proceed in the case of a futile hit and trial as in the following.



Okay, don't bother yourself; we can still get through this application without having the real license keys. First things first, decompile the shipped executable file using ILDASM that produces the following IL opcode as in the following:

  1. .module SerialCrack  
  2.   
  3. .class private abstract auto ansi sealed beforefieldinit LicenseKeyAuthentication extends [mscorlib]System.Object  
  4. {  
  5.   .field private static int32 Authentic_Key  
  6.   .method public hidebysig static bool  VerifyKey(int32 key) cil managed  
  7.   {  
  8.     .maxstack  2  
  9.     .locals init ([0] bool CS$1$0000)  
  10.     IL_0000:  nop  
  11.     IL_0001:  ldarg.0  
  12.     IL_0002:  ldsfld     int32 LicenseKeyAuthentication::Authentic_Key  
  13.     IL_0007:  ceq  
  14.     IL_0009:  stloc.0  
  15.     IL_000a:  br.s       IL_000c  
  16.   
  17.     IL_000c:  ldloc.0  
  18.     IL_000d:  ret  
  19.   }   
  20.   
  21.   .method private hidebysig specialname rtspecialname static void  .cctor() cil managed  
  22.   {  
  23.     // Code size       11 (0xb)  
  24.     .maxstack  8  
  25.     IL_0000:  ldc.i4     0x10f447  
  26.     IL_0005:  stsfld     int32 LicenseKeyAuthentication::Authentic_Key  
  27.     IL_000a:  ret  
  28.   }   
  29. }   
  30. .class private auto ansi beforefieldinit Program extends [mscorlib]System.Object  
  31. {  
  32.   .method private hidebysig static void  Main(string[] args) cil managed  
  33.   {  
  34.     .entrypoint  
  35.    
  36.     .maxstack  2  
  37.     .locals init ([0] int32 Keys,[1] bool CS$4$0000)  
  38.     IL_0000:  nop  
  39.     IL_0001:  ldstr      "Enter License key to unlock Software (7 digit):"  
  40.     IL_0006:  call       void [mscorlib]System.Console::Write(string)  
  41.     IL_000b:  nop  
  42.     IL_000c:  call       string [mscorlib]System.Console::ReadLine()  
  43.     IL_0011:  call       int32 [mscorlib]System.Int32::Parse(string)  
  44.     IL_0016:  stloc.0  
  45.     IL_0017:  ldloc.0  
  46.     IL_0018:  call       bool LicenseKeyAuthentication::VerifyKey(int32)  
  47.     IL_001d:  ldc.i4.0  
  48.     IL_001e:  ceq  
  49.     IL_0020:  stloc.1  
  50.     IL_0021:  ldloc.1  
  51.     IL_0022:  brtrue.s   IL_0033  
  52.   
  53.     IL_0024:  nop  
  54.     IL_0025:  ldstr      "Thank you!"  
  55.     IL_002a:  call       void [mscorlib]System.Console::WriteLine(string)  
  56.     IL_002f:  nop  
  57.     IL_0030:  nop  
  58.     IL_0031:  br.s       IL_0040  
  59.   
  60.     IL_0033:  nop  
  61.     IL_0034:  ldstr      "Invalid license key; Continue evaluation."  
  62.     IL_0039:  call       void [mscorlib]System.Console::WriteLine(string)  
  63.     IL_003e:  nop  
  64.     IL_003f:  nop  
  65.     IL_0040:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()  
  66.     IL_0045:  pop  
  67.     IL_0046:  ret  
  68.   }   

We can bypass or reveal such security constraints in multiple ways. In the Main() method declaration, if you deem over the following opcode instructions especially IL_0022 that implies that if the proper serial keys are not entered, then jump to the error message instruction. So here is the point, we change this jump code instruction to IL_0025 instead of IL_0033 then the code execution will always jump to the code block we intend it to, no matter what keys values we are entered.

  1. IL_0021: ldloc.1  
  2. IL_0022: brtrue.s IL_0033 // put IL_0025  
  3. IL_0024: nop  
  4. IL_0025: ldstr "Thank you!" 

The second trick resides again in the Main() method near to the key verify method. What this code is doing is checking the entered key values to the actual key value. If the values is correct then we can enter otherwise it throws us to the invalid message section using stloc.1. So if we change it to stloc.0 then our execution will always go to the block we intend it to, as in the following:

  1. IL_0018: call bool LicenseKeyAuthentication::VerifyKey(int32)  
  2. IL_001d: ldc.i4.0  
  3. IL_001e: ceq  
  4. IL_0020: stloc.1 //put stloc.0  
  5. IL_0021: ldloc.1  
  6. IL_0022: brtrue.s IL_0033 

Now, if you don't possess the right key values then you can still get into the software without having the exact key values as in the following:



The final trick is, if you examine the class constructor block, you can easily find the hard-coded key values. Here, in this instruction, we can guess that the key value is 0x10f447 so change it to decimal format and you have the exact key to validate. After finally employing one of these methods, you can bypass the serial key limitation despite not having the key as in the following:

  1. .method private hidebysig specialname rtspecialname static   
  2.           void  .cctor() cil managed  
  3.   {  
  4.     .maxstack  8  
  5.     IL_0000:  ldc.i4     0x10f447  
  6.     IL_0005:  stsfld     int32 LicenseKeyAuthentication::Authentic_Key  
  7.     IL_000a:  ret  
  8.   } 



Cracking Passwords

Cracking the password of software or bypassing the login screen is a sophisticated task. Sometimes the password is easily obtained or sometimes it can be very time consuming. This all depends on how exactly the password mechanism is manipulated in the system. The following Dummy Software requires a user name and password to proceed but we have no idea what the correct user credentials are. So how do we breach this security restriction?



By God's grace, we have at least the executable of this software. If we decompile this software executable into its corresponding *.il file and diagnose the corresponding method that is responsible for validating the user credentials then we might be a breach of this security restriction. Here the UserAuth() method IL code is as in the following:

  1. .method private hidebysig instance bool UserAuth(string usr,string pwd) cil managed  
  2.   {  
  3.     .maxstack  2  
  4.     .locals init ([0] string USR, [1] string PWD, [2] bool status, [3] bool CS$1$0000, [4] bool CS$4$0001)  
  5.     IL_0000:  nop  
  6.     IL_0001:  ldstr      "ajay"  
  7.     IL_0006:  stloc.0  
  8.     IL_0007:  ldstr      "1234"  
  9.     IL_000c:  stloc.1  
  10.     IL_000d:  ldc.i4.0  
  11.     IL_000e:  stloc.2  
  12.     IL_000f:  ldarg.1  
  13.     IL_0010:  ldloc.0  
  14.     IL_0011:  call       bool [mscorlib]System.String::op_Equality(stringstring)  
  15.     IL_0016:  brfalse.s  IL_0024  
  16.   
  17.     IL_0018:  ldarg.2  
  18.     IL_0019:  ldloc.1  
  19.     IL_001a:  call       bool [mscorlib]System.String::op_Equality(stringstring)  
  20.     IL_001f:  ldc.i4.0  
  21.     IL_0020:  ceq  
  22.     IL_0022:  br.s       IL_0025  
  23.   
  24.     IL_0024:  ldc.i4.1  
  25.     IL_0025:  stloc.s    CS$4$0001  
  26.     IL_0027:  ldloc.s    CS$4$0001  
  27.     IL_0029:  brtrue.s   IL_002f  
  28.   
  29.     IL_002b:  nop  
  30.     IL_002c:  ldc.i4.1  
  31.     IL_002d:  stloc.2  
  32.     IL_002e:  nop  
  33.     IL_002f:  ldloc.2  
  34.     IL_0030:  stloc.3  
  35.     IL_0031:  br.s       IL_0033  
  36.     IL_0033:  ldloc.3  
  37.     IL_0034:  ret  
  38.   } 

If we rigorously scrutinize that code then we can reach a conclusive result by obtaining some significant information. We can easily determine here that instructions IL_0001 and IL_0007 are storing the actual user name and password information as “ajay” and “1234”.

  1. IL_0000: nop  
  2. IL_0001: ldstr "ajay"  
  3. IL_0006: stloc.0  
  4. IL_0007: ldstr "1234"  
  5. IL_000c: stloc.1 

The second important thing is, we can conclude from these opcodes that IL_000d is responsible for setting a boolean value to true and false. As in the UserAuth() method, if the user enters a correct user name and password then this boolean value is set to true, otherwise it would be always false.

IL_000d: ldc.i4.0.

So here is the trick, if we change it to True right here, then it doesn't matter what the user inputs because the boolean value would always be true.

IL_000d: ldc.i4.1

In an another observation, we can imply some substantial information from the btnLog_Click() method. In fact, this method takes a user name and password from the user and validates them against the predefined parameters.

  1. .method private hidebysig instance void  btnLog_Click(object sender,class [mscorlib]System.EventArgs e) cil managed  
  2.   {  
  3.     .maxstack  3  
  4.     .locals init ([0] bool CS$4$0000)  
  5.     IL_0000:  nop  
  6.     IL_0001:  ldarg.0  
  7.     IL_0002:  ldarg.0  
  8.     IL_0003:  ldfld      class [System.Windows.Forms]System.Windows.Forms.TextBox DummySoftware.Login::txtUser  
  9.     IL_0008:  callvirt   instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Text()  
  10.     IL_000d:  ldarg.0  
  11.     IL_000e:  ldfld      class [System.Windows.Forms]System.Windows.Forms.TextBox DummySoftware.Login::Password  
  12.     IL_0013:  callvirt   instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Text()  
  13.   
  14. // ---------- Modification Required here--------------------  
  15.     IL_0018:  call       instance bool DummySoftware.Login::UserAuth(string,string)  
  16.     IL_001d:  ldc.i4.0  
  17.     IL_001e:  ceq  
  18.     IL_0020:  stloc.0  
  19.     IL_0021:  ldloc.0  
  20.     IL_0022:  brtrue.s   IL_0039         
  21. //----------------------------------------------------------------  
  22.     IL_0024:  nop  
  23.     IL_0025:  ldarg.0  
  24.     IL_0026:  ldfld      class [System.Windows.Forms]System.Windows.Forms.Label DummySoftware.Login::label3  
  25.     IL_002b:  ldstr      "Login Successful"  
  26.     IL_0030:  callvirt   instance void [System.Windows.Forms]System.Windows.Forms.Control::set_Text(string)  
  27.     IL_0035:  nop  
  28.     IL_0036:  nop  
  29.     IL_0037:  br.s       IL_004c  
  30.   
  31.     IL_0039:  nop  
  32.     IL_003a:  ldarg.0  
  33.     IL_003b:  ldfld      class [System.Windows.Forms]System.Windows.Forms.Label DummySoftware.Login::label3  
  34.     IL_0040:  ldstr      "Login Failed"  
  35.     IL_0045:  callvirt   instance void [System.Windows.Forms]System.Windows.Forms.Control::set_Text(string)  
  36.     IL_004a:  nop  
  37.     IL_004b:  nop  
  38.     IL_004c:  ret  
  39.   }  

Now look at these sorts of instructions, these are actually checking the input credentials against the predefined. If the user enters the correct information then okay otherwise it throws the execution to IL_0039 that shows some invalid message or something else.

  1. IL_0018: call instance bool DummySoftware.Login::UserAuth(string,string)  
  2. IL_001d: ldc.i4.0  
  3. IL_001e: ceq  
  4. IL_0020: stloc.0  
  5. IL_0021: ldloc.0  
  6. IL_0022: brtrue.s IL_0039 

So, here is a loophole, if we throw the execution to instruction IL0024 rather than IL_0039, then our program always runs perfectly, it doesn't matter what credentials we enter.

IL_0022: brtrue.s IL_0024

In other tactics, if we bypass the equal condition, where the credentials are validated then we can breach the software easily. These instructions are responsible for equating the condition as in the following:

  1. L_0018: call instance bool DummySoftware.Login::UserAuth(string,string)  
  2. // ---------- Modification Required here--------------------  
  3. IL_001d: ldc.i4.0  
  4. //------------------------------------------------------------------  
  5. IL_001e: ceq  
  6. IL_0020: stloc.0  
  7. IL_0021: ldloc.0  
  8. IL_0022: brtrue.s IL_0039 

So, if we change the IL_001d instruction to ldc.i4.1 then the equal never would be checked and we can breach the login screen easily as in the following:




Extending trial Duration

Sometimes, we do install a beta version of software just for testing purposes but they expire after completion of their evaluation period and we can no longer use them. As an analogy, the following software calculates some math functions but it is expired now. We can resume our operation after buy the license key.



But by applying round-trip Reverse Engineering we can extend its expiry date and make it usable without investing money on the license key. First, decompile its exe file in the IL file and rigorously study it for detecting vulnerability.

  1. .method public hidebysig specialname rtspecialname instance void  .ctor() cil managed  
  2.   {  
  3.     .maxstack  8  
  4.     IL_0000:  ldarg.0  
  5. // ---------- Modification Required here--------------------  
  6.     IL_0001:  ldc.i4     0x7dd  
  7.     IL_0006:  ldc.i4.7  
  8.     IL_0007:  ldc.i4.s   30  
  9. //-------------------------------------------------------------------  
  10.     IL_0009:  newobj     instance void [mscorlib]System.DateTime::.ctor(int32,int32,int32)  
  11.     IL_000e:  stfld      valuetype [mscorlib]System.DateTime TrailSoftware.Form1::expDate  
  12.     IL_0013:  ldarg.0  
  13.     IL_0014:  ldnull  
  14.     IL_0015:  stfld      class [System]System.ComponentModel.IContainer TrailSoftware.Form1::components  
  15.     IL_001a:  ldarg.0  
  16.     IL_001b:  call       instance void [System.Windows.Forms]System.Windows.Forms.Form::.ctor()  
  17.     IL_0020:  nop  
  18.     IL_0021:  nop  
  19.     IL_0022:  ldarg.0  
  20.     IL_0023:  call       instance void TrailSoftware.Form1::InitializeComponent()  
  21.     IL_0028:  nop  
  22.     IL_0029:  nop  
  23.     IL_002a:  ret  
  24.   } 

After doing some R&D, we found some instructions that show the expiry date as 30/7/2013 of this software as in the following:

IL_0001: ldc.i4 0x7dd
IL_0006: ldc.i4.7
IL_0007: ldc.i4.s 30

So if we modify the instruction IL_0007 to some other value and recompile it using ILASM then we can still use this software.

In another code review, that checks the current date to an expiry date whether or not it is less as in the following.

  1. .method private hidebysig instance void Form1_Load(object sender, class [mscorlib]System.EventArgs e) cil managed  
  2.   {  
  3.      
  4.     .maxstack  2  
  5.     .locals init ([0] bool CS$4$0000)  
  6.     IL_0000:  nop  
  7.     IL_0001:  ldarg.0  
  8.     IL_0002:  ldfld      valuetype [mscorlib]System.DateTime TrailSoftware.Form1::expDate  
  9.     IL_0007:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()  
  10.   
  11.     IL_000c:  call       bool [mscorlib]System.DateTime::op_GreaterThan(valuetype [mscorlib]System.DateTime, valuetype [mscorlib]System.DateTime)  
  12.   
  13. // ---------- Modification Required here--------------------  
  14.     IL_0011:  ldc.i4.0  
  15.     IL_0012:  ceq  
  16.     IL_0014:  stloc.0  
  17.     IL_0015:  ldloc.0  
  18. //--------------------Until Here---------------------------------------  
  19.     IL_0016:  brtrue.s   IL_0052  
  20.   
  21. ------  

If we delete some code instruction that shows the expiry date message then we can bypass this restriction as in the following:

  1. IL_0012: ceq  
  2. IL_0014: stloc.0  
  3. IL_0015: ldloc.0 

Wipe out these aforementioned instructions from the code file, save it and recompile it again and finally run this software. Here the modified code is as in the following:

  1. IL_0011: ldc.i4.0  
  2. IL_0016: brtrue.s IL_0052 

Finally this software works fine as in the following:



Summary

In this article, we have seen how to obtain sensitive information to crack a user name, password, serials keys and extend a trial duration, or subvert the existing security mechanism without having access to real source code. We have to come to an understanding of how to manipulate IL code in respect to achieving our objective. In the forthcoming article of this series, we will address advanced Reverse Engineering subjects such as byte patching using a hex editor, CFF explorer and IDA pro.

Up Next
    Ebook Download
    View all
    Learn
    View all