Software applications can be secured with two layers of protection. The first layer is software licensing, whose aim is to enforce a license model (eg. by restricting the number of machines where the application can run). The second layer is software obfuscation, where the end goal is to make it hard or impossible for the end users read and alter the source code. In this article, we will focus on obfuscation.
Types of obfuscations
There are two ways of making it harder for the adversary to read or alter the source code. We can either achieve it by altering the source in such a way that more time is necessary to understand how the code works and to make it harder to remove existing licensing logic (eg. for key verification). This is usually what is thought of as obfuscation. The second approach is to move critical code away from the client machine to your own servers and provide it as an API endpoint that your application will call.
Both methods have their pros and cons. In the case of code obfuscation, you can relative easily increase the difficultly of reverse engineering at the cost of that eventually the source code will be reversed engineered or licensing logic bypassed. With custom API endpoints, you always retain control of code execution (since it runs on your servers) and if everything is correctly implemented, it’s impossible to reverse engineer the code. This is at the cost of requiring active internet connection to your server and potentially some regulatory issues (since data has to be transferred to your servers).
Conventional obfuscators
There are many obfuscators out there, some that even are free of charge. For the .NET platform, you can either use Ofuscar or ConfuserEx. The idea behind all of them is to make the IL code (which C# and VB.NET compile to) harder to read for an adversary. They should be quite easy to use, so you can simply add the key verification logic anywhere in the software.
API endpoints
Creating an API endpoint for highly sensitive code is the best way to protect it against reverse engineering. Although it may sound as very cumbersome to set up and maintain, the good news is that most cloud providers today support some form of serverless computing. We will describe how this is achieved using Azure Functions, but it should be fairly similar to other cloud platforms. The reason why we chose the serverless model is because it abstracts most things away, allowing you to focus on expressing the actual method. Moreover, cloud providers tend to allow a “per request” model, meaning that you do not have to pay for the time when the application is idle.
Azure Functions demo
To create an Azure function, go to the Azure portal and create a new “Function App”. You can then select either “consumption plan” or “app service plan” (please see this for more details). Once it’s set up, create a new HTTP Trigger and change the run.csx as shown below. To get the license verification to work, we will need to add an additional file, function.proj (or project.json for older versions of the runtime), which we cover further down in the article.
run.csx
#r "Newtonsoft.Json" using System.Net; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Primitives; using Newtonsoft.Json; using SKM.V3; using SKM.V3.Models; using SKM.V3.Methods; public static async Task Run(HttpRequest req, ILogger log) { // this function will return 'Hello, <name>' if the correct license key is provided. // licensekey and machinecode stored as query string if(!KeyVerification(req.Query["licensekey"], req.Query["machinecode"])) { return new BadRequestObjectResult("License key verification failed"); } string name = req.Query["name"]; return name != null ? (ActionResult)new OkObjectResult($"Hello, {name}") : new BadRequestObjectResult("Please pass a name on the query string or in the request body"); } public static bool KeyVerification(string licenseKey, string machineCode) { var RSAPubKey = "<RSA public key>"; var auth = "<access token>"; var result = Key.Activate(token: auth, parameters: new ActivateModel() { Key = licenseKey, ProductId = 3349, Sign = true, MachineCode = machineCode }); if (result == null || result.Result == ResultType.Error || !result.LicenseKey.HasValidSignature(RSAPubKey).IsValid()) { // an error occurred or the key is invalid or it cannot be activated // (eg. the limit of activated devices was achieved) Console.WriteLine("The license does not work."); return false; } else { // everything went fine if we are here! Console.WriteLine("The license is valid!"); return true; } }
function.proj
In order to add support for license key verification, we need to add Cryptolens.Licensing. Depending on the version of function apps that you are using, you might either need to create a project.json or function.proj file. The newest version of the runtime uses function.proj.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Cryptolens.Licensing" Version="4.0.9.2"/> </ItemGroup> </Project>
In case you get any issues with namespaces not being found, it can be useful to try to re-create the function entirely.
Accessing form client side
In order to access your method through the client application, we can use RestSharp or similar library. When you click on “Get function url”, you will get a string similar to “https://<cluster-name>.azurewebsites.net/api/HttpTriggerCSharp1?live=<secret key>”. The live parameter may not be present for some access levels
var client = new RestClient("https://<cluster-name>.azurewebsites.net/api/"); var request = new RestRequest("HttpTriggerCSharp1", Method.GET); //request.AddParameter("code", "<secret key>"); // depending on access level of the function in Azure // for licensing request.AddParameter("licensekey", "AAAA-BBBB-CCCC-DDDD"); request.AddParameter("machinecode", Helpers.GetMachineCode()); // parameter to our function request.AddParameter("name", "Bob"); var result = client.Get(request); Console.WriteLine(result.Content); Console.ReadLine();
If all worked out correctly, we should see “Hello, Bob” in the terminal.
Privacy
The best advice when it comes to privacy is to send as little personal identifiable information as possible. Always ask yourself what data really needs to be processed externally. Even if it is not always possible to make it entirely anonymous, it’s good to strive to at least pseudo-anonymize data (i.e. associate an id to each user instead of using their real name). In some cases, such as with IP address, you can remove the last digits, eg. from 10.1.1.5 to 10.1.1.0 without affecting the geographical data of the IP. For advanced users, you might want to look into homomorphic encryption and follow the recent research.