Dynamic Code

Switching demo to retail code with code encryption.

Code encryption allows for the encryption of specific source methods in an application, which can then be switched out depending on the features that a user has access to. For example, if an application has both basic and advanced features, the encrypted source methods for the advanced features could be stored separately from the main application executable and only loaded at runtime if the user has a license that grants them access to those features. This Dynamic Code selection allows developers to create demo or trial version of their application with limited features and unlock additional functionality by providing users with a license key. Using code encryption, developers can protect their intellectual property while providing flexible licensing options for their users.

Dynamic Code provided by code encryption allows different versions of a method to be switched at runtime based on external conditions. For example, an application may have a limited set of features in demo mode, but with the presence of a valid license file, more advanced features are enabled. This can be achieved by encrypting the code for both the limited and advanced versions of the features and using a dynamic code selection mechanism to switch between them based on the presence or absence of the license file.

Demo Application

In the classical way of implementing a demo application, the retail code is already present in the demo version, and the license file allows the execution of advanced features. However, this approach has a significant drawback - the code that runs when the license file is present is not protected, and an attacker could bypass the license validation to unlock the licensed features.

On the other hand, with dynamic code selection, the code that unlocks advanced features is encrypted and cannot be executed in any way without the license file, which carries the encryption key to decrypt and execute the code. This provides better protection against reverse engineering and unauthorized access to advanced features. The license file serves as a unique key to unlock specific code sections, which makes it difficult for attackers to bypass the licensing mechanism and gain access to advanced features.

Consider the following example project available on GitHub. The example source code can be obtained using Git from the command line by executing the following command:

git clone https://github.com/babelfornet/dynamic-code-example.git

The example shows how to use Dynamic Code to change the application behaviour based on the presence of specific files.

[Obfuscation(Feature = "msil encryption:id=feature1;source=demo;internal=true;password=1234", Exclude = false)]
public void Feature1Demo()
{
    Console.WriteLine("Feature1 Demo");
    Feature2Demo();
}

[Obfuscation(Feature = "msil encryption:id=feature2;source=demo;internal=true;password=1234", Exclude = false)]
public static void Feature2Demo()
{
    Console.WriteLine("Feature2 Demo");
}

These are two demo methods that have been tagged for replacement at runtime with different versions of the methods using dynamic code selection. The Feature1Demo() and Feature2Demo() methods are encrypted using the Code Encryption feature with ID "feature1" and "feature2", respectively. The source of the encrypted code is specified as "demo", and the internal property is set to true to keep the encrypted code inside the assembly. The password for the encryption is set to "1234".

When an application is executed without a valid license file, it is considered in demo mode. In such a case, the assembly will call the above two methods tagged to be replaced at runtime by Dynamic Code. These methods implement the basic functionality of the application, which may be limited compared to the fully licensed version.

static void Main(string[] args)
{
    var app = new App();
    app.Feature1Demo();
}

Now consider the following two methods, which will be the replacements for demo methods when the application is licensed. Note that the source is set to "retail" and the method with ID "feature1" Feature1Retail() will replace the Feature1Demo() while the one with the ID "feature2" Feature2Retail() will replace the Feature2Demo() in case a license file is found.

[Obfuscation(Feature = "msil encryption:id=feature1;source=retail;internal=true;password=4567", Exclude = false)]
public static void Feature1Retail()
{
    Console.WriteLine("Feature1 Retail");
    Feature2Retail();
}

[Obfuscation(Feature = "msil encryption:id=feature2;source=retail;internal=true;password=4567", Exclude = false)]
public static void Feature2Retail()
{
    Console.WriteLine("Feature2 Retail");
}

The above two methods are not called at runtime by the assembly, which runs in demo mode, and are encrypted with a password different from the one used for the demo methods. This password is unavailable to the application code because Babel will remove the information during the obfuscation process after encrypting the methods.

We need a way to dynamically switch between "demo" and "retail" with code encryption. As the above methods were encrypted with a password, we must define the Babel code encryption entry point to retrieve the password at runtime.

[Obfuscation(Feature = "msil encryption get password", Exclude = false)]
internal static string GetPassword(string source)
{
    Console.WriteLine($"> Get {source} password");

    if (source == "demo")
        return "1234";

    string pwd = File.ReadAllText(source + ".txt");

    Console.WriteLine($">> {source} password: {pwd}");
    return pwd;
}

The GetPassword() method is used by the BVM (Bytecode Virtual Machine) to retrieve the password needed to decrypt an encrypted method when it is called during program execution.

If the source name is "demo," the method returns a hardcoded password of "1234", which is the password used to encrypt "demo" methods Feature1Demo() and Feature2Demo(). Otherwise, it reads the password from a text file with the same name as the source and a ".txt" extension. The password is then returned to the caller. Note that the demo password is stored inside the code as the application can always run in demo mode, and there is no need to hide this password.

The switch between "demo" and "retail" sources is made by the GetSourceStream() method called by BVM when a call to one of the encrypted methods gets called.

[Obfuscation(Feature = "msil encryption get stream", Exclude = false)]
internal static object GetSourceStream(string source)
{
    Console.WriteLine($"> Get {source} stream");

    if (source == "demo")
    {
        string[] sources = { "retail", "special" };
        foreach (var src in sources)
        {
            if (File.Exists($"{src}.txt"))
            {
                Console.WriteLine($">> Switch to {src}");
                return src;
            }
        }

        return null;
    }
    
    // External sources
    if (File.Exists(source + ".eil"))
    {
        Console.WriteLine($">>> Read {source} stream");
        return File.OpenRead(source + ".eil");
    }

    return null;
}

This method plays a crucial role in dynamically selecting the appropriate encrypted methods source based on external conditions, such as the presence of a license file. In fact, when the application starts, it calls demo methods which can be decrypted by the password returned by the GetPassword() method.

In the context of dynamic code selection, the BVM (Binary Virtual Machine) is responsible for executing the encrypted code and switching between different versions of a method based on external conditions. Before running any "demo" method, the BVM calls the GetSourceStream() method, passing "demo" as a parameter. Since the "demo" methods are internal to the assembly, we do not need to return any demo source stream. However, if the "retail.txt" file is found, we want to switch to the "retail" source code, so we need to inform the BVM that we want to execute the "retail" source by returning the string "retail" from the GetSourceStream() method.

Once the BVM receives the string "retail" from GetSourceStream(), it knows that it needs to execute the "retail" code. The BVM then makes a call to GetPassword() with the argument "retail". GetPassword() reads the password for the "retail" version from the retail.txt file and returns it to the BVM, which uses it to decrypt the "retail" code. Once the "retail" code is decrypted, it is executed instead of the "demo" code. In this way, the application is able to switch between different versions of encrypted code at runtime based on the presence or absence of a license file without requiring the code to be recompiled or redeployed.

Dynamic Code allows the definition of multiple internal and external sources. For example, you can declare another external source "special" that can be loaded instead of the "retail" code when the file special.txt containing the password for decrypting code is present.

[Obfuscation(Feature = "msil encryption:id=feature1;source=special;internal=false;password=0000", Exclude = false)]
public static void Feature1Special()
{
    Console.WriteLine("Feature1 Special");
    Feature2Special();
}

[Obfuscation(Feature = "msil encryption:id=feature2;source=special;internal=false;password=0000", Exclude = false)]
public static void Feature2Special()
{
    Console.WriteLine("Feature2 Special");
}

The given code shows two methods, Feature1Special() and Feature2Special(), that are tagged with Obfuscation attributes, where the "internal" property is set to "false". This means that the obfuscated code for these methods will be generated in an external file with the extension ".eil" and not included within the assembly itself. The encrypted code external ".eil" file can be loaded and decrypted at runtime using the appropriate password specified in the Obfuscation attributes.

To run the example provided, first compile the solution in debug configuration. Then open the Obfuscation.babel project in the Babel UI and start the obfuscation process. After that, copy the files DynamicCode.exe, DynamicCode.runtimeconfig.json, and DynamicCode.deps.json into the created BabelOut folder. Next, from a PowerShell window, run the DynamicCode.exe in the BabelOut folder, and you should see the output:

> Get demo stream
> Get demo password
Feature1 Demo
Feature2 Demo

To test the "retail" source, copy the retail.txt file inside the BabelOut folder and run the DynamicCode.exe. The output should be:

> Get demo stream
>> Switch to retail
> Get retail password
>> retail password: 4567
Feature1 Retail
Feature2 Retail

To test the "special" source, copy the special.txt file inside the BabelOut folder and run the DynamicCode.exe. The output should be:

> Get demo stream
>> Switch to special
> Get special stream
>>> Read special stream
> Get special password
>> special password: 0000
Feature1 Special
Feature2 Special

Last updated