Главная страница
Навигация по странице:

  • 3.2. Шифрование кода программы

  • Лекции. Введение. Защита программного обеспечения от несанкционированного использования с помощью программноаппаратных средств


    Скачать 4.72 Mb.
    НазваниеВведение. Защита программного обеспечения от несанкционированного использования с помощью программноаппаратных средств
    АнкорЛекции
    Дата19.11.2022
    Размер4.72 Mb.
    Формат файлаdoc
    Имя файлаLektsii_PASOIB.doc
    ТипДокументы
    #797594
    страница6 из 12
    1   2   3   4   5   6   7   8   9   ...   12
    Глава 3. Общие методы защиты программ от отладки и дизассемблирования
    3.1. Использование недокументированных команд и недокументированных возможностей процессора

    Один из методов, используемый для затруднения отладки и дизассемблировании программ, заключается в привлечении редко используемых инструкций процессора, недокументированных инструкций, или инструкций имеющих скрытый результат. В данном случае, не все злоумышленники хорошо знакомы с такого рода командами и скрытыми возможностями процессора. Недостаток данных методов – жесткая привязка к процессору. Кроме этого, не гарантируется поддержка недокументированных инструкций в будущих модулях процессорах, а значит и совместимость с ними разработанных защит.В качестве примеров «сокрытия» реальных инструкций можно привести примеры, приведенные в таблице 2.1.Рассмотрим особенности записи кодов инструкций в процессоре INTEL.

    Реализация процессора INTEL предполагает следующий формат инструкций для него:

    Префикс | Опкод | ModR / M | SIB | Смещение | Непосредственный операнд

    Детальное рассмотрение данной структуры выходит за пределы данного пособия. Остановимся только на механизмах, которые могут быть использованы при защите ПО.Префиксы в кодах инструкций делятся на следующие типы.

    Префиксы блокировки и повторения – говорят о том, что код инструкции относится к действиям блокировки или повторения.

    Префиксы переопределения сегмента. В данном случае, префикс указывает на то, с каким сегментом должна осуществляться работа команды. Соответствия между значениями префиксов и сегментами приведены в таблице 2.2.

    Таблица 2.2. Соответствия префиксов и кодируемых ими сегментов

    Значение префикса

    Сегмент

    2Eh

    CS

    36h

    SS

    3Eh

    DS

    26h

    ES

    64h

    FS

    65h

    GS

    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. Кодирование регистров в инструкциях

    Код

    Инструкция

    000

    ES

    001

    CS

    010

    SS

    011

    DS

    100

    FS

    101

    GS

    110

    Зарезервирован

    111

    Зарезервирован

    Две последние кодовые комбинации (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. Изменение длины команды.

    Данный прием направлен на противодействие отладчикам и основан на том, что реальный процессор при выполнении команд вычисляет адрес следующей до выполнения предыдущей. Многие же отладчики, вычисляют адрес следующей команды после выполнения предыдущей (это проще реализуемо). Модифицировав программу в процессе работы (работает только в реальном режиме), можно воспользоваться данной особенностью для реализации защиты.

    00000100:

    810600010200

    add word ptr [0100],01

    00000106:

    B406

    mov ah,06

    00000108:

    B207

    mov dl,07

    0000010A:

    CD21

    int 21

    0000010C:

    C3

    Ret

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

    00000100:

    810600010200

    add word ptr [0100],01

    00000105:

    00B406B2

    add [si] [0B206],dh

    С помощью данного способа можно строить защиты на эмуляторы процессоров (см. п. 4).
    3.2. Шифрование кода программы

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

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

    Допустим, например, что секретный ключ расшифровывает рабочий текст программы, а берется, например, из электронного ключа, либо представляет собой вводимую пользователем последовательность. В данном случае, взлом становится очень трудным делом, а иногда и невозможным в приемлемые сроки. Для того, чтобы вычислитель пароль и расшифровать программу, злоумышленнику, как минимум, нужна будет легальная копия программы, а при правильно построенной защите недостаточно будет даже этого. Затягивание времени взлома позволит некоторое время поддержать объемы продаж.

    Выделяют два вида шифрования кода программы – статическую и динамическую.

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

    Реализация простейшей процедуры шифрования кода программы по XOR может выглядеть следующим образом.




    LEA SI, beginCrypt;

    начало зашифрованного блока

    Repeat:

    Xor Byte ptr [SI], 077h







    INC SI







    CMP SI, offset endCrypt







    JNA Repeat




    beginCrypt










    ……




    endCrypt







    Следует отметить, что для зашифрованных блоков программы, дизассемблер выдаст неверные результаты.

    Атака на статическую шифровку для приведенного выше примера может осуществляться снятием дамба памяти, заменой зашифрованного текста расшифрованным с помощью 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) элементы. Вероятнее всего из них и составлен пароль.

    С каждой итерацией число символов, общее для всех последовательностей стремительно уменьшается, а вместе с ним и число вероятных паролей.
    1   2   3   4   5   6   7   8   9   ...   12


    написать администратору сайта