23.3. Защита программ
Полностью защитить программу от несанкционированного тиражирования, применяя только программные решения, невозможно. Если программа может быть запущена, она может быть взломана.
Однако существуют идеи, способные значительно затруднить работу противника. В середине 2000 года в конференции новостей fido7.ra.crypt было опубликовано сообщение, автором которого являлся человек под псевдонимом stpark. В сообщении перечислялось несколько интересных методов, разработанных специалистами по защите и анализу программ для собственных нужд, не получивших открытой реализации и, возможно, именно поэтому не взломанных. Далее приведены три из них:
- перекрестная проверка целостности исполняемого модуля и используемых им динамически загружаемых библиотек;
- защита, выполняющаяся одновременно в нескольких потоках, где каждый поток контролирует целостность кода программы, выявляет непредусмотренные задержки в выполнении других потоков и постоянно изменяет внутреннее состояние модуля защиты;
- применение виртуальных машин для выполнения специальным образом обработанного кода.
Еще одна идея, которую собирались реализовать (а может быть, уже и реализовала) компания Protection Technology, заключалась в разработке специального компилятора языка С, который бы создавал код, очень сложный для дизассемблирования.
Смысл этой идеи в том, что даже простейшие операции можно записать таким образом, что будет далеко не очевидно, что же они делают. И это очень часто получается при включении оптимизации в компиляторе. Например, эквивалентный ассемблерный текст следующей простейшей функции на языке С приведен в листинге 23.1:
int divFn (int x) { return x / 10; )
Данная функция выполняет одну-единственную операцию — целочисленное
деление аргумента на 10.
Листинг 23.1. Функция целочисленного делении на 10
mov
есх,
х
mov
еах,
66666667h
imul
ecx
mov
eax,
edx
sar
eax,
2
mov
ecx,
еах
shr
ecx,
lFh
add
eax,
ecx
retn
А вот пример другой функции, ассемблерный код которой приведен в листинге 23.2:
int caseFn (int x) { return x > 100 ? 15 : 25; }
Эта функция в зависимости от значения аргумента возвращает одно из двух возможных значений.
Листин 23.2 Функции выбора результата по значению аргумента
mov
ecx,
[esp+arg_0]
хог
eax,
eax
стпр
ecx,
64h
setXe
al
dec
eax
and
al,
0F6h
add
eax,
19h
retn
Обе ассемблерные функции, приведенные выше, можно написать гораздо короче и понятнее, но при оптимизации по скорости выполнения именно эти варианты являются наилучшими. В первом примере удалось избавиться от очень медленной операции деления, а во втором — от команды условного перехода, также изрядно влияющей на скорость. Но оптимизацию выполнил компилятор, а человеку с первого взгляда совсем не просто будет понять, что делает каждая из функций. Хотя, немного подумав, разобраться все-таки реально. К тому же, существуют учебники, из которых можно узнать о хитростях, используемых при оптимизации, и научиться их понимать.
Но почти для любой конструкции языка подобным образом можно придумать несколько альтернативных способов представления в системе команд микропроцессора. И если компилятор станет случайным образом выбирать один из многих возможных вариантов для каждого оператора, разобраться в порождаемом им машинном коде человеку будет очень и очень непросто.