In the last blog post of the "Protecting your precious code" series we allowed our obfuscator to be much smarter in respect to obfuscating public and internal members as well as private members. One thing that was noticeable however was that the strings inside the obfuscated assembly still gave a telling story, as seen in the image below. Today we take a look at breaking into some obfuscated code, and why basic obfuscation is not enough to provide any sort of real obfuscation protection.
Our code after obfuscation still displays a telling story!
The "hackable" program
To start off this post we'll take a look at the program that we are going to hack. It is very basic, however this is irrelevant - it is the process which is important.
class Program
{
static void Main()
{
//First perform a check of the licensing key
try
{
CheckRegistrationDetails();
}
catch (InvalidRegistrationKeyException ex)
{
Console.WriteLine(ex.Message);
Environment.Exit(1);
}
//Display the welcome message
Console.WriteLine("Welcome to our program!");
}
///
/// Checks the registration details.
///
private static void CheckRegistrationDetails()
{
string value = ConfigurationManager.AppSettings["RegKey"];
DateTime expiryDate;
if (!DateTime.TryParse(value, out expiryDate))
throw new InvalidRegistrationKeyException(Resources.InvalidRegistrationKey);
if (DateTime.Now > expiryDate)
throw new InvalidRegistrationKeyException(Resources.InvalidRegistrationKey);
//We're ok
}
}
As you can see, the code doesn't do much. It simply checks the "Registration Key" to see if it is valid - if it is not valid it throws an exception, otherwise it carries on and executes the program. The registration key in this particular example is simply a date that the program expires. The actual encryption of the registration key is irrelevant in this case as this article isn't about writing key generation software - it's about hacking an obfuscated assembly...
The obfuscated version of this program looks effective... or is it?
The hackable program after obfuscation
While the code is fairly simple, the obfuscation still hides all that it needs to in this case. In fact, it even goes so far as to fool Reflector somewhat!
Reflector getting confused
Running the program simply gives the message "Invalid Registration Key" if it fails registration checking, and "Welcome to our program" if successful.
So given all of this, what is our plan of attack?
Finding our registration checking code...
Well the first step to getting inside this ever so complex program is to find the code inside the assembly which does the actual registration checking. This is a simple program therefore we could skip this step - however we will still do this for completeness. The easiest way is to figure out what text is displayed upon a failed check. If you run this program you will see that the text "Invalid Registration Key" is displayed if the registration key is invalid. NB This isn't shown in the source code listed above as I have instead included it as a resource to the assembly.
We'll solve this simple problem with reflector this time around, however I generally prefer using the decompiled IL code directly as then file based search tools can be used. The step by step process to find the code in Reflector is to do the following:
- Click on the HackableProgram assembly in the assembly tree
- Click on the "View" menu, and select "Search"
- A search box will appear, press "Ctrl+S" to perform a string search (or alternatively click the corresponding box).
- Type in "Invalid Registration Key" and wait for Reflector to search your code. A single line will appear. Single clicking this will display the line of code associated with this string constant.
Reflector Search
- Double click the found item; this will hide the search box, but more importantly select the method in the Assembly tree. In this particular case, this has found the resource manager wrapper used - we need to dive deeper...
- Right click the method in the assembly tree view and click "Analyze"
- Expand the "Used By" box, select the displayed method and press the space bar; we are now taken to our original "CheckRegistrationKey" method - we've found the code we need to look at!
Reflector Analyzer
Well now we've found the registration checking method; let's figure out a way in...
Way's in...
So we've found the code which occurs upon a failed registration key; how can we break the checking scheme so that it accepts valid outputs? Well we could:
- Change the Environment.Exit call to another less intrusive command
- Reverse the logic for parsing the registration key information
- Reverse the logic for checking the expiry date
- Or last but not least, avoid calling the registration key checking code altogether!
Of all of the methods listed below, I will simply reverse the logic for parsing the registration key information - these are nice and easy ones to hack as you'll soon see. Now to put theory into practice and hack our program!
Hacking it
The process we are going to take to hack this program is to:
- Break it apart using ILDASM
- Change the necessary source code
- Put it all back together using ILASM
The first step is to pull it apart; I won't go into details for using ILDASM, however if you wish to find out more information about this tool then please check out the information on MSDN. Once we have pulled it apart we should have three files for this assembly: an IL file, a RES file and a resources file. We'll only need to look at and change the IL file - nice and easy!
Once we've opened up the IL program in a text editor, we need to then find the "CheckRegistrationDetails" method so that we can hack it. The problem is - how do we find it when all our method names are in unprintable characters? Well in this case, we simply find something that is searchable and search from there. For example, in reflector "ConfigurationManager.AppSettings["RegKey"] is unobfuscated - therefore searchable. Now, in IL this method will be in it's expanded form - therefore if we do a search for "ConfigurationManager::get_AppSettings" we should find all of the instances of ConfigurationManager.AppSettings[] being used. In this program it is relatively straight forward as there is only one usage; if there were multiple usages then we'd need to search the code around it in the IL to make sure that we're in the right place (e.g. the key loaded into the AppSettings indexer).
So we've found our place in the code - the two portions of IL code that are of interest too us are, firstly the DateTime.TryParse if statement:
IL_0014: call bool [mscorlib]System.DateTime::TryParse(string,
valuetype [mscorlib]System.DateTime&)
IL_0019: stloc.2
IL_001a: ldloc.2
IL_001b: brtrue.s IL_0028
... and secondly the expiry date check:
IL_0028: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
IL_002d: ldloc.1
IL_002e: call bool [mscorlib]System.DateTime::op_GreaterThan(valuetype [mscorlib]System.DateTime,
valuetype [mscorlib]System.DateTime)
IL_0033: ldc.i4.0
IL_0034: ceq
IL_0036: stloc.2
IL_0037: ldloc.2
IL_0038: brtrue.s IL_0045
Above I mentioned that we would simply reverse the if statements before throwing the exception. The way that this is done in IL is to simply reverse the logic of the "break" statements. The way that the runtime works in the background is that it first loads the result of the if statement into a register, and then breaks to a different point of code depending on the result of this. Psuedo code of the two examples listed above are:
var value = DateTime.TryParse(value); if (value) goto Line_0028; throw exception;
and...
var value = expiry > now; if (value) goto Line_0045; throw exception;
Funny huh? It's reversed our logic - just one of the many wonders of IL! Well, that's ok - we're going to simply change the brtrue.s statements to brfalse.s statements. This has the affect of throwing the exception only if we have a valid registration key.
Once we've changed those two statements, we recompile the program using ILASM. Again, more information upon using ILASM can be found on MSDN.
After recompiling our program and providing it a rubbish registration key... it works as expected! And so we have hacked our program...
Running with an invalid registration key
Stopping the madness!
Oh man! We've been through all this trouble of making an obfuscater however it is so easily breakable! What can we do!?!?
Well, rest easy - we can make it even harder to break the code using two methods we will visit in the next few articles: string encryption and tamper proofing. If you think about it, half the problem is FINDING the code that needs to be hacked. If we could get rid of as much incriminating information as possible then that will surely make it harder for people to break our code's security? That is where string encryption comes in; tamper proofing on the other hand deals with the code in an "anti-trust" scenario. i.e. we regularly check to see if our original program code has been tampered with and if we detect tampering throw random exceptions.
There is hope! Although, we must remember that where there is a will, there is always a way. We can only hope to make it as tough as possible for the would be thief!
Conclusion/Next Week
This post has largely been a "HOWTO" manual about how to break into an assembly that has been obfuscated. The purpose of this is to really demonstrate that basic obfuscation is nowhere near enough protection to stop hackers from breaking into your code. In the following weeks we will take a look at encrypting all our strings to stop manual searching, and also tamper proofing our application to catch anyone trying to manipulate our program using IL hacking techniques like we've seen today.
Well, I hope this article comes of use to you. If you do find it useful then please remember to vote for me! Thanks, and until next time...
Labels: hacking, obfuscation, protecting your precious code