When I was newbie in .NET, it was a shocked for me to learn that .NET assemblies can be decompile. But today I know that this is a normal situation for a language which is compiling to an intermediate language. As we know that, after compiling a .NET project the output will be in MSIL (Microsoft Intermediate Language). And during the application lifecycle this codes will be compiled to Native Codes by the JIT (Just-In-Time Compiler) when they first used and cached in that computer for the future usages. Anyway this is another subject for us how JIT works, but here the problem is our assemblies are in MSIL format which has about 229 opcodes and all these opcodes has a standard working procedure. So if we know these opcodes and how they are working, we can produce the .NET codes by reverse engineering. This is simply what decompilers do. But why there are no stable decompilers for the native exes? The answer is simple, because there are no metadata for the native exes. In the .NET I can say that, metadata of that assembly is really very important to resolve the Type and Member structures of that assembly. Anyway there are a lot of decompilers for the .NET which can decompile codes as same as with the original codes. Xenocode Fox Decompiler and Reflector are the most know ones.

Below is an example which shows us what a decompiler can do.

Original CodeDecompiled Code
private void btnRun_Click(object sender, EventArgs e)
{
    Stopwatch watch = new Stopwatch();
   
    int iteration = 0;
    if (!int.TryParse(txtNumIteration.Text, 
out iteration))
        iteration = 10000000;
    int value = 0;
    if (!int.TryParse(txtNumParam.Text, 
out value))
        value = 50;
    IExample ex = null;
    int caseCount;
    switch (cmbCases.SelectedIndex)
    {
        case 0:
            caseCount = 5;
            ex = new Example5();
            break;
        case 1:
            caseCount = 10;
            ex = new Example10();
            break;
        case 2:
            caseCount = 20;
            ex = new Example20();
            break;
        case 3:
            caseCount = 50;
            ex = new Example50();
            break;
        default:
            caseCount = 99;
            ex = new Example99();
            break;
    }
    int r = 0;
    watch.Start();
    for (int i = 0; i < iteration; i++)
    {
        r = ex.GetSwitchResult(value);
    }
    watch.Stop();
    long swResult = watch.ElapsedMilliseconds;
    watch.Start();
    for (int i = 0; i < iteration; i++)
    {
        r = ex.GetIfResult(value);
    }
    watch.Stop();
    AppendItem(iteration, caseCount, 
value, swResult, watch.ElapsedMilliseconds);
}
private void btnRun_Click (object sender, EventArgs e)
{
     int caseCount;
     Stopwatch watch = new Stopwatch();
     int iteration = 0;
     if (!int.TryParse(txtNumIteration.Text,
 out iteration))
     {
          iteration = 10000000;
     }
     int value = 0;
     if (!int.TryParse(txtNumParam.Text,
 out value))
     {
          value = 50;
     }
     IExample ex = null;
     switch (cmbCases.SelectedIndex)
     {
          case 0:
          {
               caseCount = 5;
               ex = new Example5();
               break;
          }
          case 1:
          {
               caseCount = 10;
               ex = new Example10();
               break;
          }
          case 2:
          {
               caseCount = 20;
               ex = new Example20();
               break;
          }
          case 3:
          {
               caseCount = 50;
               ex = new Example50();
               break;
          }
          default:
          {
               caseCount = 99;
               ex = new Example99();
               break;
          }
     }
     int r = 0;
     watch.Start();
     for(int i = 0; i < iteration < i++)
     {
          r = ex.GetSwitchResult(value);
     }
     watch.Stop();
     long swResult = watch.ElapsedMilliseconds;
     watch.Start();
     for (int i = 0;i < iteration; i++)
     {
          r = ex.GetIfResult(value);
     }
     watch.Stop();
     AppendItem(iteration, caseCount,
 value, swResult, watch.ElapsedMilliseconds);
}

Decompiled code was generated by using Xenocode Fox

As you see here, the output is nearly as same as the original code when we decompile the assembly with Xenocode Fox.

So the question is how we can protect our assemblies from decompiling. Actually the answers is simple, except we don’t make our .net assemblies native, we cannot. By one way or other decompilers can decompile our codes back to the source codes. But against the decompilers there are Obfuscators which helps us to protect our codes from decompiling. If we have to explain what an obfuscator do, simply, Obfuscators changes the names of our Types and Members (properties, methods, fields, events etc.) to unreadable or unmeaning full names in the metadata of our assembly. By this way when someone decompiles our codes they can only see the unmeaning full class and member names which is really hard to understand the code. Generally we make the type and member names as understandable. For example if we write a class which indicates a User’s information, we will probably make the class name as User. So after decompiling the person who decompiles the code, sees this User class and he/she understands that this class is for the user information. So if we change the name of this class to an understandable name (for ex: xbc3453vbgf345) then it will be really hard to understand what this class is for. Except from changing the type and member names some obfuscators also can make .net assemblies as native, or some of them can injects useless codes to our methods which cause our methods hard to understand.

There are a lot of Obfuscators like Xenocode PostBuildCodeVeilSalamander etc. I’m using PostBuild to obfuscate my codes and I can say that it is really enough to protect my assemblies. Not only changing the type and member names but also we can make our assemblies Native by using PostBuild, or we can allow it to injects some codes which protects our methods to be decompiled (by injecting some untargeted branches) etc. etc. Making our assemblies as native will stops cares about decompiling. Code injection will obstruct Reflector and Fox to decompile that methods. PostBuild also can make our assemblies smaller by compressing and removing unused metadata by dead code elimination. This will increase our assemblies’ performance.

As conclusion, I can say that using a good obfuscator is enough to protect our codes. There is no need to care about decompilers. Even I develop a decompiler, I still continue to develop my other projects by using .NET and I have no care about this.

Share: