DOWN THE RABBIT HOLE.
Быстрое развитие мультимедиа и Интернет технологий в последние годы вызывает
потребность в защите такой интеллектуальной собственности, как программные
продукты (ПП).
Разработка наиболее эффективного метода защиты для того или иного программного
продукта, в нынешнее время, становиться одной из важных задач большинства
программистов, которые занимаются разработкой специализированного, платного
программного обеспечения (ПО), так как это позволяет им продавать свой
интеллектуальный труд, и исключить возможности его нелегального использования
среди потребителей, говоря иными словами, пользователь не сможет использовать
оригинальную, лицензионную копию определенной программы предварительно не купив,
не заплатив денег её разработчику.
Затраты производителей на создание эффективного метода защиты их программных
продуктов окупаются и компенсируют потенциальный ущерб, наносимый нелегальным
копированием и использованием программ.
Существуют два основных способа защиты интеллектуальной собственности, и
следовательно, самих программных продуктов:
1) Юридический (законный). Данный способ защиты заключается в создании
определенных актов, в соответствии с законом, которые будут охранять
интеллектуальную собственность (в нашем случае программные продукты) от
нелегального использования. Данный способ включает в себя такие методы как
патентование, оформление авторских прав на интеллектуальную собственность и т.д.
Также он предусматривает возможность лицензирования ПП, так, например большинство
ПП поставляются вместе с лицензией, которая подтверждает право пользователя
использовать этот ПП, то есть, покупая лицензионную копию программы, пользователь
в некой мере производит покупку лицензии на право работы с ее копией. Можно
выделить два основных вида лицензий на программные продукты:
- Временная. Позволяет использовать ПП неограниченному числу пользователей в
течение ограниченного периода времени.
- Оптимальная. Позволяет использовать ПП ограниченному числу пользователей в
течение неограниченного периода времени.
2) Технический. Реализуется путем включения в ПП, какого либо из существующих
методов защиты, который будет запрещать его нелегальное использование. По
сравнению с юридическим способом защиты ПП, он является наиболее
распространенным, так как он практичен, и сравнительно не дорогой в реализации
(в дальнейшем, будет приводиться именно его описание).
Ниже приводится поверхностное описание наиболее распространенных на данный
момент методов защиты ПП.
Выполнение на стороне сервера.
Данный метод защиты основан на технологии клиент-сервер, он позволяет
предотвратить отсылку кода программы пользователям, которые будут с ней работать,
так как сама программа храниться, и выполняется на сервере, а пользователи
используя клиентскую часть этой программы, получают результаты ее выполнения
(рисунок 0000).
Рисунок 0000. Выполнение на стороне сервера
Клиентскую часть можно распространять бесплатно, это позволит пользователям
платить только за использование серверной части.
Часто данный метод защиты используется в различных программах-сценариях,
которые располагаются на WEB серверах, и выполняют общие операции, например,
контролируют работу других программ (front-end), а также передают им, какие либо
данные для обработки.
Недостатком данного метода является то, что он устанавливает зависимость
пропускной способности сети и тех данных, с которыми будет работать программа
(соответственно работа с мультимедиа данными требует наиболее максимальной
пропускной способности сети). Поэтому данный метод наиболее эффективен для
простых программ (сценариев), потребность в которых очень большая среди
пользователей.
Преимущество такого метода, заключается в том, что злоумышленнику в данном
случае нужно будет для начала скомпрометировать сам сервер, только после чего он
сможет получить копию требуемой программы.
Данный метод не позволяет защитить программу от нелегального копирования,
поэтому после того как ее копия попадет к злоумышленнику, он сможет делать с ней
что захочет (whatever you want :).
Водяной знак (software watermark) и отпечаток пальца.
Использование водяных знаков, основывается на записи в код программы скрытой
информации (кому принадлежат авторские права и т.д.), которая позволяет истинному
автору программы доказать то, что она является именно его интеллектуальной
собственностью (но обычно использование водяных знаков не ограничивается только
этим).
Такая скрытая информация, обозначим ее "W" (которая и называется водяным
знаком), должна быть записана в программу "Р", таким образом, чтобы:
- "W" было надежно расположено (запрятано) в "Р", и могло быть впоследствии
извлечено, без каких либо изменений (повреждений).
- "W" не влияло на работу "Р".
- "W" несло какую-то определенную информацию, которая позволит доказать, то что
ее присутствие в "Р" неслучайное, то есть является результатом преднамеренных
действий.
Также для увеличения эффективности водяного знака, его можно записать более чем
один раз, в разных местах программы.
Отпечаток пальца (software fingerprint) эта иная технология, так как она кроме
записи информации, позволяющей доказать интеллектуальное право собственности на
программу, требует записи в каждую копию программы уникального идентификационного
кода, присваиваемого каждому покупателю программы, что позволяет в последствии
быстро отследить нарушителя авторского права, который, например, будет нелегально
перепродавать программу (search and destroy them :).
Недостаток этих методов состоит в том, что у злоумышленника может появиться
возможность подвергнуть изменению водяной знак или отпечаток пальца.
Установка подлинности кода (tamper-proofing).
В данном случае, в программу помещается процедура проверки целостности самой
программы, что позволяет определить была ли программа изменена (были ли внесены
какие-то либо изменения в ее код). Если эта процедура обнаруживает, что в
программу внесены изменения, она делает программу не функциональной (рисунок 0001).
Это позволяет защитить программный продукт, от изменений со стороны
злоумышленника (применения им крэков). Работа этой процедуры должна быть
неприметной для тех, кто будет работать с программой, это позволит сбить с толку
неопытных взломщиков. Также ее программная реализация не должна, иметь вид "if
(programm_code_original()) { i = 1} else { i = 0}", так как такую процедуру
проверки будет легко обойти, например для этого достаточно будет определить
место, где она вызывается, и изменить само условие проверки. Процедура проверки
должна быть разбита на как можно большее количество этапов, которые будут
выполняться в разное время работы самой программы. Если все этапы проверки будут
успешно пройдены, тогда можно будет предположить, что программа оригинальна, и не
содержит дефектов (изменений). Существуют такие пути проверки целостности
программы:
- проверка идентичности оригинальной и запускаемой программы. Обычно для этого
определяется контрольная сумма запущенной программы, которая потом сверяется с
записанной в процедуру проверки, контрольной суммой оригинальной программы. Для
осуществления быстрой проверки, используют такие алгоритмы: CRC или MD4/5
(алгоритмы резюмирования сообщений).
- проверка результатов работы программы. То есть осуществляется проверка выходных
значений функций, которые очень чувствительны, к каким либо возможным изменениям
в программном коде.
- создание запускаемой программы на лету, в соответствии с ее оригинальным
образом. Это позволяет избежать возможности выполнения изменений внесенных в
программу, так как они не будут учитываться при ее создании.
Шифрование программного кода.
Используется, для того чтобы предотвратить вмешательство в программу, а также
усложнить изучение взломщиком того, как устроена программа, как она работает, как
в ней реализован метод защиты и т.д.
Данный метод защиты предусматривает зашифровывание кода программы, после чего
она в зашифрованном виде поставляется конечным пользователям (иногда эффективно
зашифровывать только наиболее важные, критические, участки кода, а не весь код
программы). Когда пользователь запускает такую программу, вначале будет запущена
процедура расшифровки программы, которой потребуется ключ, с помощью которого
будет расшифрована запускаемая программа. См. рисунок 0010.
Сам ключ обычно представляет собой последовательность байт (символов), который
генерируется в результате определенных (математических) операций.
Он может быть привязан к уникальным характеристикам компьютера пользователя,
который приобрел программу и работает с ее лицензионной копией (принцип "одна
машина - одна копия"), но такой способ генерации ключа, создает определенные
неудобства для конечных пользователей, так как могут возникнуть определенные
трудности при использовании такой программы, на другом компьютере.
В последнее время становиться актуально для расшифровки программы использовать
электронные ключи, они являются наиболее надежным и эффективным методом защиты
дорогостоящих ПП. Он предоставляет высокую стойкость ко взлому и не ограничивает
использование легальной копии программы на разных компьютерах.
Электронный ключ представляет собой небольшое устройство, которое
подсоединяется к одному из портов компьютера (COM,LPT,USB).
Но все же существует возможности обхода защиты программ зашифрованных с
использованием электронных ключей, некоторые из них перечислены ниже:
- Изготовление аппаратной копии ключа. Этот метод основывается на считывании
содержимого микросхемы памяти ключа, после чего полученные данные переносятся в
микросхему другого ключа. Способ этот достаточно трудоемкий и может применяться,
если память ключа не защищена от считывания.
- Изготовление эмулятора ключа. Заключается в создании программы (драйвера),
которая эмулирует работу электронного ключа, в результате защищенная программа
сможет работать без электронного ключа, для этого перед ее запуском нужно будет
всеголиж запустить программу эмулятор.
Способ шифрования программ имеет недостатки, одним из которых является то, что
у взломщика есть возможность, после приобретения лицензионной копии программы,
произвести извлечение расшифрованных частей программы в процессе ее работы из
памяти. Поэтому сразу после исполнения расшифрованного кода его необходимо
выгружать из памяти.
1. Обфускация, как один из методов защиты программ
В большинстве случаев для обхода защиты, взломщику требуется изучить принцип
работы ее кода, и то, как она взаимодействует с самой защищаемой программой, этот
процесс изучения называется процессом реверсивной (обратной) инженерии. Этот
процесс часто зависит от свойств человеческой психики, поэтому использование этих
свойств позволяет снизить эффективность самого процесса реверсивной инженерии.
Обфускация ("obfuscation" - запутывание), это один из методов защиты
программного кода, который позволяет усложнить процесс реверсивной инженерии кода
защищаемого программного продукта.
Обфускация может применяться не только для защиты ПП, она имеет более широкое
применение, например она, может быть использована создателями вирусов, для защиты
их творений и т.д.
Суть процесса обфускации заключается в том, чтобы запутать программный код и
устранить большинство логических связей в нем, то есть трансформировать его так,
чтобы он был очень труден для изучения и модификации посторонними лицами (будь то
взломщики, или программисты которые собираются узнать уникальный алгоритм работы
защищаемой программы).
Из этого следует, что обфускация одна не предназначена для обеспечения наиболее
полной и эффективной защиты программных продуктов, так как она не предоставляет
возможности предотвращения нелегального использования программного продукта.
Поэтому обфускацию обычно используют вместе с одним из существующих методов
защиты (шифрование программного кода и т.д.), это позволяет значительно повысить
уровень защиты ПП в целом (рисунок 0011, "Т1,Т2,...,Tn" - процессы обфускации).
Процесс обфускации как метод зашиты, можно считать сравнительно новым (первые
статьи, посвященные обфускации, как методу защиты кода программных продуктов,
появились примерно три-четыре года назад), и перспективным.
Обфускация соответствует принципу экономической целесообразности, так как ее
использование не сильно, увеличивает стоимость программного продукта, и позволяет
при этом снизить потери от пиратства, и уменьшить возможность плагиата в
результате кражи уникального алгоритма работы защищаемого программного продукта.
2. Процесс обфускации
Существуют различные определения процесса обфускации. Рассматривая данный
процесс с точки зрения защиты ПП, и трансформации кода программы без возможности
в последствии вернуться к его первоначальному виду (трансформация "в одну
сторону"), можно дать такое определение:
Определение. Пусть "TR" будет трансформирующим процессом, тогда при "PR1 =TR=>
PR2" программа "PR2" будет представлять собой трансформированный код программы
"PR1". Процесс трансформации "TR" будет считаться процессом обфускации если,
будут удовлетворены такие требования:
- код программы "PR2" в результате трансформации будет существенно отличаться от
кода программы "PR1", но при этом он будет выполнять те же функции что и код
программы "PR1", а также будет работоспособным.
- изучение принципа работы, то есть процесс реверсивной инженерии, программы
"PR2" будет более сложным, трудоемким, и будет занимать больше времени, чем
программы "PR1".
- при каждом процессе трансформации одного и того же кода программы "PR1", код
программ "PR2" будут различны.
- создание программы детрансформирующей программу "PR2" в ее наиболее похожий
первоначальный вид, будет неэффективно.
Так как код, получаемый после осуществления обфускации, над одной и той же
программой, разный то процесс обфускации можно использовать для быстрой
локализации нарушителей авторских прав (то есть тех покупателей, которые будут
заниматься нелегальным распространением купленных копий программ). Для этого
определяют контрольную сумму каждой копии программы прошедшей обфускацию, и
записывают ее вместе с информацией о покупателе, в соответствующую базу данных.
После этого для определения нарушителя, достаточно будет, определив контрольную
сумму нелегальной копии программы, сопоставить ее с информацией хранящейся в
базе данных.
Программный код может быть представлен в двоичном виде (последовательность
байтов представляющих собой так называемый машинный код, который получается
после компиляции исходного кода программы) или исходном виде (текст содержащий
последовательность инструкций какого-то языка программирования, который понятен
человеку, этот текст в последствии будет подвержен компиляции или интерпретации
на компьютере пользователя).
Процесс обфускации может быть осуществлен над любым из выше перечисленных
видов представления программного кода, поэтому принято выделять следующие уровни
процесса обфускации:
- низший уровень, когда процесс обфускации осуществляется над ассемблерным кодом
программы, или даже непосредственно над двоичным файлом программы хранящим
машинный код.
- высший уровень, когда процесс обфускации осуществляется над исходным кодом
программы написанном на языке высокого уровня.
Осуществление обфускации на низшем уровне считается менее комплексным
процессом, но при этом более трудно реализуемым по ряду причин. Одна из этих
причин заключается в том, что должны быть учтены особенности работы большинства
процессоров, так как способ обфускации, приемлемый на одной архитектуре, может
оказаться неприемлемым на другой.
Также на сегодняшний день процесс низкоуровневой обфускации исследован мало
(наверно потому что не успел получить широкой популярности).
Большинство существующих алгоритмов и методов обфускации (включая те которые
будут рассмотрены ниже) могут быть применены для осуществления процесса
обфускации как на низшем, так и на высшем уровне.
Также иногда может быть неэффективно, подвергать обфускации весь код программы
(например, из-за того, что в результате может значительно снизится время выполнения
программы), в таких случаях целесообразно осуществлять обфускацию только наиболее
важных участков кода.
Далее в тексте употребляются такие понятия:
Исходная программа - защищаемая программа, подвергающаяся процессу обфускации.
Злоумышленник - личность, занимающаяся изучением алгоритма работы исходной
программы (реверсивной инженерией), для каких либо своих корыстных целей.
Объект - имя какого-то хранилища данных, например переменной, массива и т.д.
Деобфускация - процесс, который позволяет обойти обфускацию, используется
злоумышленниками (деобфускация описана в отдельном разделе данного материала).
3. Оценка процесса обфускации
Существует много методов определения эффективности применения того или иного
процесса обфускации, к конкретному программному коду.
Эти методы принято разделять на две группы: аналитические и эмпирические.
Аналитические методы основываются на трех величинах характеризующих насколько
эффективен тот или иной процесс обфускации:
- Устойчивость - указывает на степень сложности осуществления реверсивной
инженерии над кодом прошедшим процесс обфускации.
- Эластичность - указывает на то насколько хорошо данный процесс обфускации,
защитит программный код от применения деобфускаторов.
- Стоимость преобразования - позволяет оценить, насколько больше требуется
системных ресурсов для выполнения кода прошедшего процесс обфускации, чем для
выполнения оригинального кода программы.
Их наиболее эффективно применять при сравнении различных алгоритмов обфускации,
но при этом они не могут дать абсолютного ответа на вопрос насколько эффективно
применение того или иного алгоритма, именно к данному программному коду.
Эмпирические же методы могут дать приемлемый ответ на такой вопрос, т.к. они
основываются на статистических данных получаемых в результате исследований. Для
проведения одного из таких исследований нужна группа людей (как можно лучше
знакомых, с реверсивной инженерией), фрагмент кода защищаемой программы, и набор
различных алгоритмов обфускации.
Результаты такого исследования будут включать в себя минимальное количество
времени, которое потребовалось группе людей, для того чтобы изучить каждый фрагмент
кода прошедшего один из алгоритмов обфускации.
4. Алгоритмы процесса обфускации
Алгоритм обфускации в большинстве случаев рассматривается как алгоритм,
которого должен придерживаться обфускатор (независимая программа, которая
осуществляет процесс обфускации над переданным ей кодом).
На данный момент существуют различные алгоритмы осуществления процесса
обфускации, начиная от общих (абстрактных) алгоритмов процесса обфускации и
заканчивая более продвинутыми. Эти алгоритмы создавались в соответствии с
возможностями того или иного языка программирования, и на сегодня большинство из
них адаптировано непосредственно под языки программирования высокого уровня. Ниже
представлено короткое описание некоторых из них.
Алгоритм Колберга ("Collberg`s algorithm").
Данный алгоритм оперирует следующими входными значениями:
- программа "А" состоящая из исходных или объектных (двоичных) файлов "{С1,С2}".
- стандартные библиотеки, используемые программой "{L1,L2}".
- набор трансформирующих процессов "Т{Т1,Т2}".
- определенный фрагмент кода "S", который извлекается из программы "А", и
который непосредственно будет подвержен трансформации.
- набор функций "Е{Е1,Е2}" которые будут определять эффективность применения
определенных трансформирующих процессов "{Т1,Т2}" к фрагменту кода "S".
- набор функций "I{I1,I2}" которые будут определять важность фрагмента кода "S",
и в зависимости от этого будут задавать определенное значение переменной
"RequireObfuscation" (чем "S" важнее тем эта переменная будет хранить большее
значение).
- две числовые переменные "AcceptCost" > 0, "RequireObfuscation" > 0, где первое
хранит информацию о доступном максимальном увеличении системных ресурсов
по требующихся программе "А" после того как она подвергнется обфускации, а
вторая переменная будет хранить значение требуемого уровня осуществления
обфускации (чем важнее фрагмент кода "S", тем это значение должно быть больше).
Алгоритм Колберга имеет такую последовательность операций:
- Загрузка элементов "{С1,С2}" программы "А".
- Загрузка библиотек "{L1,L2}".
- Осуществление обфускации над программой "А", путем выделения фрагмента кода
"S" и определения наиболее эффективного процесса трансформации для него. Этот
этап повторяется до тех пор, пока не будет, достигнут требуемый уровень обфускации
"RequireObfuscation" или допустимое увеличение ресурсов "AcceptCost".
- Генерация трансформируемой программы "А`".
Алгоритм Колберга считается общим алгоритмом осуществления !процесса
обфускации (то есть он не определяет, как именно должен осуществляться, тот или
иной !метод обфускации), ниже будет рассмотрен более специализированный алгоритм,
так как он описывает последовательность осуществления одного из методов
обфускации, а именно обфускации управления.
Chenxi Wang`s алгоритм.
В качестве входных данных алгоритм принимает типичную
процедуру, написанную на языке высокого уровня. Процесс обфускации каждой такой
процедуры состоит из трех этапов:
- создание графа потока управления этой процедуры (граф задаётся множеством
блоков и множеством связей соединяющих их), после чего граф разбивается, путем
замены циклических конструкций в нем на конструкции типа "if (условие) goto",
(Рис. 0100).
- нумерация всех блоков в графе, и добавление в код процедуры переменной
(например "swVar") хранящей номер следующего выполняемого блока
- приведение графа к однородному ("плоскому") виду (Рис. 0101)
Выше описанный вариант алгоритма обфускации ("Chenxi Wang`s algorithm")
является не сильно устойчивым, так как определить следующий выполняемый блок,
нетрудно (он в нашем случае будет храниться в переменной "swVar"). Поэтому для
повышения его устойчивости вводят массив (например "@gg"), содержащий помимо
номеров блоков, не нужную информацию, в результате запись "$swVar = S6", можно
заменить на нечто подобное "$swVar = $gg[$gg[1] + $gg[3]]".
5. Виды обфускации
Процессы обфускации можно классифицировать по видам, в зависимости от способа
модификации кода программы.
5.1 Лексическая обфускация
Наиболее простая, заключается в форматировании кода программы, изменении его
структуры, таким образом, чтобы он стал нечитабельным, менее информативным, и
трудным для изучения.
Обфускация такого вида включает в себя:
- удаление всех комментариев в коде программы, или изменение их на
дезинформирующие
- удаление различных пробелов, отступов которые обычно используют для лучшего
визуального восприятия кода программы
- замену имен идентификаторов (имен переменных, массивов, структур, хешей,
функций, процедур и т.д.), на произвольные длинные наборы символов, которые
трудно воспринимать человеку
- добавление различных лишних (мусорных) операций
- изменение расположения блоков (функций, процедур) программы, таким образом,
чтобы это не коим образом не повлияло на ее работоспособность.
Изменение глобальных имён идентификаторов следует производить в каждой единице
трансляции (один файл исходного кода), так чтобы они имели одинаковые имена (в
противном случае защищаемая программа может стать не функциональной). Также
следует учитывать специфические идентификаторы, принятые в том языке
программирования, на котором написана защищаемая программа, имена таких
идентификаторов, лучше не изменять, например, в PERL-е к таким идентификаторам
можно отнести "@ARGV", "$_", "$^O" и т.д.
Ниже представлен пример фрагмента исходного кода программы (написанной на
Perl), до и после прохождения лексической обфускации.
До лексической обфускации:
my $filter;
if (@pod) {
my ($buffd, $buffer) = File::Temp::tempfile(UNLINK => 1);
print $buffd "";
print $buffd @pod or die "";
print $buffd
close $buffd or die "";
@found = $buffer;
$filter = 1;
}
exit;
sub is_tainted {
my $arg = shift;
my $nada = substr($arg, 0, 0); # zero-length
local $@; # preserve caller's version
eval { eval "#" };
return length($@) != 0;
}
sub am_taint_checking {
my($k,$v) = each %ENV;
return is_tainted($v);
}
После лексической обфускации:
sub z109276e1f2 { ( my $z4fe8df46b1 = shift ( @_ ) ) ; ( my
$zf6f94df7a7 = substr ( $z4fe8df46b1 ,
(0x1eb9+ 765-0x21b6) , (0x0849+ 1465-0x0e02) ) ) ; local $@ ;
eval { eval ( (
"" ) ) ; } ; return ( ( length ( $@ ) != (0x26d2+ 59-0x270d) ) )
; } my ( $z9e5935eea4 ) ; if ( @z6a703c020a ) { ( my (
$z5a5fa8125d , $zcc158ad3e0 ) =
File::Temp::tempfile ( "" , (0x196a+ 130-0x19eb) ) ) ; print (
$z5a5fa8125d "" ) ; ( print ( $z5a5fa8125d @z6a703c020a
) or die ( ( ( ( "" . $zcc158ad3e0 ) . "\x3a\x20" ) . $! ) ) ) ;
print ( $z5a5fa8125d "" ) ; ( close ( $z5a5fa8125d ) or die ( ( (
( "" ) ) ) ; ( @z8374cc586e = $zcc158ad3e0 ) ; ( $z9e5935eea4 =
(0x1209+ 1039-0x1617) ) ; } exit ; sub z021c43d5f3 { ( my (
$z0f1649f7b5 , $z9e1f91fa38 ) = each ( %ENV ) ) ; return (
z109276e1f2 ( $z9e1f91fa38 ) ) ; }
Данная обфускация программного кода, по сравнению с остальными, позволяет
сравнительно быстро привести исходный код программы, в нечитабельное состояние.
Один из ее недостатков состоит в том, что она эффективна только для осуществления
высокоуровневой обфускации.
5.2 Обфускация данных
Такая обфускация связана с трансформацией структур данных. Она считается более
сложной, и является наиболее продвинутой и часто используемой. Ее принято делить
на три основные группы, которые описаны ниже.
Обфускация хранения. Заключается в трансформации хранилищ данных, а также самих
типов данных (например, создание и использование необычных типов данных,
изменение представления существующих и т.д.). Ниже приведены основные методы,
позволяющие осуществить такую обфускацию:
- изменение интерпретации данных определенного типа. Как известно сохранение,
каких либо данных в хранилищах (переменных, массивах и т.д.) определенного типа
(целое число, символ) в процессе работы программы, очень распространенное
явление. Например, для перемещения по элементам массива очень часто используют
переменную типа "целое число", которая выступает в роли индекса. Использование
в данном случае переменных иного типа возможно, но это будет не тривиально и
может быть менее эффективно. Интерпретация комбинаций разрядов
содержащихся в хранилище данных осуществляется в зависимости от его типа. Так,
например, можно сказать, что 16-разрядная переменная целого типа содержащая
комбинации разрядов 0000000000001100 представляет целое число 12, но это
простое соглашение, данные в такой переменной можно интерпретировать по-разному
(не обязательно как 12, а, например как 1100 и т.д.).
- изменение срока использования хранилищ данных, например переход от локального
их использования к глобальному и наоборот. В качестве примера можно привести
две различные функции (язык PERL):
sub func1 { ... $a ... ; }
sub func2 { ... $b ... ; }
если эти две функции не могут выполняться в процессе программы одновременно,
значит, для них может быть создана одна глобальная переменная, которая будет
замещать переменные $a,$b, например:
$AB = 0 ;
sub func1 { ... $AB ... ; }
sub func2 { ... $AB ... ; }
- преобразование статических (неменяющихся) данных в процедурные. Большинство
программ, в процессе работы, выводят различную информацию, которая чаще всего в
коде программы представляется в виде статических данных
таких как строки, которые позволяют визуально ориентироваться в ее коде и
определять выполняемые операции. Такие строки также желательно предать
обфускации, это можно сделать, просто записывая каждый символ строки, используя
его ASCII код, например символ "A" можно записать как 16-ричное число "0х41",
но такой метод банален. Наиболее эффективный метод, это когда в код программы
в процессе осуществления обфусации добавляется функция, генерирующая
требуемую строку в соответствии с переданными ей аргументами, после этого
строки в этом коде удаляются, и на их место записывается вызов этой функции с
соответствующими аргументами. Например, фрагмент кода (написанный на PERL):
print "LOL\n" ;
$var = "101" ;
после обфускации, будет похож на что-то вроде:
sub string {
my ($i) = @_ ;
my $k = 0 ;
$str = "" ;
while (1)
{
l1: if ($i == 1) {$str .= "L";$k = 0;goto l7;}
l2: if ($i == 4) {$str .= "S";$k = 0;goto l7;}
l3: if ($i == 3) {$str .= "1";$k = -1;goto l7;}
l4: if ($i == 4) {$str .= "m";$k = 3;goto l7;}
l5: return $str;
l6: if ($k == 0) {$str .= "1";$k += 2;goto l5;}
else {$str .= "L";$k -= 1;goto l5;}
l7: if ($k < 1) {$str .= "0";$k++;goto l6;}
}
}
...
print string(1)."\n" ;
$var = string(3) ;
Также к статическим данным относятся числовые константы, которые могут быть
также трансформированы, например число 1 можно представить как: (a + 1 - b), где
a = b;
- разделение переменных. Переменные фиксированного диапазона могут быть разделены
на две и более переменных. Для этого переменную "V" имеющую тип "x" разделяют на
"k" переменных "v1,...,vk" типа "y" то есть "V == v1,...,vk". Потом создается
набор функций позволяющих извлекать переменную типа "x" из переменных типа "y" и
записывать переменную типа "x" в переменные типа "y". В качестве примера
разделения переменных, можно рассмотреть способ представления одной переменной
"B" логического типа (boolean) двумя переменными "b1, b2" типа короткого целого
(short), значение которых будет интерпретироваться таким образом:
B | b1 | b2
----------------
false | 0 | 0
true | 0 | 1
true | 1 | 0
false | 1 | 1
Тогда такой фрагмент кода (написан на С++):
bool B ;
B = false ;
if (B) {...}
будет представлен так:
short b1, b2 ;
b1 = b2 = 1 ; // или b1 = b2 = 0
if (!(b1 & b2)) {...}
- изменение представления (или кодирование). Например, целочисленную переменную
"i", в ниже представленном фрагменте кода (написанном на С):
...
int i = 1;
...
while (i < 1000)
{
... A[i] ...
i++ ;
}
можно заменить, выражением "i` = c1*i + c2" где "c1,c2" являются константами, в
результате, фрагмент выше приведенного кода измениться и будет сложен для
восприятия:
...
int i = 11;
int c1 = 8, c2 = 3 ;
...
while (i < 8003)
{
... A[(i - 3)/8] ...
i += 8 ;
}
Обфускация соединения. Один из важных этапов, в процессе реверсивной инженерии
программ, основан на изучении структур данных. Поэтому важно постараться, в
процессе обфускации, усложнить представление используемых программой структур
данных. Например, при использовании обфускации соединения это достигается
благодаря соединению независимых данных, или разделению зависимых. Ниже приведены
основные методы, позволяющие осуществить такую обфускацию:
- объединение переменных. Две или более переменных "v1,...,vk" могут быть
объединены в одну переменную "V", если их общий размер ("v1,...,vk") не
превышает размер переменной "V". Например, рассмотрим простой пример объединения
двух коротких целочисленных переменных "X","Y" (размером 16 бит) в одну
целочисленную переменную "Z" (размером 32 бита). Для этого воспользуемся
формулой
Z(X,Y) = 2^16 * Y + X
которая позволит, пренебрегая сложением, определять значение Y, т.е. пусть X =
= 12, Y = 4 => Z = 65536 * 4 + 12 = 262156, теперь зная "Z" для нахождения "Y"
можно 262156 / 65536 = 4.000183105 или приблизительно 4.
При осуществлении арифметических операций над значениями переменных "X", "Y"
хранящихся в "Z" нужно учитывать выше приведенную формулу, т.е.:
Z(X+n,Y) = 2^16 * Y + (X + n) = Z(X,Y) + n ;
Z(X,Y-n) = 2^16 * (Y - n) + X = 2^16 * Y - 2^16 * n + X =
= Z(X,Y) - 2^16 * n ;
и т.д. В результате код до обфускации (язык Си):
short X = 12, Y = 4 ;
X += 5 ;
трансформируется в:
int Z = 262156 ;
Z += 5 ;
- реструктурирование массивов, заключается в запутывании структуры массивов, путем
разделения одного массива на несколько подмассивов, объединения нескольких
массивов в один, сворачивания массива (увеличивая его размерность) и наоборот,
разворачивая (уменьшая его размерность). Например, один массив "@A" можно
разделить на несколько подмассивов "@A1, @A2", при этом один массив "@A1" будет
содержать четные позиции элементов, а второй "@A2" нечетные позиции элементов
массива "@A". Поэтому такой фрагмент кода (PERL):
@A = qw{a b c d e f} ;
$i = 3 ;
$A[$i] = ... ;
можно заменить на:
@A1 = qw{2 4 0} ;
@A2 = qw{1 3 5} ;
$i = 3 ;
if (($i % 2) == 0) { $A1[$i / 2] = ... ; }
else { $A2[$i / 2] = ... ; }
Под сворачиванием массива понимается создание из одномерного массива,
двумерного. Например, одномерный массив "A" из предыдущего примера, имеющий
размер 5 можно заменить двумерным массивом "B" размером 2, после чего код (язык
С++):
int A[] = {1, 2, 3, 4, 5, 6} ;
for (int i = 0 ; i < 6 ; i++)
{
A[i] = A[i] + 1 ;
printf("%d\n", A[i]) ;
}
можно изменить на:
int A[2][3] = {{1,2,3},
{4,5,6}} ;
for (int i = 0 ; i < 2 ; i++)
{
for (int ii = 0 ; ii < 3 ; ii++)
{
A[i][ii] = A[i][ii] + 1 ;
printf("%d\n", A[i][ii]) ;
}
}
- изменение иерархий наследования классов, осуществляется путем усложнения
иерархии наследования при помощи создания дополнительных классов или
использования ложного разделения классов.
Обфускация переупорядочивания. Заключается в изменении последовательности
объявления переменных, внутреннего расположения хранилищ данных, а также
переупорядочивании методов, массивов (использование нетривиального представления
многомерных массивов), определенных полей в структурах и т.д.
5.3 Обфускация управления
Обфускация такого вида осуществляет запутывание потока управления, то есть
последовательности выполнения программного кода.
Большинство ее реализаций основывается на использовании непрозрачных предикат,
в качестве, которых выступают, последовательности операций, результат работы
которых сложно определить (само понятие "предикат" выражает свойство одного
объекта (аргумента), или отношения между несколькими объектами).
Определение. Предикат "Р" считается непрозрачным предикатом, если его результат
известен только в процессе обфускации, то есть после осуществления процесса
обфускации, определение значения такого предиката, становится трудным.
Обозначим непрозрачный предикат, возвращающий всегда значение TRUE как "Р(t)",
а возвращающий значение FALSE, как "Р(f)", тогда непрозрачный предикат, который
может возвратить любое из этих двух значений (то есть или TRUE, или FALSE, что
нам неизвестно) как "Р(t,f)". Эти обозначения, будут использоваться дальше в
контексте описания обфускации управления.
Непрозрачные предикаты могут быть:
- локальными - вычисления содержаться внутри одиночного выражения (условия),
например (запись "(f)" после условия проверки, указывает, что это предикат типа
"P(f)"):
if (($a * $b) == (101-303))(f) {...}
- глобальными - вычисления содержаться внутри одной процедуры (функции), например
(PERL):
sub func
{
$ab=$a*$b ;
...
$val=101-303 ;
...
if ($ab == $val)(f) {...}
}
- межпроцедурными - вычисления содержаться внутри различных процедур (функций):
... $ab ... ;
... $val ... ;
sub func1 { $ab=$a*$b; ... }
sub func2 { $val=101-303; ... }
...
if ($ab == $val)(f) {...}
...
Рассмотрим простые примеры трансформации фрагмента кода программы с помощью
непрозрачных предикатов,
На рисунке 0110(1) один блок программы "(A)", разбит (трансформирован) на
несколько независимых блоков "(A1; A2)", которые соединены по средствам
непрозрачного предиката возвращающего всегда значение TRUE "Р(t)". В результате
трансформации такого рода возникает представление того, что блок "А2" выполняется
не всегда, поэтому для того чтобы определить условия выполнения блока "А2",
злоумышленнику прежде придется узнать значение, которое возвращает используемый
непрозрачный предикат "Р(t)".
На рисунке 0110(2) помимо блока "А2", используется еще один блок "А2" над
которым был произведен процесс обфускации (обозначим его как "А2`"), то есть эти
два блока ("А2" и "А2`") имеют различный код, но выполняют одинаковые функции,
следовательно, можно утверждать, что "А2 = А2`", и поэтому неважно какой из этих
блоков будет выполнен в процессе работы программы, из этого следует, что для
соединения блока "А1" с "А2" и "А2`" эффективно будет использовать непрозрачный
предикат, который может возвратить любое из значений, а именно TRUE или FALSE
"Р(t,f)".
И на рисунке 0110(3) используется новый блок "А3", который содержит
произвольный набор, каких либо операций (недостижимый код), это может позволить
сбить с толку злоумышленника, так как сами блоки "А1" и "А2" соединены по
средствам непрозрачного предиката возвращающего всегда значение FALSE "Р(f)".
Рисунок 0110. Использование простых непрозрачных предикатов
Эффективность обфускации управления в основном зависит от используемых
непрозрачных предикат, это вынуждает создавать как можно сложные для изучения, и
простые, гибкие в использовании непрозрачные предикаты, но в равной степени также
не маловажную роль имеет время их выполнения, а также количество выполняемых
операций, помимо всего этого предикат не сильно должен отличаться от тех функций,
которые выполняет сама программа, и не должен содержать чрезмерное количество
вычислений, в противном же случае злоумышленник, сможет сразу его обнаружить.
Так как часто для деобфускации используют технологию статического анализа, а
одним из ее недостатков является сложность (трудоемкость) статического анализа
структур указателей, то обычно в процессе обфускации управления используют
устойчивые непрозрачные предикаты, которые позволяют использовать недостатки
технологии статического анализа.
Основная идея устойчивых непрозрачных предикатов состоит в том, что в
программу, в процессе обфускации добавляется код, который создает набор
динамических структур, а также глобальных указателей, которые будут ссылаться на
различные элементы внутри этих структур. Помимо этого, данный код должен иногда
обновлять эти структуры (добавлять новые элементы в них, объединять или разделять
некоторые их них, изменять значения глобальных указателей, и т.д.), но таким
образом, чтобы при этом были сохранены некоторые условия, например "указатель p и
q никогда не будут указывать на один и тот же элемент" или "указатель p может
ссылаться (указывать) на указатель q" и т.д. Эти условия в последствии позволяют
создавать требуемые непрозрачные предикаты.
На рисунке 0111, представлен пример использования устойчивых непрозрачных
предикатов. На начальном этапе работы программы, код который был в нее добавлен в
процессе обфускации, создает динамическую структуру "struct", и два глобальных
указателя "p, q" которые указывают на произвольные элементы, внутри этой
структуры, "Рисунок 0111(1)", поэтому для этих указателей справедливо, что условие:
if (p==q) { ... }
соответствует предикату "Р(t,f)".
На следующих этапах процедура "Insert()" добавляет в эту структуру новый элемент,
"Рисунок 0111(2)", и изменяет значение указателя "q", "Рисунок 0111(3)", при этом
условие для указателей "p, q" продолжает оставаться неизменным.
После выполнения следующей процедуры, происходит разделение динамической
структуры, на две отдельные, таким образом, что указатели "p, q" теперь указывают
на элементы, которые находятся в различных структурах, "Рисунок 0111(4)", и
поэтому для них теперь справедливо иное условие:
if (p==q) { ... }
Которое соответствует предикату "Р(f)".
Рисунок 0111. Использование устойчивых непрозрачных предикатов
Таких манипуляций с указателями, и структурами, можно делать очень много, они
могут быть добавлены в разные участки программы, и их можно усложнить, а также
добавить какие-то уникальные процедуры для работы со структурами. При этом
существующие алгоритмы статического анализа становятся не эффективны.
Методы позволяющие осуществить обфускацию управления, классифицируются на три
основных группы:
Обфускация вычислительная. Изменение касающиеся главной структуры потока
управления. К ним можно отнести:
Обфускация соединения. Объединение или разделение определенных фрагментов кода
программы, для того чтобы убрать логические связи между ними. Ниже приведены
основные методы, позволяющие осуществить такую обфускацию:
- встраивание функций, осуществляется путем встраивания кода функции, в места ее
вызова (если ее код будет встроен во все места ее вызова, тогда саму функцию
можно убрать из кода программы).
- извлечение функций, является обратным действием, по отношению к встраиванию
функций. Осуществляется в результате объединения некоторой группы
взаимосвязанных операторов в коде исходной программы в отдельную функцию (при
необходимости для этой функции можно определить некоторые аргументы), которой
потом замещают эти группы операторов. Но следует учесть, что такое
преобразование может быть снято компилятором в процессе компиляции кода
программы.
- чередование, объединение фрагментов кода программы (функций например),
выполняющих различные операции, воедино (в одну функцию, при этом в такую
функцию, следует добавить объект, в зависимости от значения которого, будет
выполняться код одной из объединенных функций). Например, после объединения
функций (PERL):
... func1() ; ... func2() ; ...
sub func1 { # код func1 }
sub func2 { # код func2 }
...
можно получить:
...
$V = $V*31337 / 13 ; # $V == 0
... func12() ; ...
$V += 7 ; # $V == 7
... func12() ; ...
sub func12
{
if ($V) { # код func2 } else { # код func1 }
}
...
- клонирование, данный метод позволяет усложнить анализ контекста использования
функций, и объектов используемых в коде исходной программы. Процесс
клонирования функций состоит в выделении определенной функции "F", часто
используемой в коде программы, после чего над кодом этой функции осуществляется
трансформация, и создается ее клон "F`", который также будет добавлен в код
исходной программы, при этом часть вызовов функции "F" в коде исходной
программы, будет замещена на вызов функции "F`". В результате этого у
злоумышленника создастся представление о том, что функции "F", и "F`" различны.
Клонирование объектов осуществляется аналогичным способом.
- трансформация циклов. Циклы встречаются в коде различных программ, и их также
можно придать трансформации.
Блокирование циклов, заключается в добавлении вложенных циклов в существующие,
в результате работа существующих циклов будет заблокирована, на какой-то
диапазон значений. Например, имея 2 цикла (PERL):
for ($i = 1 ; $i =< n ; $i++)
{
for ($ii = 1 ; $ii =< n ; $ii++)
{
$a[$i, $ii] = $b[$i, $ii] ;
}
}
можно создать 4 цикла (функция "min" должна возвращать минимальное значение,
одного из своих аргументов):
for ($I = 1 ; $I =< n ; $I += 64)
{
for ($II = 1 ; $II =< n ; $II += 64)
{
# первые два цикла будут заблокированы до тех пор,
# пока не будут перебраны все значения "n" или
# пока не будут перебраны все значения
# из промежутка ($I,63) и ($II,63)
for ($i = I ; $i =< min($I + 63, n) ; $I += 64)
{
for ($ii = II ; $ii =< min($II + 63, n) ; $ii++)
{
$a[$i, $ii] = $b[$i, $ii] ;
}
}
}
}
- Развертка циклов, повторение тела цикла один или более раз (если количество
выполняемых циклов известно в процессе осуществления обфускации (например,
равно "N"), то цикл, может быть, развернут полностью, в результате повторения
его тела в коде N раз):
for ($i = 1 ; $i =< n - 1 ; $i++) #PERL
{
# тело цикла
}
после простой развертки:
for ($i = 1 ; $i < n - 1 ; $i++)
{
# тело цикла
}
# тело цикла
- Разделение циклов, цикл состоящий из более чем одной независимой операции можно
разбить на несколько циклов (которые должны выполняться одинаковое количество
раз), предварительно разбив на несколько частей, его тело. Например, следующий
цикл (PERL):
for ($i = 1 ; $i < n ; $i++)
{
$a[$i] += $c ;
$x[$i+$i]=$d+$x[$i+1] * $a[$i] ;
}
после разделения, может быть представлен:
for ($i = 1 ; $i < n ; $i++)
{
$a[$i] += $c ;
}
for ($i = 1 ; $i < n ; $i++)
{
$x[$i+$i]=$d+$x[$i+1] * $a[$i] ;
}
Желательно осуществлять над исходным циклом последовательно все
вышеперечисленные трансформации циклов, это позволит усложнить его статический
анализ.
Обфускация последовательности. Заключается в переупорядочивании блоков
(инструкций переходов), циклов, выражений.
5.4 Превентивная (prevent) обфускация
Превентивная обфускация предназначена для предотвращения применения
злоумышленником деобфускаторов, де компиляторов и остальных программных средств
деобфускации.
Она нацелена на использование недостатков, особенностей присутствующих в
наиболее распространенных программных средствах часто используемых
злоумышленниками в процессе деобфускации.
6. Процесс деобфускации
Когда мы говорим о процессе обфускации, появляется вопрос: есть ли процесс
обратный ему, который позволил бы злоумышленнику вернуть наиболее похожий
первоначальный код программы, то есть код до обфускации? На этот вопрос трудно
дать однозначный ответ, но такой процесс существует и носит он название
деобфускация. Но другой не менее важный вопрос, это как его можно реализовать.
С одной стороны к процессу деобфускации можно отнести процесс оптимизации
программного кода, так как они оба, в той или иной степени, противоположны
процессу обфускации. В процессе обфускации в программный код часто производиться
добавление лишних операций, они обычно не коим образом не влияют на результаты
работы самой программы, и предназначены для сбития с толку и усложнения процесса
изучения кода программы потусторонними (BEYOND:) лицами.
В свою очередь процесс оптимизации программного кода направлен на ликвидацию
лишних операций, поэтому в частных случаях он может выступать в качестве
квинтэссенции :) процесса деобфускации.
Следует отметить, что большинство компиляторов в процессе компиляции исходного
кода, автоматически осуществляют процесс оптимизации, поэтому если обфускация
осуществляется над исходным кодом программы (обфускация высокого уровня),
возникает определенная вероятность, того, что ее эффективность после, компиляции
снизиться. Если же такой исходный код будет обрабатываться интерпретатором (то
есть не будет подвержен компиляции), эффективность осуществленного процесса
обфускации, не измениться.
К процессу деобфускации, также можно отнести и процесс декомпиляции, который
позволяет, имея двоичный код программы получить наиболее схожее исходное
представление этого кода на языке высокого уровня, который более понятен
человеку, это позволит упростить процесс реверсивной инженерии. (Следует отметить,
что осуществление обфускации на низшем уровне, позволяет наиболее полно усложнить
возможный процесс декомпиляции программного кода.)
На сегодняшний день существует много материала касающегося как процесса
оптимизации, так и процесса декомпиляции, поэтому он может быть использован для
начального изучения процесса деобфускации.
Ниже представлен простой образец классификации методов процесса деобфускации:
- нахождение и оценка непрозрачных конструкций (предикатов), статический анализ,
которых очень сложен.
- сопоставление с образцом. Осуществляется различными способами, наиболее
распространенны два из них. Первый, это когда берется несколько одних и тех же
программ, прошедших процесс обфускации (так как процесс обфускации в
большинстве случаев уникален, то их код также будет разный, хотя они и будут
выполнять идентичные действия), и производиться сравнение фрагментов их кода,
для выявления вставленного в процессе осуществления обфускации лишнего кода,
который в последствии просто убирается. Второй способ сопоставления с образцом,
осуществляется путем поиска в коде программы наиболее распространенных
конструкций, применяемых в процессе обфускации. Такие конструкции могут,
например, храниться и обновляться в соответствующей базе данных, или быть
получены путем изучения работы самого обфускатора.
- выделение в программе фрагментов кода, которые никоим образом не связанны с
основными задачами, которые должна выполнять программа, то есть обнаружение
ненужных (лишних) участков кода.
- статистический анализ, заключается в динамическом анализе кода программы.
Например, нахождение непрозрачных предикат может осуществляться путем выделения
и дальнейшего изучения в анализируемом коде программы тех предикат, которые в
процессе его выполнения возвращают всегда одно и тоже значение. Статистический
анализ также может быть использован для оценки корректности осуществленного
процесса деобфускации, для этого параллельно запускается программа "А" и
программа, полученная в результате деобфускации "А`", им передаются
эквивалентные входные данные, и происходит сравнение выходных. Если выходные
данные одинаковы, то можно предположить, что процесс деобфускации был
осуществлен правильно.
- анализ потока данных, основывается на изучении того, как в процессе работы
программы изменяются в ней данные (переменные, массивы).
Cтатический анализ - это семейство технологий анализирования программ, где
анализируемую программу фактически не требуется запускать, при этом требуемую
информацию о ней получают при помощи специальных программ. Например, статический
анализ программ, представленных в двоичном виде, можно осуществить, используя
декомпилятор, а представленных в исходном виде, используя какой либо текстовый
редактор. Технологии статического анализа отличаются от большинства существующих,
ее основное качество заключается в том, что она является более комплексной, и
базируется на семантике (определяет смысловое значение предложений
алгоритмического языка) самого кода программы.
Статический анализ позволяет исследовать программу, и выявить некоторые причины
ее возможного поведения во время ее работы, то есть результаты статического
анализа нельзя считать абсолютно точными.
В свою очередь динамический анализ заключается в анализе/тестировании программы
во время ее выполнения. Он считается точным, так как он исследует фактическое
поведение программы, во время ее работы.
Динамический анализ обычно осуществляется быстрее, чем статический, так как
время его выполнения чаще всего зависит от скорости выполнения анализируемой
программы. Статический же анализ обычно требует много вычислений и является
длительным, особенно когда анализируются большие программы. Недостаток
динамического анализа заключается в том, что полученные результаты могут не
соответствовать результатам, получаемым при последующих запусках одной и той же
программы.
Основные проблемы деобфускации, связаны с требуемым количеством вычислений, и
сложностью ее алгоритмов.
WHICH DREAMED IT?
В данной статье были перечислены наиболее распространенные методы борьбы с
компьютерным пиратством, а также был поверхностно описан, такой метод защиты
программных продуктов, как обфускация.
Если Вы обнаружили какие-то недочеты, допущенные мной при создании данного
материала, то Вы можете уведомить меня об этом (email: <_LynX@bk.ru>), и я
постараюсь их исправить.
Для более серьезного изучения обфускации, Вы можете воспользоваться, списком
литературы, который представлен ниже.
P.S. Все ручейки пройдены. Удачи! (Удача не решающий фактор, но важный :)
/\ /\
@ @ ... `why your cat grins like that?' ...
| ... `It's a Cheshire cat,' said the Duchess ...
\___/
Список используемой литературы:
"A taxonomy of Obfuscating Transformations" , авторы Christian Collberg, Clark
Thomborson, Douglas Low.
"General Method of Program Code Obfuscation", автор Gregory Wroblewski.
"Static and dynamic analysis: synergy and duality", автор Michael D. Ernst
"Анализ запутывающих преобразований программ", автор Чернов А. В.
"Software protection", авторы Christian S. Collberg, Clark Thomborson.
"ALICE'S ADVENTURES IN WONDERLAND", автор Lewis Carroll.
"Through the looking glass", автор Lewis Carroll.