Лекции. Введение. Защита программного обеспечения от несанкционированного использования с помощью программноаппаратных средств
Скачать 4.72 Mb.
|
Глава 3. Общие методы защиты программ от отладки и дизассемблирования 3.1. Использование недокументированных команд и недокументированных возможностей процессора Один из методов, используемый для затруднения отладки и дизассемблировании программ, заключается в привлечении редко используемых инструкций процессора, недокументированных инструкций, или инструкций имеющих скрытый результат. В данном случае, не все злоумышленники хорошо знакомы с такого рода командами и скрытыми возможностями процессора. Недостаток данных методов – жесткая привязка к процессору. Кроме этого, не гарантируется поддержка недокументированных инструкций в будущих модулях процессорах, а значит и совместимость с ними разработанных защит.В качестве примеров «сокрытия» реальных инструкций можно привести примеры, приведенные в таблице 2.1.Рассмотрим особенности записи кодов инструкций в процессоре INTEL. Реализация процессора INTEL предполагает следующий формат инструкций для него: Префикс | Опкод | ModR / M | SIB | Смещение | Непосредственный операнд Детальное рассмотрение данной структуры выходит за пределы данного пособия. Остановимся только на механизмах, которые могут быть использованы при защите ПО.Префиксы в кодах инструкций делятся на следующие типы. Префиксы блокировки и повторения – говорят о том, что код инструкции относится к действиям блокировки или повторения. Префиксы переопределения сегмента. В данном случае, префикс указывает на то, с каким сегментом должна осуществляться работа команды. Соответствия между значениями префиксов и сегментами приведены в таблице 2.2. Таблица 2.2. Соответствия префиксов и кодируемых ими сегментов
3. Префиксы переопределения размеров операндов (префикс 66h). Данный префикс используется в 16-разрядном режиме для манипулирования с 32-битными операндами и наоборот. 4. Префиксы переопределения размеров адреса (префикс 67h). Если в процессорной команде используется более одного префикса из одной группы, то действие команды неопределено и по-разному реализовано на различных типах процессоров. Приведем наиболее типичные приемы использования недокументированных команд и недокументированных возможностей процессора INTEL. 1. В качестве одной из недокументированных возможностей процессора INTEL можно использовать недокументированную возможность использования префиксов, например, префикса переопределения размеров операндов. Согласно стандарту, он используется только при наличии в команде каких-либо операндов. Однако, на практике для реального процессора, данный префикс может быть поставлен совершенно перед любой командой, и это будет работать на реальном процессоре. Например, как это ни странно, реальный процессор воспримет инструкцию 0x66 CLI (использование префикса перед оператором запрещения прерываний). В связи с тем, что данная особенность не документирована (предполагается, что никому в голову писать подобные вещи), , то как правило, отладчики и дизассемблеры не воспринимают подобные инструкции и отказываются корректно интерпретировать данный код. Тоже самое следует сказать и про префиксы переопределения размеров адреса. Данные префиксы согласно документации используются только в командах, оперирующих с адресами памяти. Их использование в других командах не документировано, но процессор будет эти команды выполнять. В качестве примера такой инструкции можно привести инструкцию 0x67 STI. Приведенные методы позволяют противостоять, также, и виртуальным эмуляторам процессоров (см. п. 4). 2. Использование префикса переопределения размеров операндов совместно с инструкцией RETN Достаточно мощным приемом противодействия отладчикам и дизассемблерам, основанным на использовании недокументированных возможностей, является использование префикса переопределения размеров операндов совместно с командой RETN. Казалось бы, раз команда RETN не имеет операндов, то префикс 66 процессор игнорирует, но это не так. Дело в том, что RETN работает с неявным операндом-регистром ip/eip. Именно этот операнд и изменяет префикс. В реальном и 16-разрядном режиме указатель команд всегда обрезается до 16 бит и на первый взгляд все сработает корректно, однако, при записи префикса, стек окажется несбалансированным. Из него вместо одного слова возьмется два. Как правило, это приводит к возникновению исключительной ситуации 0Ch – исчерпание стека. Это приводит к зависанию большинства отладчиков, а дизассемблеры на смогут отследить стек. Однако, данный пример будет работать только в реальном режиме. Под Windows перехватить прерывание 0Ch не представляется возможным. 3. Префиксы переопределения сегментов также могут встречаться перед любой командой, в том числе и в командах, не обращающихся к памяти. Например, команда CS:NOP корректно выполняется, а вот некоторые дизассемблеры сбиваются при этом. 4. Использование дублирующих префиксов, то есть записи префиксов вида 0x66,0x66 непосредственно перед командой. Хотя фирма INTEL не гарантирует корректную работу своих процессоров при обнаружении такого рода инструкций, но фактически все процессоры правильно интерпретируют данные ситуации. Иное дело – отладчики и дизассемблеры, которые спотыкаются и начинают некорректно вести себя. Следует отметить, также, что процессором INTEL корректно выполняются и инструкции вида DS:FS:CS:Mov ax, [100] (последний префикс перекрывает все остальные), а отладчики и дизассемблеры сбиваются при их анализе. Данный пример хорошо работает под Windows и другими операционными системами. 5. Обращение к недокументированным регистрам. В процессорах INTEL регистры в настоящее время кодируются 3-мя битами следующим образом (таблица 2.3). Таблица 2.3. Кодирование регистров в инструкциях
Две последние кодовые комбинации (110 и 111) в настоящее время зарезервированы и не используются. При попытке их использования вызывается исключительная ситуация 06h, которую можно перехватить (под ДОС). Отладчики же и дизассемблеры при встрече с такого рода инструкциями начинают вести себя странно и непредсказуемо. Одни не генерируют при этом прерывания, чем и выдают себя, другие начинают некорректно работать. Поведение де дизассемблеров в этом случае тоже разнообразно. Ниже приведен пример того, как различные дизассемблера воспринимают такого рода инструкции. HIEW 8E ??? F8 clc C3 retn Qview 8EF8 mov !s, ax C3 ret IDA Pro Db 8E Db 0F8 DB C3 Несуществующие регистры можно эмулировать в обработчике прерывания int 06h, однако данная защита не будет работать под Win32. 6. Изменение длины команды. Данный прием направлен на противодействие отладчикам и основан на том, что реальный процессор при выполнении команд вычисляет адрес следующей до выполнения предыдущей. Многие же отладчики, вычисляют адрес следующей команды после выполнения предыдущей (это проще реализуемо). Модифицировав программу в процессе работы (работает только в реальном режиме), можно воспользоваться данной особенностью для реализации защиты.
Эта защита основана на том, что после отработки первой команды, она станет сразу же другой, и, при этом, на байт короче. Реальный процессор, в отличие от многих отладчиков, перейдет не на ячейку 106, а на ячейку 105 и будет выполнять следующий код:
С помощью данного способа можно строить защиты на эмуляторы процессоров (см. п. 4). 3.2. Шифрование кода программы Все приведенные выше методы противодействия отладке и дизассемблированию имеют один серьезный недостаток – они привязываются либо к особенностям процессора, либо к особенностям отладчиков (дизассемблеров). Квалифицированные взломщики могут без особых проблем выявить и обойти все эти методы противодействия (кроме, может быть перемешивания кода). Наиболее предпочтительным способом защиты ПО от отладки и дизассемблирования является способ, основанный на шифровании кода программы на некотором секретном ключе. При этом предъявляется требование того, чтобы секретность ключа не могла быть нарушена путем исследования кода программы и дискового пространства ПК. Таким образом, ключ не должен никаким образом фигурировать не в программе, а также не должен храниться ни в каком файле, ни в реестре …, где он может быть обнаружен путем исследования работы программы различными средствами мониторинга. Допустим, например, что секретный ключ расшифровывает рабочий текст программы, а берется, например, из электронного ключа, либо представляет собой вводимую пользователем последовательность. В данном случае, взлом становится очень трудным делом, а иногда и невозможным в приемлемые сроки. Для того, чтобы вычислитель пароль и расшифровать программу, злоумышленнику, как минимум, нужна будет легальная копия программы, а при правильно построенной защите недостаточно будет даже этого. Затягивание времени взлома позволит некоторое время поддержать объемы продаж. Выделяют два вида шифрования кода программы – статическую и динамическую. В первом случае, дешифровщик отрабатывает только один раз, после чего исходный текст будет полностью восстановлен. Такой подход имеет простую реализацию, но крайне неэффективен. Злоумышленник может снять дамп памяти в момент окончания работы расшифровщика и записать его на диск. Это невозможно сделать при динамической расшифровке, когда ни в какой момент времени код не будет расшифрован полностью. При вызове процедуры он расшифровывается, а при выходе – зашифровывается вновь. Реализация простейшей процедуры шифрования кода программы по XOR может выглядеть следующим образом.
Следует отметить, что для зашифрованных блоков программы, дизассемблер выдаст неверные результаты. Атака на статическую шифровку для приведенного выше примера может осуществляться снятием дамба памяти, заменой зашифрованного текста расшифрованным с помощью HIEW редактора и последующей заменой xor 77 на xor 00. При динамической расшифровке не следует зашифрованные процедуры располагать одну за другой или передавать параметры в процедуры расшифровки и зашифровки однообразно, иначе злоумышленник может достаточно быстро расшифровать данные процедуры с помощью специально написанных скриптов в дизассемблере IDA PRO. Рекомендуется располагать между зашифрованными процедурами случайное число незначащих байт. Методы атаки на шифрование кода При атаке на шифр считается, что криптоалгоритм известен злоумышленнику с точностью до реализации (вычислить сам криптоалгоритм не составляет сложности) и требуется найти только его ключ. 1. Достаточно часто разработчики ПО шифруют и расшифровывают код программы не по самому ключу, а по его свертке CRC. При этом, в качестве CRC используются достаточно плохие функции, дающие очень малую свертку (например, один байт). В данном случае, при взломе злоумышленник перебирает не пароли, а хэш-значения. При достаточно короткой свертке, можно повысить скорость перебора на несколько порядков. 2. Расшифровка кода ведется на достаточно длинном ключе, однако для контроля правильности введенного ключа, считается его CRC, после чего CRC ключа проверяется на соответствие требуемому. В данном случае, злоумышленник находит в программе алгоритм контроля CRC в программе, находит тот CRC, которому должен удовлетворять пароль, после чего пишет процедуру перебора различных ключей на соответствие их CRC. Таких паролей, как правило, получается много, и чем хуже CRC, тем их больше. Проведя расшифровку кода программы на данных паролях, у злоумышленника появляется проблема выбора единственно верного исходного текста среди расшифрованных вариантов (все остальные – неверные). При этом, нет никаких достаточно строгих критериев, позволяющих автоматически отличить ложные варианты. Как правило, злоумышленник должен вводить пароль, запускать программу, она вешается, выходить, вводить пароль и т.д. Однако, злоумышленник может воспользоваться различного рода эвристиками, позволяющими ему сократить варианты перебора. Например, он может попытаться использовать косвенное представление об исходном тексте. Можно по типу данных предугадать вероятность того или иного символа, проверить определенные фрагменты на совпадение со словарем, поискать некоторые закономерности, однако эти эвристики будут работать достаточно медленно, и нет никакой гарантии, что мы не пропустим нужный вариант. Таким образом, данный метод защиты наряду с контролем корректности ввода ключа является достаточно стойким и ставит много проблем перед злоумышленником, однако, и тут есть уязвимые места. В данном случае, злоумышленник может пойти другим путем – атакой шифра по открытому тексту. Если злоумышленник обладает хотя бы частью открытого текста, то он может обратить операцию шифрования и найти ключ (рассматривается случай шифрования путем сложения открытого текста с ключом по модулю 2 – xor). Действительно, если X xor Key = Y, то Y xor Key = X, X xor Y = Key. Если ключ шифрования равен 16 бит, то достаточно знать всего лишь 2 байта исходного текста, чтобы найти ключ и применить его для всего текста. Инструкции, встречающиеся в исходном тексте мы можем предположить с достаточно большой вероятностью. Весьма вероятно, что в приведенном шифротексте встречается инструкция int 21 (0x21CD), следует поискать также такие последовательности, как CopyRight, OK, и т.д. Всегда встречаются вызовы стандартных библиотек. Такого рода эвристические элементы могут значительно упростить задачу злоумышленнику. 3. Для защиты от подобного анализа можно посоветовать разработчикам защит использовать достаточно длинные ключи, так что нельзя будет с большой вероятностью подобрать отрезок S открытого текста такой большой длины. Однако, в данном случае злоумышленник может применить «атаку по маске». Суть ее состоит в следующем - пусть нам не известно достаточно длинной строки открытого текста, но мы знаем наверняка много коротких, и с некоторой достоверностью расстояние L между ними. Алгоритм «атаки по маске» следующий. 1. Пусть S0 – одна из существующих коротких последовательностей. Применим к ней атаку по открытому тексту f(S0) и, в результате, получим большое количество подходящих, но ложных ключей, которые короче требуемого. Настоящий пароль включает в себя некоторые элементы полученного множества. Возьмем другую известную последовательность S1 и повторим аналогичную операцию. Выберем теперь общие для f(S0) и f(S1) элементы. Вероятнее всего из них и составлен пароль. С каждой итерацией число символов, общее для всех последовательностей стремительно уменьшается, а вместе с ним и число вероятных паролей. |