Demystifying dot NET Reverse Engineering: Advanced Round-trip Engineering
Before going through this article, I highly advice you to read all previous ones in the series since I will not re-explain some techniques and re-describe some tools previously presented. Here, I will assume that you understood the basics and everything that was presented prior:
- Article 1: Demystifying Dot NET Reverse Engineering- Big Introduction
- Article 2: Demystifying dot NET Reverse Engineering – Introducing Byte Patching
- Article 3: Demystifying dot NET Reverse Engineering- Advanced Byte Patching
- Article 4: Demystifying dot NET Reverse Engineering- Introducing Round-trip Engineering
This article is only a part of a whole, and it aims to go deeper into IL assembly language exploited in reversing non-obfuscated (until now) dot NET assemblies and modules. (Managed dot NET applications are called assemblies and managed dot NET executables are called modules; a managed dot NET application can be a single module assembly or a multi-module assembly)
As previously presented, disassembling and reassembling a managed executable is called round tripping. Round tripping in our case concerns managed portable executable files and is divided into two main steps:
- Disassembling the PE file into an .il file (which contains ILAsm source code) and into managed and unmanaged resource files, done using ILDASM.
- Reassembling files gotten in step 1 into a new PE file using ILASM compiler.
Settling for simply disassembling and reassembling is not that interesting a task unless you modifyoralter the ILAsm source code before reassembling it. Round tripping (as most of reverse engineering techniques) is not “breaking software protections” specifically, but we will focus only on this in our upcoming studies as we saw in the article “Demystifying dot NET Reverse Engineering- Introducing Round-trip Engineering.”
The last thing we did was to remove the Crack Me’s nag screen but we still faced serial number protection which we will analyze in upcoming paragraphs.
Serial check analysis
Going back to ILDASM, let’s reconsider the tree structure we’ve got:
Each node represents a namespace we can explore that has class objects which, once expanded, deliver their methods and properties. Here we have an interesting class name “GenSerial” which contains a very interesting method:
With this IL assembly content:
.method public instance object CalculSerial() cil managed
// Code size 43 (0x2b)
.locals init (object V_0)
IL_0001: call class CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyComputer CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyProject::get_Computer()
IL_0006: callvirt instance class [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ComputerInfo [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ServerComputer::get_Info()
IL_000b: callvirt instance string [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ComputerInfo::get_OSVersion()
IL_0010: ldstr “.”
IL_0015: ldstr “”
IL_001a: callvirt instance string [mscorlib]System.String::Replace(string,string)
IL_001f: stfld stringCrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::SerialNumber
IL_0025: ldfld stringCrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::SerialNumber
} // end of method GenSerial::CalculSerial
Things get a bit more complicated here and would need clarification:
.method public instance object CalculSerial() cil managed: As seen in article 4, this is the MethodDef metadata item. The only difference is that this method has a return type which is object.
.maxstack directive is described in article 4 (please refer to background section).
.locals init (object V_0):As in any development language, variables are very important, and ILAsm has its way of declaration; .locals init (object V_0) defines a local variable called objectV_0.The init keyword indicates to the Just-in-Time compiler that it must initialize all local variables to zero on entry to the method.
ldarg.0 is the instruction that loads argument 0 on the stack, but instance methods like the one we have has the “this” (used in high level languages) instance references as the first argument of the method signature, so ldarg.0 here loads instance pointer on the stack.
call class CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyComputer
CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyProject::get_Computer(), MyComputer here is inheriting from a base class defined in Microsoft.VisualBasic namespace. Call here invokes the static method get_Computer()which resides in the class MyProject under the namespace “My” and will return an instance of CrackMe3_InfoSecInstitute_dotNET_Reversing.My.MyComputer. This property “get_Computer” (like in most high level languages getters and setters) will access an instance of Microsoft.VisualBasic.Devices.Computer object (including details will multiply the size of this article!).
callvirt instance class [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ComputerInfo [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ServerComputer::get_Info() calls the virtual method/property (getter) get_Info() and the use of the keyword instance means that the we are dealing with instance methods instead of static ones. Referring to Microsoft MSDN ComputerInfo class provides properties for getting information about the computer’s memory, loaded assemblies, name, and operating system. ServerComputer class provides properties for manipulating computer components with an inheritance hierarchy like this:
callvirt instance string [Microsoft.VisualBasic]Microsoft.VisualBasic.Devices.ComputerInfo::get_OSVersion()calls the virtual method get_OSVersion() from the dot NET Frameworks class library ComputerInfo. As a result of this call, a string is put onto the evaluation stack and from the method name we can guess that our program accesses the operating system version.
The instruction ldstr “.” and ldstr “”creates, respectively,a string object from the string constant “.” (dot) then loads a reference to it onto the stack thendoes the same thing to the string constant “” (empty string).
Call instance string [mscorlib]System.String::Replace(string, string):This instruction calls the Replace(string,string) function from the dot NET Frameworks class library, parameters / method arguments are taken from the stack (“.” And “”) and the result is pushed back onto the stack. This means it takes the computer version, replaces every dot “.” by nothing then puts back the result onto the stack.
stfld string CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::SerialNumber:This instruction sets the result of the function Replace(string, string) on the field SerialNumber which is previously declared as private and is equivalent to a private variable in high level languages.
ldarg.0loads the reference of the object itself (equivalent to this in high level languages) then using ldfld string CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::SerialNumber takes the instance from the stack and loads the value of the instance field SerialNumber on it.
Ret is, as you probably guessed, the instruction that returns from a called method to a call site and since our method returns an object, one value of type object is on the evaluation stack of the calling method.
At this point we should know that the field SerialNumber contains the version of our operating system without dots. We can then easily retrieve the full OS version which in my case is 6.1.7601.65536, and by applying the Replace() function I get 61760165536,which was a successful serial number:
This was fun, so let’s now move to serious things because until now we did not alter the code and we did not use the ILASM assembler! We want the Crack Me to tell us the right serial number instead of showing us a “Wrong serial number” message box.
Let’s have a look again into ILDASM then expand the second node:
By verifying the methods that Form1 class contains, we can easily find the method called once we click on the button “Register”:
The actual method’s IL code should be (hopefully) clearer soI won’t explain the code again line by line:
.method private instance void reg_Btn_Click(object sender,class [mscorlib]System.EventArgs e) cil managed
// Code size 69 (0x45)
IL_0001: callvirt instance class [System.Windows.Forms]System.Windows.Forms.TextBox CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::get_txt_Serial()
IL_0006: callvirt instance string [System.Windows.Forms]System.Windows.Forms.TextBox::get_Text()
IL_000c: ldfld class CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::cGenSerial
IL_0011: callvirt instance object CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::CalculSerial()
IL_0017: call bool [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::ConditionalCompareObjectEqual(object, object, bool)
IL_001c: brfalse.s IL_0032
IL_001e: ldstr “Serial is correct!”
IL_0023: ldc.i4.s 64
IL_0025: ldstr “Congratulation”
IL_002a: call valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object, valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle, object)
IL_0030: br.s IL_0044
IL_0032: ldstr “Wrong serial number.”
IL_0037: ldc.i4.s 16
IL_0039: ldstr “Error”
IL_003e: call valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object, valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle, object)
} // end of method Form1::reg_Btn_Click
If you followed all articles aboutDemystifying dot NET Reverse Engineering, you should be able to understand what this method does exactly.And by the way, please remark System.EventArgswhich means that we are under an event which is “click.” Clicking on the button “Register” flags the method reg_Btn_Click(…).
We can split this code into blocks:
- First, the Crack Me gets the text we typed as a serial number using method get_text().
- Then, it calls an instance of the class which calculates the correct serial number using CalculSerial().
- At this point, we have three items on the top of the evaluation stack as expected using the the directive .maxstack 3, which are two objects: the serial number typed by the user retrieved using get_text() method,the correct serial number calculated via the function CalculSerial(), and a four byte zero loaded by the instruction ldc.i4.0 to let the Crack Me correctly use the API ConditionalCompareObjectEqual(…), which represents the overloaded Visual Basic equals (=) operator with a Boolean return type. The loading of zero as TextCompare parameter means that the comparison is not case sensitive, but this is not thatimportant in this case since our serial number is numeric.
- If comparison returns false, this means that objects loaded as parameters are not equal and that the serial number given by the user is not correct, so brfalse.sbranches/jumps to IL_0032whichloads a reference to the string “Wrong serial number” and prepares the message box to show after it clears the evaluation stack using pop instruction; otherwise, it prepares the “Serial is correct!” message box and clears the evaluation stack using pop instruction before unconditionally transferring the program to ret instruction (the end of the method) using br.s IL_0044 instruction.
We know what this method does exactly but we have to use some “creative” round tripping to let the Crack Me change its behavior so that it shows the correct serial number instead of showing the wrong serial message box. Basically, we need to change the seportions of the code:
But into what? Obviously, we have to call the function that calculates and returns the correct serial number, remove the “Wrong serial number” message, and show the correct serial number.
Let’s imagine how the evaluation stack should look like after the changes:
“The serial number is:”
Message box style
Message box title
First thing we should notice is that we are going to load 4 items onto the stack, but the directive .maxstack was previously established to prepare the just in time compiler to reserve only 3 slots for this method. So the first change we have to do is modifythe .maxstack value:
- .maxstack 4
The second thing we should do is remove the line IL_0032: ldstr “Wrong serial number” and prepare the string we want to load. The use of labels is optional but we need to use at least one label to mark the offset at which we started modifications:
- IL_Patch: ldstr “The serial number is: “
Now we have to call an instance of the class that has the function which calculates the correct serial number. We will simply take the same instructions used by the Crack Me to perform comparison:
- IL_Patch0: ldarg.0
- IL_Patch1: ldfld class CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::cGenSerial
- IL_Patch2: callvirt instance object CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::CalculSerial()
Technically our serial is “ready” but we cannot show an “object” in a text box. We have to get the string value from the serial number object; in dot NET Frameworks, the method which can return a boxed copy of an object is RuntimeHelpers.GetObjectValueand we have to give its full signature:
- IL_Patch3: call object [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::GetObjectValue(object)
Now the evaluation stack contains two values: “The serial number is:” string and the serial number string, so we need to concatenate them to produce one value/item. By browsing Microsoft MSDN we find String.Concat (object,…) method, so by giving its full signature we get:
- IL_Patch4: call string [mscorlib]System.String::Concat(object, object)
We are almost done, but we still have to do one more change regarding the branch brfalse.s.
Instead of jumping to the offset at label IL_0032, the Crack Me should go to our patch:
- IL_001c: brfalse.s IL_Patch
Now we are done! After these changes the modified code must look like this:
Save your .il file and let’s reassemble it using ILASM assembler to test what we did (if you have trouble with this step, please refer to this):
C:WindowsMicrosoft.NETFrameworkv4.0.30319>ilasm C:UsersSoufianeDesktopround-tcrackme3.il -res=C:UsersSoufianeDesktopround-tcrackme3.res
The compilation process was successful:
Resolving local member refs: 0 -> 0 defs, 0 refs, 0 unresolved
Writing PE file
Operation completed successfully
And the result was successful as well:
We can improve this by customizing this message box to display a better result, such as changing the title and the message box style:
We can go deeper in our Crack Me tweaking and make changes to the way it writes the correct serial in the text box area instead of showing it in a message box:
callvirt instance class [System.Windows.Forms]System.Windows.Forms.TextBox CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::get_txt_Serial()
ldfld class CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial CrackMe3_InfoSecInstitute_dotNET_Reversing.Form1::cGenSerial
callvirt instance object CrackMe3_InfoSecInstitute_dotNET_Reversing.GenSerial::CalculSerial()
call string [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Conversions::ToString(object)
callvirt instance void [System.Windows.Forms]System.Windows.Forms.TextBox::set_Text(string)
The result is quite nice:
By clicking once, Crack Me will write the serial number, but clicking twice will show you a “Serial is correct” message box.