Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / MSIL

Don’t rely on obfuscation

3.16/5 (32 votes)
21 Feb 2008CPOL5 min read 2  
An article demonstrating why you should not rely on obfuscation to protect your .NET applications.

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:

C#
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();
    }
}

When we obfuscate this code and try to decompile it, we get (I use Lutz reflector to do the decompile):

C#
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:

MSIL
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, 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:

MSIL
.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 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:

MSIL
.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

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:

C#
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:

MSIL
.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, 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)