📜 ⬆️ ⬇️

Intel® Tamper Protection Toolkit - Obfuscating compiler and code integrity checking tools


Most recently, Intel has released a very interesting set of tools for software developers, which allows you to add protection from hacking software and significantly complicate the life of program hackers. This set includes an obfuscating compiler, a tool for creating a signature file used to check the integrity of loadable dynamic libraries, as well as a library of integrity checking functions and additional useful tools. Intel® Tamper Protection Toolkit beta can be downloaded for free on the Intel website .

Obfusts compiler


One of the tools of the Tamper Protection Toolkit is the obfuscating compiler - iprot. With this tool you can protect the executable code from static and dynamic analysis / changes. The result of the obfuscating compiler is a self-encrypting, self-modifying code that resists modification, static analysis and is functionally equivalent to the initial code.

The obfustive compiler works with dynamic library functions. However, very often applications are written in such a way that the confidential code is located inside the application. In order to protect this code, you need to do a little refactoring of the application: select confidential functions into a separate dynamic library.


')
The results of the obfuscating compiler will create another dynamic library, the functions of which will be processed. These functions further and should be used for work.

Naturally, code protection is not free. One of the protection fees is to slow down the code and increase the size of the function code. The obfuscating compiler has a couple of parameters with which you can control the protection / performance ratio for the code. The performance drop can be compensated by refactoring the source code, for example, using inline functions, optimization by code size. You can try to change the parameters of the obfuscating compiler --mutation-distance and --cell-size .

The --mutation-distance parameter controls the frequency with which code self-modification occurs. A shorter distance gives us better protection but degrades performance. You can try several different values ​​of this parameter to achieve optimal performance in your opinion.

The --cell-size parameter controls the size of the decoded / open code at a specific point in time. A smaller code cell size means better protection, however, performance deteriorates. Increasing the cell size can significantly improve performance, but also significantly degrade code protection, since large parts of the code will be available (to be in open form) for analysis. By trying different values ​​of this parameter, you can achieve optimal performance.

Thus, it can be said that creating a protected and at the same time productive code is not an easy task. Its implementation may require not only varying the parameters of the obfuscating compiler, but also refactoring the source code in order to achieve better performance and smaller source code size.

It should be noted that not every code can be processed by an obfuscating compiler. There are restrictions on the source code, such as the absence of relocations, indirect jumps or function calls from other libraries, the code of which is not available at the time of processing by the obfuscating compiler.

Next, we will look at an example in which we will try to deal with some code incompatibility problems that can occur in real life.

Check the integrity of the code of the dynamic library


In order to make sure that the loadable module of the dynamic library has not been modified, the Tamper Protection Toolkit contains a special set of tools. The dynamic library signature creation tool - codebind and a small library of functions for checking the integrity of the library - codeverify.lib.



The dynamic signature digital signature tool (codebind) accepts a library name and a private DSA key that can be generated, for example, using the OpenSSL * library. The result is an additional file (secure box), which will later be used to check the integrity of the associated dynamic library.

Checking the integrity of the dynamic library is carried out using several functions that are part of the Tamper Protection Toolkit. The application needs to be modified by adding API calls. The functions of this API allow you to check the integrity of the dynamic library even before loading it into memory (static checking), and also to check whether the library code has been modified during the execution of the program (dynamic checking).

Cryptographic Library


The Tamper Protection Toolkit includes several basic cryptographic functions:

All these functions can be used in a confidential code and processed obfustsiruyuschim compiler, which is part of the Tamper Protection Toolkit.

Thus, using the tools and functions of the Tamper Protection Toolkit, you can create a reliable protection of confidential code or information contained in a user application. Obfusts compiler allows you to create self-encrypting, self-modifying code that resists modification and static analysis. The Codebind tool will help in creating a digital signature file, and the library of integrity checking functions will help to check whether the functions of the dynamic library have been modified both on disk, before loading, and after loading the code into memory. The functions of the crypto library will help in the creation of cryptographic algorithms and their protection with the help of an obfuscating compiler.

Code Obfuscation Example


In this example, we will show how you can protect the code with the obfuscating compiler, included with the Intel® Tamper Protection Toolkit. And also we will show how you can get rid of some problems that may arise in the process of obfuscation of the code.

What we need for this example

In this example, the commands used by the Visual Studio compiler are used, however, in the same way, code can be compiled with any other compiler.

The obfuscating compiler, included in the Intel® Tamper Protection Toolkit, takes as input the path to the dynamic library (dll / so), the name of the function to be obfuscated and the obfuscation parameters. Therefore, to work, we first need to create a dynamic library. An example for creating a dynamic library can be found on the disk in the tutorials \ obfuscation_tutorial folder.

To build a dynamic library, use Visual Studio *. Run Visual Studio command line, type the following command:

cl /GS- /GR- src_compatible.c /link /DLL /NOENTRY /OUT:.\src_compatible.dll 

As a result of the execution of this command, the src_compatible.dll dynamic library should appear. The following options were used for compilation.

To make the test work, compile the loadutil.c test application and related.h .

 cl loadutil.c /link /OUT:.\loadutil.exe 

After the test application is assembled, you can run the test and check that the dynamic library we have compiled works:

 loadutil src_compatible.dll 

The application should write Called get_symbol () function successfully in the command window.

Next, create an obfuscated version of our dynamic library using the following command:

 iprot src_compatible.dll -o obfuscated.dll get_symbol 

The obfuscated.dll library should appear on the disk, the name of which we give to the input of our test application:

 loadutil obfuscated.dll 

The application should write Called get_symbol () function successfully in the command window again. Thus, an obfuscated library is functionally fully equivalent to an unobfuscated library. However, if you take a look at dynamic libraries with the HEX editor, you will find it almost impossible to understand the code of the obfuscated library.
So, we built our first simple obfuscated dynamic library. Let's see what problems and difficulties may be encountered in obfuscating more complex C / C ++ code and how to deal with it.

Tips on how to get around the pitfalls


The obfustive compiler included in the Tamper Protection Toolkit has several limitations on the code that it tries to process, namely:

The C language contains quite a few constructs that can generate indirect transitions and relocations. Next, we look at some examples of such constructions and ways how to bypass the generation of unwanted code.

Consider an example that contains code that the obfuscating compiler cannot handle - src_incompatible.c . It can be taken in the tutorials / obfuscation_tutorial folder.
First we build a dynamic library from this file:

 cl /GS- /GR- src_incompatible.c /link /DLL /NOENTRY /OUT:.\Incompatible.dll 

Next, let's obfusk one of the functions of this dynamic library:

 iprot Incompatible.dll -o Obfuincompatible.dll get_symbol 

As a result, you should see something like the following message:

[parsing_flow-1]: Processing 'get_symbol' ...
[warning-1]: warning: minimal mutations detected at top level loop; adding more
[scheduling-1]: Setting top level mutation distance: 1
[analysis-1]:
[PROC 0: 0x10001010: 12 <-0]

iprot: unsupported memory reference with relocation in acquired code at 0x1000101c:
mov al, byte ptr [eax + 10002000h]

The obfuscating compiler met relocation and could not continue processing. This happened because the get_symbol () function uses a call to the global variable alphabet . The compiler generates a relocation that the obfuscating compiler cannot handle. One way to get rid of relocation is to pass a pointer as a parameter when calling a function:
Decision number 1
  char API get_symbol(char const* alphabet_data, unsigned int alphabet_size, unsigned int s_idx) { if (s_idx < alphabet_size) return alphabet_data[s_idx]; return ' '; } 


You can do otherwise, instead of using global data, use a local variable.
Decision number 2
  char API get_symbol_second(unsigned int s_idx) { char alphabet_local[26]; alphabet_local[0] = 'a'; alphabet_local[1] = 'b'; alphabet_local[2] = 'c'; alphabet_local[3] = 'd'; alphabet_local[4] = 'e'; alphabet_local[5] = 'f'; alphabet_local[6] = 'g'; alphabet_local[7] = 'h'; alphabet_local[8] = 'i'; alphabet_local[9] = 'j'; alphabet_local[10] = 'k'; alphabet_local[11] = 'l'; alphabet_local[12] = 'm'; alphabet_local[13] = 'n'; alphabet_local[14] = 'o'; alphabet_local[15] = 'p'; alphabet_local[16] = 'q'; alphabet_local[17] = 'r'; alphabet_local[18] = 's'; alphabet_local[19] = 't'; alphabet_local[20] = 'u'; alphabet_local[21] = 'v'; alphabet_local[22] = 'w'; alphabet_local[23] = 'x'; alphabet_local[24] = 'y'; alphabet_local[25] = 'z'; if (s_idx < sizeof(alphabet_local)) return alphabet_local[s_idx]; return ' '; } 

Next, let's obfuscate our library with the following command:

 iprot Incompatible.dll -o Obfuincompatible.dll get_next_state 

As a result, you should get the following:

[parsing_flow-1]: Processing 'get_next_state' ...
[warning-1]: warning: minimal mutations detected at top level loop; adding more
[scheduling-1]: Setting top level mutation distance: 2
[analysis-1]:
[PROC 0: 0x10001030: 18 <-0]

iprot: unsupported code in 0x10001055:
jmp dword ptr [100010A0h + edx * 4]

Obfustsiruyuschy compiler met indirect transition, which could not handle.
If you look at the get_next_state () function code, you can see the use of a switch , which generates an indirect transition. You can easily get rid of the indirect transition using an if-else if .
Decision
  my_state API get_next_state(my_state in_state) { if(ST_UNINITIALIZED == in_state) return ST_CONNECTING; if(ST_CONNECTING == in_state) return ST_NEGOTIATING; if(ST_NEGOTIATING == in_state) return ST_INITIALIZING; if(ST_INITIALIZING == in_state) return ST_PROCESSING; if(ST_PROCESSING == in_state) return ST_DISCONNECTING; if(ST_DISCONNECTING == in_state) return ST_FINISHED; return ST_UNINITIALIZED; } 


In order to get rid of the generation of PIC code for Android, you can use the compile -fno-pic option.

Code integrity check example


In this example, we will show how to use software features called binding and binary data integrity provided by the Tamper Protection Toolkit. These functions can help you significantly complicate the life of hackers of your program code. The integrity check function checks the data on the disk, as well as the binary code loaded into the program memory. Following the steps in this example, you will learn how to use code binding and validation.

What we need for this example

This example uses the commands of the Visual Studio compiler, however, in the same way, you can build code with any other compiler.

Let's start by building the first component of our example, the dynamic module.dll . The source code can be found on disk in the tutorials / code_verification folder .

 cl module.c /link /DLL /OUT:module.dll 

Next, build an application that uses functions from the dynamic library.

 cl sample_app_without_verification.cpp /link module.lib 

If you run the compiled application, you should see the following

> sample_app_without_verification
sum (3,5) returns 8
sum (3,5) + global_array [3] returns 12

Now everything is ready to protect our dynamic library using the functions of binding and integrity checking. Using one of the Tamper Protection Toolkit tools, we will create a special signature file with the .sb extension, called the “secure box”. This file contains the data used by the toolkit functions to verify the integrity of the module.dll dynamic library.
First we use OpenSSL to generate the necessary keys.

 md keys openssl dsaparam -out keys/dsaparam.pem 2048 openssl gendsa -out keys/prikey.pem keys/dsaparam.pem openssl dsa -in keys/prikey.pem -outform der -out keys/pubkey.der -pubout 

To generate the signature file, we will use the codebind.exe program. The input to the code binding function with a signature is a dynamic library and a private key generated in advance. The result of the binding process is a signature file - a “secure box”. The name of the signature file and its extension is arbitrary, at the user's choice. In this example, the extension ".sb" will be used. The command to bind looks like this:

 codebind -i <path to the dll/so file> -k <path to private key> -o <output path to secure box file> 

To link our dynamic library using a private key generated using OpenSSL, run the following command:

 codebind -i module.dll -k keys/prikey.pem -o module.sb 

If your platform does not support the “Intel® Secure Key” you will see the following message:

codebind: Intel® Secure Key (RDRAND instruction) is not supported.
Use "--seed" program option

In this case, issue the following command using the "--seed" key with a random number:

 codebind -i module.dll -k keys/prikey.pem -o module.sb --seed 0xabba 

In case of successful linking, the file “module.sb” should appear in the directory, we will use it to check the integrity of our dynamic library.

So, by the present moment we have a dynamic library - module.dll, containing the functionality that we want to protect. The application “sample_app_without_verification”, calling functions from our dynamic library. A pair of keys: public and private, the last one we used to create a signature file - “module.sb”.

The next step of our example will be to add code that will check the integrity of our dynamic library statically and dynamically in the process of calling functions from it.

The first step on this path is to turn the public key into a form that can be used in the integrity check code. To do this, you can use a tool called “bin2hex”, which takes as input a public key and generates a text file (".h"), which contains the public key, in the form suitable for "C" compilation.
 bin2hex keys/pubkey.der pubkey.h 


The following steps will help us add the integrity check of our dynamic library to our application sample_app_without_verification.cpp. After all the pieces of code are added to the source file, you should get a code that matches the sample_app_with_verification.cpp file.
So let's start by including the header files we need:

  #include "codeverify.h" #include "pubkey.h" #include <fstream> #include <memory> #if defined(_WIN32) #include <windows.h> #else #include <dlfcn.h> #endif 


Next, add error handling codes and declare the necessary variables and functions:

Error codes and announcements
  enum { V_STATUS_OK = 0, /*!< Indicates no error */ V_STATUS_NULL_PTR = -1, /*!< Input argument is null pointer */ V_STATUS_BAD_ARG = -2, /*!< Bad input argument */ V_STATUS_KEY_GETSIZE_FAILED = -3, /*!< Key get size failed */ V_STATUS_KEY_INIT_FAILED = -4, /*!< Key init failed */ V_STATUS_VER_GETSIZE_FAILED = -5, /*!< Verification get size failed */ V_STATUS_VER_INIT_FAILED = -6, /*!< Verification init failed */ V_STATUS_VERIFICATION_FAILED = -7, /*!< Verification failed */ V_STATUS_RANGE_SAFE_FAILED = -8, /*!< Is Range Safe failed */ V_STATUS_ERR = -9 /*!< Unexpected error */ }; CodeVerify *c_verifier = 0; unsigned char * ReadFromFile(const string & file_name, unsigned int &fsize); int InitVerification(void *handle, unsigned char *sb, unsigned int sb_size); 


Add code to load our dynamic library and signature file

Variable declaration and file reading
 #if defined _WIN32 string dll_name = "module.dll"; //path to dll. string sb_name = "module.sb"; //path to sb. #else string dll_name = "libmodule.so"; //path to shared library. string sb_name = "module.sb"; //path to sb. #endif //Read SB to buffer: unique_ptr<unsigned char[]> sb; unsigned int sb_size = 0; sb.reset(ReadFromFile(sb_name, sb_size)); if(!sb) { cout << "SB file reading failed!" << endl; return V_STATUS_ERR; } 


At the end of the file we add the function of reading from files used in the code:

File read function
  unsigned char * ReadFromFile(const string & file_name, unsigned int &fsize) { ifstream f; f.open (file_name, ifstream::in | ifstream::binary); if(f) { f.seekg(0, ios::end); unsigned int size = (unsigned int)f.tellg(); f.seekg(0, ios::beg); // allocate memory to contain file data unique_ptr<unsigned char[]> res(new unsigned char[size]); f.read((char*)res.get(), size); if(!f) { f.close(); return 0; } f.close(); fsize = size; return res.release(); } return 0; } 


The integrity check context is initialized within the InitVerification () function, which takes a pointer to the file and the data from the signature file as input parameters.

Initialization
  //Get DLL handle: #if defined _WIN32 HMODULE handle = GetModuleHandle(dll_name.c_str()); #else Dl_info dl_info; dladdr((void*)sum, &dl_info); void *handle = dlopen(dl_info.dli_fname, RTLD_NOW); #endif if(!handle) { cout << "Dll handle can't be obtained, dll-name: " << dll_name.c_str() << endl; return V_STATUS_ERR; } int ret = V_STATUS_OK; ret = InitVerification(handle, sb.get(), sb_size); if(V_STATUS_OK != ret) { cout << "InitVerification failed! Error code: " << ret << endl; #if defined _WIN32 if(c_verifier) delete [](char*)c_verifier; #else if(handle) dlclose(handle); if(c_verifier) delete [](char*)c_verifier; #endif return V_STATUS_ERR; } 


Add the implementation of the InitVerification () function to the end of the file:

InitVerification () function
 int InitVerification(void *handle, unsigned char *sb, unsigned int sb_size) { DECLARE_pubkey_der; VerificationKey *m_verifier = 0; unsigned int size = 0; int err = V_STATUS_ERR; if(!handle || !sb) return V_STATUS_NULL_PTR; if(!sb_size) return V_STATUS_BAD_ARG; DEFINE_pubkey_der; //Get size of VerificationKey context: if(VK_STATUS_OK != VerificationKey_GetSize(pubkey_der, sizeof(pubkey_der), &size)) { return V_STATUS_KEY_GETSIZE_FAILED; } //VerificationKey context memory allocation: m_verifier = (VerificationKey *)(new char[size]); //Init VerificationKey context: if(VK_STATUS_OK != VerificationKey_Init(m_verifier, pubkey_der, sizeof(pubkey_der))) { err = V_STATUS_KEY_INIT_FAILED; if(m_verifier) delete [] (char*)m_verifier; return err; } //Get size of CodeVerify context: if(CV_STATUS_OK != CodeVerify_GetSize(sb,sb_size,m_verifier,&size)) { err = V_STATUS_VER_GETSIZE_FAILED; if(m_verifier) delete [] (char*)m_verifier; return err; } //CodeVerify context memory allocation: c_verifier = (CodeVerify*)(new unsigned char[size]); //Init CodeVerify context: if(CV_STATUS_OK != CodeVerify_Init(c_verifier,size,(const void *)handle,sb,sb_size,m_verifier)) { err = V_STATUS_VER_INIT_FAILED; if(m_verifier) delete [] (char*)m_verifier; return err; } err = V_STATUS_OK; if(m_verifier) delete [] (char*)m_verifier; return err; } 


After the integrity check context is initialized, everything is ready to add a dynamic integrity check of the dynamic library loaded into memory. This is done by calling the CodeVerify_Verify () function. This function can be called an unlimited number of times, in different places in the code, to check the integrity of the loaded library. This function has an input parameter -work_factor , with which you can reduce the size of the checked memory of the loaded library. For example, with work_factor = 3, a full check of the loaded dynamic library will complete in three function calls. The variable pass_count , which is passed each time the CodeVerify_Verify () function is called , contains the number of full checks of the dynamic library.
If you use global variables and do not want someone to change them during the execution of the application code, you can use the CodeVerify_IsRangeSafe () function to verify that the integrity of the data you are interested in is checked by calling the CodeVerify_Verify () function:

Variable checking and shutdown
  ret = CodeVerify_IsRangeSafe(c_verifier, global_array, sizeof(global_array)); if(CV_STATUS_OK != ret) { cout << "IsRangeSafe failed!" << endl; #if defined _WIN32 if(c_verifier) delete [](char*)c_verifier; #else if(handle) dlclose(handle); if(c_verifier) delete [](char*)c_verifier; #endif return V_STATUS_RANGE_SAFE_FAILED; } cout << "Range verification was successfully done!" << endl; 


After you finish using the codeverify library functions , do not forget to free up the memory used to check the integrity and unload the dynamic library from the memory:

Resource release
 #if defined _WIN32 if(c_verifier) delete [](char*)c_verifier; #else if(handle) dlclose(handle); if(c_verifier) delete [](char*)c_verifier; #endif 


To build a program with the integrity check code enabled, you can use the following compilation command:

 cl sample_app_without_verification.cpp /I../../inc /link ../../lib/win-x86/codeverify.lib module.lib 

If the compilation was successful and the application was successfully created, you can run it and you should get the following:

> sample_app_without_verification
Range verification was successfully done!
sum (3,5) returns 8
sum (3,5) + global_array [3] returns 12

If you try to change the code of the module.dll functions, rebuild it and try to run it with the test application, without recreating the digital signature file, the integrity check should notice the dof and give an error:

> sample_app_without_verification
InitVerification failed! Error code: -6

That's probably all you need to do with the Intel® Tamper Protection Toolkit to check the integrity of the loadable dynamic library.

I hope this brief excursion into the Intel® Tamper Protection Toolkit was helpful and helped you understand what it is, what it can do and how it can be used to protect its software. Good luck!

* Other names and trademarks are the property of their respective owners.

Source: https://habr.com/ru/post/273183/


All Articles