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 the 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 a financial point of view 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 a 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.
- 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 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:
- .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
- {
-
- .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 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.
- IL_0021: ldloc.1
- IL_0022: brtrue.s IL_0033
- 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 is doing is checking the entered key values to the actual key value. If the values are 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:
- 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
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:
- .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 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:
- .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 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”.
- IL_0000: nop
- IL_0001: ldstr "ajay"
- IL_0006: stloc.0
- IL_0007: ldstr "1234"
- 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 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.
- .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()
-
-
- 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 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.
- 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 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:
- L_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, 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 the 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 buying 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.
- .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
- {
- .maxstack 8
- IL_0000: ldarg.0
-
- 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 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.
- .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)
-
-
- IL_0011: ldc.i4.0
- IL_0012: ceq
- IL_0014: stloc.0
- IL_0015: ldloc.0
-
- IL_0016: brtrue.s IL_0052
-
- ------
- }
If we delete some code instruction that shows the expiry date message then we can bypass this restriction as in the following:
- IL_0012: ceq
- IL_0014: stloc.0
- IL_0015: ldloc.0
Wipeout 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:
- IL_0011: ldc.i4.0
- 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 with 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.