Don’t rely on obfuscation
Managed code unlike native code have been known to be easily decompiled to it’s source code easing its reverse engineering thus giving the need to what we call obfuscation to 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 generate a code that gives build errors when being compiled again. But although obfuscation sometimes proves to be efficient, it has major weakness 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# .net as my managed code and preemptive dotfuscator that comes as a community edition with Microsoft Visual Studio will be my obfuscation tool.
Say that 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)
{
//we authenticate the user here using the method Authenticate()
if (Authenticate())
{
//if the user credential is valid then...
MessageBox.Show("access granted");
this.Run();
}
else
{
//else we kick him/her out
MessageBox.Show("invalid credentials");
this.Close();
}
}
So 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 have been renamed but the messages strings are untouched also the .net framework used classes and methods like MessageBox class and Show() method still not renamed which is a big problem, compiling the resulting code from decompiling obfuscated code might result build time errors because the obfuscated code might have the same names for methods and classes but this isn’t the same for IL (intermediate language) so if we simply used ildasm to disassemble the exe assembly 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
{
// Code size 44 (0x2c)
.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
} // end of method a::a
As we can see again that our messages string is written in plain text also the used .net framework namespaces and here comes our message box again System.Windows.Forms.MessageBox::Show(string)
So what the problem in this? The problem 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 easily I would change from 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 new built application when I supply an invalid user name and password 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? So what if we obfuscate every available string in my application too, I will be doing this using the evaluation version of dotfuscator which have a feature called “string encryption” which personally I don’t consider encryption rather than ecoding or obfuscation because you can’t encrypt things and supply the encryption algorithm and key with it. So here is the disassemebled code after the string obfuscation
.method private hidebysig instance void
eval_a(object A_0,
class [mscorlib]System.EventArgs A_1) cil managed
{
// Code size 71 (0x47)
.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 ) // .s.b.k.s.l.o
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 // .b.q.`.c.l.e.y.f
10 70 12 7F 14 66 ) // .p...f
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
} // end of method eval_a::eval_a
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 classes and methods names still in plain text and readable to anyone and that is because you can obfuscate anything but the .net framework namespaces, classes and methods because if you obfuscated there names how you are going to call them on your user machine?
so 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 so 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 again my application using ilasm and when I run it I will get another access granted message and as you can see it took me only 5 minutes
but that because the application flow was so clear and it wasn’t obfuscated, right? So 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
{
// Code size 81 (0x51)
.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 // W9Y9[?];_.a.cDe.
67 1A 69 0B 6B 02 6D 1A 6F 15 71 16 ) // g.i.k.m.o.q.
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 // W1Y4[*]?_.a.c.eF
67 0B 69 18 6B 09 6D 0A 6F 15 71 1C 73 00 75 1F // g.i.k.m.o.q.s.u.
77 19 79 16 7B 0F ) // w.y.{.
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
} // end of method eval_a::eval_a
So as we so now there lots of branches but with the bare eye inspection all of them are pointing to other branches that also is pointing to another one till they reach the real branch also with bare eye inspection none of them have 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 because the code isn’t complex enough, what if we made the code a little bit more complex and used the previous way to obfuscate it?
So a code that looks like this
private void btnSubmit_Click(object sender, EventArgs e)
{
if (CheckConnection())
{
if (CheckDB())
{
//we authenticate the user here using the method Authenticate()
if (Authenticate())
{
//if the user credential is valid then...
MessageBox.Show("access granted");
this.Run();
}
else
{
//else we kick him out
MessageBox.Show("invalid credentials");
this.Close();
}
}
}
}
Would look like this after disassembling it
.method private hidebysig instance void
eval_a(object A_0,
class [mscorlib]System.EventArgs A_1) cil managed
{
// Code size 81 (0x51)
.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 // W9Y9[?];_.a.cDe.
67 1A 69 0B 6B 02 6D 1A 6F 15 71 16 ) // g.i.k.m.o.q.
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 // W1Y4[*]?_.a.c.eF
67 0B 69 18 6B 09 6D 0A 6F 15 71 1C 73 00 75 1F // g.i.k.m.o.q.s.u.
77 19 79 16 7B 0F ) // w.y.{.
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
} // end of method eval_a::eval_a
This time it’s harder to crack it but with bare eye inspection we can see there are only 2 conditioned branches at lines IL_000f and IL_0017 before where I found the Form::Close() method, so I can try my luck with them or I would just change the 1st one before where I found the Form::Close() method and the MessageBox::Show(string) method and build the application again and again I get another access granted message.
So what is the 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 I’ve demonstrated how it’s easy to crack any application that is relying only on obfuscation for protection, and i did it without any special tools in only a few minutes.
Thanks for reading and I’m waiting for your comments and feedback
