Содержание к диссертации
Введение
Глава 1. Проблема защиты программ от компьютерного пиратства и постановка задачи исследования 8
1.1. Защита программ от анализа и модификации как фактор противодействия компьютерному пиратству 8
1.2. Современные методы защиты программ 10
1.2.1. Методы, основанные на особенностях среды функционирования программы 10
1.2.2. Автоматическая упаковка исполняемого модуля 12
1.2.3. Методы защиты программ, основанные на запутывании кода и данных... 15
1.3. Анализ существующих подходов к обфускации 17
1.3.1. Обфускация на основе виртуализации кода 17
1.3.2. Задачи обфускации в криптографии 22
1.3.3. Обфускация по Бараку 28
1.3.4. Обфускация на уровне промежуточного представления программы 31
1.3.5. Обфускация методом Вонга 33
1.4. Постановка задачи исследования и общая схема ее решения 35
1.4.1. Особенности защиты программного обеспечения на основе существующих методов обфускации 35
1.4.2. Постановка задачи разработки методик защиты программ от анализа и модификации на основе запутывания кода и данных 36
1.4.3. Общая схема решения задачи исследования 38
1.5. Выводы по главе 1 39
Глава 2. Правила построения запутывающих преобразований 42
2.1. Функциональные свойства подпрограмм и отношения между ними JVP-полнота задачи деобфускации 42
2.3. Правила построения запутывающих преобразований 50
2.4. Выводы по главе 2 55
Глава 3. Разработанные методики обфускации 56
3.1. Общая структура процесса обфускации 56
3.2. Обфускация на уровне промежуточного представления 58
3.2. Обфускация на уровне машинного кода 72
3.3. Контроль целостности запутанного кода 82
3.4. Метод запутывания графа потока управления при помощи сетей Петри 84
3.5. Внедрение кода защиты в приложение 87
3.6. Методика перевода машинного кода в промежуточное представление 90
3.10 Выводы по главе 3 104
Глава 4. Практическая реализация методик обфускации и их оценка 107
4.1. Концепция построения обфускатора. Платформозависимый компонент... 107
4.2. Платформонезависимый компонент 110
4.3. Оценки увеличения объема кода и замедления 114
4.4. Оценка качества запутывающих преобразований 117
4.5. Выводы по главе 4 123
Заключение 125
Список литературы: 127
Приложения
- Методы, основанные на особенностях среды функционирования программы
- Обфускация на основе виртуализации кода
- Правила построения запутывающих преобразований
- Метод запутывания графа потока управления при помощи сетей Петри
Введение к работе
Современный этап развития общества характеризуется интенсивным развитием информатизации. Как следствие этого развивается компьютерное пиратство. Для противодействия компьютерному пиратству активно используются технологии автоматической защиты программного обеспечения от анализа и несанкционированной модификации. Эти технологии широко используются в системах управления цифровыми правами, а также незаменимы при решении задач сокрытия вредоносного кода.
Основы методологии создания подобного рода механизмов заложены такими учёными, как Варновский Н.П, Захаров В.А., Гайсарян С.С, Чернов А.В, Коллберг К.
Вместе с тем существующие методики рассчитаны либо на наличие исходного кода программы, что чрезвычайно затрудняет решение задачи контроля целостности программы - либо обладают чрезвычайно высоким замедлением. Более того, для большинства методик автоматической защиты программ, не требующих наличия исходного кода, существуют механизмы автоматической деактивации защиты. Данное обстоятельство определяет необходимость наличия адекватных методик противодействия такого рода механизмам. Эти методики должны быть автоматическими, не требовать наличия исходного кода и не должны опираться на недокументированные особенности платформы, на которой выполняется защищенная программа.
В настоящее время наиболее распространена методика автоматической защиты программ от анализа на основе технологии виртуализации машинного кода. Данная методика ставит в соответствие каждой машинной инструкции одну или несколько инструкций автоматически сгенерированного виртуального процессора, называемых байт-кодом или псевдокодом. В тело защищаемой программы встраивается защищенный от анализа интерпретатор, задачей которого является выполнение сгенерированного на этапе защиты байт-кода. Основным недостатком такого подхода является низкая скорость работы защищенного та-
5 ким образом кода. Более того, в большинстве случаев все-таки возможно создание автоматического декомпилятора псевдокода в исходный машинный код.
В связи с этим для повышения стойкости к автоматическим средствам деактивации защиты и обеспечения более высокого быстродействия защищенной программы по сравнению с существующими методиками необходимо создание новых подходов. В основе их лежит принцип программного «черного ящика», реализация которого предполагает использование технологий запутывания кода и данных программы или обфускации.
Данное обстоятельство определяет актуальность разработки методик обфускации кода и данных программы, не требующих для своей работы исходного кода программы и обеспечивающих стойкость по отношению к автоматическим утилитам деактивации защиты.
Исходя из вышесказанного, целью диссертационной работы является повышение качества защиты программных продуктов от компьютерного пиратства, объектом исследования является автоматическая защита программного обеспечения от анализа и несанкционированной модификации, а предметом исследования являются методики обфускации программ, не требующие наличия исходного кода.
Научная задача состоит в разработке методик и алгоритмов автоматической защиты программного обеспечения от анализа и несанкционированной модификации на основе запутывания кода и данных. Эти методики и алгоритмы должны обеспечивать стойкость по отношению к алгоритмам деобфускации, работающим за полиномиальное время.
В рамках решения указанной задачи в диссертации:
Обоснована необходимость добавления в запутываемую программу дополнительных входных и выходных данных, называемых далее «фальшивым» глобальным контекстом.
Сформулирована и доказана теорема о TVP-полноте задачи деобфускации.
3. Обосновано применение разработанных правил построения запутывающих
преобразований.
Разработана методика статического и полу статического анализа машинного кода программы с целью его последующего перевода в промежуточное представление.
Разработана методика запутывания программы на уровне промежуточного представления, а также методики противодействия деобфусцирующим преобразованиям на уровне промежуточного представления.
Разработана методика запутывания программы на уровне машинного кода, а также методики противодействия деобфусцирующим преобразованиям на уровне машинного представления.
Научная новизна диссертации заключается в следующем:
Сформулирована и доказана теорема о NP-полноте задачи деобфускации при добавлении к запутываемой программе дополнительных входных и выходных данных, подтверждающая отсутствие алгоритмов деобфускации полиномиальной сложности в общем случае.
Разработаны методика и алгоритм перевода машинного кода в промежуточное представление на основе частичной эмуляции и анализа псевдонимов, позволяющие производить обфускацию без привязки к конкретной аппаратной платформе.
Разработаны методика и алгоритм запутывания кода и данных программы на уровне промежуточного представления программы, позволяющие усложнить анализ и модификацию защищенной программы.
Выведены правила, которым должны соответствовать запутывающие преобразования. Эти правила позволяют воспрепятствовать созданию алгоритмов автоматической деобфускации защищенной программы.
Разработаны методика и алгоритм перевода кода из промежуточного представления в машинное с последующей обфускацией на уровне машинного кода, позволяющие обеспечить защиту от модификации кода программы.
Практическая значимость диссертационной работы состоит в том, что разработанная совокупность методик и алгоритмов позволяет эффективно решать задачи защиты программного обеспечения от анализа и модификации. Разработанные методики являются автоматическими, не требуют наличия исходных
7 текстов защищаемых программ и могут применяться пользователями, не обладающими специфическими знаниями и навыками в области защиты программного обеспечения.
На защиту выносятся следующие научные положения:
Формулировка и доказательство теоремы о NP-полноте задачи деобфускации в случае добавления к запутываемой программе дополнительных входных и выходных данных.
Правила построения запутывающих преобразований, позволяющих воспрепятствовать созданию алгоритмов деобфускации, работающих за полиномиальное время.
Методики и алгоритмы перевода кода из машинного представления в промежуточное, а также методики и алгоритмы обфускации кода и данных, как на уровне промежуточного представления, так и на уровне машинного кода
В первой главе анализируется текущее состояние проблемы автоматической защиты программного обеспечения от компьютерного пиратства и ставится задача исследования.
Во второй главе рассматривается теоретический аппарат, описывающий процесс обфускации, доказывается JVP-полнота задачи деобфускации, а также описываются и обосновываются разработанные правила пострения запутывающих преобразований.
В третьей главе подробно описаны разработанные методики обфускации и перевода кода из машинного представления в промежуточное.
В четвертой главе рассматривается практическая реализация разработанных методик, а также производятся количественные оценки качества запутывающих преобразований.
В заключении изложены основные теоретические и практические результаты диссертационной работы.
Методы, основанные на особенностях среды функционирования программы
Под средой функционирования программы понимается аппаратная платформа и операционная система (программная платформа). Программы, созданные для разных программных платформ сильно различаются, несмотря на то, что аппаратная платформа одна и та же. Это связано с особенностями функционирования различных операционных систем. Часть этих особенностей недокументи-рованна, что может быть использовано для защиты программ от анализа и модификации.
Приведем несколько примеров использования недокументированных возможностей семейства Windows NT для защиты программного обеспечения.
1. Использования структуры блока окружения процесса (РЕВ). При создании процесса в семействе Windows NT создается структура РЕВ, которая содержит информацию о функционировании данного процесса. Эта структура недо-кументированна, однако, ее состав остается неизменным, начиная с Windows NT, заканчивая 32-х битной редакцией Windows Vista. Содержащуюся в РЕВ информацию можно использовать для защиты программ от дампа. В частности, многие программы используют интерфейсную функцию GetModuleHandle для получения базового адреса подгруженного в процесс модуля. Однако, для целей защиты программ вызов внешних функций, особенно из кода защиты, нежелателен. Поэтому вместо вызова этой функции используется прямой поиск адреса загруженного модуля в РЕВ. Таким образом, аналитику достаточно сложно определить момент получения базового адреса модуля, подгруженного в адресное пространство процесса. Недостатком данного метода является его недокументиро-ванность. Т.е. разработчики MS Windows могут поменять состав и местоположение РЕВ-структуры в памяти не только в новых версиях операционной системы, но и в пакетах обновления. В этом случае программы, использующие прямой доступ к РЕВ, перестанут корректно работать.
2. Исполнение кода на стеке. Данный прием позволяет создать машинный код в области стековой памяти и передать на него управление. Создание такого кода достаточно трудно заметить, а после выхода из него, данный код может быть успешно уничтожен. Для кода на стеке и передачи на него управления нет необходимости вызывать какие-либо дополнительные внешние функции, что не привлекает внимания аналитика. Однако, начиная с Windows ХР SP2, исполнение кода на стеке запрещено. Большинство х86-совместимых процессоров поддерживают такой механизм на аппаратном уровне (DEP). Таким образом, данный прием не работает на современных аппаратных и программных платформах без вызова дополнительных внешних функций. Аналогичное можно сказать и про исполнение кода в «куче». В последних версиях Windows исполнение кода в «куче» также запрещено.
3. Подмена адресов функций-обработчиков прерываний в таблице дескрипторов прерываний (IDT). Данный подход позволяет воспрепятствовать дампу программы в семействе операционных систем Windows NT, а, следовательно, изучению кода защиты. Часть страниц приложения помечаются, как выгруженные, посредством установки соответствующего бита в структуре-описателе страницы (РТЕ). В IDT происходит установка собственного обработчика прерывания ошибки страницы. В данном обработчике происходит проверка типа доступа к странице. Если происходит попытка чтения, то подгружается случайно выбранная страница. Правильная трансляция адресов происходит только в случае, когда происходит доступ на выполнение. Таким образом, попытка считать код приложения приведет к считыванию случайных данных. Однако, данный подход отличается сложностью реализации и нестабильностью работы защищенных таким образом программ. Нестабильность работы связана прежде всего с тем, что в последних версиях операционных систем Windows линейки NT происходит проверка целостности ядра операционной системы, в которую входит и проверка адресов обработчиков IDT (Patch Guard). Поэтому для корректного функционирования данного механизма необходимо отключить такого рода про 12 верки, что является прямым вмешательством в работу операционной системы и нарушением ее безопасности. Данные обстоятельства не позволяют широко использовать подмену адресов обработчиков в IDT для защиты программного обеспечения от анализа и модификации.
4. Использование механизма структурной обработки исключений (SEH). В 32-х разрядных операционных системах Windows для обработки исключительных ситуаций используется частично документированный механизм структурной обработки исключений. При помощи механизма SEH возможно производить простейшее обнаружение работы под отладкой, заменять инструкции ветвления посредством преднамеренной генерации исключений с последующей подменой соответствующего адреса передачи управления в структуре, передаваемой по указателю в обработчик исключения. Однако основным недостатком данного подхода является его известность и наличие большого количества инструментов, позволяющих скрыть наличие отладчиков и утилит деактивации защиты.
Все вышеописанные подходы объединяет то, что в них широко используются недокументированные возможности операционной системы. По сути, стойкость защит, построенных на этих методах, зависит от осведомленности аналитика об их существовании. Для каждого из вышеприведенных приемов существуют эффективные методы взлома. Более того, в силу недокументированности этих подходов, их использование может привести к нестабильному функционированию защищенной с их помощью программы.
Обфускация на основе виртуализации кода
Использование виртуальных машин для защиты программного обеспечения, безусловно, имеет свои преимущества. Прежде всего - это сокрытие логики работы программы, благодаря использованию виртуального процессора. Действительно, для того, чтобы изучить логику работы программы, необходимо предварительно проанализировать логику работы виртуального процессора. Рассмотрим виртуализацию кода более подробно. Пользователь помечает каким-либо образом, какие именно участки кода ему хотелось бы перевести в код виртуальной машины. На этом этапе также производится верификация этих участков ко 18 да, позволяющая определить, возможно, ли перевести данный набор инструкций в инструкции виртуальной машины. После этого выбранные участки кода переводятся в псевдокод, а интерпретатор псевдокода записывается в тело приложения..
Как было сказано выше, одной из самых распространенных виртуальных машин, используемых для защиты приложений, является VMProtect. Уровень защиты от исследования, который она обеспечивает, также достаточно высок. Рассмотрим более подробно принцип её работы на примере функции TestFunc (см. приложение 1)
В данной работе не будет приведено детальное пошаговое описание процесса исследования псевдокода. Будут отмечены лишь основные свойства данного продукта, позволяющие сделать определенные выводы об уровне защиты. Рассмотрим принципы функционирования виртуальной машины VMProtect. Прежде всего, на стеке сохраняются регистры основного назначения и регистр флагов; Следует сразу заметить, что в виртуальной машине все регистры отображены на память (в данном случае это стек): Далее происходит выделение рабочего стекового пространства, с которым работает сама, виртуальная машина, а затем управление передается цикл выборки команд виртуальной машины. После. того, как все команды были выполнены, значения регистров восстанавливаются и происходит выход из виртуальной машины.
Рассмотрим какие:команды ВМі выполняются для. функции TestFuncQ. 1. Сначала выполняются команды копирования сохраненных предварительно значений регистров и флагов вовнутренний выделенный рабочий буфер: В листинге (см. приложение 2) инструкциявыделения этого буфера располагается по адресу .vmp0:00413115. Следует отметить,.что эти значения копируютсяше последовательно друг за;другом, а в произвольном порядке. Это необходимо: для усложнения написания программ-декомпиляторов; Код, выполняющий- эти действия; приведен в приложении 3. Внутренний стек ВМ I Рост адресов Сохраненные в начале \ :; -значения (safe_frame) ki Рис. 1.3. Стек виртуальной машины. На рисунке 1.3 схематично изображен стек ВМ. Как видно, он состоит из двух частей: safe_frame и внутреннего стека ВМ, выделенного инструкцией по адресу . vmp0:00413115 (см. приложение 2). Первый набор команд копирует то, что было сохранено в safe_frame во внутренний стек ВМ. 2. Команды сохранения численных параметров вызываемой функции на стеке. Как видно из листинга, представленного в приложении 4, эти параметры сохраняются в safe_frame. Аналогичным образом сохраняются на стеке адреса выводимых в тестовом примере строк (см. приложение 5). 3. Команды сохранения новой точки входа. На листинге, описанном в приложении 6, показана команда сохранения на стеке (safe frame) значения, сохраненного по адресу .vmpO:0041310A (см. приложение 2). Это команда чтения значений из внутреннего стека ВМ с последующим их сохранением в safe_frame. Далее это значение суммируется со значением новой точки входа, предварительно сохраненной в safe_Jrame на 4 байта выше. На вершине safe_frame, в свою очередь, сохраняются флаги (см. приложение 7). Впоследствии значение регистра флагов снимается с вершины стека (safe_frame) и кладется во внутренний стек ВМ. 4. Команды вычисления адреса, по которому будет осуществлен переход. Команда считывания адреса считывает адрес вызываемой функции (а точнее адрес её переходника) и кладет его на вершину safe_frame. После этого вычисляется «истинный» адрес вызываемой функции (см. приложение 8).
Состояние стека safe_frame перед вызовом внешней функции 6. Вызов внешней подпрограммы. На листинге, приведенном в приложении 9, изображен вызов внешней подпрограммы. При этом восстанавливаются значения регистров, флагов. Т.е. восстанавливается состояние подпрограммы, каким оно должно быть на момент вызова внешней функции.
Для вычисления ряда арифметических операций используется базис ИЛИ-НЕ (см приложение 10). Т.е. каждая логическая инструкция преобразуется в выполняющий аналогичные действия набор логических операций в базисе ИЛИ-НЕ.
Рассмотрим действия, предпринимаемые аналитиком для восстановления исходного кода (кода до виртуализации).
1. Анализ принципа работы ВМ. Принцип работы данной ВМ был описан выше. Хотя, следует отметить особо, что аналитик исследует ВМ более тщательно. Он отыскивает характерные для данной ВМ точки программы, к которым можно впоследствии «привязаться» при написании инструментария для восстановления кода.
2. Написание декомпилятора. Исходя из полученной в результате анализа информации, создается декомпилятор виртуальной машины. Т.е. инструмент, позволяющий исходный или близкий по смыслу и обладающий схожими слож-ностными характеристиками код. Декомпилятор не обязательно должен быть абсолютно автоматическим средством. Это может быть интерактивная программа (или плагин к IDA) на вход которой подается недостающая информация.
Основными недостатками виртуальной машины VMProtect являются, прежде всего, низкая скорость работы виртуализованного кода и известность её поведения, а, следовательно, возможность написания полуавтоматических или даже автоматических средств декомпиляции. По сути дела, стойкость технологий виртуализации кода по отношению к автоматическим или полуавтоматическим средствам декомпиляции определяется качеством технологий обфускации, используемых для запутывания кода и данных самой ВМ.
Правила построения запутывающих преобразований
Как известно, первым шагом при применении практически любого алгоритма оптимизации является построение графа контроля управления. Только после решения этой задачи можно построить уравнения потока данных подпрограммы. Маскировка CFG может осуществляться различными методами. Основными из них является замена инструкций ветвления их эквивалентами, в которых происходит расчет адреса перехода и замена инструкций ветвления механизмами генерации исключительных ситуаций.
1. Рассчитываемые переходы. Зачастую большая часть переходов в исходной подпрограмме — переходы с заранее известной точкой, в которую этот переход должен быть осуществлен. Например, инструкция утр ітт32 осуществляет переход по адресу равному сумме адреса следующей по порядку инструкции и ітт32. Очевидно, что для того, чтобы определить тот адрес, на который должен быть осуществлен переход, достаточно посчитать сумму адреса инструкции, следующей за инструкцией перехода и ітт32. Т.е., говоря проще, нет необходимости исследовать код, предшествующий инструкции перехода. Замена простых относительных переходов вычисляемыми приведет к тому, что для вычисления адреса перехода необходимо будет произвести анализ кода, в котором данный переход вычисляется. При использовании глобального «мусорного» контекста можно добиться зависимости вычисления адреса перехода от переданных в подпрограмму параметров.
2. Системозависимые переходы. В данном случае переходы могут осуществляться при помощи системных механизмов, например, механизма структурной обработки исключений (SEH). В этом случае обработчик специально сгенерированного программой исключения передает управление в необходимую точку программы посредством сохранения нового указателя EIP в структуре CONTEXT, передаваемой обработчику исключения. Еще один пример - переопределение ряда векторов прерываний в таблице дескрипторов прерываний с последующей генерацией этих прерываний (исключений) самой программой.
Существует масса системозависимых методов передачи управления в программе по определенному адресу. Основными их недостатком, прежде всего, является недокументированность этих методов. Из этого следует, что программы, отлично работающие в одном семействе ОС, будут работать со сбоями или не будут работать вообще в другом семействе ОС. Более того, зачастую, такие программы «ведут» себя по разному на разных процессорах или на одних и тех же ОС с разными пакетами обновления. Разработчики ОС крайне не рекомендуют использовать такие приемы программирования.
Граф контроля управления называется приводимым тогда и только тогда, когда мы можем разделить дуги на две непересекающиеся группы, часто именуемые прямыми и обратными дугами. Эти группы обладают следующими свойствами:
1. Прямые дуги образуют ацикличный граф, в котором из начального узла может быть достигнут любой узел.
2. Обратные дуги состоят только из дуг, головы которых доминируют над хвостами. Из вышеперечисленных свойств очевидно, что в приводимом графе потока управления единственный вход в цикл осуществляется через его заголовок; нет никаких переходов в середину цикла извне. Известно, что анализ приводимых графов потока управления гораздо проще, чем анализ неприводимых CFG. Более того, ряд алгоритмов оптимизации применим только по отношению к приводимым CFG. В любом случае, замена приводимого графа потока управления неприводимым существенно усложнит применение ряда алгоритмов оптимизации по отношению к данной подпрограмме.
Очевидно, что определение регистра еах достигает точки перед инструкцией в позиции j и уничтожается инструкцией в позиции j. Однако регистровая переменная еах не является активной (или является мёртвой) в промежутке меж 53 ду инструкциями в позициях і и j. Таким образом, очевидно, что необходимости в определении переменной еах в позиции і нет.
Следует остановиться более подробно на вопросе «перемешивания» «реального» и «мусорного» контекстов. Это важно, т.к. если «реальный» код будет использовать только «реальный» контекст, а мусорный, в свою очередь, будет использовать только «мусорный», то хакеру гораздо проще будет отделить первый от второго и написать автоматический (или полуавтоматический) алгоритм такого отделения. Это, в свою очередь, позволит автоматически отделить мусорный код от настоящего. Прежде всего, необходимо сделать так, чтобы «реальные» инструкции могли использовать для своих целей «мусорный» контекст. С другой стороны, «мусорный» код тоже должен «уметь» работать с «реальным» контекстом. Обозначим VREAL множество всех ячеек памяти, используемых «реальными» инструкциями. Обозначим VFAKE множество всех ячеек памяти, используемых добавленными фальшивыми инструкциями.
Данное требование является не слишком жестким. Действительно, «мусорные» инструкции могут лишь читать из «реального» контекста и при этом вышеуказанное требование выполняется. Гораздо более правильно было бы сформулировать это требование следующим образом: vlUnv (211) где V EAI,VpAKE - множества всех ячеек памяти, из которых производят чтение соответственно «реальные» и «мусорные» инструкции; VREAL V1FAKE - множества всех ячеек памяти, в которые производят запись соответственно «реальные» и «мусорные» инструкции. Правило 5: Множества ячеек памяти для «реального» и «мусорного» контекстов, с которыми работает запутанная программа, должны соответствовать системе (2.11),
Определения глобальных фальшивых переменных должны уничтожаться тогда и только тогда, когда данная глобальная переменная используется в качестве одного из параметров в определении, уничтолсающем её предыдущее значение.
Действительно, глобальные по отношению к процедуре переменные компилятор старается не использовать в качестве временных. Для этого гораздо более подходит локальный контекст. Очевидно, что запутанный код должен «вести себя» похожим образом. По сути дела, правило 6 является несколько более строгим вариантом правила 4.
В фальшивом глобальном контексте наиболее удобно сохранять результаты каких-либо промежуточных значений локальных переменных, регистров. Сохранять константы в глобальном контексте не рекомендуется, т.к. в обычном коде редко можно встретить такие конструкции.
«Мертвый» код не должен сильно отличаться от реально выполняемого кода. Все правила, приведенные выше, должны относится, в том числе, и к неліу.
В данном правиле подразумевается, что семейство инструкций, используемых в реально выполняемом коде, должно совпадать с семейством инструкций, используемых в «мертвом» коде. Например, если в реально выполняющемся коде используется стандартное подмножество из инструкций общего назначения, то использование в мёртвом коде инструкций из семейства FPU или инст 55 рукций, не характерных для той среды, в которой должен будет выполняться этот код, приведет к упрощению обнаружения «мертвого» кода.
Эффективное использование вышеприведенных правил позволит существенно усложнить процесс автоматической деобфускации подпрограммы. Более того, благодаря использованию глобального контекста, достаточно сложно будет понять, что именно делает подпрограмма без детального анализа подпрограмм, которые с ней взаимодействуют.
Метод запутывания графа потока управления при помощи сетей Петри
Пример сети Петри, используемой при запутывании кода от исследования и отладки Данный подход в своей реализации является системозависимым, а, следовательно, не универсальным. Однако сама идея метода представляется достаточно интересной, т.к. позволяет существенно затруднить отладку запутанного таким образом кода. Предлагается код подпрограммы разделить на отдельные участки, которые будут выполняться каждый в своем потоке. Каждый такой участок выполняется при выполнении перехода сети Петри.
Предположим, что /, ...t1 - некие участки кода программы, и последовательность их выполнения важна, а позициирх ...р1 соответствуют определенным наборам входных данных. Предположим также, что все эти участки кода выполняются каждый в своем отдельном потоке, а последовательность их выполнения регулируется посредством механизмов синхронизации операционной системы. Пусть известно максимально допустимое время выполнения каждого из этих участков кода - Т; max. Будем считать, что при времени выполнения /-го участка кода большем, чем Tt max, последовательность срабатывания переходов изменится, и переход t7 не сработает. Таким образом, установка точки останова в одном из вышеупомянутых потоков приведет к изменению последовательности срабатывания переходов, а, следовательно, реверсирование кода становится более проблематичной задачей. Реализовать данный подход можно при помощи структурной схемы, изображенной на рисунке 3.13. Совершенно очевидно, что для корректной работы запутанной подпрограммы необходимо сделать так, чтобы контекст, с которым работает рабочий код, не менялся в результате переключения потоков. Под контекстом понимается следующее; значения регистров, значения стековых переменных, значения флагов, значения глобальных областей памяти. Т.е., грубо говоря, переключение между потоками должно происходить совершенно прозрачно, не изменяя ничего из вышеперечисленного. Коды пролога и эпилога нужны как раз для этой цели. Код пролога восстанавливает контекст, который был сохранен кодом эпилога предыдущего участка. Менеджер сети Петри необходим для того, чтобы решить какому именно из потоков передать управление.
Структурная схема подпрограммы, защищенной при помощи сети Петри 3. Менеджер сети Петри «запускает» объекты-переходы, содержащие в себе в числе всего прочего вызовы функции WaitForMultipleObjects. Перед этими вызовами стоят вызовы функций задержки по времени, определяющие, как раз, какой из переходов сработает первым. Как только этот переход срабатывает, все остальные объекты-переходы «блокируются», т.е. сработать уже не могут до следующего вызова менеджера сети Петри. 4. Далее происходит передача управления на защищенный контейнер соответствующего объекта-перехода (размораживание соответствующего потока). 5. Поток, в который было передано управление, восстанавливает контекст, выполняет рабочий код, а затем сохраняет полученный контекст и управление передается менеджеру сети Петри. 6. Менеджер сети Петри, в свою очередь, «переставляет» фишки, и запускает снова объекты-переходы. 7. Процесс повторяется до тех пор, пока не отработает переход, содержащий последний участок кода. Для сети Петри, изображенной на рисунке 3.12, это переход t1. Назовем этот переход последним переходом. 8. После того, как отработает последний переход, менеджер сети Петри освобождает все выделенные ресурсы и управление передается далее по коду. Данный подход имеет свои сложности реализации, связанные с синхронизацией потоков, поэтому по умолчанию не используется.
При помощи описанного в данной работе алгоритма обфускации можно решать и задачу внедрения постороннего кода в приложение [10], [11]. В нашем случае это будет код, осуществляющий функции защиты приложения от нелегального распространения. Например, код, в котором происходит обращение к ключу Guardant. Следует отметить, что для эффективного внедрения кода в приложение необходимо соблюсти ряд условий. 1. Время выполнения подпрограмм, в которые происходит внедрение, существенно увеличится. В силу этого скорость данные подпрограмм не должна сильно влиять на скорость выполнения приложения.
Желательно, чтобы подпрограммы, в которые происходит внедрение, содержали участки кода, выполняющие нетривиальные задачи. Например, если в подпрограмме происходят какие-либо расчеты, то при соблюдении 1-го условия данная подпрограмма подходит для внедрения в неё стороннего кода.
Для выявления таких подпрограмм необходимо профилирование исходной программы. Эта задача выходит за рамки данной работы и не будет далее рассматриваться. Предположим, что мы выделили те подпрограммы, в которые можно осуществить внедрение. CFG исходной подпрограммы Перенос базового блока в интегрируемый код с последующей обфускацией как на уровне входных данных блока, так и интегрируемой подпрограммы
Итак, у нас есть интегрируемая подпрограмма и подпрограмма, в которую происходит внедрение. Следует заметить, что внедрить интегрируемую подпрограмму в исходную не получится, т.к. в обеих подпрограммах наверняка есть базовые блоки и переходы между ними. Вместо попыток решать задачу внедрения интегрируемой подпрограммы в исходную можно выделить базовый блок исходной подпрограммы и внедрить его в интегрируемую подпрограмму, как это показано на рисунке 3.14. При этом все операнды инструкций внедряемого базо вого блока становятся аргументами, значения которых необходимо передать в интегрируемую подпрограмму. Инструкции внедряемого базового блока не обязательно должны следовать друг за другом. Они могут быть «разбросаны» по всей интегрируемой подпрограмме. Важно лишь сделать так, чтобы эти инструкции всегда выполнялись при вызове интегрируемой подпрограммы и каждая из них выполнялась один раз. Этого можно достичь либо посредством включения этих инструкции в базовые блоки, которые выполняются всегда при вызове интегрируемой подпрограммы - либо, включив указанные инструкции в базовые блоки ветвлений, применив при этом технологию дублирования кода. Эти базовые блоки не должны находиться в теле цикла.
Пример графа потока управления интегрируемой подпрограммы. После внедрения базового блока в интегрируемую подпрограмму необходимо применить описанный выше механизм обфускации как по отношению к исходной подпрограмме, так и по отношению к интегрируемой подпрограмме. В исходной подпрограмме вместо «позаимствованного» базового блока ставятся инструкции задания аргументов, вызов интегрируемой подпрограммы и инструкции восстановления контекста. Для улучшения стойкости метода можно применить многоступенчатый метод обфускации по отношению к исходной подпрограмме. Исходная подпрограмма предварительно обрабатывается алгоритмом обфускации, описанным выше без разбиения базовых блоков на функции. После такой обработки выделяется уже запутанный базовый блок, производится его внедрение в интегрируемую подпрограмму, а затем производится об-фускация интегрируемой подпрограммы и исходной подпрограммы с последующим разбиением базовых блоков на подпрограммы.
На рисунке 3.15 приведен пример графа потока управления интегрируемой подпрограммы. В блоки 1 и 6 возможно внедрение базового блока из исходной подпрограммы, т.к. управление в эти блоки попадает всегда при передаче управления в интегрируемую подпрограмму. Внедрение также возможно и в блоки 3 и 5, однако инструкции, внедряемые в эти блоки, должны быть продублированы. В блоки 2 и 4 внедрение произвести невозможно, т.к. они входят в тело цикла.