Introduction

We have done lots of IL grammar so far. As I warned you earlier, Reverse engineering could be exercising both offensive and defensive motives. Now, it is 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 limitation without actually having the access of real source code. We are not going to perform sort of binary or byte code patching in the context reversing the code as we have typically, employed hex editor, IDA Pro or ollydb software tools in respective of playing with real bytes but in this article, we would be confronted only with IL opcodes instead in order to divert the actual program logic flows as per out requirement to achieve the aforesaid objective.

Cracking Serial Keys

Software are usually developed by means of financial points of view in this commercial world. That is, the vendor who developed the software won’t be giving out the software free of cost. Aside from that, they won’t expose the source code to the client because that is their intellectual property. However, they can launch a beta version or be flexible when running the software until a special trial duration regarding the deployment of their product to the market. So, it is obligatory to buy the license key of that particular software, otherwise it will stop working after the completion of its specific evaluation duration.

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

Suppose a renowned company developed an application which requires a 7 digit license key (hard-coded 1111111) in order to unlock the software and proceed. Fortunately, some individuals somehow got this software executable (maybe the beta version) through some means. However they don’t have the license key to unlock it.

using System;

namespace CILComplexTest
{

    static class LicenseKeyAuthentication
    {
        private static int Authentic_Key = 1111111;

        public static bool VerifyKey(int key)
        {
            return key == Authentic_Key;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("Enter License key to unlock Software (7 digit):");
            var Keys = Int32.Parse(Console.ReadLine());

            if (LicenseKeyAuthentication.VerifyKey(Keys))
            {
                Console.WriteLine("Thank you!");
            }
            else
            {
                Console.WriteLine("Invalid license key; Continue evaluation.");
            }

            Console.ReadKey();
        }
    }
}

After executing this software. It prompts the user to enter a seven digit license key. Otherwise it will not let you go further in case of futile hit and trial as follows.


Ok, don’t bother yourself; we can still get through this application without having the real license keys. First, decompile the shipped executable file using ILDASM, which will produce the following IL opcode:

.module SerialCrack

.class private abstract auto ansi sealed beforefieldinit LicenseKeyAuthentication extends [mscorlib]System.Object
{
  .field private static int32 Authentic_Key
  .method public hidebysig static bool  VerifyKey(int32 key) cil managed
  {
    .maxstack  2
    .locals init ([0] bool CS$1$0000)
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  ldsfld     int32 LicenseKeyAuthentication::Authentic_Key
    IL_0007:  ceq
    IL_0009:  stloc.0
    IL_000a:  br.s       IL_000c

    IL_000c:  ldloc.0
    IL_000d:  ret
  }

  .method private hidebysig specialname rtspecialname static void  .cctor() cil managed
  {
    // Code size       11 (0xb)
    .maxstack  8
    IL_0000:  ldc.i4     0x10f447
    IL_0005:  stsfld     int32 LicenseKeyAuthentication::Authentic_Key
    IL_000a:  ret
  }
}
.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] int32 Keys,[1] bool CS$4$0000)
    IL_0000:  nop
    IL_0001:  ldstr      "Enter License key to unlock Software (7 digit):"
    IL_0006:  call       void [mscorlib]System.Console::Write(string)
    IL_000b:  nop
    IL_000c:  call       string [mscorlib]System.Console::ReadLine()
    IL_0011:  call       int32 [mscorlib]System.Int32::Parse(string)
    IL_0016:  stloc.0
    IL_0017:  ldloc.0
    IL_0018:  call       bool LicenseKeyAuthentication::VerifyKey(int32)
    IL_001d:  ldc.i4.0
    IL_001e:  ceq
    IL_0020:  stloc.1
    IL_0021:  ldloc.1
    IL_0022:  brtrue.s   IL_0033

    IL_0024:  nop
    IL_0025:  ldstr      "Thank you!"
    IL_002a:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_002f:  nop
    IL_0030:  nop
    IL_0031:  br.s       IL_0040

    IL_0033:  nop
    IL_0034:  ldstr      "Invalid license key; Continue evaluation."
    IL_0039:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_003e:  nop
    IL_003f:  nop
    IL_0040:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
    IL_0045:  pop
    IL_0046:  ret
  }
}

We can bypass or reveal such security constraints multiple ways. In the Main() method declaration, if you look over the following opcode instructions, you’ll notice IL_0022, which implies that if the proper serial key is not entered, jump to the error message instruction. So, we change this jump code instruction to IL_0025 instead of IL_0033 so that the code execution will always jump to the correct code block, no matter what key values we enter.

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

The second trick resides again in the Main() method near to the key verify method. What this code does is it compares the entered key values to the actual key value. If the values match then we can continue; otherwise it throws us to the invalid message section using stloc.1. So if you change it to stloc.0, then our execution will always go in the right block:

IL_0018:  call       bool LicenseKeyAuthentication::VerifyKey(int32)
IL_001d:  ldc.i4.0
IL_001e:  ceq
IL_0020:  stloc.1     //put  stloc.0
IL_0021:  ldloc.1
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:


For the final trick, 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 as 0x10f447, so just change it to decimal format and you’ll get the exact key to validate. After finally employing one of these above methods, you can bypass the serial key limitation despite not having the key:

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


Cracking Passwords

Cracking passwords of software or bypassing login screens is one of the most sophisticated tasks. Sometimes a password is easily obtained, but sometimes it could 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 about correct user credentials. So how do we breach this security restriction?


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

  .method private hidebysig instance bool UserAuth(string usr,string pwd) cil managed
  {
    .maxstack  2
    .locals init ([0] string USR, [1] string PWD, [2] bool status, [3] bool CS$1$0000, [4] bool CS$4$0001)
    IL_0000:  nop
    IL_0001:  ldstr      "ajay"
    IL_0006:  stloc.0
    IL_0007:  ldstr      "1234"
    IL_000c:  stloc.1
    IL_000d:  ldc.i4.0
    IL_000e:  stloc.2
    IL_000f:  ldarg.1
    IL_0010:  ldloc.0
    IL_0011:  call       bool [mscorlib]System.String::op_Equality(string, string)
    IL_0016:  brfalse.s  IL_0024

    IL_0018:  ldarg.2
    IL_0019:  ldloc.1
    IL_001a:  call       bool [mscorlib]System.String::op_Equality(string, string)
    IL_001f:  ldc.i4.0
    IL_0020:  ceq
    IL_0022:  br.s       IL_0025

    IL_0024:  ldc.i4.1
    IL_0025:  stloc.s    CS$4$0001
    IL_0027:  ldloc.s    CS$4$0001
    IL_0029:  brtrue.s   IL_002f

    IL_002b:  nop
    IL_002c:  ldc.i4.1
    IL_002d:  stloc.2
    IL_002e:  nop
    IL_002f:  ldloc.2
    IL_0030:  stloc.3
    IL_0031:  br.s       IL_0033
    IL_0033:  ldloc.3
    IL_0034:  ret
  }

If we rigorously scrutinize that code, we can reach some conclusive result by obtaining some significant information. We can easily judge here that instructions IL_0001 and IL_0007 store actual user name and password information as “ajay” and “1234″:

    IL_0000:  nop
    IL_0001:  ldstr      "ajay"
    IL_0006:  stloc.0
    IL_0007:  ldstr      "1234"
    IL_000c:  stloc.1

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

   IL_000d:  ldc.i4.0

So here is the trick: if we change it to True right here, then it won’t matter what the user would input since the Boolean value would always be true.

    IL_000d:  ldc.i4.1

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

.method private hidebysig instance void  btnLog_Click(object sender,class [mscorlib]System.EventArgs e) cil managed
  {
    .maxstack  3
    .locals init ([0] bool CS$4$0000)
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  ldarg.0
    IL_0003:  ldfld      class [System.Windows.Forms]System.Windows.Forms.TextBox DummySoftware.Login::txtUser
    IL_0008:  callvirt   instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Text()
    IL_000d:  ldarg.0
    IL_000e:  ldfld      class [System.Windows.Forms]System.Windows.Forms.TextBox DummySoftware.Login::Password
    IL_0013:  callvirt   instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Text()
// ---------- Modification Required here--------------------
    IL_0018:  call       instance bool DummySoftware.Login::UserAuth(string,string)
    IL_001d:  ldc.i4.0
    IL_001e:  ceq
    IL_0020:  stloc.0
    IL_0021:  ldloc.0
    IL_0022:  brtrue.s   IL_0039
//----------------------------------------------------------------
    IL_0024:  nop
    IL_0025:  ldarg.0
    IL_0026:  ldfld      class [System.Windows.Forms]System.Windows.Forms.Label DummySoftware.Login::label3
    IL_002b:  ldstr      "Login Successful"
    IL_0030:  callvirt   instance void [System.Windows.Forms]System.Windows.Forms.Control::set_Text(string)
    IL_0035:  nop
    IL_0036:  nop
    IL_0037:  br.s       IL_004c

    IL_0039:  nop
    IL_003a:  ldarg.0
    IL_003b:  ldfld      class [System.Windows.Forms]System.Windows.Forms.Label DummySoftware.Login::label3
    IL_0040:  ldstr      "Login Failed"
    IL_0045:  callvirt   instance void [System.Windows.Forms]System.Windows.Forms.Control::set_Text(string)
    IL_004a:  nop
    IL_004b:  nop
    IL_004c:  ret
  }

Now look at these instructions. These are actually checking the input credentials against the predefined. If the user enters correct information then OK; otherwise it throws the execution to IL_0039 which shows some invalid message.

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

So here is the loophole: if we throw the execution to instruction IL0024 rather than IL_0039, then our program always runs perfectly, and it doesn’t matter what credentials we are entering.

    IL_0022:  brtrue.s   IL_0024

In another tactic, 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:

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

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


Extending Trial Duration

Sometimes we install some beta version software just for testing point of view but they expire after the 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 until we don’t 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 a license key. First, decompile its exe file in the IL file and rigorously study it to detect vulnerability.

.method public hidebysig specialname rtspecialname instance void  .ctor() cil managed
  {
    .maxstack  8
    IL_0000:  ldarg.0
// ---------- Modification Required here--------------------
    IL_0001:  ldc.i4     0x7dd
    IL_0006:  ldc.i4.7
    IL_0007:  ldc.i4.s   30
//-------------------------------------------------------------------
    IL_0009:  newobj     instance void [mscorlib]System.DateTime::.ctor(int32,int32,int32)
    IL_000e:  stfld      valuetype [mscorlib]System.DateTime TrailSoftware.Form1::expDate
    IL_0013:  ldarg.0
    IL_0014:  ldnull
    IL_0015:  stfld      class [System]System.ComponentModel.IContainer TrailSoftware.Form1::components
    IL_001a:  ldarg.0
    IL_001b:  call       instance void [System.Windows.Forms]System.Windows.Forms.Form::.ctor()
    IL_0020:  nop
    IL_0021:  nop
    IL_0022:  ldarg.0
    IL_0023:  call       instance void TrailSoftware.Form1::InitializeComponent()
    IL_0028:  nop
    IL_0029:  nop
    IL_002a:  ret
  }

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

    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, which checks the current date to expiry date whether it’s less or not:

  .method private hidebysig instance void Form1_Load(object sender, class [mscorlib]System.EventArgs e) cil managed
  {

    .maxstack  2
    .locals init ([0] bool CS$4$0000)
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  ldfld      valuetype [mscorlib]System.DateTime TrailSoftware.Form1::expDate
    IL_0007:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
    IL_000c:  call       bool [mscorlib]System.DateTime::op_GreaterThan(valuetype [mscorlib]System.DateTime,
                                                                        valuetype [mscorlib]System.DateTime)
// ---------- Modification Required here--------------------
    IL_0011:  ldc.i4.0
    IL_0012:  ceq
    IL_0014:  stloc.0
    IL_0015:  ldloc.0
//--------------------Till Here---------------------------------------
    IL_0016:  brtrue.s   IL_0052

------
}

If we delete some code instructions which show the expiry date message then we can bypass this restriction:

    IL_0012:  ceq
    IL_0014:  stloc.0
    IL_0015:  ldloc.0

Wipe out these aforementioned instructions from the code file, save and recompile it, and finally run it. Here’s the modified code:

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

The software now works indefinitely.

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.


Summary

In this article, we saw how to obtain sensitive information in order to crack user names, passwords, and serials keys, and to extend trial duration without using IDA Pro or OllyDbg software. We have come to an understanding how to manipulate IL codes with respect to achieving our objective. In the forthcoming article of this series, we will address advance reverse engineering subjects such as byte patching using Hex editor, CFF explorer and IDA Pro.