Можно сказать, что взлом средств защиты ПО стал сегодня своего рода профессией или даже более того — призванием. Многие молодые люди считают его лучшим средством самоутверждения, не говоря уже о том, что это очень увлекательное занятие. Они называют себя хакерами (hackers, от глагола hack — «рубить», «кромсать») или кракерами (crackers, от глагола crack — «ломать»)1. Судя по всему, противостояние разработчиков ПО и взломщиков сохранится и в обозримом будущем, а потому проблема «вандалоустойчивой» защиты не потеряет актуальности. В данной статье мы рассмотрим некоторые технические и психологические аспекты ее создания на платформе Windows-Intel.
Философия защиты
Прежде всего нужно сказать, что хакер — не сверхъестественное существо, а самый обычный человек, и если разработчик удосужится изучить методику его работы, написание эффективной защиты перестанет быть неразрешимой задачей. Как правило, хакер хорошо знает обратное проектирование (reverse engineering), но относительно плохо (по сравнению с разработчиком) — системное программирование и математику. Кроме того, он использует некоторые специфические инструменты2, с которыми разработчик обычно не знаком, и работает не с исходным, а с двоичным кодом.
Но не стоит и приуменьшать способности хакера. Это достойный противник, не прощающий ни наивных схем защиты, ни просчетов в ее реализации. Обычно почему-то забывают, что общая стойкость защиты определяется самым слабым ее звеном, так что любые ухищрения вроде аппаратных ключей или привязки к компьютеру будут тщетными, если некоторая часть алгоритма выполняет тривиальную проверку флага «свой/чужой» на соответствие желаемому результату.
Фактически процесс противостояния разработчика и хакера носит динамический характер, и задача разработчика — всегда быть на шаг впереди. Каждая новая версия программы должна иметь новую схему защиты или, по крайней мере, настолько сильно модифицированную старую, чтобы имеющиеся у хакера наработки давали осечку. Отсюда, кстати говоря, следует еще один важный принцип: любая схема защиты должна существовать в единственном экземпляре. Ни в коем случае нельзя применять один и тот же алгоритм в разных программах. Поступая так, вы реально защищаете только одну программу, а остальные «сдаете без боя».
Следует помнить, что абсолютно надежной защиты не существует. Любую защиту можно сломать, поэтому необходимо искать оптимальное соотношение затрат на создание защиты и прогнозируемых затрат на ее взлом, учитывая также ценовое соотношение с самим защищаемым ПО.
Пожалуй, единственный способ надежно защитить свою программу на уровне идеологии — это перевести ее из разряда ПО в разряд платформ. Под платформой здесь понимается достаточно сложный программный комплекс, эксплуатация которого требует тесного сотрудничества с производителем. Разумеется, такое возможно только для высокотехнологичных программных систем корпоративного назначения. Но если ПО позволяет сместить акценты в эту сторону (что, например, происходит сегодня с мультимедийными программами, которые плавно перетекают из домашних компьютеров в офисы), то производитель получает дополнительный рычаг для улучшения защиты.
Метафизика защиты
Очевидно, что схема защиты должна быть рассчитана не столько на выполнение фискальных функций по отношению к рядовому пользователю, сколько на противодействие попыткам хакера вывести из строя данный фискальный функционал. Поэтому самым первым этапом в алгоритме защиты, пожалуй, стоит назвать, образно выражаясь, обнаружение хакера. Конечно, обнаружить можно не самого хакера, а инструменты, которыми он пользуется. Отсюда следует вывод: перед тем, как писать какую-либо защиту, разработчик должен выяснить, чем в данный момент пользуются хакеры (ясно, что через полгода ситуация кардинально изменится, точно так же, как и на всем рынке информационных технологий).
Как правило, в арсенале хакера присутствуют несколько отладчиков, дизассемблеров и программ мониторинга системы (таких, как FileMon и RegMon). Все они, за исключением дизассемблеров, выполняются параллельно с программой, а значит, могут быть ею обнаружены.
Самый простой способ — вызов функции EnumWindows, которая перечисляет созданные в системе окна верхнего уровня. Получив их дескрипторы, программист может идентифицировать запущенные процессы (по заголовкам окон, названиям исполняемых модулей, информации о версиях файлов). Можно также воспользоваться «отладочной» по своему назначению библиотекой ToolHelp.
В Windows NT средств для контроля за состоянием системы больше, чем в Windows 95/98 (например, там имеется специальная функция IsDebuggerPresent для обнаружения отладчика). Поскольку примерно с 2000 г. прогнозируется массовый переход на новую версию Windows NT, можно предполагать, что разработчики ПО скоро получат дополнительные средства для борьбы с хакером. Еще один популярный способ обнаружения отладчика — это измерение времени выполнения критических участков кода, которое позволяет выявить пошаговый режим работы. Весьма полезно также проверить, не поставлены ли точки останова (байт 0xCC) на некоторые функции API или самой программы: таким путем хакеры часто пытаются определить, в какой момент создается окно регистрации или проверяются данные защиты. В листинге показана проверка на наличие точки останова функции CreateWindowExA.
Проверка наличия точки - останова в функции CreateWindowExA
extrn @CheckCRC$qv: FAR extrn CreateWindowExA: FAR public checkdeb ; прототип - extern ?C? int WINAPI checkdeb(void); checkdeb proc near push esi push ds push cs pop ds ; lea esi, @CheckCRC$qv; в случае прикладных ; функций Си++ необходимо учитывать искажение имен ; (name mangling) lea esi, CreateWindowExA ; функция, импортируемая из DLL mov esi,[esi+2] ; для функции из DLL сперва ; находим ее адрес в таблице переадресации, ; этот шаг следует опустить в случае контроля ; внутримодульной функции mov eax,[esi] ; берем первые байты функции and eax,0FFh ; конкретно самый первый cmp eax,0CCh ; есть точка останова? mov eax,100h je Debug_here xor eax,eax Debug_here: pop ds pop esi ret ; если все ?чисто?, возвращаем 0 checkdeb endp
Иногда интересную информацию можно получить, изучив системное окружение с помощью функции GetEnvironmentStrings.
Если обнаружен отладчик или программа мониторинга, то встает вопрос о выборе схемы противодействия. Активная схема предполагает, что необходимо попытаться «завесить» неприятельскую программу или всю систему целиком, благо таких способов много. Можно, например, слегка испортить стек или просто вызвать функцию DestroyWindow для окна отладчика. Более экзотические способы нарушения штатной работы нежелательной программы, зависящие от особенностей ее реализации, могут включать некорректное взаимодействие с ней по протоколу DDE или COM: в первом случае иногда достаточно передать мусор вместо дескриптора блока данных, во втором — лишний раз (преждевременно) вызвать метод Release для используемого объекта.
Но активная схема противодействия не лишена недостатков. Во-первых, отладчики и мониторы запускаются не только в преступных целях, так что в результате контратаки может невинно пострадать законопослушный пользователь. Во-вторых, выполняя активные действия, защита обнаруживает себя и дает возможность локализовать «очаг сопротивления».
Более хитрой и, по-видимому, действенной является пассивная схема, когда защита при хакерской атаке просто-напросто выключается (переводится в «спящий» режим). В результате у хакера складывается впечатление, что программа уже взломана или вообще не содержит защиты, но стоит ее запустить на «чистом» компьютере, как защита «просыпается».
Математика защиты
Как правило, защита оперирует не менее чем двумя наборами данных, полученными из различных источников (например, введенные пользователем имя и фамилия и полученный от разработчика регистрационный ключ). Чтобы определить, легально ли используется программа (или в каком режиме она используется — демонстрационном или полнофункциональном), над этими наборами выполняются определенные преобразования, и результаты затем сравниваются. Понятно, что соответствующие фрагменты кода — главный объект внимания хакера.
По большому счету хакер может применить при взломе два различных подхода: либо, исследовав алгоритмы преобразования данных, научиться самому генерировать регистрационные ключи, которые программа будет воспринимать как правильные (это более «престижный» метод), либо просто испортить защиту — слегка «подправить» программу, так чтобы защита перестала работать или считала бы любого пользователя легальным.
Один из возможных способов борьбы с попытками воспроизвести алгоритм генерации ключей — создание самомодифицирующегося кода. В этом случае статический код, анализируемый дизассемблером, не дает необходимых сведений для восстановления алгоритма генерации регистрационных ключей, и возникает необходимость анализа программы в режиме отладки. Если предположить, что мы успешно засекаем отладчики и не активизируем защиту в их присутствии, то локализовать нужный фрагмент кода будет сложно. А поскольку нельзя перманентно модифицировать код, который динамически формируется только на стадии выполнения программы, отключить защиту тоже становится невозможно или, по крайней мере, очень трудно (ведь это требуется делать для большего объема кода в различных частях программы).
Реализовать самомодифицирующийся код на языке высокого уровня довольно сложно — лучше использовать ассемблер. Можно, правда, обойтись и без модифицирующегося кода, но тогда желательно, чтобы преобразования данных при проверке прав пользователя не сводились к элементарным операциям.
Например, разработчики условно-бесплатных программ довольно часто применяют для получения регистрационного ключа из имени пользователя (или наоборот) сложные комбинации побитовых и целочисленных арифметических операций. Однако настырного хакера такие упражнения по большей части только радуют: утилиту регистрации нелегальных пользователей он строит за несколько часов. Единственный выход — использовать что-нибудь более «зубодробительное», скажем шифрование с открытым ключом или нетривиальные операции с плавающей запятой.
Схема шифрования может быть следующей. Разработчик заблаговременно генерирует пару ключей — открытый и закрытый. Пользователь, купивший программу, посылает разработчику сведения о себе, а тот, в свою очередь, шифрует их с помощью закрытого ключа и отсылает зашифрованные данные вместе с открытым ключом обратно пользователю. Встроенный в программу механизм защиты расшифровывает с помощью открытого ключа информацию, полученную от разработчика, и сравнивает ее с имеющимися данными о пользователе. Очевидно, что знание алгоритма дешифрования не даст хакеру возможности создать регистрационную утилиту: ведь для нее необходимо не только воспроизвести алгоритм шифрования, но и знать закрытый ключ разработчика. Разумеется, надежность такой защиты зависит от криптостойкости алгоритма шифрования и длины ключа. Windows NT, а также 98 и 95 начиная с версии OSR2 содержат программный интерфейс CryptoAPI, позволяющий использовать в защите ПО коммерческие системы шифрования, в том числе с применением пластиковых карт и других аппаратных средств.
Суть подхода, основанного на нетривиальных математических операциях, такова. Пусть для преобразования информации о пользователе X в регистрационный ключ Y служит некая функция Y=F(X), определенная на пространстве действительных чисел; при этом обратная функция X=F-1(Y), преобразующая регистрационный ключ в информацию о пользователе, такова, что ее значение невозможно рассчитать исходя из F (за достаточно долгий период времени). Понятно, что такая ситуация примерно аналогична возникающей при шифровании с открытым ключом.
Существует и другой вариант — выбрать функцию F сложной формы и рассчитывать ее в алгоритме защиты без использования записи в аналитическом виде. Как это осуществить? Например, с помощью нейронной сети обратного распространения. Разработчик создает и обучает нейронную сеть на аппроксимацию F, после чего обученная сеть внедряется во фрагмент защиты. Еще больше осложнить жизнь хакеру можно, разбив F на две ступени. В этом случае функция F запишется в виде суперпозиции функций H1 и H2. В схеме защиты необходимо аппроксимировать функции H1 и H2-1, которые будут применяться следующим образом:
Z1=H1(X)
Z2=H2-1(Y)
Числа Z1 и Z2 получаются соответственно из информации о пользователе и проверяемого регистрационном ключа. Их совпадение означает правомочное использование программы. Данный подход затрудняет эксплуатацию еще одного излюбленного метода работы хакера, заключающегося в «тупом» переборе значений регистрационного ключа.
Разумеется, если код программы не самомодифицирующийся, хакеру несложно будет «выломать» из нее всю эту математику. Здесь наиболее простым и в то же время эффективным способом борьбы является подсчет контрольных сумм для критически важных участков кода.
Хронология защиты
Довольно часто защита ПО содержит фрагмент, отвечающий за отслеживание срока пробной эксплуатации программы. Участок кода, в котором время, прошедшее с момента установки программы, сравнивается с разрешенным периодом бесплатного использования, должен защищаться с помощью методов, аналогичных рассмотренным ранее. Однако «хронология защиты» имеет одно дополнительное уязвимое место: хакер может модифицировать дату установки программы (или дату ее последнего запуска — это иногда практикуется для того, чтобы предотвратить, по крайней мере теоретически, действенность трюков с переводом системных часов). Ни реестр, ни файловая система не гарантируют «тайну вкладов». Можно порекомендовать хранить дату в открытом или закодированном виде, например, в реестре и добавлять к ней контрольную сумму. В Windows NT имеется полезная функция RegQueryInfoKey, которая сообщает дату создания ключа реестра.
Химия защиты
Напоследок приведу несколько несложных правил, которые позволят разработчику «схимичить» при создании защиты ПО.
- Применяйте нелинейные (в том числе событийно-управляемые) алгоритмы защиты. Например, можно разбить проверку прав пользователей на несколько частей и выполнять их в произвольном порядке в различные моменты времени.
- Используйте для хранения данных защиты системные ресурсы Windows: дополнительную память, выделяемую для параметров окон и доступную посредством функций SetWindowLong/GetWindowLong или SetProp/GetProp; атомы; локальные хранилища потоков (TlsAlloc) и т. п.
- В рамках алгоритма защиты сравнивайте два числа каким-нибудь нестандартным способом: например, рассчитывается величина, которая в случае правильной регистрации должна быть отрицательной, и из нее извлекается квадратный корень, а установка флага «свой-чужой» выносится в обработку ошибки области определения.
- Напишите несколько замысловатых функций, имитирующих действия с якобы регистрационной информацией, — это отвлечет хакера от настоящей защиты.
- Разместите в разных частях программы несколько различных по содержанию функций с одинаковым эффектом.
- Создайте несколько разных переменных с различными значениями для обозначения одного и того же события (например, попытки нарушения защиты).
- Не обращайтесь к переменным, содержащим информацию о защите, напрямую; используйте косвенную адресацию, возможно, с применением стандартных библиотечных функций работы с памятью.
- Выполняйте стандартные операции в ответ на нестандартные сообщения Windows, а защитные действия, наоборот, в ответ на стандартные сообщения, такие как WM_PAINT; подмену можно производить прямо в цикле обработки сообщений.
- Если программа рассчитана на работу с Internet, не упустите возможность встроить в нее проверку базы пользователей через сеть.
Этот список каждый разработчик легко продолжит сам. Много свежих идей можно почерпнуть из Internet и телеконференций. Все это требуется творчески переработать (чтобы алгоритм защиты был уникальным, хотя и основанным на известных методах) и объединить — избыточность в защите никогда не мешает. Довольно часто кажущиеся прямо-таки идеальными схемы защиты на самом деле легко взламываются с помощью простого, но не учтенного программистом приема. Лучше всего, конечно, иметь знакомого «честного» хакера, который возьмется «поломать» вашу программу, чтобы выявить слабые места защиты, но такая возможность есть не у всех. Прочим остается лишь на свой страх и риск принять условия игры и вступить в интеллектуальное противоборство со взломщиками.
Со Станиславом Коротким можно связаться по e-mail stasson@orc.ru.1. Словом «хакер» первоначально обозначали программистов со специфическим «машинно-ориентированным» стилем работы, не обязательно даже занимающихся чем-то противозаконным. Сейчас так чаще всего называют тех, кто взламывает серверы удаленных информационных систем (в то время как кракер — это именно специалист по взлому программ), но термин вполне применим и к компьютерным взломщикам вообще.
2. В большинстве случаев разработчик все-таки знаком с инструментами хакера, но только не подозревает, что это они и есть, поскольку использует их не для взлома, а для отладки программы.