В нашей статье мы покажем, как Intel Tamper Protection Toolkit позволяет защитить критически важные участки кода и ценные данные в утилите шифрования Scrypt против статического/динамического реверс-инженеринга и изменений. Scrypt – наиболее новая и безопасная функция выработки ключа по паролю, широко применяемая на практике. Однако существует угроза фальсификации параметров функции scrypt, что приведет к появлению уязвимостей пароля пользователя. Инструментарий позволяет уменьшить эти угрозы. Мы определяем модель угроз для рассматриваемого приложения, объясняем как провести его рефакторинг для дальнейшей защиты, учитывая особенности применения инструмента Tamper Protection к приложению.
Основной целью этой статьи является демонстрация возможностей Intel Tamper Protection Toolkit по защите от атак на критически важные участки кода и ценные данные, находящиеся в реальных приложениях. Инструментарий позволяет противодействовать статическому и динамическому реверс-инженерингу путем обфускации и препятствовать внесению изменений в защищаемое приложение путем контроля целостности во время выполнения.

Здесь мы рассмотрим только один компонент инструментария, называемый iprot, который используется для обфускации, и применим его к утилите шифрования Scrypt версии 1.1.6. Утилита представляет собой простую функцию scrypt выработки ключа на основе введенного пароля. Выбор в пользу нее был сделан по нескольким причинам. Во-первых, код ее функций содержит часто используемые приложениями возможности: операции чтения и записи файлов, распределение памяти, криптографические функции и системные вызовы. Во-вторых, она включает специфичный математический аппарат. В-третьих, утилита достаточно компактна, но позволяет при этом показать широкий круг проблем, с которыми могут столкнуться разработчики на практике в процессе защиты их собственных приложений. Наконец, scrypt является современной и безопасной функцией выработки ключа, которая активно используется на практике, например, новое шифрование диска, базирующееся на scrypt, встроено в Android 4.4.

Обфускация кода


Рассмотрим пример исходного кода для функции sensitive, который представлен в Листинге ниже, и скомпилируем для него динамическую библиотеку.

#define MODIFIER (0xF00D)
int __declspec(dllexport) sensitive(const int value) {
    int result = 0;
    int i;
    for (i = 0; i < value; i++) {
        result += MODIFIER;
    }
    return result;
}

Исходный код функции sensitive

Сделаем реверс-инжениринг получившейся библиотеки, используя IDA Pro. Рисунок демонстрирует порядок исполнения кода с логикой и данными для вычисления. Таким образом хакер сможет легко увидеть значение MODIFIER в коде и поменять его.


Порядок исполнения в дизассемблированном коде

Различные приемы обфускации кода могут помочь спрятать детали реализации, усложнить реверс-инжениринг и предотвратить изменения кода. Обфускация кода – это процесс преобразования кода в такой, для которого сложно сделать реверс-инжениринг и понять логику и данные программы, при этом код будет иметь прежнее функциональное назначение. Обфускация используется, чтобы избежать кражи секретных данных в коде, их изменений и защитить интеллектуальную собственность разработчика.

Intel Tamper Protection Toolkit


Intel Tamper Protection Toolkit – это продукт, который используется для обфускации кода и проверки целостности приложения во время выполнения для исполняемых файлов под ОС Microsoft Windows* и Android*. Применяя Tamper Protection Toolkit, можно защитить ценный код и данные в приложении от статического и динамического реверс-инженеринга и внесения изменений. Исполняемые файлы, защищенные с помощью инструмента, не требуют специальных загрузчиков или дополнительного программного обеспечения и могут быть запущены на любом процессоре Intel.
Инструментарий Intel Tamper Protection Toolkit Beta можно скачать здесь.
В этой статье мы будем использовать следующие компоненты Intel Tamper Protection Toolkit, для того чтобы обфусцировать критические участки кода и защитить утилиту шифрования от возможных атак:
  • iprot – обфускатор, создающий самомодифицирующийся и самозашифровывающийся код;
  • crypto library – библиотека с набором базовых криптографических операций: алгоритмы безопасного хеширования, коды аутентификации (проверки подлинности) сообщений и симметричные шифры.

Обфускатор получает на вход динамическую библиотеку (.dll) и список экспортных функций. На выходе получается динамическая библиотека с обфусцированными экспортными функциями. Начиная с их адресов, код поданной на вход динамической библиотеки разбирается и преобразуется в специальное внутреннее представление. Ветвления, переходы и вызовы, если они достижимы, также разбираются и преобразуются. Для того, чтобы код можно было обфусцировать, следует учитывать несколько ограничений. В коде не может быть низкоуровневой работы с памятью, внешних недостижимых вызовов функций, непрямых переходов и глобальных переменных.
В этой статье мы рассказываем, какие подводные камни встречались при внесении изменений в код с целью его обфускации и каким образом можно обойти возникшие трудности.
Обфусцируем динамическую библиотеку, рассмотренную в предыдущей секции, используя iprot:
iprot sensitive.dll sensitive -o sensitive_obf.dll

Попробуем сделать реверс инжениринг обфусцированного кода, используя IDA Pro.

sensitive PROC NEAR
        jmp     ?_001                                   
?_001:  push    ebp                                     
        push    eax                                     
        call    ?_002                                   
?_002   LABEL NEAR
        pop     eax                                     
        lea     eax, [eax+0FECH]                        
        mov     dword ptr [eax], 608469404              
        mov     dword ptr [eax+4H], 2308                
        mov     dword ptr [eax+8H], -443981824          
        mov     dword ptr [eax+0CH], 1633409            
        mov     dword ptr [eax+10H], -477560832         
        mov     dword ptr [eax+14H], 15484359           
        mov     dword ptr [eax+18H], -1929379840        
        mov     dword ptr [eax+1CH], -1048448        
        <….> 

Дизассемблированный обфусцированный код

Мы можем легко заметить, как отличается обфусцированный код от первоначального. IDA Pro не смог показать схему для порядка исполнения обфусцированного кода, и значение MODIFIER исчезло. Также обфусцированный код защищен от статического и динамического изменения.

Функция выработки ключа по паролю


Функции выработки ключа по паролю (англ. PBKDF) используются для преобразования введенного пользователем пароля в ключ (бинарный набор данных), который может использоваться в криптографических алгоритмах. PBKDF очень важная составляющая защиты приложений, потому что введенный пользователем пароль небезопасно использовать в криптографическом алгоритме ввиду его недостаточной энтропии. Эти функции широко используются при защите приложений, например, криптографические ключи получаются из паролей в PGP системах для шифрования/дешифрования данных на диске. Также, операционные системы используют эти функции для проверки пользовательского пароля (аутентификации).

В общем случае математическое выражение для PBKDF имеет следующий вид:
y=F(P, S, d, t1, …, tn)
где y – выработанный функцией ключ, P – пароль, S – соль, d – длина вырабатываемого ключа и t1,…,tn – параметры, определяемые количеством аппаратных ресурсов, таких как тактовая частота процессора, объем оперативной памяти, требуемой для вычисления функции. Соль S служит для создания разных ключей при заданном пароле. Параметры t1,…,tn играют роль определения аппаратных ресурсов, потребляемых для вычисления функции, и могут быть настроены таким образом, чтобы усложнить ее вычисление и добавить дополнительную защиту против атаки грубой силой (brute-force), использующей распараллеливание на аппаратном уровне на обычных GPU.


Схема использования PBKDF

Существует два способа восстановления пользовательского пароля:
  1. Злоумышленник восстанавливает пароль, используя выработанный ключ, который получен им в результате утечки;
  2. Злоумышленник восстанавливает пароль, используя зашифрованные или подписанные выработанным ключом данные.

Для первого случая Intel Tamper Protection Toolkit поможет предотвратить утечку ключа путем сокрытия кода, выполняемого для его выработки и в дальнейшем использующего выработанный ключ.
Второй случай Intel Tamper Protection Toolkit не может предотвратить, но поможет проверить, что злоумышленник не изменил параметры, используемые для генерации ключа, на небезопасные.
Приведем примеры функций выработки ключа по паролю, используемые на практике:
  • Password-based Key Derivation Function (PBKDF2). Это функция вида y=F(P, S, c), где c – количество итераций для регулирования процессорного времени, требуемого на вычисление функции F для любых P, S. PBKDF2 может быть реализована для систем с очень маленьким объемом оперативной памяти, что делает атаку грубой силы с помощью GPU очень эффективной. Не смотря на это многие продукты продолжают использовать PBKDF2.
  • bcrypt. Эта функция является более стойкой к подобному типу атак с помощью GPU, так как использует больший фиксированный объем оперативной памяти.

Современной и наиболее безопасной функцией, разработанной Колином Персивалем (Colin Percival), является scrypt. Она имеет следующую математическую формулу:

y=F(P, S, d, N, r, p),

где y – выработанный функцией ключ, d – длина вырабатываемого ключа, P – пользовательский пароль, S – соль, p, r и N – параметры для установки процессорного времени и объема оперативной памяти, требуемой для выработки ключа. Значения параметров N, r, p, d могут быть открытыми и обычно они хранятся вместе с ключом или с зашифрованными данными.

В зависимости от значений параметров N, r, p выработка одного и того же ключа может потребовать разного количества процессорного времени и объема памяти. Например, если параметры запрашивают ~100 мс и ~20 МБ, то атака грубой силой на обычном GPU против функции scrypt будет не такой эффективной, как против PBKDF2, требующей малый объем оперативной памяти и позволяющей выполнять параллельные вычисления для разных паролей на GPU.

Утилита шифрования Scrypt


Утилита шифрования Scrypt использует алгоритм AES в режиме CTR и выработанный функцией scrypt по пользовательскому паролю ключ для работы с входными файлами. Она содержит обязательные и дополнительные параметры для запуска.
Обязательными являются:
  • пароль, который используется функцией scrypt для выработки ключа;
  • режим: шифрование или расшифрование;
  • имя входного файла.

Дополнительные параметры:
  • -t время в секундах, требуемое для выработки ключа;
  • -m доля объема оперативной памяти, используемая для выработки ключа;
  • -M количество байт оперативной памяти, используемой для выработки ключа;
  • имя выходного файла.

Например, запуск утилиты командой

scrypt enc infile -t 0.1 -M 20971520

потребует 100мс процессорного времени и 20МБ оперативной памяти для выработки ключа. Такие значения параметров усложнят распараллеливание перебора при атаке грубой силой.
Рисунок ниже представляет работу утилиты Scrypt в случае, когда пользователь ввел имя входного файла для шифрования, пароль и параметры, определяющие требуемые аппаратные ресурсы.
Дадим описание шагов, выполняемых утилитой при шифровании:
  1. Scrypt Сбор и преобразование параметров. Программа подбирает параметры процессорного времени и объема оперативной памяти, требуемые для выработки ключа и преобразует их в параметры, воспринимаемые функцией scrypt.
  2. Scrypt Выработка ключа. Функция scrypt вырабатывает по пользовательскому паролю и параметрам N, r, p, рассчитанным на предыдущем шаге, 64-байтный ключ. Младшие 32 байта ключа dk1 используются для вычисления кода аутентификации для параметров N, r, p, соли и зашифрованных данных. Таким образом, в процессе расшифрования можно проверить правильность введенного пароля и целостность зашифрованных данных. Старшие 32 байта ключа dk2 используются для шифрования входного файла алгоритмом AES в режиме CTR.
  3. Вычисление кода аутентификации для параметров scrypt. На этом шаге вычисляется код аутентификации (проверки подлинности) для параметров N, r, p и соли, используемый для выработки ключа.
  4. OpenSSL шифрование 32-байтными блоками AES в режиме CTR. Шифрование входного сообщения с dk2, используя 32-байтный шифр AES в режиме CTR.
  5. Вычисление кода аутентификации для зашифрованных данных. Наконец, код аутентификации вычисляется для шифрования данных, используя dk1 для обеспечения целостности. Выходной файл содержит зашифрованные данные, параметры N, r, p, соль, использованные при шифровании и коды аутентификации, которые обеспечивают целостность зашифрованных данных и параметров.



Схема шифрования утилитой Scrypt

Возможные угрозы


Анализируя работу утилиты в режиме шифрования, мы определим модель угроз. Значения параметров N, r, p, соли и выработанный ключ, получаемые на промежуточных шагах, являются критически важными данными и требуют защиты от изменений в реальном времени. Например, в режиме отладки злоумышленник может установить другие значения параметров N, r, p с целью ослабления сопротивляемости выработанного ключа атаке грубой силой.

Рисунок ниже иллюстрирует процесс расшифрования, когда пользователь ввел имя входного файла с зашифрованным текстом, N, r, p, солью, кодами аутентификации и пароль.
Дадим описание шагов, выполняемых утилитой при расшифровании:
  1. Scrypt Установка параметров. Входной файл для расшифрования содержит зашифрованные данные, коды аутентификации hmac1, hmac2 и параметры N, r, p, соль, использованные при шифровании. На этом шаге эти параметры вычитываются из входного файла и передаются в функцию выработки ключа.
  2. Scrypt Выработка ключа. Функция scrypt вырабатывает ключ для пароля и параметров N, r, p, соль, полученных на предыдущем шаге. Младшие 32 байта и старшие 32 байта этого ключа обозначены на рисунке dk1 и dk2 соответственно.
  3. Scrypt Проверка целостности параметров и пароля. Целостность N, r, p, соли и корректность пароля проверяются с помощью кода аутентификации. Для проверки корректности пароля утилита вычисляет код аутентификации для параметров N, r, p, соли, используя dk1, и сравнивает полученное значение со значением hmac1. Если они совпадают, значит пароль верен.
  4. Проверка целостности зашифрованных данных. Для проверки того, что зашифрованные данные не были изменены, вычисляется код аутентификации для данных, используя dk1, и сравнивается со значением hmac2. Если они совпадают, то данные не были испорчены и могут быть расшифрованы на следующем шаге.
  5. OpenSSL 32-байтное блочное расшифрование алгоритмом AES в режиме CTR. Наконец, данные расшифровываются с использованием 32-байтного блочного алгоритма AES в режиме CTR с использованием dk2. Выходной файл содержит расшифрованные данные.



Схема расшифрования утилитой Scrypt

Портирование утилиты под Windows


Целью работы является защита утилиты шифрования Scrypt под ОС Windows с помощью Tamper Protection toolkit. Исходная версия утилиты написана под ОС Linux, поэтому первой задачей становится ее портирование под ОС Windows.
Платформо-зависимый код будет размещен между следующими условными директивами:

#if defined(WIN_TP)
// Код под ОС Windows
#else
// Код под ОС Linux
#endif  // defined(WIN_TP)


Директива препроцессора WIN_TP отделяет предназначенный для ОС Windows код. WIN_TP должен быть определен для сборки под ОС Windows, в противном случае будет выбран для сборки код под ОС Linux.
Мы используем среду разработки Microsoft* Visual Studio 2013 для сборки и отладки утилиты. Существуют отличия между некоторыми объектами ОС Windows и ОС Linux, такими как процесс, поток, управление памятью и файлами, инфраструктурами сервисов, пользовательскими интерфейсами и так далее. Мы должны были учитывать все эти различия при портировании утилиты. Опишем их ниже.
  • Утилита использует функцию getopt() для разбора аргументов командной строки. Список доступных аргументов программы приведен выше. Функция getopt() находится в заголовочном файле unitstd.h согласно набору стандартов POSIX. Мы используем реализацию get_opt() из открытого проекта getopt_port. Для этого добавим файлы getopt.h и getopt.c из проекта getopt_port в наш проект.
  • Оставшаяся функция gettimeofday(), объявленная в POSIX API, используется утилитой для измерения salsa opps и подсчета числа операций в секунду salsa20/8, выполненных на пользовательской платформе. Метрика salsa opps используется утилитой для подбора более безопасных значений параметров N, r, и p, так что алгоритм scrypt выполняет операции salsa20/8 минимальное количество раз, которое позволяет избежать атаки перебором. Мы добавили реализацию функции gettimeofday() в файл scryptenc_cpuperf.c.
  • Перед запуском алгоритма конфигурации утилита запрашивает у операционной системы количество доступной оперативной памяти, которая будет захвачена вызовом функции getrlimit(RLIMIT_DATA, …) из набора POSIX для выработки ключа. В ОС Windows жесткий и нежесткий лимиты для максимального размера сегмента данных процесса (инициализированные и неинициализированные данные и куча) установим равными 4ГБ. Все это показано в коде ниже.

    /* ... RLIMIT_DATA... */
    #if defined(WIN_TP)
    rl.rlim_cur = 0xFFFFFFFF;
    rl.rlim_max = 0xFFFFFFFF;
    if((uint64_t)rl.rlim_cur < memrlimit) {
    	memrlimit = rl.rlim_cur;
    }
    #else
    if (getrlimit(RLIMIT_DATA, &rl))
    	return (1);
    if ((rl.rlim_cur != RLIM_INFINITY) &&
         ((uint64_t)rl.rlim_cur < memrlimit))
    	memrlimit = rl.rlim_cur;
    #endif  // defined(WIN_TP)
    
  • Дополнительно, для компилятора MSVS была добавлена директива для определения inline функций в файле sysendian.h.

    #if defined(WIN_TP)
    static __inline uint32_t
    #else
    static inline uint32_t
    #endif  // WIN_TP
    be32dec(const void *pp);
    
  • Мы портировали функцию tarsnap_readpass(…) для выполнения скрытого ввода пароля в терминале. Функция отключает отображение символов в окне терминала и маскирует пароль пробельными символами. Пароль сохраняется в выделенном в памяти буфере и отправляется в следующие функции конфигурации Scrypt и выработки ключа.

    /* В случае чтения из терминала, пробуем выключить вывод символов */
    #if defined(WIN_TP)
    if ((usingtty = _isatty(_fileno(readfrom))) != 0) {
    	GetConsoleMode(hStdin, &mode);
    	if (usingtty)
    		mode &= ~ENABLE_ECHO_INPUT;
    	else
    		mode |= ENABLE_ECHO_INPUT;
    	SetConsoleMode(hStdin, mode);
    }
    #else
    if ((usingtty = isatty(fileno(readfrom))) != 0) {
    	if (tcgetattr(fileno(readfrom), &term_old)) {
    		warn("Cannot read terminal settings");
    		goto err1;
    	}
    	memcpy(&term, &term_old, sizeof(struct termios));
    	term.c_lflag = (term.c_lflag & ~ECHO) | ECHONL;
    	if (tcsetattr(fileno(readfrom), TCSANOW, &term)) {
    		warn("Cannot set terminal settings");
    		goto err1;
    	}
    }
    #endif  // defined(WIN_TP)
    
  • Оригинальная функция getsalt() для получения псевдослучайной последовательности выполняет чтение специального файла /dev/urandom, входящего в состав ОС Unix. На Windows мы используем инструкцию rdrand() из аппаратного генератора случайных чисел, доступного на чипах семейства Intel Xeon и Core, начиная с Ivy Bridge. Стандартная функция C для генерации псевдослучайной последовательности намеренно не используется, так как в этом случае не может быть обфусцирована функция getsalt() с помощью инструмента обфускации Tamper Protection. Функция getsalt() должна быть защищена обфускатором от статического и динамического внесения изменений и реверс-инженеринга, так как соль, производимая этой функцией категоризирована нами в 3 разделе как защищаемый объект. Ниже приведены изменения, внесенные в код, для получения соли.

    #if defined(WIN_TP)
    	uint8_t i = 0;
    
    	for (i = 0; i < buflen; i++, buf++)
    	{
    		_rdrand32_step(buf);
    	}
    #else
    	/* Открываем /dev/urandom. */
    	if ((fd = open("/dev/urandom", O_RDONLY)) == -1)
    		goto err0;
    	/* Читаем байты, пока не заполним buffer. */
    	while (buflen > 0) {
    		if ((lenread = read(fd, buf, buflen)) == -1)
    			goto err1;
    		/* Случайный поток не должен закончиться, пока не заполнен buffer. */
    		if (lenread == 0)
    			goto err1;
    		/* Заполнение по частям */
    		buf += lenread;
    		buflen -= lenread;
    	}
    	/* Закрытие генератора случайного потока */
    	while (close(fd) == -1) {
    		if (errno != EINTR)
    			goto err0;
    	}
    #endif // defined(WIN_TP)
    


Защита утилиты с помощью Intel Tamper Protection Toolkit


Теперь мы произведем рефакторинг кода утилиты для защиты всех важных данных, определенных в нашей модели угроз. Защита таких данных достигается путем обфускации кода с помощью инструмента iprot, обфусцирующего компилятора из набора. Будем также придерживаться принципа разумности и обфусцировать только те функции, которые создают, обрабатывают и используют важные данные.
Нам уже известно, что обфускатор принимает на вход динамическую библиотеку и вырабатывает бинарный файл, содержащий только защищенные функции, указанные в командной строке. Поэтому мы поместим все функции, работающие с важными данными внутрь динамической библиотеки для дальнейшей ее обфускации. Оставшиеся функции, такие как разбор аргументов командной строки, чтение пароля, мы оставим в незащищенном виде в основном исполняемом файле.

Новая структура защищаемой утилиты представлена на рисунке ниже. Утилита разделена на две части: главный исполняемый файл и динамическую библиотеку, которая будет обфусцирована. Главный исполняемый файл отвечает за разбор аргументов командной строки, чтение пароля и загрузку входного файла в память. Динамическая библиотека содержит экспортные функции, такие как scryptenc_file, scryptdec_file, которые работают с важными данными (N, r, p, соль).

Структура ключевых данных, используемых в динамической библиотеке, называется контекстом Scrypt и содержит проверочную информацию HMAC для параметров функции scrypt: N, r, p и соль. Информация HMAC в контексте используется для проверки целостности контролируемых параметров доверенными функциями, такими как scrypt_ctx_enc_init, scrypt_ctx_dec_init, scryptenc_file и scryptdec_file, которые были добавлены в результате рефакторинга кода. Эти доверенные функции будут устойчивы к изменениям, так как мы намеренно их обфусцируем с помощью инструмента. Две новые функции scrypt_ctx_enc_init и scrypt_ctx_dec_init потребовались для инициализации контекста функции scrypt для каждого из режимов: шифрование, расшифрование.


Архитектура защищенной утилиты Scrypt

Дадим подробное описание к рисунку, как работает утилита в каждом из режимов: шифрование и расшифрование.
Шифрование:
  1. Утилита использует функцию getopt() для разбора аргументов командной строки. Список аргументов приведен выше.
  2. Входной файл для шифрования/расшифрования и пароль считываются в выделенный буфер памяти.
  3. В основном исполняемом файле вызывается функция scrypt_ctx_enc_init для инициализации контекста функции scrypt с вычислением подходящих безопасных значений параметров (N, r, p и соли), учитывая переданные значения аргументов maxmem, maxmemfrac и maxtime, которые задают требуемое процессорное время и объем оперативной памяти для выработки ключа. В конце этого вызова используется ключ HMAC (значение хеш-суммы) для контекста, которое частично инициализировано и требует адрес выделенной памяти, используемой scrypt для вычислений. Один аргумент передается по ссылке, куда функция возвращает количество байт, требуемых алгоритму scrypt для установки параметров.
  4. Утилита в основном исполняемом файле динамически выделяет память, размер которой был возвращен функцией инициализации.
  5. Для шифрования функция scrypt_ctx_enc_init повторно вызывается из основного исполняемого файла. Функция проверяет целостность контекста scrypt, используя значение HMAC. Если проверка целостности пройдена, то заполняются значения адресов в контексте из пространства выделенной на предыдущем шаге памяти для вычислений scrypt и обновляется HMAC. Чтение файла и динамическое распределение памяти перемещено в основной исполняемый файл, чтобы избежать появления необфусцируемого кода в динамической библиотеке. Код, который содержит системные вызовы и стандартные С-функции, генерирующие непрямые переходы и перераспределение памяти, не могут быть обфусцированы.
  6. Выполняется вызов экспортной функции шифрования scryptenc_file, использующей введенный пароль. Функция проверяет целостность контекста функции scrypt с параметрами (N, r, p и соль), используемыми для выработки ключа. Если проерка пройдена, то вызывается алгоритм scrypt для выработки ключа. Выработанный ключ затем используется для шифрования. Экспортная форма функции имеет тот же выход, что и оригинальная функция утилиты scrypt. Это означает, что выход имеет то же значение хеша, используемое для проверки целостности зашифрованных данных и правильности введенного пароля в процессе расшифрования.

Расшифрование:
  1. Утилита использует функцию getopt() для разбора аргументов командной строки.
  2. Входной файл для шифрования/расшифрования и пароль считываются в выделенный буфер памяти.
  3. В основном исполняемом файле вызывается функция scrypt_ctx_dec_init для проверки, что полученные из зашифрованного файла параметры корректны и функция выработки ключа может быть вычислена с доступным объемом оперативной памяти и процессорным временем. Один аргумент передается по ссылке, куда функция возвращает количество байт, требуемых алгоритму scrypt для установки параметров.
  4. Утилита в основном исполняемом файле динамически выделяет память, размер которой был возвращен функцией инициализации.
  5. Для расшифрования функция scrypt_ctx_dec_init повторно вызывается из основного исполняемого файла. Она выполняет действия, аналогичные функции шифрования.
  6. Выполняется вызов экспортной функции расшифрования scryptdec_file, использующей введенный пароль. Функция проверяет целостность контекста функции scrypt с параметрами (N, r, p и соли), используемыми для выработки ключа. Если провекра пройдена, то вызывается алгоритм scrypt для выработки ключа. Используя значения хешей в зашифрованных данных, функция проверяет правильность пароля и целостность зашифрованных данных.

В защищенной утилите мы заменяем OpenSSL реализацию алгоритма AES в режиме CTR и функцию вычисления кода аутентификации на аналогичные функции из библиотеки Intel Tamper Protection Toolkit crypto library. В отличие от OpenSSL, crypto library удовлетворяет всем ограничениям на исходный код и может быть обфусцирована с помощью инструмента iprot и использована с обфусцируемым кодом без изменений. Алгоритм AES вызывается внутри функций scryptenc_file и scryptdec_file для шифрования/расшифрования входного файла и использует выработанный по паролю ключ. Функция вычисления кода аутентификации вызывается в экспортных функциях (scrypt_ctx_enc_init, scrypt_ctx_dec_init, scryptenc_file и scryptdec_file) для проверки целостности данных контекста scrypt перед их использованием. В защищенной утилите все экспортные функции динамической библиотеки обфусцированы с помощью iprot.

Tamper Protection помогает нам достичь цели уменьшения угроз. Наше решение представляет собой переработанную утилиту с обфусцированной с помощью iprot динамической библиотекой. Решение устойчиво к атакам, определенным ранее и это может быть доказано: контекст scrypt может быть обновлено только через экспортные функции, потому что они содержат собственный ключ HMAC для перерасчета значения HMAC в контексте. Также эти функции и проверочные данные HMAC защищены от внесения изменений и реверс-инженеринга с помощью обфускатора. В дополнение, другие важные данные, такие как вырабатываемый функцией scrypt ключ, защищены, поскольку выработка происходит внутри обфусцированных экспортных функциях scryptenc_file и scryptdec_file. обфусцирующий компилятор iprot вырабатывает код, который является самомодифицирующимся во время выполнения и защищенным от внесения изменений и отладки.

Рассмотрим как функция scrypt_ctx_enc_init защищает контекст scrypt. Основной исполняемый файл с помощью указателя buf_p указывает который раз вызывается функция scrypt_ctx_enc_init. Если указатель пуст (значение равно null), то функция вызывается певый раз, в противном случае второй раз. Во время первого вызова происходит инициализация параметров scrypt, вычисляется HMAC и возвращается количество требуемой памяти для вычислений функции scrypt. Все это иллюстрирует следующий код.

	// Первое выполнение: возвращение объема памяти для вычисления функции scrypt
	if (buf_p == NULL) {  		
		// Подбор параметров scrypt и инициализация окружения 
		// <...> 

		// Вычисление HMAC
		itp_res = itpHMACSHA256Message((unsigned char *)ctx_p, sizeof(scrypt_ctx)-sizeof(ctx_p->hmac),
						hmac_key, sizeof(hmac_key),
						ctx_p->hmac, sizeof(ctx_p->hmac));

		*buf_size_p = (r << 7) * (p + (uint32_t)N) + (r << 8) + 253;
	} 


Во время второго вызова buf_p указывает на выделенную память, переданную в функцию scrypt_ctx_enc_init. Используя значение HMAC, функция проверяет целостность контекста, чтобы удостовериться, что никакие данные не были изменены между первым и вторым вызовом функции. После этого она инициализирует адрес внутри контекста, используя указатель buf_p, и перерасчитывает значение HMAC для измененного контекста. Код, выполняемый при повторном вызове, приведен ниже.

// Второе выполнение: память для функции scrypt выделена
	if (buf_p != NULL) {
		// Проверка HMAC
		itp_res = itpHMACSHA256Message(
			(unsigned char *)ctx_p, sizeof(scrypt_ctx)-sizeof(ctx_p->hmac),
			hmac_key, sizeof(hmac_key),
			hmac_value, sizeof(hmac_value));
		if (memcmp(hmac_value, ctx_p->hmac, sizeof(hmac_value)) != 0) {
			return -1;
		}

		// Инициализация указателей буферов для вычислений scrypt:
		// ctx_p->addrs.B0 = …

		// Перерасчет HMAC
		itp_res = itpHMACSHA256Message(
			(unsigned char *)ctx_p, sizeof(scrypt_ctx)-sizeof(ctx_p->hmac),
			hmac_key, sizeof(hmac_key),
			ctx_p->hmac, sizeof(ctx_p->hmac));
	}


Мы уже знаем, что обфускатор накладывает некоторые ограничения на исходный код, чтобы его можно было обфусцировать: не должно быть релокаций и непрямых переходов (англ. indirect jump) в коде. Конструкции языка C содержащие глобальные переменные, системные вызовы и стандартные С-функции, могут порождать релокации и непрямые переходы. Код выше содержит одну стандартную С-функцию memcmp, что делает код необфусцируемым с помощью iprot. По этой причине мы реализуем несколько собственных стандартных С-функций, таких как memcmp, memset, memmove, используемых в утилите. Также мы заменим все глобальные переменные в динамической библиотеке на локальные и позаботимся о том, чтобы данные инициализировались на стеке.

Кроме того, мы столкнулись с проблемой обфускации кода, содержащего значения типа double, которая не описана в документации к инструменту. Например, в коде ниже показано, что функция pickparams для ограничения числа операций salsa20/8 использует тип переменной double со значением 32768. Это значение не инициализируется на стеке и компилятор располагает его в сегменте данных исполняемого файла, что генерирует в коде релокацию.

	double opslimit;
#if defined(WIN_TP)
	// unsigned char d_32768[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x40};
	unsigned char d_32768[sizeof(double)];
	d_32768[0] = 0x00;
	d_32768[1] = 0x00;
	d_32768[2] = 0x00;
	d_32768[3] = 0x00;
	d_32768[4] = 0x00;
	d_32768[5] = 0x00;
	d_32768[6] = 0xE0;
	d_32768[7] = 0x40;
	double *var_32768_p = (double *) d_32768;
#endif

	/* Минимальное количество повторений операции salsa20/8. */
#if defined(WIN_TP)
	if (opslimit < *var_32768_p)
		opslimit = *var_32768_p;
#else
	if (opslimit < 32768)
		opslimit = 32768;
#endif


Мы устранили эту проблему просто инициализировав на стеке нужную последовательность байт в 16-ричном виде, представляющую требуемое значение типа double, и создали указатель типа double на адрес этой последовательности. Возможно, некоторые небольшие утилиты наподобие double2hex могут помочь разработчикам получить 16-ричное представление для значений double и могут быть использованы как вспомогательный инструмент.
Для обфускации динамической библиотеки с помощью iprot, мы используем следующую команду:
iprot scrypt-dll.dll scryptenc_file scryptdec_file scrypt_ctx_enc_init scrypt_ctx_dec_init -c 512 -d 2600 -o scrypt_obf.dll

Интерфейс защищенной утилиты не изменился. Сравним необфусцированный и обфусцированный код. Дизассемблерный код, приведенный ниже, показывает существенное различие между ними.

# необфускированный код
scrypt_ctx_enc_init PROC NEAR
        push    ebp
        mov     ebp, esp
        sub     esp, 100
        mov     dword ptr [ebp-4H], 0
        mov     eax, 1
        imul    ecx, eax, 0
        mov     byte ptr [ebp+ecx-1CH], 1
        mov     edx, 1
        shl     edx, 0
        mov     byte ptr [ebp+edx-1CH], 2
        mov     eax, 1
        shl     eax, 1
        mov     byte ptr [ebp+eax-1CH], 3
        mov     ecx, 1
<…>

# обфускированный код с параметрами по умолчанию
scrypt_ctx_enc_init PROC NEAR
        mov     ebp, esp
        sub     esp, 100
        mov     dword ptr [ebp-4H], 0
        mov     eax, 1
        imul    ecx, eax, 0
        mov     byte ptr [ebp+ecx-1CH], 1
        push    eax
        pop     eax
        lea     eax, [eax+3FFFD3H]
        mov     dword ptr [eax], 608469404
        mov     dword ptr [eax+4H], -124000508
        mov     dword ptr [eax+8H], -443981569
        mov     dword ptr [eax+0CH], 1633409
        mov     dword ptr [eax+10H], -477560832
<…>

В результате обфускации производительность утилиты понизилась и размер библиотеки увеличился. Обфускатор позволяет разработчикам выбирать между большей безопасностью и большей производительностью с помощью опций: размер ячейки и расстояние между точками мутаций. В нашем случае обфускатором используются 512-байтные ячейки и 2600-байтные расстояния мутаций. Ячейкой называется подпоследовательность инструкций из оригинального исполняемого файла. Ячейки в обфусцированном коде зашифрованы до тех пор, пока не потребуется выполнить код, хранящийся в них. После расшифровывания ячейки и полного выполнения содержащегося в ней кода, она зашифровывается обратно.
Исходный код утилиты, защищаемой с помощью Intel Tamper Protection Toolkit, скоро появится на Github.

Благодарности


Мы благодарим Рагхудип Каннавара за идею защиты утилиты шифрования Scrypt и Андрея Сомсикова за многочисленные полезные обсуждения.

Ссылки


  1. K. Grasman. getopt_port on github
  2. C. Percival. The scrypt encryption utility
  3. C. Percival. “Stronger key derivation via sequential memory-hard functions”.
  4. C. Percival, S. Josefsson (2012-09-17). “The scrypt Password-Based Key Derivation Function”. IETF.
  5. N. Provos, D. Mazieres, J. Talan Sutton 2012 (1999). “A Future-Adaptable Password Scheme”. Proceedings of 1999 USENIX Annual Technical Conference: 81–92.
  6. W. Shawn. Freebsd sources on github

Авторы: Роман Казанцев, Денис Катеринский, Таддеус Летнес
{Roman.Kazanstev, Denis.Katerinskiy, Thaddeus.C.Letnes}@intel.com

Комментарии (17)