Skip to content

Getting started and best practices

This guide illustrates the steps required to bootstrap a protection project and is meant to provide useful tips to setup the ideal protection project for your needs.

Define the goals of the project

Solidshield is a technology that provides solutions to various independent issues related to software security. While it is possible to join multiple features in a single project, it is important to identify the goals early. The main ways Solidshield can be used are:

  • Protect a software generically against reverse engineering and tampering
  • Protect sensible routines of a software against reverse engineering and tampering, for example feature limiting or licensing routines
  • Protect routines containing Intellectual Property against reverse engineering
  • Integrate Licensing and Digital Rights Management schemes
  • Integrate mechanisms for the mitigation of cyber-threats

While generic protection, with the use of Systemic, and licensing options (activation, launch control, dongle) can be used without particular care, techniques involving code obfuscation require mastering a few key concepts.

Code obfuscation primer

Code obfuscation works by expanding the original code with new specially crafted one with the purpose of complexifying the reverse engineering of its original semantics and possibly adding integrity checks.

For this reason, obfuscation impacts the performances of protected code and Solidshield offers a palette of different code obfuscation techniques that best suits different use-case, according to the unique static and dynamic properties of the code being protected.

In this terms it appears clear that the job of setting up a protection project it is just an optimization problem in which the variables to be minimized are:

  • the amount of unaltered code, sensible code which in the protected program is left untouched as in the original program
  • the performance overhead, the timing penalty the protection introduces executing the protected version of the code

This optimization problem is often referred to as the "code security VS execution performance" trade-off.

Implementation best practice

Follows a list of tips to effectively converge toward the optimal protection setup

1. Identify the perimeter of sensible code to protect

Try to identify the list of sensible functions that should be protected and possibly sort the list by priorities, the MoSCoW (Must Have, Should Have, Could Have, Won't Have this time) prioritization technique can be handy.

2. Consider the ingredients VS recipe dilemma

Often, when defining the list of code to protect, users are misled into selecting code that does not containing much value but impacts performances heavily.
A typical example is the selection of standard or open-source crypto or math functions, which are very common ingredients possibly used on similar algorithms of your competition.
What the protection engineer should focus on is on how these common ingredients are pieced together, what we refer to as the recipe, which normally identifies as the set of functions that orchestrate the work of the standard data crunching code.

3. Establish a performance testing pipeline early

Given the optimization nature of the problem of code obfuscation, it is of paramount importance to setup an effective feedback loop to measure the impact on performances of each setting so to refine the project setup.

feedback loop

To measure the performance of functions to be protected, you can define a specific "protection testing" build target of the program and adding some code as follows:

Say we want to protect the function BubbleSort,

BubbleSort(sortArray, SORTBUFF_SIZE);

we can wrap it with the following code:

#include <time.h>    // at the top of the file
#define BILLION 1E9  // at the top of the file

struct timespec requestStart, requestEnd;
clock_gettime(CLOCK_REALTIME, &requestStart);
BubbleSort(sortArray, SORTBUFF_SIZE);
clock_gettime(CLOCK_REALTIME, &requestEnd);
double accum = ( requestEnd.tv_sec - requestStart.tv_sec ) + ( requestEnd.tv_nsec - requestStart.tv_nsec ) / BILLION;
printf( " -> Time: %lf\n", accum );

This way we will be able to monitor performances on the original unprotected version and with every different protection settings.

Target machine

In case the target machine running the final deployed program, has fixed specs that differ from the ones where running the tests, it is wise to validate performances on it before finalizing the protection.

For complex protection projects, where big amounts of functions are protected and monitored for performance, consider serializing time measurements to a file, so to ease and automate its evaluation.

4. Identify the best protection mode

For each protected candidate, test independently and identify the best protection mode.

Here is a summary of all modes, organized in a overhead VS security chart: make sure to read the code obfuscation page for further information.

modes matrix

If a test with a given protection modes yields unsatisfying result, try to fall back to the following entry of the table.
Normally with a few iterations of the feedback loop you should be able to reach the optimal solution.

If the kind of function / algorithm you are trying to protect does not allow any code expansion as-is, possibly because it contains loops that iterates a massive amount of times, you can consider the following step.

5. Smart refactorings

The basic idea is to separate heavy worker-loops from the main orchestrating code, which is where, presumably and hopefully, lies the recipe code worth protecting.

Imagine we have a function like follows:

void toProtect(void *someParameter) {
  // initialization code (important, omitted)
  // ...
  // read values (relevant, omitted)
  // ...
  // assigments (ingredient, not very important)
  for(int i=0; i<8388608; i++){
    decrypt(...)
  }
  // important processing (light loop, kept)
  for(int i=0; i=200; i++){
    // important code (omitted)
  }
  // epilog (important, omitted)
  // ...
}

If we consider that most of the overhead is generated by the big assignments loop in the middle and if that's a viable option, we could perform a smart refactoring modifying the code of the function as follows:

void heavyLoop(){
  // assigments
  for(int i=0; i=8388608; i++){
    decrypt(...)
  }
}

void toProtect(void *someParameter) {
  // initialization code (important, omitted)
  // ...
  // read values (relevant, omitted)
  // ...
  // assigments (ingredient, not very important, extracted)
  heavyLoop();
  // important processing (light loop, kept)
  for(int i=0; i=200; i++){
    // important code (omitted)
  }
  // epilog (important, omitted)
  // ...
}

Now, since the not very important loop that was causing most of the overhead has been extract, we should be able to safely protect the toProtect function.

6. Adjusting Solidshield VM settings

Adjusting VM settings provides the highest resolution and thus yields the smallest improvements. It can be considered as the final fine-tuning after all the more macroscopic optimizations above have been performed.


Last update: 2021-08-10