Энциклопедия антиотладочных приемовЛеонид «Cr@wler» Исупов (crawler@xakep.ru) Процесс отладки защищенной программы порой напоминает прохождение хитроумного квеста. Одно из самых больших удовольствий для реверсера (иногда - и большая головная боль) - заниматься преодолением защит, которые «завязаны» на обработке исключений. Если ты еще не знаком с принципом защитных механизмов, построенных на его основе, все, что изложено ниже, тебе очень пригодится. «На закуску» - еще один защитный трюк: использование одной интересной особенности работы функции vsprintf (), входящей в msvcrt, которая буквально «убивает» отладчик OllyDbg. «Исключительная» защитаЧасто в процессе работы программы возникают ситуации, которые невозможно предусмотреть. Например, попытка записи в ячейку памяти, которая принадлежит странице с неустановленным атрибутом «writeable», или же деление на ноль. Для таких ситуаций программисты Microsoft создали механизм обработки исключений. Обработчик исключений, или SEH (англ. «Structured Exception Handling») - часть кода, на которую возложена функция обработки ошибок для данного треда. Нужно пояснить, что представляет собой тред ((от англ. «thread», нить). Думаю, ты знаешь, что код программы может быть «распараллелен», то есть несколько частей программы могут выполняться одновременно – в контексте единственного процесса (например, в графическом редакторе одновременно могут выполняться печать изображения и его редактирование). Каждая такая часть программы и называется тредом. При этом для каждого треда может быть установлен собственный обработчик исключений. По умолчанию обработкой исключений занимается системный обработчик. Каким образом установить обработчик собственный? Указатель на структуру, содержащую адрес обработчика (который также совпадает и с адресом указателя на сам обработчик), находится по адресу FS:[0]. Следовательно, для того, чтобы заменить системный обработчик собственным, необходимо поместить в стек структуру, содержащую адрес нового обработчика, и указатель на старый обработчик. После чего поместить по адресу FS:[0] указатель на новую структуру. Не будем терять время, рассмотрим на практике один из антиотладочных приемов. Если ты читал предыдущий выпуск журнала, то помнишь, что мы исследовали небольшую программу-«дрозофилу» (воспользуемся термином, который иногда в своих статьях использует Крис), написанную на ассемблере. Ее можно найти на нашем диске; точка входа располагается по адресу 0x0401000, а код, который выдает окошко с надписью «Hello, World!», имеет размер всего 26h байт. Соответственно, начиная с адреса 0x401026, располагается «выравнивающий» секцию массив нулевых байтов.
После установки обработчика исключений происходит инициирование исключительной ситуации (инструкция «CALL 00401033» уходит в бесконечную рекурсию, что неминуемо вызывает переполнение стека). Если проанализировать этот код, можно заметить, что инструкция, расположенная по адресу 00401038, никогда не будет выполнена. Адрес, содержащийся в EIP, изменится после того, как исключение будет сгенерировано, и он будет равен содержимому указателя на обработчик исключения – 0x40103A. Поэтому по адресу 00401038 может быть размещена любая инструкция. Впрочем, использование в этом месте команды JMP позволяет ввести отладчик в заблуждение. Например, если операнд этой инструкции будет равен 0040103E, все последующие инструкции восстановления регистров будут трактоваться как данные, ибо ни одна часть программы не ссылается на них, а перед ними расположена инструкция безусловного перехода. Кроме того, ссылка на машинный код, который является серединой инструкции JMP, приводит к тому, что она рассматривается отладчиком как следующий код:
И уж совсем «добить» реверсера можно, если разместить после команды JMP SHORT 00401000 безусловный переход на середину некоторой инструкции, размещенной в пределах секции кода, заставляя отладчик делать ошибочную попытку интерпретировать инструкции как данные. Это может быть реализовано следующим образом:
Этот код практически не поддается анализу, что доказывает: механизм исключений - очень эффективный и мощный метод антиотладки. Интересно, что инструкции, располагающиеся вблизи точки входа, не интерпретируются в качестве кода даже после того, как точку останова устанавливают на «данные», располагающиеся по адресу 0x401000. Исправить ситуацию может только грамотный разбор антиотладочного кода профессиональным реверсером и множественные правки кода в процессе отладки. Метод часто используется и для изменения EIP, то есть адреса выполняемой инструкции. Часть кода, которая, казалось бы, должна выполняться, может быть предварена не слишком явно определенным исключением. В результате, разбор структуры программы оказывается для реверс-инженера практически непосильной задачей. «Кормим» vsprintf () спецификаторамиОтладчики, хотя и созданы специалистами по реверс-инжинирингу, являются программами и имеют уязвимости, свойственные подавляющему большинству других программных продуктов. Порой удается использовать эти ошибки, чтобы предотвратить отладку. Отладчик OllyDbg содержит уязвимость, связанную с использованием ошибочного выполнения функции vsprintf (), входящей в msvcrt. Функция входит в семейство функций «printf». Сайт http://www.opennet.ru описывает его так: «Функции семейства printf выводят данные в соответствии с параметром format, описанным ниже. Функции printf и vprintf направляют данные в стандартный поток вывода stdout; fprintf и vfprintf направляют данные в заданный поток вывода stream; sprintf, snprintf, vsprintf и vsnprintf направляют данные в символьную строку str. Функции vprintf, vfprintf, vsprintf, vsnprintf эквивалентны соответствующим функциям printf, fprintf, sprintf, snprintf, исключая то, что они вызываются с va_list, а не с переменным количеством аргументов. Эти функции не вызывают макрос va_end , и поэтому значение ap после вызова неопределенно. Приложение может позже само вызвать va_end(ap). Эти восемь функций выводят данные в соответствии со строкой format, которая определяет, каким образом последующие параметры (или доступные параметры переменной длины из stdarg(3)) преобразуют поток вывода». Спецификаторы преобразований, начинающиеся символом «%», указывают, что за строкой следует параметр. Если в вызове функции указан спецификатор «%s», параметр типа const char * будет преобразован в указатель на символьный массив – строковой указатель. При этом функцией будут выведены символы, вплоть до символа-терминатора («NULL»). В чем причина ошибочного выполнения функции vsprintf () в OllyDbg? Оказывается, в определенных случаях отладчик передает данные, встречающиеся в программе, непосредственно функции vsprintf (), без каких-либо дополнительных проверок. Представь себе, что в строке, переданной функции, содержатся спецификаторы преобразований. Если будет выполнено преобразование параметра в указатель, который будет указывать на неинициализированную область памяти, программа завершится с сообщением об ошибке. Исключение не будет обработано стандартным обработчиком, и процесс, породивший его, будет завершен. Ты, наверное, догадался, что в случае вызова некоторых API-функций в процессе исследования программы данным процессом будет являться отладчик. Это нас и интересует больше всего :). Ошибка использования функции vsprintf () при передаче ей символьной строки, в которой содержатся спецификаторы преобразования «%s», встречается в отладчике OllyDbg версии 1.10. Именно эта версия и полюбилась тысячам реверсеров по всему миру! К сожалению, отладчики уровня ядра и некоторые «прикладные» отладчики не имеют этой ошибки. Но наша цель - один из наиболее популярных отладчиков - OllyDbg. Ниже мы рассмотрим код, который демонстрирует использование уязвимости. Программа может генерировать текстовые отладочные сообщения. Для этих целей в WIN32 служит функция OutputDebugStringА() библиотеки «kernel32.dll». Функцией OutputDebugStringA() регистрируется собственный обработчик исключений (мы говорили об обработчиках выше), после чего вызывается RaiseException(), инициирующая программное исключение. Если в системе присутствует отладчик, установленный по умолчанию, обработка сгенерированного исключения будет передана ему. В ином случае будет использоваться обработчик, установленный самой функцией. В том, что обработчик действительно установлен, легко убедиться, протрассировав по <F7> функцию OutputDebugStringA() до момента вызова внутренней функции, содержащей следующий код:
После выполнения этого кода и возврата в функцию OutputDebugStringA() обработчик будет установлен.
Параметр *lpArguments формируется на основе входной строки для функции OutputDebugStringA(). Так как указатель будет иметь недопустимое значение, исключение не будет обработано. А это и приводит к «умиранию» отладчика. На практике осуществить антиотладочный прием, описанный выше, очень легко (в качестве «подопытной» программы снова воспользуемся нашей «дрозофилой»):
В принципе, если вызов функций – как высокоуровневых, так и NativeAPI – спрятан (этого можно добиться, используя некоторые методики, вроде Stolen code, то есть метод перемещения кода функций в PE-файл). Все попытки исследования сводятся к перемещению в ядро выполнением команды SYSENTER, что заставляет реверс-инженера использовать низкоуровневые отладчики, например, SoftIce. В свою очередь, это связано с большими трудозатратами. Поэтому эта защитная «фишка» является очень эффективной, если подходить к ее реализации с умом. |