Reverse engineering

Extreme .NET Reverse Engineering - 5

Ajay Yadav
August 6, 2013 by
Ajay Yadav

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.

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.

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.

[c]
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();

}

}

}

[/c]

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:

[plain]
.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

}

}

[/plain]

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.

[plain]

IL_0021: ldloc.1

IL_0022: brtrue.s IL_0033 // put IL_0025

IL_0024: nop

IL_0025: ldstr "Thank you!"

[/plain]

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:

[plain]

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

[/plain]

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:

[plain]

.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

}

[/plain]


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:

[plain]

.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

}

[/plain]

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

[plain]

IL_0000: nop

IL_0001: ldstr "ajay"

IL_0006: stloc.0

IL_0007: ldstr "1234"

IL_000c: stloc.1

[/plain]

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.

[plain]

IL_000d: ldc.i4.0

[/plain]

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.

[plain]

IL_000d: ldc.i4.1

[/plain]

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.

[plain]

.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

}

[/plain]

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.

[plain]

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

[/plain]

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.

[plain]

IL_0022: brtrue.s IL_0024

[/plain]

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:

[plain]

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

[/plain]

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.

[plain]

.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

}

[/plain]

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

[plain]

IL_0001: ldc.i4 0x7dd

IL_0006: ldc.i4.7

IL_0007: ldc.i4.s 30

[/plain]

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:

[plain]

.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

------

}

[/plain]

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

[plain]

IL_0012: ceq

IL_0014: stloc.0

IL_0015: ldloc.0

[/plain]

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

[plain]

IL_0011: ldc.i4.0

IL_0016: brtrue.s IL_0052

[/plain]

The software now works indefinitely.


Summary

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.

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.

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