Блог → Защита программ: внешний загрузчик исполняемых модулей

Итак, цикл статей по проблеме защиты программного обеспечения от нелегального копирования, почти завершён, полагаю, что эта статья станет последней. Впрочем, это совершенно не означает, что тема закрыта навсегда, напротив, я не исключаю, что ещё не раз вернусь к ней, с учётом всего нового, что происходит в мире разработки ПО. Более того, тема эта настолько обширна и многогранна, что заявлять о том, что цикл моих заметок покрывает все аспекты проблемы, было бы просто глупо! Поэтому, я не прощаюсь с вами, и надеюсь, что когда-нибудь, на страницах сайта ITpool, мы ещё вернёмся к этому вопросу.

А сейчас, без долгих церемоний, к делу! У нас остался ещё один важный аспект, на который мне хотелось бы обратить внимание читателей - это построение внешнего загрузчика исполняемых модулей. Как уже не раз отмечалось, если программа перед запуском проверяет некоторые условия (некопируемая метка, пароль и т.д.), то в процессе запуска и/или во время исполнения такой программы, должен выполняться код проверки легальности запуска.

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

Большинство программ загружаются в память, запускаются, а затем удаляются из памяти средствами операционной системы. Если же для защиты программы от несанкционированного запуска, трассировки и дисассемблирования зашифровать загрузочный модуль, то ОС не сможет выполнить загрузку. Для загрузки программ DOS использует функцию 4Bh прерывания 21h (загрузка программ и оверлеев), предполагая, что загрузочный модуль имеет определённую структуру. Естественно, в зашифрованном модуле эта структура не соблюдается и попытка загрузить такую программу окончится неудачей. Для запуска зашифрованных программ нужен специальный загрузчик (назовем его EXECUTOR), который будет считывать программу (PROG) в память, расшифровывать её, выполнять работу DOS по настройке адресов и заполнению служебных полей, и только после этого, передавать ей управление. Кроме того, EXECUTOR может выполнять проверку легальности запуска защищенных программ. После запуска программы, сам загрузчик может "исчезать" из памяти, чтобы не отнимать ресурсы у программы. Локализация проверочных кодов в загрузчике позволяет избежать увеличения размеров защищаемых программ. Если же загрузчик не копировать на жёсткий диск и всегда запускать его с дискеты, которая хранится у владельца защищенных программ, то получить незащищенную копию зашифрованных программ не сможет даже самый опытный взломщик! В данном случае дискета с загрузчиком будет играть роль физического ключа для расшифрования программ. Здесь же мы рассмотрим лишь основные этапы его работы. Вначале работы EXECUTOR проводит подготовку среды (иногда применяют термин "окружение", от англ. environment) для запускаемой программы. Что такое окружение? Это область памяти, в которой хранятся значения некоторых системных переменных в виде текстовых строк, например, PATH = c:;d:;.

Каждая строка окружения оканчивается нулём, а конец окружения обозначается двумя нулями подряд. Начиная с версии MS-DOS 3.30, после окружения располагается дополнительная строка, содержащая полное имя запущенной программы. Для каждой программы DOS создает отдельную копию окружения. Сегментный адрес среды помещается в префикс программного сегмента (PSP). В нашем случае нет необходимости выделять новую память под окружение запускаемой программы. Так как после запуска программы загрузчик уже не нужен, мы можем воспользоваться окружением, которое создало DOS для самого загрузчика при его запуске. Для этого достаточно после окружения загрузчика вписать имя запускаемой программы. Если подобным же образом повторно использовать и PSP загрузчика (для этого надо загружать запускаемую программу в то же место памяти, куда был загружен EXECUTOR), то подготовку среды можно считать законченной, т.к. в PSP уже стоит правильный адрес окружения.

Отмечу одну деталь, связанную с записью полного имени запускаемой программы после окружения. Это имя может оказаться длиннее, чем полное имя загрузчика и не поместиться в область памяти окружения (обычно под окружение выделяется небольшой блок памяти). В этом случае можно попробовать расширить блок памяти окружения или запросить пользователя скопировать EXECUTOR в один каталог с PROG, и ещё раз запустить его. При этом длины полных имен загрузчика и программы будут практически одинаковы.

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

Теперь можно считать программу с диска в память, начиная со следующего после PSP загрузчика параграфа. На третьем этапе производится расшифрование программы в памяти. Далее необходимо определить формат загрузочного модуля: EXE или СОМ. Если модуль имеет COM-формат, то достаточно заполнить некоторые поля PSP, соответствующим образом инициализировать регистры, и передать управление загруженной программе (с адреса CS:100h). С этого момента программа начнет выполняться так, как если бы она была загружена DOS. Если же модуль имеет формат EXE, то необходимо сделать несколько дополнительных шагов:
- считать информацию из ЕХЕ-заголовка и таблицы перемещаемых ссылок модуля;
- переместить модуль на длину заголовка в сторону младших адресов памяти;
- настроить перемещаемые ссылки в соответствии с информацией из таблицы перемещаемых ссылок;
- перераспределить память программы в соответствии с информацией из ЕХЕ-заголовка (для этого достаточно изменить поле длины блока памяти в МСВ).

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

Защита прикладных пакетов и данных

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

Этого можно добиться, применяя замену прерываний int 13h или int 21h на функции, совмещающие чтение (запись) с расшифрованием (зашифрованием) в оперативной памяти. Основные моменты, представляющие сложность в этом случае:
- корректная подмена int 21h и int 13h прерываний;
- распознание защищенных структур среди незащищенных;
- применение качественных алгоритмов шифрования;
- необходимость незначительного замедления процессов чтения и записи.

Давайте рассмотрим все эти моменты подробнее. Проблемы с корректной подменой прерываний int 21h и int 13h состоит в том, что оба они используют регистр флагов для возврата признака ошибки (бит CF). Поэтому, если эти прерывания заменены новой функцией, то флаг, полученный в результате выполнения исходного прерывания, должен быть помещен непосредственно в стек.



В этом месте исходное прерывание дало бы Флаг 2, но если ничего не сделать, то из стека будет извлечен Флаг 1. Поэтому:



В этом случае из стека будет извлечен Флаг 2.

Распознавание защищенных файлов может быть проведено, например, по именам файлов или их расширениям. Сложнее дело обстоит с 13h-м прерыванием, которое, как известно, работает с секторами. Отличать свои сектора от чужих, которые не нужно расшифровывать, можно по месту их расположения на диске (что достаточно надежно, но требует перенастройки после оптимизации диска), по признакам сектора, например, по контрольной сумме байт или слов, по характерным байтам на фиксированных местах. Последний способ плох тем, что дает правильный результат лишь с некоторой вероятностью. Поэтому, программы, основанные на поиске частных закономерностей в секторах, иногда приводят к сбоям или искажениям информации. Это усугубляется тем, что необходимо контролировать кроме чтения еще и запись, иначе простое копирование даст при замененном прерывании нешифрованную информацию, при чтении расшифрует и в таком виде запишет.

В коде ниже приводится пример программы, проводящей преобразование файла с характерным признаком - первый байт "*" при его чтении. В этом случае распознавание защищенных файлов будет происходить достаточно надёжно. Но рассмотрим операцию простого копирования с участием замененного этой программой 21h-го прерывания. Защищенный файл будет считан в оперативную память, расшифрован и уже в таком виде записан на диск (т.е. расшифрованным). Далее, алгоритм преобразования - суммирование с фиксированным байтом - очень просто вскрывается, буквально за 256 переборов, с помощью несложной маленькой программы. Поэтому, если вы задумали защищать от копирования данные, то необходимо учесть все указанные моменты.

/*Программа EXECUTOR запуска закодированных EXE- и СОМ-файлов с встроенной защитой от трассировки. В этой программе предполагается использование модели памяти TINY компилятора Turbo C++ 1.0 Borland International. Исполняемый код получаем в виде СОМ-модуля. */

#include <stdio.h>
#include <dos.h>
#include <dir.h>
#include <io.h>
#include <string.h>
#include <fcntl.h>
#include <alloc.h>

/* описание вызываемых функций */
int executor (void);
void mov_mem (unsigned srcseg, unsigned srcoffs, unsigned destseg, unsigned destoffs, unsigned long len); int read_file (unsigned bufseg, int fh, unsigned long flen);

/* буферы для хранения векторов перехваченных прерываний */
static unsigned int02_seg, int03_seg, int01_seg;
static unsigned int02_ofs, int03_ofs, int01_ofs;
int func(void);
asm jump dw 0 /* буфер адреса перехода на основной код */

/* Обработчики прерываний 1h, 2h, 3h - реализуют защиту от трассировки */
void far new_intl (void)
{
asm push ax
asm push bx
asm push dx
asm push cx
asm push es
asm push ds
asm mov bx,cs
asm mov ds,bx

/* код проверки условий выполнения программы */