Блог → Антиотладочные приёмы для среды Windows. Часть 2

Я продолжаю заметку о противодействию отладке программ в среде Windows. Первую часть вы можете найти в моём блоге, а мы же идём дальше. Как можно противостоять трассировке? Принципиальная возможность создания подлинно "невидимых" отладчиков так и остаётся - просто возможностью, большинство из них позволяют себя обнаружить даже непривилегированному коду. Наибольшие нарекания вызывает использование однобайтового кода 0xCC (INT3) для создания точки останова - вместо поручения этой задачи отладочным регистрам, специально для нее предназначенным. Так поступают SoftIce, Turbo Debugger, Code Viewer и отладчик, интегрированный в Microsoft Visual Studio. Причём последний неявно использует точки останова при пошаговом прогоне программы, помещая в начало следующей инструкции этот пресловутый байт 0xCC. Тривиальная проверка собственной целостности позволяет обнаружить факт установки точек останова, свидетельствующий об отладке. Не стоит использовать конструкции наподобие

if (CalculateMyCRC() != MyValidCRC)
{ printf("Hello, Hacker!
");
return; }


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

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

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

Взять потоки под контроль можно введением в каждый из них точки останова. Но если потоков окажется больше четырёх (а что мешает разработчику защиты их создать?) отладочных регистров на всех не хватит, и тогда уже придется прибегать к использованию кода 0xCC, который защитному механизму ничего не стоит обнаружить! Ситуация усугубляется тем, что большинство отладчиков, в том числе и хваленый SoftIce, очень плохо переносят программы со структурной обработкой исключений (SEH).

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

Если попытаться прогнать приведенный пример под SoftIce вплоть до версии 4.05 включительно (остальные я не проверял, ввиду их отсутствия, но, скорее всего, они
будут вести себя точно так же), он, достигнув строки int c=c/(a-b) внезапно "слетит", теряя контроль над отлаживаемым приложением. Теоретически, исправить ситуацию можно заблаговременной установкой точки останова на первую команду блока _except, но, попробуй-ка вычислить, где расположен этот блок, не заглядывая в исходный текст, которого у хакера заведомо нет!

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

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

Как противостоять контрольным точкам останова? Контрольные точки, установленные на важнейшие системные функции - очень мощное оружие в руках взломщика. Пусть, к примеру, защита пытается открыть ключевой файл. Под Windows существует только один документированный способ это сделать - вызвать функцию CreateFile (точнее, CreateFileA или CreateFileW для ASCII и UNICODE-имени файла, соответственно). Все остальные функции, наподобие OpenFile, доставшиеся в наследство от ранних версий Windows, на самом деле представляют собой переходники к CreateFile.

Зная об этом, взломщик может заблаговременно установить точку останова на адрес начала этой функции (благо он ему известен) и мгновенно локализовать защитный код, вызывающий эту функцию, ну а остальное, как говорится - уже дело техники. Но не всякий взломщик в курсе, что открыть файл можно и другим путём - вызвать функцию ZwCreateFile (равно как и NtCreateFile), экспортируемую NTDLL.DLL, или обратиться напрямую к ядру вызовом прерывания INT 0x2Eh. Сказанное справедливо не только для CreateFile, но и для всех остальных функций ядра. Причем для этого не нужны никакие привилегии, и такой вызов можно осуществить даже из прикладного кода!

Опытного взломщика такой трюк надолго не остановит, но почему бы не приготовить ему маленький сюрприз, поместив вызов INT 0x2E в блок _try? Это приведёт к тому, что управление получит не ядро системы, а обработчик данного исключения, находящийся за блоком _try. Взломщик же, не имеющий исходных текстов, не сможет быстро определить, относится ли данный вызов к блоку _try или нет. Отсюда: он может быть легко введён в заблуждение, достаточно имитировать открытие файла, не выполняя его на самом деле! Кроме того, ничего не мешает использовать прерывание INT 0х2Е для взаимодействия компонентов свой программы, при этом взломщику будет очень непросто отличить, какой вызов пользовательский, а какой - системный.

Хорошо, с ядром всё понятно, но как же быть с функциями модулей USER и GDI - например, с GetWindowsText, использующейся для считывания введенной пользователем ключевой информации (как правило, серийного номера или пароля)? Тут нам на помощь приходит то обстоятельство, что практически все эти функции начинаются с инструкций PUSH EBP\MOV EBP,ESP, которые прикладной код может выполнить и самостоятельно, передав управление не на начало функции, а на три байта ниже. Поскольку PUSH EBP изменяет стек, приходится прибегать к передаче управления посредством JMP вместо CALL. Контрольная точка, установленная взломщиком на начало функции, не возымеет никакого действия! Такой трюк может сбить с толку даже опытного хакера, хотя рано или поздно он все равно раскусит обман, но...

Если есть желание окончательно отравить взломщику жизнь, следует скопировать системную функцию в свой собственный стек и передать на него управление - контрольные точки взломщика "отдыхают"! Основная сложность заключается в необходимости распознания всех инструкций с относительными адресными аргументами и их соответствующей коррекции. Например, двойное слово, стоящее после инструкции CALL, представляет собой не адрес перехода, а разность целевого адреса и адреса следующей за CALL инструкции. Перенос инструкции CALL на новое место потребует коррекции её аргумента. Впрочем, эта задача не так сложна, как может показаться на первый взгляд (глаза боятся, а руки делают), и результат оправдывает средства - во-первых, при каждом запуске функции можно произвольным образом менять её адрес, во-вторых, проверкой целостности кода легко обнаружить программные точки останова - а аппаратных точек на все вызовы просто не хватит!

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

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

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

Как обнаружить отладку средствами Windows? В своей книге "Секреты системного программирования в Windows 95" Мэт Питтрек описал структуру информационного блока потока (Thread Information Block), рассказав о назначении многих недокументированных полей. Особый интерес для данной статьи представляет двойное слово, лежащее по смещению 0x20 от начала структуры TIB, и содержащее контекст отладчика (если данный процесс отлаживается) или ноль в противном случае. Информационный блок цепочки доступен через селектор, загруженный в регистр FS, и без проблем может читаться прикладным кодом.

Если двойное слово FS:[0x20] не равно нулю - процесс находится под отладкой. Это до такой степени заманчиво, что некоторые программисты включили такую проверку в свои защиты, не обратив внимания на её "недокументированность". В результате, их программы не смогли функционировать под Windows NT, поскольку она хранит в этом поле не контекст отладчика, а идентификатор процесса, который никогда не бывает равным нулю, отчего защита ошибочно полагает, что находится под отладкой. Это обстоятельство было подробно описано самим же Мэтом Питтреком в майском номере журнала "Microsoft Systems Journal" за 1996 год, в статье "Under The Hood". Этот случай в очередной раз подтвердил - не стоит без особой необходимости использовать недокументированные особенности, как правило, они приносят больше проблем, чем пользы.

Прежде чем решиться добавить в свою программу защиту, следует основательно взвесить все "за" и "против". Большинство ведущих фирм давно отказались от защит, делая ставку на массовости продаж своих продуктов (правда, Microsoft сообщила, что планирует добавить в новый Office привязку к "железу" компьютера - так что хакеры без работы не останутся). Какой бы совершенной защитой ни была, её всё равно взломают, это только вопрос времени. Если защищённый продукт популярен, сколько бы он не стоил - десять долларов или десять тысяч долларов, участь его заранее предрешена.

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

В результате пользователь "обожжётся" пару раз на некачественных "кряках" и начнёт склоняться к мысли о приобретении лицензионной версии (ходят слухи, что некоторые авторы сами распространяют некачественные "кряки" - естественно, от имени другого лица). Это желание будет особенно сильно, если: защитный механизм не ущемляет никаких прав легального пользователя и не вызывает проблем совместимости; создатель программы заботится о своей клиентуре, предоставляя ей бесплатную техническую поддержку (а не только её видимость), регулярно выпускает новые версии и т.д. Цена продукта, как показывает практика, не является решающим фактором в выборе между пиратской и "фирменной" копией.

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