Hello world!
Today, we continue the “Trolling the Decompiler” series (first part here: Prevent Reflector from Decompiling) but now with a more serious approach - this one should work on any decompiler.
The point is: it is rather difficult to make .NET programs run with a key or license; since these can be reverted back to their source code, anyone can alter it or just learn to create fake keys that will be seen as valid.
Possible Solution
One way to make an application a little bit more difficult to crack would be to deliver it as a program that decrypts instructions, compiles and runs them only when needed. This way, if someone finds out where the source code is stored, it will still be encrypted and without a key (or license) it is unusable.
We’re kinda writing polymorphic stuff here - AVs won’t be happy; actually…only 2/57 don’t like it, we’re good.
1. Making the Compiler
We’re not really going to reinvent the wheel here - .NET seems to allow us to use the original compiler to produce an Assembly
. Just as always, we start with a CodeDomProvider
, add a bunch of settings using CompilerParameters
and a few source codes.
CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateExecutable = true;
parameters.GenerateInMemory = true;
parameters.TreatWarningsAsErrors = false;
parameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Drawing.dll");
CompilerResults result = provider.CompileAssemblyFromSource(parameters, getContents());
If you look around, there’s also an article that provides a little bit more detail about how to compile code at runtime using CSharpCodeProvider
and ICodeCompiler
which are now considered obsolete, but the code is similar.
2. Running the Compiled Assembly
What we’re interested in is result.CompiledAssembly
- in order to run it, we have to create an instance of the method that serves as entrypoint and then invoking it.
Short note: If the assembly that you’re trying to run belongs to a Console Application and this program has the same project type, you might need to call FreeConsole()
and then AllocConsole()
. Without recreating the Console, there seems to be no output from the compiled assembly.
This is how we can run the compiled code:
Assembly assembly = result.CompiledAssembly;
MethodInfo methodInfo = assembly.EntryPoint;
object entryPointInstance = assembly.CreateInstance(methodInfo.Name);
methodInfo.Invoke(entryPointInstance, null);
3. Encrypting & Attaching Source Code
This is one of the tough parts - we take the source codes of the files that we want to secure and encrypt them (I use AES with Rijndael’s algorithm), then attach the results at the end of the executable that we’ve been working on at the previous steps.
Here, the content of the executable and each source code are separated by a sequence of 3 FS (File Separator Character). It’s not the clean way to handle this… don’t use it in serious projects; but for this tutorial, it should be fine.
FS = 28(dec) = 1C(hex);
The method that I use looks like this:
static void appendContents(String fileName)
{
FileStream fstream = new FileStream(fileName, FileMode.Append);
fstream.WriteByte(CHAR_FS);
fstream.WriteByte(CHAR_FS);
fstream.WriteByte(CHAR_FS);
string[] sourceFiles = Directory.GetFiles(Path.GetDirectoryName
(Assembly.GetEntryAssembly().Location), "*.cs", SearchOption.AllDirectories);
for (int i = 0; i < sourceFiles.Length; i++)
{
byte[] buffer = File.ReadAllBytes(sourceFiles[i]);
if (buffer.Length > 2 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0XBF)
{
byte[] newBuffer = new byte[buffer.Length - 3];
Array.Copy(buffer, 3, newBuffer, 0, buffer.Length - 3);
newBuffer = EncryptMessage(newBuffer, "abcdabcdabcdabcdabcdabcdabcdabcd");
fstream.Write(newBuffer, 0, newBuffer.Length);
}
else
{
buffer = EncryptMessage(buffer, "abcdabcdabcdabcdabcdabcdabcdabcd");
fstream.Write(buffer, 0, buffer.Length);
}
fstream.WriteByte(CHAR_FS);
fstream.WriteByte(CHAR_FS);
fstream.WriteByte(CHAR_FS);
}
}
I’ll not add EncryptMessage()
’s code here since it’s not related to the actual subject - you can find it below, in the complete source code.
4. Extracting & Decrypting Source Code
Procedure that runs before the whole compile & run thingy - we look for any sequence of 3 FS characters, skip the executable’s content, take the encrypted source code and run it through the decryption method - the result is pure C# code that will be given to the compiler.
Remember to replace the "abcdabcdabc..."
decryption key with what the user inputs in order to use the program (like a license) - line 31.
static String[] getContents()
{
byte[] bytes = File.ReadAllBytes(Assembly.GetEntryAssembly().Location);
int i = 0;
List<String> sourceFiles = new List<String>();
for (i = 0; i < bytes.Length - 2; i++)
{
if (bytes[i] == bytes[i + 1] && bytes[i + 1] == bytes[i + 2] && bytes[i + 2] == 28)
{
i += 3;
break;
}
}
List<Byte> sourceFileBuffer = new List<Byte>(4000);
for (; i < bytes.Length - 2; i++)
{
if (bytes[i] == bytes[i + 1] && bytes[i + 1] == bytes[i + 2] && bytes[i + 2] == 28)
{
sourceFiles.Add(Encoding.Default.GetString(DecryptMessage
(sourceFileBuffer.ToArray(), "abcdabcdabcdabcdabcdabcdabcdabcd")));
sourceFileBuffer.Clear();
i += 2;
}
else
sourceFileBuffer.Add(bytes[i]);
}
return sourceFiles.ToArray();
}
Final Notes & Complete Source Code
Below, you’ll find the source code I ended up with while writing this article. It’s more like a fast way to explain an idea - it needs some “patching”.
In order to actually use it, you should split this into 2 programs - one for encrypting and attaching and the other to do the decryption, compilation & execution. You send only the latter one to the user - so he won’t get the encryption key - this or switch to an asymmetric algorithm. Also don’t forget to remove the hardcoded decryption key and ask the user for his own.
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
const int CHAR_FS = 28;
public static byte[] EncryptMessage(byte[] text, string key)
{
RijndaelManaged aes = new RijndaelManaged();
aes.KeySize = 256;
aes.BlockSize = 256;
aes.Padding = PaddingMode.Zeros;
aes.Mode = CipherMode.CBC;
aes.Key = Encoding.Default.GetBytes(key);
aes.GenerateIV();
string IV = Encoding.Default.GetString(aes.IV);
ICryptoTransform AESEncrypt = aes.CreateEncryptor(aes.Key, aes.IV);
byte[] buffer = text;
return Encoding.Default.GetBytes(Encoding.Default.GetString
(AESEncrypt.TransformFinalBlock(buffer, 0, buffer.Length)) + IV);
}
public static byte[] DecryptMessage(byte[] text, string key)
{
RijndaelManaged aes = new RijndaelManaged();
aes.KeySize = 256;
aes.BlockSize = 256;
aes.Padding = PaddingMode.Zeros;
aes.Mode = CipherMode.CBC;
aes.Key = Encoding.Default.GetBytes(key);
byte[] IV = new byte[32];
Array.Copy(text, text.Length - 32, IV, 0, 32);
byte[] text2 = new byte[text.Length - 32];
Array.Copy(text, text2, text2.Length);
aes.IV = IV;
ICryptoTransform AESDecrypt = aes.CreateDecryptor(aes.Key, aes.IV);
return AESDecrypt.TransformFinalBlock(text2, 0, text2.Length);
}
static void appendContents(String fileName)
{
FileStream fstream = new FileStream(fileName, FileMode.Append);
fstream.WriteByte(CHAR_FS);
fstream.WriteByte(CHAR_FS);
fstream.WriteByte(CHAR_FS);
string[] sourceFiles = Directory.GetFiles(Path.GetDirectoryName
(Assembly.GetEntryAssembly().Location), "*.cs", SearchOption.AllDirectories);
for (int i = 0; i < sourceFiles.Length; i++)
{
byte[] buffer = File.ReadAllBytes(sourceFiles[i]);
if (buffer.Length > 2 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0XBF)
{
byte[] newBuffer = new byte[buffer.Length - 3];
Array.Copy(buffer, 3, newBuffer, 0, buffer.Length - 3);
newBuffer = EncryptMessage(newBuffer, "abcdabcdabcdabcdabcdabcdabcdabcd");
fstream.Write(newBuffer, 0, newBuffer.Length);
}
else
{
buffer = EncryptMessage(buffer, "abcdabcdabcdabcdabcdabcdabcdabcd");
fstream.Write(buffer, 0, buffer.Length);
}
fstream.WriteByte(CHAR_FS);
fstream.WriteByte(CHAR_FS);
fstream.WriteByte(CHAR_FS);
}
}
static String[] getContents()
{
byte[] bytes = File.ReadAllBytes(Assembly.GetEntryAssembly().Location);
int i = 0;
List<String> sourceFiles = new List<String>();
for (i = 0; i < bytes.Length - 2; i++)
{
if (bytes[i] == bytes[i + 1] && bytes[i + 1] == bytes[i + 2] && bytes[i + 2] == 28)
{
i += 3;
break;
}
}
List<Byte> sourceFileBuffer = new List<Byte>(4000);
for (; i < bytes.Length - 2; i++)
{
if (bytes[i] == bytes[i + 1] && bytes[i + 1] == bytes[i + 2] && bytes[i + 2] == 28)
{
sourceFiles.Add(Encoding.Default.GetString
(DecryptMessage(sourceFileBuffer.ToArray(), "abcdabcdabcdabcdabcdabcdabcdabcd")));
sourceFileBuffer.Clear();
i += 2;
}
else
sourceFileBuffer.Add(bytes[i]);
}
return sourceFiles.ToArray();
}
static void Main(string[] args)
{
CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
CompilerParameters parameters = new CompilerParameters();
parameters.GenerateExecutable = true;
parameters.GenerateInMemory = true;
parameters.TreatWarningsAsErrors = false;
parameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Drawing.dll");
if (args.Length > 0)
{
appendContents(args[0]);
return;
}
CompilerResults result = provider.CompileAssemblyFromSource(parameters, getContents());
if (result.Errors.Count > 0)
{
foreach (CompilerError er in result.Errors)
Console.WriteLine(er.ToString());
Console.ReadLine();
return;
}
Assembly assembly = result.CompiledAssembly;
MethodInfo methodInfo = assembly.EntryPoint;
object entryPointInstance = assembly.CreateInstance(methodInfo.Name);
methodInfo.Invoke(entryPointInstance, null);
}
}
}
Proof of concept
I made a short youtube video where I run this - look it up in the original article.