Introduction
Managed code, unlike native code, has been known to be easily decompiled to its source code, easing its reverse engineering, thus giving the need to what we call obfuscation. We change the managed code after compiling it in a way that makes decompilers obsolete and makes decompiling it useless, as the decompilation will generate garbage code that can’t be understood or compiled again after modifying it. Obfuscation is mostly done with renaming the names of classes, methods, and variables into random names, rendering it unreadable when it’s decompiled, and in the case of some obfuscators, the output obfuscated application, when decompiled, generates a code that gives build errors when being compiled again. But although obfuscation sometimes proves to be efficient, it has major weaknesses and limitations that makes relying on it is not a good decision.
For the sake of demonstration, in this article, I’m going to use C# as my managed code language, and the preemptive Dotfuscator that comes as a community edition with Microsoft Visual Studio will be my obfuscation tool.
The example
Say, we have this application that checks if the user is authenticated or not before doing an action:
private void btnSubmit_Click(object sender, EventArgs e)
{
if (Authenticate())
{
MessageBox.Show("access granted");
this.Run();
}
else
{
MessageBox.Show("invalid credentials");
this.Close();
}
}
When we obfuscate this code and try to decompile it, we get (I use Lutz reflector to do the decompile):
private void a(object A_0, EventArgs A_1)
{
if (this.c())
{
MessageBox.Show("access granted");
this.b();
}
else
{
MessageBox.Show("invalid credentials");
base.Close();
}
}
As it’s obvious, most of the code has been renamed, but the message strings are untouched. Also, the .NET framework has used classes and methods like the MessageBox
class and the Show()
method which are still not renamed, which is a big problem. Compiling the resulting code from decompiling the obfuscated code might result in build time errors because the obfuscated code might have the same names for the methods and classes, but this isn’t the same for IL (Intermediate Language). So, if we simply used ildasm to disassemble the EXE for this application, we will get this:
C:\Program Files\Microsoft Visual Studio 8\VC>ildasm
C:\Dotfuscated\password.exe /out=c:\password.il
.method private hidebysig instance void
a(object A_0,
class [mscorlib]System.EventArgs A_1) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance bool a::c()
IL_0006: brfalse.s IL_001a
IL_0008: ldstr "access granted"
IL_000d: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_0012: pop
IL_0013: ldarg.0
IL_0014: call instance void a::b()
IL_0019: ret
IL_001a: ldstr "invalid credentials"
IL_001f: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_0024: pop
IL_0025: ldarg.0
IL_0026: call instance void [System.Windows.Forms]System.Windows.Forms.Form::Close()
IL_002b: ret
}
As we can see, again, our message strings are written in plain text. So are the used .NET framework namespaces. And here comes our message box again: System.Windows.Forms.MessageBox::Show(string)
.
So, what is the problem in this? The problem is that the old cracking techniques that were used with Win32 applications can still be applied to .NET assemblies very easily. So, if I’m an experienced cracker, I would disassemble this application into IL and search for the “invalid credentials” string that shows in my face every time I write in an invalid password, and look up a few lines till I find the branching statement at line IL_0006, and very easily, I could brfalse
to brtrue
and build the application using ILAsm.
C:\Program Files\Microsoft Visual Studio 8\
VC>ilasm c:\password.il /out=c:\password.exe
So next time I run the newly built application, I supply an invalid user name and password, and I will get the welcome message saying “access granted” instead of being kicked out. And as it’s very obvious, the same can be applied for cracking license keys and similar stuff.
But that was because my current obfuscation tool didn’t obfuscate the messages strings, right? What if we obfuscate every available string in my application? I will be doing this using the evaluation version of Dotfuscator which has a feature called “string encryption”, which I personally don’t consider encryption, rather encoding or obfuscation, because you can’t encrypt things and supply the encryption algorithm and key with it. So, here is the disassembled code after the string obfuscation:
.method private hidebysig instance void
eval_a(object A_0,
class [mscorlib]System.EventArgs A_1) cil managed
{
.maxstack 9
.locals init (int32 V_0)
IL_0000: ldc.i4 0x3
IL_0005: stloc V_0
IL_0009: ldarg.0
IL_000a: call instance bool eval_a::eval_c()
IL_000f: brfalse.s IL_002c
IL_0011: ldstr bytearray (F0 90 F2 90 F4 96 F6 92 F8 8A FA 88 FC DD FE 98
00 73 02 62 04 6B 06 73 08 6C 0A 6F )
IL_0016: ldloc V_0
IL_001a: call string a$PST06000001(string, int32)
IL_001f: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_0024: pop
IL_0025: ldarg.0
IL_0026: call instance void eval_a::b()
IL_002b: ret
IL_002c: ldstr bytearray (F0 98 F2 9D F4 83 F6 96 F8 95 FA 92 FC 99 FE DF
00 62 02 71 04 60 06 63 08 6C 0A 65 0C 79 0E 66
10 70 12 7F 14 66 )
IL_0031: ldloc V_0
IL_0035: call string a$PST06000001(string,
int32)
IL_003a: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_003f: pop
IL_0040: ldarg.0
IL_0041: call instance void [System.Windows.Forms]System.Windows.Forms.Form::Close()
IL_0046: ret
}
Note: the “eval” prefix is because I’m using an evaluation version of the Dotfuscator.
As we can see, all of the strings have been obfuscated, but still all the .NET framework used class and method names are still in plain text and readable to anyone, and that is because you can obfuscate anything but the .NET framework namespaces, classes, and methods. This is because if you obfuscated their names, how are you going to call them on your user machine?
Again, if I’m an experienced cracker and I know what I’m looking for, I will be looking for the most rare .NET framework methods and classes that have been called within this application. For example, the MessageBox
is a very good example. Also, the Form::Close()
is another good option. I would search for them in the new IL, and again look up for a few lines searching for the branching statement till I find it at line IL_000f, and again, I will change it from brfalse
to brtrue and build
my application again using ILAsm. When I run it, I will get another access granted message, and as you can see, it took me only 5 minutes.
But, that is because the application flow is so clear and it wasn’t obfuscated, right? What if we obfuscate the application flow too using the “Control Flow Obfuscation” feature in Dotfuscator? The output IL is going to look like this:
.method private hidebysig instance void
eval_a(object A_0,
class [mscorlib]System.EventArgs A_1) cil managed
{
.maxstack 2
.locals init (int32 V_0)
IL_0000: ldc.i4 0xa
IL_0005: stloc V_0
IL_0009: ldarg.0
IL_000a: call instance bool eval_a::eval_c()
IL_000f: brfalse.s IL_0036
IL_0011: ldc.i4.1
IL_0012: br.s IL_0017
IL_0014: ldc.i4.0
IL_0015: br.s IL_0017
IL_0017: brfalse.s IL_0019
IL_0019: br.s IL_001b
IL_001b: ldstr bytearray (57 39 59 39 5B 3F 5D
3B 5F 13 61 11 63 44 65 01
67 1A 69 0B 6B 02 6D 1A 6F 15 71 16 )
IL_0020: ldloc V_0
IL_0024: call string a$PST06000001(string,
int32)
IL_0029: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_002e: pop
IL_002f: ldarg.0
IL_0030: call instance void eval_a::b()
IL_0035: ret
IL_0036: ldstr bytearray (57 31 59 34 5B 2A 5D
3F 5F 0C 61 0B 63 00 65 46
67 0B 69 18 6B 09 6D 0A 6F 15 71 1C 73 00 75 1F
77 19 79 16 7B 0F )
IL_003b: ldloc V_0
IL_003f: call string a$PST06000001(string, int32)
IL_0044: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_0049: pop
IL_004a: ldarg.0
IL_004b: call instance void [System.Windows.Forms]System.Windows.Forms.Form::Close()
IL_0050: ret
}
There are lots of branches, but with a bare eye inspection, all of them are pointing to other branches that again are pointing to others till they reach a real branch. Also, with a bare eye inspection, none of them have a condition - they just branch. So it would be very easy to spot the real branch that we are seeking at line IL_000f, and do the same again by changing the condition from false to true, and build the application, and again we will get the previous result.
But that is because the code isn’t complex enough. What if we make the code a little bit more complex and use the previous way to obfuscate it?
A code that looks like this:
private void btnSubmit_Click(object sender, EventArgs e)
{
if (CheckConnection())
{
if (CheckDB())
{
if (Authenticate())
{
MessageBox.Show("access granted");
this.Run();
}
else
{
MessageBox.Show("invalid credentials");
this.Close();
}
}
}
}
would look like this after disassembling:
.method private hidebysig instance void
eval_a(object A_0,
class [mscorlib]System.EventArgs A_1) cil managed
{
.maxstack 2
.locals init (int32 V_0)
IL_0000: ldc.i4 0xa
IL_0005: stloc V_0
IL_0009: ldarg.0
IL_000a: call instance bool eval_a::eval_c()
IL_000f: brtrue.s IL_0036
IL_0011: ldc.i4.1
IL_0012: br.s IL_0017
IL_0014: ldc.i4.0
IL_0015: br.s IL_0017
IL_0017: brfalse.s IL_0019
IL_0019: br.s IL_001b
IL_001b: ldstr bytearray (57 39 59 39 5B 3F 5D
3B 5F 13 61 11 63 44 65 01
67 1A 69 0B 6B 02 6D 1A 6F 15 71 16 )
IL_0020: ldloc V_0
IL_0024: call string a$PST06000001(string,
int32)
IL_0029: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_002e: pop
IL_002f: ldarg.0
IL_0030: call instance void eval_a::b()
IL_0035: ret
IL_0036: ldstr bytearray (57 31 59 34 5B 2A 5D 3F
5F 0C 61 0B 63 00 65 46
67 0B 69 18 6B 09 6D 0A 6F 15 71 1C 73 00 75 1F
77 19 79 16 7B 0F )
IL_003b: ldloc V_0
IL_003f: call string a$PST06000001(string,
int32)
IL_0044: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_0049: pop
IL_004a: ldarg.0
IL_004b: call instance void [System.Windows.Forms]System.Windows.Forms.Form::Close()
IL_0050: ret
}
This time, it’s harder to crack, but with a bare eye inspection, we can see there are only two conditional branches at lines IL_000f and IL_0017, before where I found the Form::Close()
method. I can try my luck with them, or I would just change the first one before where I found the Form::Close()
method and the MessageBox::Show(string)
method, and build the application again and again until I get another access granted message.
So, what is the conclusion?
Conclusion
Well, obfuscation is a good way to protect our intellectual properties and it’s better than just leaving our confidential information in plain text. But, as I’ve just demonstrated through this article, we can’t rely on obfuscation to protect our applications as it’s easy to crack any application that is relying only on obfuscation for protection. And I did that without any special tools, and in only a few minutes.
Thanks for reading, and I’m waiting for your comments and feedback.