Intel® Tamper Protection Toolkit — обфусцирующий компилятор и средства проверки целостности кода
Совсем недавно компания Intel выпустила очень интересный набор инструментов для разработчиков программного обеспечения, позволяющий добавить защиту программного кода от взлома и существенно усложнить жизнь взломщикам программ. Этот набор включает в себя обфусцирующий компилятор, средство для создания файла подписи, используемого для проверки целостности загружаемых динамических библиотек, а также библиотеку функций проверки целостности и дополнительные полезные инструменты. Intel Tamper Protection Toolkit beta можно совершенно бесплатно скачать на сайте Intel.
Обфусцирующий компилятор
Одним из инструментов Tamper Protection Toolkit является обфусцирующий компилятор — iprot. С помощью этого инструмента можно защитить исполняемый код от статического и динамического анализа/изменения. Результатом работы обфусцирующего компилятора является самошифрующийся, самомодифицирующийся код, сопротивляющийся модифицированию, статическому анализу и функционально эквивалентный начальному коду.
Обфусцирующий компилятор работает с функциями динамических библиотек. Однако очень часто приложения написаны так, что конфиденциальный код располагается внутри приложения. Для того, чтобы этот код защитить, нужно сделать небольшой рефакторинг приложения: выделить конфиденциальные функции в отдельную динамическую библиотеку.
В результаты работы обфусцирующего компилятора будет создана другая динамическая библиотека, функции которой будут подвергнуты обработке. Эти функции далее и надо использовать для работы.
Естественно, защита кода не бывает бесплатной. Одной из плат за защиту является замедление работы кода и увеличение размера кода функций. У обфусцирующего компилятора есть пара параметров, с помощью которых можно контролировать соотношение защита/производительность для кода. Падение производительности можно компенсировать рефакторингом исходного кода, например, использовать инлайн функции, оптимизацию по размеру кода. Можно попробовать поменять параметры обфусцирующего компилятора --mutation-distance и --cell-size.
Параметр --mutation-distance контролирует частоту, с которой происходит само-модификация кода. Меньшая дистанция дает нам лучшую защиту, но ухудшает производительность. Можно попробовать несколько различных значений этого параметра, чтобы добиться оптимальной на ваш взгляд производительности.
Параметр --cell-size контролирует размер декодированного/открытого кода в определенный момент времени. Меньший размер ячейки кода означает лучшую защиту, однако ухудшает производительность. Увеличивая размер ячейки можно существенно улучшить производительность, но и существенно ухудшить защиту кода, т.к. большие участки кода будут доступны (находиться в открытом виде) для анализа. Попробовав различные значения этого параметра, можно добиться оптимальной производительности.
Таким образом можно сказать, что создание защищенного и в тоже время производительного кода не простая задача. Для ее осуществления могут потребоваться не только варьирование параметров обфусцирующего компилятора, но и так же рефакторинг исходного кода, что бы добиться лучшей производительности и меньшего размера исходного кода.
Надо заметить, не всякий код может быть обработан обфусцирующим компилятором. Есть ограничения на исходный код, такие как отсутствие релокаций (relocations), косвенных переходов (inderect jumps) или вызовов функций из других библиотек, код которых недоступен на момент обработки обфусцирующим компилятором.
Далее мы рассмотрим пример, в котором мы постараемся побороться с некоторыми проблемами несовместимости кода, которые могут встретиться в реальной жизни.
Проверка целостности кода динамической библиотеки
Для того, что бы убедиться в том, что загружаемый модуль динамической библиотеки не подвергался модификациям, Tamper Protection Toolkit содержит специальный набор средств. Инструмент создания подписи динамической библиотеки — codebind и небольшую библиотеку функций для проверки целостности библиотеки — codeverify.lib.
Инструмент создания цифровой подписи динамической библиотеки (codebind) принимает на вход имя библиотеки и приватный DSA ключ, который можно сгенерировать, например, с помощью библиотеки OpenSSL*. В результате получается дополнительный файл (secure box), который в дальнейшем будет использоваться для проверки целостности связанной с ним динамической библиотеки.
Проверка целостности динамической библиотеки осуществляется с помощью нескольких функций, являющихся частью Tamper Protection Toolkit. Приложение нужно модифицировать, добавив вызовы API. Функции этого API позволяют проверить целостность динамической библиотеки еще до загрузки в память (статическая проверка), а также проверять, не подвергался ли модификации код библиотеки в процессе выполнения программы (динамическая проверка).
Библиотека криптографических функций
В состав Tamper Protection Toolkit входят несколько базовых криптографических функций:
- Функции создания однонаправленного хэша: HMAC SHA256
- Функции шифрования с симметричным ключом: AES (CTR/GCM)
- И некоторые другие.
Все эти функции могут быть использованы в конфиденциальном коде и обработаны обфусцирующим компилятором, входящим в состав Tamper Protection Toolkit.
Таким образом, с помощью инструментов и функций Tamper Protection Toolkit можно создать надежную защиту конфиденциального кода или информации, содержащейся в пользовательском приложении. Обфусцирующий компилятор позволяет создать самошифрующийся, самомодифицирующийся код, сопротивляющийся модифицированию и статическому анализу. Инструмент Codebind поможет в создании файла цифровой подписи, а библиотека функций проверки целостности поможет проверить, не модифицировались ли функции динамической библиотеки как на диске, до загрузки, так и после загрузки кода в память. Функции крипто библиотеки помогут в создании криптографических алгоритмов и защиты их с помощью обфусцирующего комплиятора.
Пример обфускации кода
В этом примере мы покажем как можно защитить код с помощью обфусцирующего компилятора, входящего в состав Intel Tamper Protection Toolkit. А так же мы покажем, как можно избавиться от некоторых проблем, которые могут возникнуть в процессе обфускации кода.
Что нам понадобится для этого примера
- Понимание базовых основ С/С++. Знакомство в общих чертах с релокациями (relocations) и косвенными переходами (indirect jump).
- С/С++ компилятор, например, Intel; Compiler или Microsoft Visual Studio*
- Intel Tamper Protection Toolkit
В этом примере используются комманды компилятора Visual Studio, однако аналогичным образом можно собрать код и любым другим компилятором.
Обфусцирующий компилятор, входящий в набор средств Intel Tamper Protection Toolkit, принимает на вход путь к динамической библиотеке (dll/so), имя функции, которую надо будет обфусцировать и параметры обфускации. Поэтому для работы нам сначала надо создать динамическую библиотеку. Пример для создания динамической библиотеки можно найти на диске папке tutorials\obfuscation_tutorial.
Для сборки динамической библиотеки воспользуемся Visual Studio*. Запустите командную строку Visual Studio, наберите следующую команду:
cl /GS- /GR- src_compatible.c /link /DLL /NOENTRY /OUT:.\src_compatible.dll
В результате выполнения этой комманды должна появиться src_compatible.dll динамическая библиотека. Для компиляции были использованы следующие опции
- /GR- — эта опция выключает проверку типов объектов в процессе выполнения
- /GS- — эта опция выключает проверку переполнения.
- /NOENTRY — эта опция отключает создание main entry в динамической библиотеке.
Чтобы тест заработал, скомпилируйте тестовое приложение loadutil.c и related.h.
cl loadutil.c /link /OUT:.\loadutil.exe
После того, как соберется тестовое приложение, можно запустить тест и проверить, что собранная нами динамическая библиотека работает:
loadutil src_compatible.dll
Приложение должно написать Called get_symbol () function successfully в командном окне.
Далее, создадим обфускированную версию нашей динамической библиотеки с помощью следующей команды:
iprot src_compatible.dll -o obfuscated.dll get_symbol
На диске должна появиться библиотека obfuscated.dll, имя которой мы и подадим на вход нашего тестового приложения:
loadutil obfuscated.dll
Приложение снова должно написать Called get_symbol () function successfully в командном окне. Таким образом, обфусцированная библиотека функционально полностью эквивалентна не обфусцированной библиотеки. Однако, если вы взгляните на динамические библиотеки с помощью HEX редактора, вы обнаружите, что понять код обфусцированной библиотеки практически невозможно.
Итак, мы построили нашу первую простейшую обфусцированную динамическую библиотеку. Давайте посмотрим, какие проблемы и трудности могут встретится при обфусцировании более сложного С/С++ кода и как с этим бороться.
Советы, как обойти подводные камни
Обфусцирующий компилятор, входящий в состав Tamper Protection Toolkit, имеет несколько ограничений на код, который он пытается обработать, а именно:
- Код, генерирующий релокации (relocations)
- Код, генерирующий косвенные переходы (inderect jumps)
- Код, генерирующий PIC глобальные ссылки (специфика Anroid*)
Язык С содержит довольно много конструкций, которые могут генерировать косвенные переходы и релокации. Далее мы посмотрим на некоторые примеры таких конструкций и способы, как можно обойти генерацию нежелательного кода.
Рассмотрим пример, содержащий код, который не может обработать обфусцирующий компилятор — src_incompatible.c. Его можно взять в папке tutorials/obfuscation_tutorial.
Сперва построим динамическую библиотеку из этого файла:
cl /GS- /GR- src_incompatible.c /link /DLL /NOENTRY /OUT:.\Incompatible.dll
Далее, обфускируем одну из функций этой динамической библиотеки:
iprot Incompatible.dll -o Obfuincompatible.dll get_symbol
В результате, вы должны увидеть примерно следующее сообщение:
[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:0×10001010:12<-0]
iprot: unsupported memory reference with relocation in acquired code at 0×1000101c:
mov al, byte ptr [eax+10002000h]
Обфусцирующий компилятор встретил релокацию и не смог продолжить обработку. Это произошло потому, что в функции get_symbol () используется обращение к глобальной переменной alphabet. Компилятор генерирует релокацию, которую не может обработать обфусцирующий компилятор. Один из способов избавиться от релокации — передать указатель как параметр при вызове функции:
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 ' ';
}
Можно поступить иначе, вместо глобальных данных использовать локальную переменную.
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 ' ';
}
Далее, обфусцируем нашу библиотеку со следующей командой:
iprot Incompatible.dll -o Obfuincompatible.dll get_next_state
В результате, должно получиться следующее:
[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:0×10001030:18<-0]
iprot: unsupported indirect jump in acquired code at 0×10001055:
jmp dword ptr [100010A0h+edx*4]
Обфусцирующий компилятор встретил косвенный переход, который не смог обработать.
Если заглянуть в код функции get_next_state () можно увидеть использование switch, который и генерирует косвенный переход. От косвенного перехода можно легко избавиться используя if-else if.
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;
}
Для того, чтобы избавиться от генерации PIC кода для Android, можно использовать опцию компиляции -fno-pic.
Пример проверки целостности кода
В этом примере мы покажем, как использовать функции программного обеспечения, называющиеся связывание (binding) и проверка целостности (integrity verification) бинарных данных, предоставляемых Tamper Protection Toolkit. Эти функции могут помочь вам существенным образом осложнить жизнь взломщикам вашего программного кода. Функция проверки целостности проверяет данные на диске, а так же бинарный код, загруженный в память программы. Следуя шагам из этого примера, вы научитесь как можно использовать связывание и проверку кода.
Что нам понадобится для этого примера
- Понимание базовых основ С/С++. Знакомство в общих чертах с релокациями (relocations) и косвенными переходами (indirect jump).
- С/С++ компилятор, например, Intel Compiler или Microsoft Visual Studio*
- OpenSSL* библиотека для генерации ключей.
- Intel Tamper Protection Toolkit
В этом примере используются команды компилятора Visual Studio, однако аналогичным образом можно собрать код и любым другим компилятором.
Давайте начнем со сборки первого компонента нашего примера — динамическую библиотеку module.dll. Исходный код можно найти на диске в папке tutorials/code_verification.
cl module.c /link /DLL /OUT:module.dll
Далее соберем приложение, использующее функции из динамической библиотеки.
cl sample_app_without_verification.cpp /link module.lib
Если запустить собранное приложение, то вы должны увидеть следующие
>sample_app_without_verification
sum (3,5) returns 8
sum (3,5) + global_array[3] returns 12
Теперь все готово для защиты нашей динамической библиотеки с помощью функций связывания и проверки целостности. С помощью одного из средств Tamper Protection Toolkit мы создадим специальный файл подписи с расширением .sb, называемый «secure box». Этот файл содержит данные, используемые функциями тулкита для проверки целостности динамической библиотеки module.dll.
Для начала воспользуемся OpenSSL, что бы сгенерировать необходимые ключи
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
Для генерации файла подписи воспользуемся программой codebind.exe. Входными данными для функции связывания кода с подписью является динамическая библиотека и приватный ключ, сгенерированный заранее. Результатом процесса связывания является файл подписи — «secure box». Имя файла подписи и его расширение произвольно, на выбор пользователя. В этом примере будет использоваться расширение ».sb». Команда для связывания выглядит так:
codebind -i -k -o
Что бы связать нашу динамическую библиотеку с помощью приватного ключа сгенерированного с помощью OpenSSL запустите следующую команду:
codebind -i module.dll -k keys/prikey.pem -o module.sb
Если ваша платформа не поддерживает «Intel Secure Key» вы увидите следующее сообщение:
codebind: Intel Secure Key (RDRAND instruction) is not supported.
Use »--seed» program option
В этом случае выполните следующую команду используя ключ »--seed» со случайным числом:
codebind -i module.dll -k keys/prikey.pem -o module.sb --seed 0xabba
В случае успешного связывания, в директории должен появиться файл «module.sb», его мы будем использовать для проверки целостности нашей динамической библиотеки.
Итак, к настоящему моменту у нас есть динамическая библиотека — module.dll, содержащая функциональность, которую мы хотим защитить. Приложение «sample_app_without_verification», вызывающее функции из нашей динамической библиотеки. Пара ключей: публичный и приватный, последний мы использовали для создания файла подписи — «module.sb».
Следующим шагом нашего примера будет добавление кода, который будет проверять целостность нашей динамической библиотеки статически и динамически в процессе вызова функций из нее.
Первый шаг на этом пути — превратить публичный ключ в вид, который можно будет использовать в коде проверки целостности. Для этого можно воспользоваться инструментом под названием «bin2hex», который принимает на входе публичный ключ и генерирует текстовый файл (».h»), который содержит публичный ключ, в виде пригодном для «C» компиляции.
bin2hex keys/pubkey.der pubkey.h
Последующие шаги помогут нам добавить проверку целостности нашей динамической библиотеки в наше приложение sample_app_without_verification.cpp. После того, как все кусочки кода будут добавлены в исходный файл, должен получиться код, совпадающий с файлом sample_app_with_verification.cpp.
Итак, начнем с включения нужных нам файлов заголовков:
#include "codeverify.h"
#include "pubkey.h"
#include
#include
#if defined(_WIN32)
#include
#else
#include
#endif
Далее, добавим коды для обработки ошибок и декларацию нужных переменных и функций:
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);
Добавим код, для загрузки нашей динамической библиотеки и файла подписи
#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 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;
}
В конец файла добавим функцию чтения из файлов, используемую в коде:
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 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;
}
Инициализация контекста проверки целостности происходит внутри функции InitVerification (), которая принимает указатель на файл и данные из файла подписи в качестве входных параметров.
//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;
}
Добавим реализацию функции InitVerification () в конец файла:
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;
}
После инициализации контекста проверки целостности все готово для того, чтобы добавить динамическую проверку целостности динамической библиотеки, загруженной в память. Это делается с помощью вызова функции CodeVerify_Verify (). Эту функцию можно вызывать неограниченное количество раз, в разных местах кода, чтобы проверить целостность загруженной библиотеки. У этой функции есть входной параметр -work_factor, с помощью которого можно уменьшить размер проверяемой памяти загруженной библиотеки. Например, при work_factor = 3 полная проверка загруженной динамической библиотеки завершится за три вызова функции. Переменная pass_count, передаваемая каждый раз при вызове функции CodeVerify_Verify (), содержит количество полных проверок динамической библиотеки.
Если вы используете глобальные переменные и не хотите, чтобы кто-либо их изменял в процессе выполнения кода приложения, можно воспользоваться функцией CodeVerify_IsRangeSafe () чтобы убедиться, что целостность интересующих вас данных проверяется вызовом функции CodeVerify_Verify ():
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;
После того, как вы завершите использовать функции библиотеки codeverify, не забудьте высвободить использованную для проверки целостности память и выгрузить динамическую библиотеку из памяти:
#if defined _WIN32
if(c_verifier) delete [](char*)c_verifier;
#else
if(handle) dlclose(handle);
if(c_verifier) delete [](char*)c_verifier;
#endif
Для сборки программы с включенным кодом проверки целостности можно воспользоваться следующей командой компиляции:
cl sample_app_without_verification.cpp /I../../inc /link ../../lib/win-x86/codeverify.lib module.lib
Если компиляция прошла успешно и приложение было успешно создано, можно его запустить и вы должны получить следующее:
> sample_app_without_verification
Range verification was successfully done!
sum (3,5) returns 8
sum (3,5) + global_array[3] returns 12
Если попробовать изменить код функций библиотеки module.dll, пересобрать ее и попробовать запустить с тестовым приложением, не пересоздавая файла цифровой подписи, проверка целостности должна заметить подмему и выдать ошибку:
> sample_app_without_verification
InitVerification failed! Error code: -6
Вот пожалуй и все, что нужно сделать с помощью инструментов Intel Tamper Protection Toolkit, чтобы проверить целостность загружаемой динамической библиотеки.
Надеюсь данный краткий экскурс в инструмент Intel Tamper Protection Toolkit был полезен и помог вам понять, что он из себя представляет, что умеет делать и как им можно воспользоваться для защиты своего программного обеспечения. Удачи!
* Другие наименования и товарные знаки являются собственностью своих законных владельцев.