R0 CREW

Malware Analysis Tutorial 4: Int 2D Anti-Debugging (Part II) - (Перевод: Prosper-H)

Перейти к содержанию

Цели урока:

  1. Проанализировать различия в поведении отладчика при наличии инструкции INT 2Dh.
  2. Рассмотреть отладку и модификацию двоичных исполняемых файлов.
  3. Разобраться с основными конструкциями, управляющими потоком исполнения на x86 платформе.

Задача дня:

  1. Найти, как можно больше возможных способов сделать так, чтобы нормальное выполнение программы отличалось от выполнения в отладочной среде (используя инструкцию INT 2D).

1. Введение

Поведение инструкции INT 2D может зависеть от многих факторов, например: SEH обработчик устанавливается самой программой, программа запущена под отладчиком уровня пользователя (Ring 3), операционная система выполняется в отладочном режиме (debugged mode), the program logic of the OS exception handler (KiDispatch), the value of registers when int 2d is requested (determining the service that is requested). Позже мы воспользуемся экспериментальным подходом, чтобы рассмотреть все возможные различия в поведении программы в тот момент, когда она выполняется на виртуальной машине, в отладочной среде.

2. Подготовка

В этом уроке, помимо Immynity Debugger, мы будем использовать WinDbg. Прежде чем продолжить, нам нужно правильно настроить его на наших машинах «Host XP» и «Guest XP».

Если вы не устанавливали «Guest VM», то следуйте инструкциям из первого урока. Обратите особое внимание на Раздел 3.1 (где описывается, как правильно настроить последовательный порт в «Guest XP»). В дальнейшем мы будем предполагать, что «pipe path» на «Host XP» установлен в «\.\pipe\com_11», а «Guest XP» использует COM1. Для установки WindDbg на хостовой машине, можно следовать инструкциям из MSDN.

Далее, чтобы заставить все это работать, нам нужно настроить «Guest XP».

  1. Изменение c:\boot.ini. Нам нужно создать второй вариант загрузки операционной системы для ее загрузки в отладочном режиме. Ниже показано, как нужно модифицировать ваш boot.ini файл. Обратите внимание, что мы устанавливаем COM1 в качестве отладочного порта.
[boot loader]
timeout=30
default=multi(0)disk(0)rdisk(0)partition(1)\WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional" /noexecute=optin /fastdetect
[B]multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="DEBUGGED VERSION" /noexecute=optin /fastdetect /debug /debugport=com1 /baudrate=115200[/B]
  1. Ручная настройка COM-портов. В некоторых версиях XP, COM-порты должны быть настроены в ручную. Как это сделать описано jorgensen в туториале «How to Add a Serial Port in Windows XP and 7 Guest» (смотрите раздел касаемый XP). Он состоит из двух шагов: (1) вручную добавить COM-порт в Control Panel и (2) вручную настроить COM1 как номер порта.
  2. Тестовый запуск WinDbg. Запустите «XP Guest» в режиме отладки (debug mode)

На «Host XP» запустите окно командной строки «Windows SDK 7.1», в поставке которого идет WinDbg. Смените директорию на «c:\Program Files\Debugging Tools for Windows(x86)» и введите команду указанную ниже. После чего перед вами должно появиться окно изображенное на рис. 1.

windbg -b -k com:pipe,port=\\.\pipe\com_11

Вы могли заметить, что в данный момент у вас нет доступа к «Guest XP». Это происходит потому, что WinDbg останавливает выполнение операционной системы. Просто введите «g» (обозначающее «go») в окне WinDbg и дайте «Guest XP» продолжить свою работу.

Рис. 1. Окно WinDbg

3. Эксперимент 1: Int 2d на Cygwin

Далее, мы продемонстрируем некоторые интересные свойства инструкции INT 2D, используя для этой цели простую программу Int2dPrint.exe. Программа написана на C++, ее исходный код представлен ниже. Выводом (output) программы должна быть строка «AAAABBBB». Мы добавили fflush(stdout), перед каждым оператором printf(), чтобы осуществить вывод в «eager mode» (прим. пер. т.е. в данном случае, происходит принудительное сбрасывание буфера ввода, что приводит к немедленному выводу на экран), помимо этого есть 5-ть целочисленных операций, которые, позже, позволят вставить дополнительный машинный код.

Листинг 1: Исходный код Int2dPrint.exe

#include <stdio.h>

int main(){
  int a = 0;
  int b = 0;
  int c = 0;
  int d = 0;
  int e = 0;
  printf("AAAA");
  fflush(stdout);

  a = 0; b = 0; c = 0; d = 0; e = 0;
  printf("BBBB");
  fflush(stdout);
}

На рис. 2 показано ассемблерное представление скомпилированного кода программы. Очевидно, что инструкции «MOV [EBP-xx], 0» между адресами 0x4010BA и 0x4010D6 соответствуют целочисленным присваиваниям «int a=0, etc» в исходном коде программы. Инструкция «MOV [ESP], 0X402020», по адресу 0x4010DD, помещает параметр (который является указателем на строку «AAAA») в стек, для использования его в функции printf(). Также заметьте, что до вызова fflush, по адресу 0x4010F4, программа вызывает cygwin.__getreent. Это нужно для того, чтобы получить специальную структуру, из которой можно извлечь stdout (дескриптор файла, отвечающего за стандартный вывод). Фактически, в данной структуре, stdout размещен по смещению 0x8.

Рис. 2. Бинарный код Int2dPrint.c

3.1 Patching двоичного кода

Приступим к подготовке патча для Int2dPrint.exe. Двоичная программа скомпилирована при помощи g++ под Cygwin. Для ее запуска, вам нужно иметь cygwin1.dll в папке «bin» Сygwin. Либо вы можете скомпилировать свою версию программы, используя любой другой отладчик.

Убедитесь, что «XP Guest» выполняется в режиме NON-DEBUG!

Теперь добавим следующим ассемблерный код по адресу 0x4010F9 в Int2dPrint.exe (это первый «int a=0» перед printf(«BBBB»)). Должно быть интуитивно понятно, что код проверяет значение регистра EAX после вызова инструкции INT 2D. Если EAX = 0 (тут «JZ» значит Jump if Zero), то программа перейдет по адресу 0x401138, который пропускает printf(«BBBB»). Обратите внимание, что это происходит только тогда, когда инструкция «inc EAX» была пропущена.

Листинг 2: Вставляемый ассемблерный код

xor EAX, EAX       # set EAX=0;
int 2d                      # invoke the exception handler
inc EAX                 # if executed, will set EAX=1
cmp EAX, 0
JZ 0x401138         # if EAX=0, will skip printf("BBBB");

Ниже показано, как можно пропатчить код, используя IMM:

  1. В панели CPU нажмите «right click» по адресу 0x4010F9, затем выберите «Assemble» (либо просто нажмите «Пробел», разместив указатель мыши по указанному адресу) и введите приведенный выше код.
  2. В панели CPU нажмите «right click», выберите «Copy to Executable => All Modified» и нажмите «Copy All». После этого отобразится окно с измененными инструкциями. Просто закройте его и нажмите «Yes», чтобы сохранить изменения. Сохраните файл как Int2dPrint_EAX_0_JZ0.exe. Имя файла предполагает, что EAX, входящий параметр для сервиса INT 2D, равен 0, и мы ожидаем, что исходя из этого, будет пропущен вызов printf(“BBBB”), если EAX = 0, т.е выводом программы должна быть строка «AAAA» (конечно, это зависит от того, будет ли выполнена инструкция «INC EAX» или нет).

На рис. 3, можно видеть дизассемблерное представление программы Int2dPrint_EAX_0_JZ0.exe. Установите брэйкпойнт на адрес 0x004010BA и, шаг за шагом, выполните программу в IMM. Выполняя программу, вы обнаружите, что ее выводом будет строка «AAAA», т.е. был пропущен вывод строки «BBBB». Похоже, что заключение, о расщеплении байта инструкцией INT 2D, подтверждается. Можете запустить программу в окне командной строки, вывод будет тем же.

Рис. 3. Дизассемблерное представление Int2dPrint_EAX_0_JZ0.exe

Подождите, а как насчет другого эксперимента? Давайте изменим инструкцию по адресу 0x401101 на «JNZ 0x401138» (назовем программу с этим изменением Int2dPrint_EAX_0_JNZ0.exe). Каким будет вывод? «AAAABBBB»? Вы обнаружите, что в IMM, выводом программы будет строка «AAAABBBB»; но если вы запустите ее в окне командной строки, то она сгенерирует только «AAAA»!!!Заметьте, что мы исключили возможность того, что «вывод программы» мог застрять в буфере, потому что мы вызываем fflush(stdout) для принудительного вывода всех данных. Что это значит? Тут может быть два варианта:

  1. Тем или иным образом, инструкция «INC EAX» загадочным образом была выполнена (при обычном выполнении Int2dPrint_EAX_0_JNZ0.exe). В этом нет никакого смысла, поскольку до адреса 0x401101, программа идентична с программой Int2dPrint_EAX_0_JZ0.exe
  2. Есть что-то хитрое в коде обработчика исключений (это может быть SEH устанавливаемый самой программой или KiDispatch в ядре).

Позже мы вернемся к этому странному поведению и дадим ему объяснение.

3.2 Эксперименты с Kernel-Debugging Mode

Давайте перезагрузим «Guest XP» в DEBUG режиме (но не запуская WinDbg на хостовой машине). Давайте повторно запустим две программы, после чего вы сможете сделать некоторые интересные выводы. Обе программы приведут к зависанию «Guest XP».

Хорошо, теперь давайте снова перезагрузим «Guest XP» в DEBUG режиме и запустим WinDbg на хостовой машине (нажмите дважды «g», что бы давать возможность операционной системе продолжить свою работу). Теперь запустите программу Int2dPrint_EAX_0_JNZ0.exe из окна командной строки. Что вы наблюдаете? На рис. 4 отображен результат: отладчик остановился на адресе 0x4010fd (это адрес инструкции «INC EAX») с исключением 80000003 (в Windows этому коду исключения соответствует «BREAKPOINT»)! Если вы нажмете «g», программа продолжит свое выполнение и выведет строку «AAAA»! В то время, как в non-debugged режиме, в окне командной строки, она выведет «AAAABBBB»! (прим. пер. тут автор немного запутался, «AAAABBBB» будет выведена не в окне командной строки, а при отладке в IMM; в окне командной строки будет всегда выводиться только строка «AAAA»)

Рис. 4. Результат выполнения Int2dPrint_EAX_0_JNZ0.exe

3.2 Обсуждение

Подведем итог нашим наблюдениям (Таблица 1). Я не обсуждаю тут некоторые другие эксперименты, но вы можете их повторить, если хотите.

Табл. 1. Итоги «Эксперимент 1»

Если говорить проще, то: INT 2Dh это более мощная техника, для определения наличия отладчика, чем считалось раньше (см. список литературы из Урока 3). Она может быть использована для обнаружения как Ring 3 (user mode), так и Ring 0 (kernel mode) отладчиков. Например, используя табл. 1, мы можем легко сказать, выполняется ли Windows в DEBUG режиме (т.е. включен отладчик ядра) или нет, и присоединен ли ядерный отладчик, такой как WinDbg, к COM-порту или нет. Мы так же можем сказать, существует ли отладчик пользовательского уровня, такой как IMM, независимо от того, выполняется ли Windows в NON-DEBUG или DEBUG режиме. Сложность заключается в том, что конечный результат инструкции INT 2D зависит от множества факторов, и «Эксперимент 1» покрывает только часть из них. Ниже приведены некоторые из основных фактов:

  1. EAX, ECX, EDX являются параметрами сервиса INT 2D. Значение регистра EAX (1, 2, 3, 4) представляет: printing, interactive prompt, load image, unload image соответственно. За дополнительными деталями обращайтесь к уроку Almeida. Обратите внимание, что мы устанавливаем EAX в 0, что не ожидается со стороны сервиса! (нормальное значение должно быть от 1 до 4)
  2. После выполнения инструкции INT 2D, процессор находит вектор прерываний и переходит на процедуру обработчика, которая является частью операционной системы.
  3. OC оборачивает детали аппаратного исключения и генерирует такую структуру ядра, как Exception_Record, которая содержит Exception Code: 80000003 (представляющий собой «breakpoint»).
  4. Затем управление передается вызову ядра KiDispatchException, который имеет очень сложное поведение, в том случае, когда Windows работает в привилегированном режиме (kernel mode). За подробностями обращайтесь к G. Nebbett, “Windows NT/2000 Native API Reference” (на стр. 441 дан псевдо код KiDispatchException). Например, Windows в DEBUG режиме, вначале передает исключение отладчику (вызывая DbgkForwardException), а затем вызывает пользовательские обработчики прерываний SEH, после чего передает исключение отладчику во второй раз.

Перейдем к краткому объяснению всех типов поведений, которые мы наблюдали раньше.

Случай 1: Режим NON-DEBUG и Command Windows (столбец 2 в Таблице 1): это единственный случай, когда Int2dPrint_EAX_0_JZ0.exe и Int2dPrint_EAX_0_JNZ0.exe ведут себя одинаково. Есть только одно объяснение: инструкция INC EAX не выполняется, но не потому, что обработка исключения ведет себя по разному в отладочной среде (debugged environment), а потому, что процесс завершает свою работу. Для иллюстрации этого утверждения, посмотрите на два скриншота на рис. 5, которые сгенерировал отладчик IMM с помощью (View->SEH Chain). На рис. 5 (a) отображена цепочка SEH-обработчиков. Вы можете видеть, что в момент, когда программа была только запущена, существовал стандартный (default) обработчик kernel32.7C839AC0 (означает, что адресом обработчика является 7c839ac0 и то, что он размещен в kernel32). Если вы установите брэйкпойнт прямо перед printf(), то вы заметите, что цепочка SEH-обработчиков теперь включает еще один обработчик от Cygwin рис. 5 (b)! [B]Это обработчик Cygwin, который просто завершает процесс (без сообщения о каких-либо ошибках); если бы был только один kernel32 обработчик, то он бы вывел стандартное диалоговое окно об ошибке.[/B]

Рис. 5. SEH Chain для Int2dPrint_EAX_0_JZ0.exe до и после достижения main()

Случай 2: Режим NON-DEBUG и IMM Debugger (столбец 3 в Таблице 1): Исходя из логики двух программам, можно прийти к выводу, что однобайтовая инструкция сразу после INT 2Dh будет пропущена! Здесь есть два замечания: (1) обработчик Cygwin НИКОГДА не будет выполнен! Это происходит потому, что Immunity Debugger первым получает управление (вспомните логику KiDispatchException и KiForwardException to debugger port). (2) Immunity Debugger изменяет значение регистра EIP потому, что исключение является брэйкпойнтом (смотрите обсуждение в статье Ferrie, о поведение IMM [1]). В результате произойдет сдвиг на один байт, однако, это также зависит от поведения ядра (look at the EIP-- operation in KiDispatchException (see pp. 439 of Nebbett’s book [2]). Совокупный эффект приведет к сдвигу на один байт. Заметьте, что если заменить IMM другим Ring 3 отладчиком, например, таким как IDA, то у вас может быть другой результат.

Случай 3: Режим DEBUG без присоединенного WinDbg и CMD shell (столбец 4 в Таблице 1): Windows заморожена! Причина вполне очевидна: нет отладчиков, прослушивающих отладочный порт, из-за чего исключение не обрабатывается (т.е. никто не изменяет значение регистра EIP).

Случай 4: Режим DEBUG без присоединенного WinDbg и Run in IMM (столбец 5 в Таблице 1): Это похоже на Случай 2. Если вы нажмете F9 (и запустите программу) в IMM, то вы заметите, что IMM остановится на [B]ВТОРОЙ инструкции[/B] после INT 2Dh (т.е. на «CMP EAX, 0») потому, что это breakpoint exception, но kernel debugging service в действительности не срабатывает. Если вы нажмете F9 (continue), то программа продолжит свое выполнение и продемонстрирует тоже поведение, как и в Случае 2. И снова, расщепление байта является комбинированным результатом IMM и поведения ядра (на исключение INT).

Случай 5: Режим DEBUG c присоединенным WinDbg и Run in CMD shell (столбец 6 в Таблице 1): В этом случае, WinDbg останавливается на [B]инструкции сразу после INT 2Dh[/B] (т.е. «INC EAX») и если продолжить, то выполнится инструкция «INC EAX».

Случай 6: Режим DEBUG с присоединенным WinDbg и Run in IMM (столбец 7 в Таблице 1): В этом случае, WinDbg никогда не перехватит исключение, поскольку первым его обработает Ring 3 отладчик IMM, и как в Случае 4, IMM изменит значение регистра таким образом, что он остановится на [B]ВТОРОЙ ИНСТРУКЦИИ[/B] после INT 2D. Интересно заметить, что даже при присутствии WinDbg, если вы запускаете Ring 3 отладчик, то он отменяет обработку брэйкпойнтов отладчиком WinDbg и обрабатывает их сам. Это, конечно, понятно – думать про использование Visual Studio в отладочном (debugged) режиме, в момент отладки программы, и это естественно вначале передать событие о брэйкпойнте Visual Studio. Но, как только пользовательский отладчик заявляет, что исключение было обработано, нет необходимости передавать его для обработки ядерному отладчику.

Очевидно, что [B]отладчик IMM имеет «дефек» в своей реализации[/B]. Во-первых, он в слепую обрабатывает исключения от брэйкпойнтов, даже если этот брэйкпойнт, вызвавший исключение, не входил в список зарегистрированных (т.е. не был установленным с помощью IMM, например, пользователем). Во-вторых, сервис ядра обрабатывает изменение регистра EIP по-разному для INT 3 и INT 2D (не смотря на то, что оба обернуты как 80000003-е исключение). Поэтому, когда IMM не различает такие случаи, происходит своеобразный эффект изменения регистра EIP на большее значение, чем положено. В итоге это приводит к расщеплению байта.

3.3 Задачи дня

Все обсуждаемое выше основывается на том предположении, что во время вызова INT 2D регистр EAX = 0. Обратите внимание, что это значение является неожиданным для ядра Windows – допустимы значения 1, 2, 3 и 4 (debug print, interactive, load image, unload image). Сегодня ваша задача состоит в том, чтобы понять, что будет происходить, если EAX установить в 1, 2, 3, 4 или в любое другое неожидаемое значение. Каково будет поведение системы? Выполнив эти эксперименты, вы откроете для себя интересные вещи.

4. Эксперимент 2: notepad.exe

Есть еще одна интересная область, которую мы не исследовали: это когда пользователь установил собственный SEH. Int2d программы хороший этому пример. Начальный код перед функцией main устанавливает SEH-обработчик предлагаемый Cygwin’ом, что приводит к немедленному завершению процесса, при вызове этого обработчика. В то же время, интересно наблюдать и за поведением стандартного обработчика kernel32. Эксперимент представленный ниже проливает некоторый свет.

4.1 Experiment Design

Когда мы в notepad.exe используем меню «File => Open», то всегда видим появляющееся диалоговое окно. Наш план состоит в том, чтобы вставить код из Раздела 3.1 перед вызовом диалогового окна, и понаблюдать за расщеплением байта.Первая проблема заключается в том, чтобы определить местоположение кода отвечающего за запуск диалогового окна. Для ее решения, мы воспользуемся одним из трюков Immunity Debugger. Хорошо известно, что user32.dll предоставляет важные системные функции, которые связанны с графическим интерфейсом пользователя. Поэтому, мы можем исследовать функции user32.dll используя следующий подход:

  1. Откройте notepad.exe (находится в «C:\Windows») используя Immunity Debugger
  2. Выберите «View => Executable Modules»
  3. Нажмите «right click» на «user32.dll» и выберите «View => Names». Это действие покажет адреса всех экспортируемых функций из DLL. Просмотрите список функций и найдите такие функции как: CreateDialogIndirectParamA и CreateDialogIndirectParamW. Нажмите «F2», чтобы установить брэйкпонты на каждую из них.
  4. Теперь нажмите F9, чтобы запустить notepad.exe. Нажмете «File => Open» и IMM остановится на адресе [B]7E4EF01F[/B]. Вернитесь назад в окно «View => Names», там вы обнаружите, что этот адрес соответствует функции [B]CreateDialogIndirectParamW[/B].
  5. Удалите все лишние брэйкпойнты (за исключение CreateDialogIndirectParam), чтобы мы не отвлекались на другие срабатывания. Вы можете сделать это в окне «View => Breakpoints».
  6. Перезапустите программу (убедитесь, что ваш BP установлен), нажмите «File => Open», после чего вы остановитесь на функции CreateDialogIndirectParamW. Теперь мы воспользуемся одной из приятных возможностей IMM. Нажмите [B]«Debug => Execute Till User Code»[/B] (что позволит нам выйти прямо на код notepad.exe!). Обратите внимание, что поскольку диалоговое окно является модальным (т.е. окном, которое висит до тех пор, пока вы не ответите в нем), то вы должны вернуться назад к notepad.exe и закрыть это диалоговое окно. После чего, IMM остановится на инструкции [B]0x01002D89[/B] в нутрии notepad.exe! То есть прямо после вызова функции [B]GetOpenFileNameW[/B].

Рис. 6. Дизассемблерное представление notepad.exe

Дизассемблирование notepad.exe является довольно простым делом. По адресу 0x01002D27, он, для диалогового окна, устанавливает фильтр файлов «*.txt», затем по адресу 0x01002D3D вызывает функцию GetOpenFileW. Возвращаемое значение хранится в EAX. По адресу 0x01002D89 происходит проверка значения EAX. Если оно равно 0 (что, означает, что работа с диалоговым окном была отменена), то программа передает управление на адрес 0x01002DE0 (which directly exists the File->open processing).

Сейчас мы можем вставить наши инструкции (большинство из Раздела 3.1) по адресу 0x01002D27 (побочным эффектом будет то, что dialog file filter будет сломан – но это нормально). Код показан ниже (мы назовем пропатченую программу notepad_EAX_0_JZ0.exe. Подобным образом, мы можем сгенерировать notepad_EAX_0_JNZ0.exe):

xor EAX, EAX       # set EAX=0;
int 2d                      # invoke the exception handler
inc EAX                 # if executed, will set EAX=1
cmp EAX, 0
JZ 0x01002D89         # if EAX=0, will skip printf("BBBB");

Запустите notepad_EAX_0_JZ0.exe в окне командной строки (в NON-DEBUG режиме Windows) и вы увидите стандартное окно обработки исключений, выбрасываемое операционной системой. Если вы нажмете в нем на ссылку «details», то сможете посмотреть детальную информацию: заметьте, что code error = 0x80000003, а адрес исключения 0x01002D2B! Я думаю, что вы сможете легко сделать вывод о обработке исключений в kernel32.dll.

Рис. 7. Сообщение об ошибке

4.2 Вопрос

Вы уверены, [B]что сообщение об ошибке выводит обработчик kernel32.7C839AC0?[/B] Предоставьте доказательства.

5. Эксперимент 3: SEH Handler Technique

Напомним, что обработчик SEH, установленным самой программой, также может влиять на поведение INT 2D. Например, в Int2dPrint_EAX_0_JZ0.exe установлен обработчик от Cygwin1.dll, что приводит к немедленному завершению процесса, в то время как стандартный обработчик kernel32.dll выводит диалоговое окно, которое отображает отладочную информацию. В этом эксперименте мы повторим пример Ferrie из [3] и исследуем дальнейшие потенциальные возможности анти-отладки.

Рисунки 8 и 9 представляют нашу слегка адаптированную версию примера Ferrie из [3]. Программа является модифицированной копией Int2dPrint.exe. Первая часть измененного кода отображена на рис. 8, начиная с адреса 0x004010F9 и заканчивая адресом 0x0040110E. Сейчас мы кратко объясним ее логику.

В основном, код предназначен для установки нового обработчика исключений (напомним, что SEH – это связанный список, каждая запись которого состоит из двух элементов: указателя на предыдущую запись и адреса обработчика исключений). Таким образом, инструкция по адресу 0x004010FB устанавливает адрес нового обработчика, размещенного в 0x004016E8(к нему мы вернем позже), а инструкция по 0x00401100 устанавливает указатель на предыдущий обработчик. Затем инструкция по адресу 0x00401103 сбрасывает FS:[0], который всегда указывает на первый элемент в цепочке SEH. Оставшаяся часть кода осуществляет старый трюк: ставит инструкцию «INC EAX» сразу после инструкции INT 2D, и в зависимости от того, пропускается ли эта инструкция или нет, - этот код может сказать существует ли отладчик или нет.

Рис. 8. Первая часть из кода Ferrie

Теперь исследуем код обработчика расположенного по адресу 0x004016E8. Он отображен на рис. 9, начиная с адреса 0x004016E8 и заканчивая адресом 0x004016F4. Он состоит из трех инструкций. Инструкция по адресу 0x004016E8, размещает двойное слово 0x43434343 по адресу 0x00402025. Если вы изучите инструкцию по адресу 0x0040111 (Рис. 8), то вы заметите, что по адресу 0x00402025 хранится строка «BBBB». Таким образом, эта инструкция, по сути, заменяет строку «BBBB» на «CCCC» в памяти. Если выполнится SEH-обработчик и второй вызов функции printf(), то вы увидите вывод строки «AAAACCCC», вместо «AAAABBBB». Вы, возможно, зададитесь вопросом, почему просто не изменить значение регистра (например, EBX) в обработчике, чтобы указать, что SEH был выполнен? Напомним, что обработчик прерываний ОС, восстановит значения регистров из стэка ядра – независимо от того, какое значений вы установите регистру (за исключением EAX), оно будет стерто операционной системой после возвращения из процедуры обработки прерываниями.

Две последние инструкции SEH-обработчика просто возвращают 0. Обратите внимание, что как показано у Pietrek’a в [1], «0» означает ExceptionContinueExecution, т.е. исключение было обработано и прерванный процесс должен возобновиться. Существуют и другие значения, с которыми вы можете поиграться, например, «1» означает ExceptionContinueSearch, т.е. текущий обработчик не смог решить проблему и поиск по цепочке SEH должен быть продолжен для того, чтобы найти следующий обработчик. Заметьте, что эти значения определены в EXCEPT.h.

Рис. 9. Вторая часть из кода Ferrie

Там может быть еще один фактор, который влияет на результат эксперимента. Immunity Debugger может быть настроен таким образом, чтобы пропускать или не пропускать исключения к программе пользователя. Нажмите «Debuggers Option» в меню IMM, затем выберите вкладку «Exceptions» (Рис. 10). Здесь можно указать, чтобы отладчик передавал все исключения пользовательской программе (для этого нужно нажать на кнопку «add range» и выбрать все исключения). После того, как настройка будет завершена, запустите программу используя «Shift + F9». Это действие передаст исключения SEH-обработчику, который был установлен пользователем (сравните с F9).

Рис. 10. Настройка обработки исключений в IMM

Так же как и в Разделе 4, мы можем запустить нашу программу (Int2dprint_EAX0_RET0_JZ0.exe, означает установку EAX в 0, при вызове INT 2D, и возвращает 0 в SEH-обработчик), в различных средах, т.е. c включенным отладочным режимом и без него. Полученные результаты отображены на рис. 11.

Режим NON-DEBUG: при запуске из окна командной строки, выводом программы будет строка «AAAACCCC». Очевидно, что выполнился пользовательский SEH-обработчик и поэтому расщепление байта не произошло (т.е. инструкция «INC EAX» действительно выполнилась). Сравнив это с аналогичным примером из Таблицы 1, вы сразу поймете эффект возвращения 0 в SEH: что в действительности говорит ОС: «все нормально. Не убивай процесс!»

Если запустить программу в IMM, используя F9 (т.е. без передачи исключений пользовательской программе), результатом будет строка «AAAA», так как «INC EAX» была пропущена отладчиком (подобно результату из Таблицы 1) и пользовательский SEH никогда не будет выполнен; однако, если вы используете «Shift + F9», чтобы передать исключения программе, то будет выполнен SEH и «INC EAX»! Похоже, что в режиме «Shift + F9», IMM не изменяет регистр EIP (как указано в статье Ferrie)

Режим DEBUG с присоединенным WinDbg: Когда WinDbg присоединен, то программа выполняемая в командной строке выведет строку «AAAABBBB». Это значит, что инструкция «INC EAX» выполнилась, но SEH-обработчик – нет! Я думаю, что аналогично этому примеру, вы сможете объяснить и результат выполнения в IMM.

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

Рис. 11. Результаты эксперимента над примером Ferrie

5.1 Вопрос

Поиграйте с возвращаемыми значениями SEH-обработчика, верните из него значения 1, 2, любое другое число (например, отрицательное). Что вы наблюдаете?

6. Заключение

Анти-отладочная техника, основанная на прерывании INT 2D, по сути, является отпечатком ОС, т.е. в зависимости от своего поведения система сообщает свою версию и конфигурацию. С точки зрения исследователя программ, это может быть очень увлекательной проблемой, чтобы автоматически сымитировать такие анти-отладочные техники, с учетом исходного/двоичного кода операционной системы. (From the point of view of a program analysis researcher, it could be a very exciting problem to automatically generate such anti-debugging techniques, given the source/binary code of an operating system.)

© Translated by Prosper-H from r0 Crew

Ссылки

[1] M. Pietrek, “A Crash Course on the Depth of Win32Tm Structured Exception Handling,” Microsoft System Journal, 1997/01

[2] G. Nebbett, “Windows NT/2000 Native API Reference”, pp. 439-441, ISBN: 1578701996.

[3] P. Ferrie, “Anti-Unpacker Tricks - Part Three”, Virus Bulletin Feb 2009. Retrieved 09/07/2011