Безопасность в Дельфи

         

Как это работает


Исполняемый код и область данных процесса находятся в одном виртуальном адресном пространстве, поэтому мы можем работать с кодом программы как с данными. Единственное ограничение, которое устанавливает загрузчик - это защита от записи областей памяти, содержащих код. Попытка записи на страницы, содержащие код, вызывает access violation. Но возможность изменить атрибут PAGE_EXECUTE на PAGE_EXECUTE_READWRITE существует - с помощью функции VirtualProtect.

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

Текст защищаемой процедуры/функции будет выглядеть примерно так:
После соответствующей обработки компилированного модуля код программы выглядит так:

Зашифрованный код просто обходим стороной. При расшифровке у нас имеется очевидный критерий проверки: если конечный участок расшифрованного кода совпадает с начальным маркером, то расшифровано правильно. В случае удачи заменяем длинный безусловный переход "коротким", как на первом рисунке - и критический участок кода выполняется.

На приведенных рисунках не показана процедура дешифровки до исполнения и возврата зашифрованного кода на место после. Защищенная программа может исполняться в соответствии со следующими диаграммами:

- последовательно
- прямой переход
- реверс

В первом случае снятие и установка защиты, а также сам защищаемый код должны находиться внутри одной процедуры или в последовательно исполняемых процедурах / функциях. Для второго и третьего случая более естественно разместить код в двух процедурах - снятие и установка защиты - в главной, защищенный код - в вызываемой. Возможны и более сложные варианты. Далее рассмотрим подробнее первый вариант, на его основе можно будет реализовать и другие диаграммы.

Как проставлять уникальные маркеры в коде программы? Данный вопрос решается с использованием встроенного ассемблера:

asm JMP @@1 DB 'уникальный маркер' @@1: end;

Но в случае с первым маркером надо быть осторожнее, поскольку при зашифровке нам надо будет получить дальний переход, который может оказаться в прямом смысле дальним. Необходимо зарезервировать место под команду соответствующей длины:

asm DB $E9 DD длина_маркера DB 'уникальный маркер' end;

Утилита, производящая шифрование модуля, должна будет также изменить первый безусловный переход, то есть удвоить четырехбайтное значение длина_маркера и прибавить к нему расстояние между маркерами.

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

Конечно, желательно чтобы маркер выглядел как реальный машинный код, ни в коем случае это не должно быть "заметное" значение типа "FFFFFFFFFFF…" или "ТУТ ШИФРУЕМ". Я использую для их генерации датчик случайных чисел, решение не идеальное, но, по крайней мере, генерируемые значения не бросаются в глаза.

Итак, уникальные маркеры расставлены. Поиск их в готовом откомпилированном файле не сложен - открываем файл с начала и сканируем (чтобы получить файл, как указатель используем MapViewOfFile). А что сканировать во время выполнения? Ответ прост: переменная Hinstance на самом деле представляет собой адрес, по которому загрузилась наша программа/библиотека. Начиная с этого адреса и надо сканировать.

Delphi предоставляет функцию сканирования StrPos, но нам она не подойдет, поскольку и сам код программы, и искомый маркер могут содержать символ #0. Небольшая переделка ассемблерного кода - и у нас в распоряжении функция StrPosLen с двумя новыми аргументами, обозначающими длины буферов и не зависящая от концевых #0. С длиной маркера все понятно, а вот длина области сканирования тоже определяется по-разному. В первом случае это просто размер файла, во втором нам помогут функции из tlhelp32, на основе которых написана функция ModuleSize.



Содержание  Назад  Вперед