Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 130, ОКТЯБРЬ 2009 г.

Энциклопедия антиотладочных приемов

Леонид «Cr@wler» Исупов (crawler@xakep.ru)

Процесс отладки защищенной программы порой напоминает прохождение хитроумного квеста. Одно из самых больших удовольствий для реверсера (иногда - и большая головная боль) - заниматься преодолением защит, которые «завязаны» на обработке исключений. Если ты еще не знаком с принципом защитных механизмов, построенных на его основе, все, что изложено ниже, тебе очень пригодится. «На закуску» - еще один защитный трюк: использование одной интересной особенности работы функции vsprintf (), входящей в msvcrt, которая буквально «убивает» отладчик OllyDbg.

«Исключительная» защита

Часто в процессе работы программы возникают ситуации, которые невозможно предусмотреть. Например, попытка записи в ячейку памяти, которая принадлежит странице с неустановленным атрибутом «writeable», или же деление на ноль. Для таких ситуаций программисты Microsoft создали механизм обработки исключений.

Обработчик исключений, или SEH (англ. «Structured Exception Handling») - часть кода, на которую возложена функция обработки ошибок для данного треда. Нужно пояснить, что представляет собой тред ((от англ. «thread», нить). Думаю, ты знаешь, что код программы может быть «распараллелен», то есть несколько частей программы могут выполняться одновременно – в контексте единственного процесса (например, в графическом редакторе одновременно могут выполняться печать изображения и его редактирование). Каждая такая часть программы и называется тредом. При этом для каждого треда может быть установлен собственный обработчик исключений.

По умолчанию обработкой исключений занимается системный обработчик. Каким образом установить обработчик собственный? Указатель на структуру, содержащую адрес обработчика (который также совпадает и с адресом указателя на сам обработчик), находится по адресу FS:[0]. Следовательно, для того, чтобы заменить системный обработчик собственным, необходимо поместить в стек структуру, содержащую адрес нового обработчика, и указатель на старый обработчик. После чего поместить по адресу FS:[0] указатель на новую структуру.

Не будем терять время, рассмотрим на практике один из антиотладочных приемов. Если ты читал предыдущий выпуск журнала, то помнишь, что мы исследовали небольшую программу-«дрозофилу» (воспользуемся термином, который иногда в своих статьях использует Крис), написанную на ассемблере. Ее можно найти на нашем диске; точка входа располагается по адресу 0x0401000, а код, который выдает окошко с надписью «Hello, World!», имеет размер всего 26h байт. Соответственно, начиная с адреса 0x401026, располагается «выравнивающий» секцию массив нулевых байтов.
Разберем следующий код, который внесен в рассмотренную нами программу-«дрозофилу» «ex.exe» при помощи отладчика OllyDbg (не забывай, что код вносится, начиная с адреса 00401026, что требует использования LordPe для изменения точки входа в программу):

00401026 XOR EAX,EAX; EAX=0
00401028 PUSH 0040103A; помещение адреса нового обработчика в стек
0040102D PUSH DWORD PTR FS:[EAX]; помещение адреса старого обработчика в стек
00401030 MOV DWORD PTR FS:[EAX],ESP; помещение в FS:[0] указателя на структуру
00401033 CALL 00401033; генерация исключения путем переполнения стека
00401038 JMP SHORT 00401038; данная инструкция никогда не будет исполнена
0040103A POP EAX; восстановить регистр
0040103B POP EAX; восстановить регистр
0040103C POP ESP; восстановить регистр ESP
0040103D JMP SHORT 00401000; перейти к выполнению программы

После установки обработчика исключений происходит инициирование исключительной ситуации (инструкция «CALL 00401033» уходит в бесконечную рекурсию, что неминуемо вызывает переполнение стека).

Если проанализировать этот код, можно заметить, что инструкция, расположенная по адресу 00401038, никогда не будет выполнена. Адрес, содержащийся в EIP, изменится после того, как исключение будет сгенерировано, и он будет равен содержимому указателя на обработчик исключения – 0x40103A. Поэтому по адресу 00401038 может быть размещена любая инструкция. Впрочем, использование в этом месте команды JMP позволяет ввести отладчик в заблуждение. Например, если операнд этой инструкции будет равен 0040103E, все последующие инструкции восстановления регистров будут трактоваться как данные, ибо ни одна часть программы не ссылается на них, а перед ними расположена инструкция безусловного перехода. Кроме того, ссылка на машинный код, который является серединой инструкции JMP, приводит к тому, что она рассматривается отладчиком как следующий код:

0040103D EB DB EB
0040103E > C100 00 ROL DWORD PTR DS:[EAX],0 ; Shift constant out of range 1..31

И уж совсем «добить» реверсера можно, если разместить после команды JMP SHORT 00401000 безусловный переход на середину некоторой инструкции, размещенной в пределах секции кода, заставляя отладчик делать ошибочную попытку интерпретировать инструкции как данные. Это может быть реализовано следующим образом:

; Начало кода программы, который становится полностью "нечитабельным":

; Ниже расположены неверно интерпретируемые отладчиком инструкции:
00401000 DB 6A ; CHAR 'j'
00401001 DB 00
00401002 DB 68 ; CHAR 'h'
00401003 DD ex_excep.00403000 ; ASCII "Simply program"
00401007 DB 68 ; CHAR 'h'
00401008 DD ex_excep.0040300F ; ASCII "Hello, World!"
0040100C DB 6A ; CHAR 'j'
0040100D ADD AL,CH
0040100F OR EAX,6A000000
00401014 ADD AL,CH
00401016 ADD BYTE PTR DS:[EAX],AL
00401018 ADD BYTE PTR DS:[EAX],AL
0040101A JMP DWORD PTR DS:[<&kernel32.ExitProcess>]; kernel32.ExitProcess
00401020 JMP DWORD PTR DS:[<&user32.MessageBoxA>] ; user32.MessageBoxA

; Начало антиотладочного кода:

00401026 XOR EAX,EAX
00401028 PUSH ex_excep.0040103A
0040102D PUSH DWORD PTR FS:[EAX]
00401030 MOV DWORD PTR FS:[EAX],ESP
00401033 CALL ex_excep.00401033
00401038 JMP SHORT ex_excep.0040103E; фиктивный переход на середину инструкции

; Далее расположены неверно интерпретируемые отладчиком инструкции:

0040103A DB 58 ; CHAR 'X'
0040103B DB 58 ; CHAR 'X'
0040103C DB 5C ; CHAR ''
0040103D DB EB
0040103E ROL DWORD PTR DS:[EAX],0 ; Shift constant out of range 1..31
00401041 JMP SHORT ex_excep.0040100F; фиктивный переход на середину инструкции

Этот код практически не поддается анализу, что доказывает: механизм исключений - очень эффективный и мощный метод антиотладки. Интересно, что инструкции, располагающиеся вблизи точки входа, не интерпретируются в качестве кода даже после того, как точку останова устанавливают на «данные», располагающиеся по адресу 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() до момента вызова внутренней функции, содержащей следующий код:

7C8024F9 PUSH EAX
7C8024FA MOV EAX,DWORD PTR SS:[EBP-4]
7C8024FD MOV DWORD PTR SS:[EBP-4],-1
7C802504 MOV DWORD PTR SS:[EBP-8],EAX
7C802507 LEA EAX,DWORD PTR SS:[EBP-10]
7C80250A MOV DWORD PTR FS:[0],EAX
7C802510 RETN

После выполнения этого кода и возврата в функцию OutputDebugStringA() обработчик будет установлен.
Вот прототип функции, которая вызывает исключение:

RaiseException(
DWORD dwExceptionCode,
DWORD dwExceptionFlags,
DWORD nNumberOfArguments,
CONST DWORD *lpArguments
);

Параметр *lpArguments формируется на основе входной строки для функции OutputDebugStringA(). Так как указатель будет иметь недопустимое значение, исключение не будет обработано. А это и приводит к «умиранию» отладчика.

На практике осуществить антиотладочный прием, описанный выше, очень легко (в качестве «подопытной» программы снова воспользуемся нашей «дрозофилой»):

00401026 PUSH 00401033; указатель на строку
0040102B CALL OutputDebugStringA; вызов функции
00401031 JMP 00401000; переход к выполнению программы
00401033 DB "%s%s",0; строка, терминированная нулевым значением

В принципе, если вызов функций – как высокоуровневых, так и NativeAPI – спрятан (этого можно добиться, используя некоторые методики, вроде Stolen code, то есть метод перемещения кода функций в PE-файл). Все попытки исследования сводятся к перемещению в ядро выполнением команды SYSENTER, что заставляет реверс-инженера использовать низкоуровневые отладчики, например, SoftIce. В свою очередь, это связано с большими трудозатратами. Поэтому эта защитная «фишка» является очень эффективной, если подходить к ее реализации с умом.

Содержание
ttfb: 6.1380863189697 ms