R0 CREW

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

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

Цели урока:

  1. Понять общий механизм обработки прерываний на платформе X86.
  2. Разобраться с анти-отладочным методом расщепления байта (the byte scission anti-debugging technique).
  3. Узнать, как можно использовать отладчик, чтобы пропатчить программу.

Задача дня:

  1. Проанализировать код между адресами 0xaaaa и 0xaaaa. Какова его цель?

1. Введение

Для продления жизни вредоносного программного обеспечения, часто используются анти-отладочные техники, которые позволяют замедлить процесс анализа, выполняемый специалистами. В этом уроке будет рассмотрена одна из таких анти-отладочных техник, которая была реализованных в Max++ и которая основана на использовании прерывания «INT 2D». Бонфа (Bonfa) предоставил краткое введение в нее тут [1]. Наш же анализ является дополнением к «введению» Бонфы [1] и предоставляет углубленный анализ уязвимостей отладчиков.

Цель анти-отладки состоит в том, чтобы всячески препятствовать процессу реверс инженеринга. Существует несколько основных подходов: (1) определить наличие отладчика и если он присоединен, то вести себя немного иначе; (2) нарушить работу отладчика или вывести его из строя (crash). Подход (1) применяется чаще всего (см. отличный обзор тут [2]). Подход (2) встречается редко (позже мы увидим несколько примеров реализованных в Max++). Сегодня мы сосредоточимся на «Подходе (1)»

Как указано by Shields в [2], существует множество различных способов определения отладчика. Например, программа с анти-отладкой может вызвать системную библиотечную функцию такую как «isDebuggerPresent()» или проверить структуру данных в Thread Information Block (TIB/TEB) операционной системы. Эти методы, отладчик, может довольно легко обойти, используя для этого простую подмену «возвращаемого результата» или «структуры данных» операционной системы.

Инструкцией, которую мы будем пытаться проанализировать, является «INT 2D» и она размещена по адресу 0x00413BD5 (Рис. 1). Посмотрев на вредоноса, можно заметить, что точкой входа программы (Entry Point, EP) является адрес 0x00413BC8. После выполнения первых 8-ми инструкций, сразу перед выполнением инструкции «INT 2D», значение регистра EAX = 0x1. Это важный факт, который следует помнить во время последующего анализа.

Рис. 1. Скриншот точки входа Max++

2. Дополнительная информация (Background Information)

Давайте понаблюдаем за поведением Immunity Debugger (IMM). Пройдя по F8 инструкцию «INT 2D» по адресу 0x413BD5, мы, как предполагается, должны остановиться на следующей инструкции «RETN» (0x00413BD7), однако, это не так. Новым значением EIP, т.е. местоположением следующей выполняемой инструкции, является адрес 0x00413A38. Возникает вопрос: является ли поведение отладчика IMM корректным, т.е. соответствует ли это поведение нормальному выполнению Max++ без присоединенного отладчика?

Чтобы дать ответ на этот вопрос, нам нужно прочитать дополнительную информацию о прерывании «INT 2D». Пожалуйста, потратьте один час и внимательно прочитайте следующие статьи (просто ищите по ключевому слову «INT 2D» игнорируя другие части):

  1. Guiseppe Bonfa, “Step-by-Step Reverse Engineering Malware: ZeroAccess / Max++ / Smiscer Crimeware Rootkit”
  2. Tyler Shields, “Anti-Debugging - A Developer’s View”
  3. P. Ferrie, “Anti-Unpacker Tricks - Part Three”, Virus Bulletin Feb 2009

Подведем итог вышеупомянутой информации:

  1. Бонфа в [1] указывает, что инструкция «INT 2D» вызывает прерывание (исключение). Если отладчик присутствует, то он обрабатывает это исключение; если же отладчика нет, то исключение видно программе (Max++). Выполнение «INT 2D» вызывает расщепление байта (a byte scission), т.е имеется ввиду, что байт следующий сразу за инструкцией «INT 2D» будет пропущен. Как бы там ни было, но никаких объяснений, почему происходит это «расщипление» Бонфа так и не дает. В качестве решения проблемы предлагается использовать плагин StrongOD для OllyDbg, который корректно обрабатывает выполнение «INT 2D». Нам не удалось повторить успех StrongOD на IMM, однако, читателям рекомендуется попробовать его на OllyDbg.
  2. Shields в [2] дают пример трюка с «INT 2D» для высокоуровневого языка программирования. Мы позаимствовали пример из восьмого раздела. Это пример с «INT 3». В этом примере объясняется, как вредонос может обнаружить отладчик, используя для этого структуру try-catch. Когда отладчик присутствует, «try-catch» не сможет перехватить генерируемое исключение, поскольку его обработает отладчик, а значит до «try-catch» оно попросту не дойдет. Когда отладчик отсутствует «try-catch» может спокойно перехватывать исключение «INT 3» ил «INT 2D» (таким образом, следует установить флаг, который сообщит, что отладчик отсутствует).
  3. Ферри (Ferrie) в [3] дает объяснение того, почему происходит расщепление байта во время выполнения программы. Он дает отличный пример в Разделе 1.3 в [3]. Позже (в разделе 3.3), мы прокомментируем каждую строку, чтобы вам было легче разобраться. Этот пример соответствует примеру высокоуровневого языка программирования из [2], однако, на уровне ассемблера, для обработки исключений, он опирается на поддержку со стороны OC, называемую «SEH» (Structured Exception Handling). Позже, после введения в SEH в Разделе 3, мы вернемся к обсуждению этого примера и объясним его детали:

Листинг 1: Пример использования «INT 2D» из P. Ferrie, “Anti-Unpacker Tricks - Part Three”, VB2009

      1      xor eax, eax            
      2      push offset l1          
      3      push d fs:[eax]
      4      mov fs:[eax], esp
      5      int 2dh                 
      6      inc eax                 
      7      je being_debugged       
      8          ...
      9  l1: xor al, al              
      10     ret

3. Структурная обработка исключение (Structured Exception Handling, SEH)

3.1 Исключения и прерывания

Когда программа использует инструкции на подобие «INT 2D» – это говорит о том, что сейчас произойдет исключение и весь процесс, связанный с его обработкой. При работе с исключениями, полезно полностью разобраться с техническими деталями, вовлеченными в процесс их обработки. Мы рекомендуем Intell IA32 Manual [5] (ch6: interrupt and exception overview). Некоторые важные факты перечислены ниже:

  1. Прерывания происходят из-за аппаратных сигналов (например, сигналов завершения ввода/вывода (I/O completion signals) или после выполнения инструкций «INT xx»). Они происходят в случайный момент времени (например, сигналы ввода/вывода), за исключением прямого вызова инструкций INT.
  2. Исключения происходят, когда процессор обнаруживает ошибку во время выполнения инструкции.
  3. В момент, когда нормальное выполнение программы прервано, из-за возникшего прерывания или исключения, процессор переходит на обработчик прерывания (кусок кода который обрабатывает прерывания/исключения). Восстановление нормального выполнения программы происходит сразу после того, как обработчик прерывания завершил свою работу. Обработчики прерываний загружаются ОС во время начальной загрузки системы. Существует так называемая таблица векторов прерываний (Interrupt Vector Table) также называемая таблицей дескрипторов прерываний (Interrupt Descriptor Table, IDT), которая определяет, какой из обработчиков отвечает за обработку каждого конкретного прерывания.
  4. В общем целом существуют следующие прерывания/исключения: (1) исключения генерируемые программным обеспечением («INT 3» и другие «INT n» инструкции); (2) исключения генерируемые аппаратным обеспечением (в данный момент они нам не интересны); (3) fault – исключение, которое может быть исправлено, когда выполнение программы восстанавливается, она выполняет те же инструкции (что вызвали исключение) снова; (4) trap – отличается от «fault» тем, что при восстановлении выполнения программы она восстанавливает выполнение сразу со следующей инструкции (т.е. после инструкции вызвавшей исключение). (5) abort (серьезные ошибки, которые не интересны для нас в данный момент). Если вы посмотрите на Таблицу 6-1 (прим. пер. имеется ввиду в Интеловском Мануале), то деление на «0» является исключением, которое относится к fault, а «INT 3» (программный брэйкпойнт) относится к trap. Раздел 6.6 (прим. пер. Интеловский Мануал) дает ясное представление о различиях между fault и trap.
  5. Процессор, когда происходит прерывание/исключение, помещает следующую информацию (изменяется в зависимости от типа прерывания/исключения): EIP, CS, FLAG Registers и ERROR CODE в стэк. Затем находит адрес обработчика прерывания в IDT и переходит на него. Обратите внимание, что сохраненние EIP/CS (адрес возврата) зависит от того, был ли это fault или trap! После чего обработчик прерывания делает свою работу, и когда восстанавливает работу программы, использует сохраненную информацию в EIP/CS.

3.2 Structured Exception Handling

В отличии от Intel IA32 Manual, Microsoft WIN32 инкапсулирует детали обработки прерываний. В статье MSDN [6] дан краткий обзор. В Win32, сервис обработки прерываний, обрабатывает все аппаратные сигналы (irrepeatable и asynchronous) как «прерывания» (interrupts); а все другие воспроизводимые исключения (включая faults, traps и инструкции INT xx) обрабатывает как исключения, которые обрабатываются с помощью механизма называемого «структурной обработкой исключений» (Structured Exception Handling, SEH) [сюда входит и случай с INT 2D!]. M. Peitrek предоставил отличную статью [4] в Microsoft System Journal, которая раскрывает внутреннее строение SEH. Мы рекомендуем, внимательно прочитать ее вам, прежде чем продолжать нашу дискуссию.

На рис. 2 показан основной порядок обработки исключения. Когда программа генерирует ошибку (например, ошибку деления на 0), процессор генерирует исключение. Далее, смотря на IDT (interrupt dispatch table), процессор находит адрес обработчика прерывания (Interrupt Service Handler, ISR). В большинстве случаев, Win32 ISR вызовет функцию KiDispatchException (мы вернемся к ней позже). Затем ISR ищет пользовательские обработчики исключений до тех пор, пока один из обработчиков не обработает ошибку. Здесь есть несколько интересных моментов:

  1. ISR необходимо найти пользовательский обработчик исключений. Где его найти? Слово (word, 16-bit) в памяти из FS:[0] содержит адрес. Здесь FS, как CS, DS и SS, является одним из сегментных регистров используемых на платформе X86. В Win32, регистр FS всегда указывает на структуру данных TIB (Thread Information Block). В TIB хранится важная системная информация (такая как: вершина стека, последняя ошибка, идентификатор процесса) текущего потока. Первое слово (word) в памяти TIB – это адрес Exception Handler Record. Таким образом, из FS:[0], ISR может вызывать пользовательские обработчики. Больше информации по TIB, вы можете прочесть тут [8].
  2. Обратите внимание на то, что существует ЦЕПОЧКА ОБРАБОТЧИКОВ! И это очевидно, поскольку у вас мог быть вложенный оператор try-catch. Кроме того, в случае, если ошибка не была обработана пользовательской программой, то система все равно предоставит обработчик, который завершит приложение и выведет диалоговое окно ошибки, с похожим текстом: «Program error at 0xaabbcc, debug or terminate it?». Где разместить эту цепочка обработчиков? В стеке пользовательской программы. Каждый элемент этой цепочки является структурой _EXCEPTION_REGISTRATION. Для получения детальной информации прочтите это [4]. В завершение истории, со структурой _EXCEPTION_REGISTRATION из [4], приведем и опишем ее ниже:
_EXCEPTION_REGISTRATION struc
prev    dd      ?
handler dd      ?
_EXCEPTION_REGISTRATION ends

Тут «dd» обозначает «double word» (32-битное слово памяти). Поле «prev» указывает на предыдущую запись «exception registration», а поле «handler» - это адрес обработчика исключения.

  1. Как сказать ISR, что следует остановиться? Когда пользовательский обработчик возвращает 0 (ExceptionContinueExecution) – ISR может возобновить процесс пользователя. Когда обработчик возвращает 1 (ExceptionContinueSearch) – ISR должен продолжит поиск следующего обработчика в цепочке. Определение ExceptionContinueExecution можно найти в определении EXCEPTIOn_DISPOSITION в EXCPT.h (вы можете легко найти исходный код этого файла в google).

Рис. 2. Основной порядок обработки исключения

3.3 Пересмотр примера Ферри [3]

Теперь, ознакомившись с информацией из раздела 3.2, мы сможем полностью понять детали из примера Ферри. Некоторые важные моменты перечислены ниже:

  1. Инструкции со 2-ой по 4-ю создают новую запись _EXCEPTION_REGISTRATION. Вторая инструкция устанавливает адрес обработчика, третья инструкция устанавливает ссылку «prev», а четвертая инструкция делает так, чтобы FS:[0] указывал на новую запись.
  2. Девятая инструкция устанавливает значение регистра AL = 0. По сути, это нужно для того, чтобы вернуть 0 (ExceptionContinueExecution). Это сообщит IRS, что ошибка была обработана и нет необходимости искать другой обработчик. Затем IRS возобновит нормальное выполнение (старая инструкция может быть выполнена повторно или выполнение начнется со следующей инструкции. Это будет зависеть от типа fault/trap, смотрите Intel IA32 Manual Глава 6)

Листинг 2: Пример Ферри с комментариями, “Anti-Unpacker Tricks - Part Three”, VB2009

      1      xor eax, eax           # EAX = 0         
      2      push offset l1         # push the entry of new handler into stack 
      3      push d fs:[eax]        # push the old entry into stack
      4      mov fs:[eax], esp      # now make fs:[0] points to the new _Exception_Registration record
      5      int 2dh                # interrupt -> CPU will jump to l1 
      6      inc eax                # EAX = 1, will be skipped (when debugger attached)
      7      je being_debugged      # if EAX=0, an debugger is there
      8          ...
      9  l1: xor al, al            # handler: set AL=0 (this is to return 0)
      10     ret                      

3.4 Сервис INT 2D

Давайте рассмотрим некоторые важные факты, связанные с INT 2D. Almeida предоставил отличную стать о сервисе INT 2D и отладке ядра. Мы рекомендуем внимательно прочитать ее [7].

INT 2D является интерфейсом для Win32, который предоставляет услуги отладки для пользовательских (т.е. уровня пользователя) и удаленных (remote) отладчиков таких как: IMM, KD и WinDbg. Отладчики уровня пользователя обычно используют следующий вызов службы:

NTSTATUS [B]DebugService[/B](UCHAR ServiceClass, PVOID arg1, PVOID arg2)

Согласно [7], существует четыре типа ServiceClass (1: Debug printing, 2: Interactive prompt, 3: Load Image, 4: Unload image). Сам вызов DebugService, по сути, транслируется в следующий машинный код:

  EAX <- ServiceClass
  ECX <- Arg1
  EDX <- Arg2
  INT 2d

Прерывание вызывает процессор, чтобы перейти на функцию KiDispatchException, которая позже вызовет KdpTrap (если во время загрузки Windows XP, в файле boot.ini включен режим DEBUG). KdpTrap принимает EXCEPTION_RECORD созданную функцией KiDispatchException. EXCEPTION_RECORD содержит следующую информацию: ExceptionCode: BREAKPOINT, arg0: EAX, arg1: ECX, и arg2: EDX. Обратите внимание, что согласно [7] (Раздел «Notifying Debugging Events»), прерывания INT 3 (программные брэйкпойнты) также обрабатываются KdpTrap, за исключением того случая, когда arg0 = 0.

Стоит заметить, что KiDispatchException заслуживает особого внимания. Nebbettt в своей книге [9] (стр. 439 – иногда есть возможность посмотреть примеры глав в Google books) приводит псевдокод реализации KiDispatchException (в Example D.1). Вам следует прочитать этот код, поскольку там есть несколько интересных моментов. Сначала, давайте сосредоточимся на случае, когда «previous mode» программы равен «kernel mode» (т.е. это код ядра вызывает прерывание).

  1. В строке 4, тела функции, KiDispatchException уменьшает EIP на 1 в случае, если Exception code равен STATUS_BREAKPOINT (это происходит, когда вызывается INT 2D или INT 3). Отметим, что P. Ferrie в [3] дал прекрасное объяснение относительно того, почему код уменьшает EIP на 1!
  2. Рассматриваемый код несколько раз вызывает KiDebugRoutine. KiDebugRoutine является указателем на функцию. Этот указатель указывает либо на KdpTrap (если в BOOT.ini установлен режим DEBUG), либо на KdpTrapStub (которая ничего не делает).
  3. Вначале вызывается KdpTrap/KiDebugRoutine, затем пользовательский обработчик (если «search frame» равен true), после чего несколько раз вызывается KiDebugRoutine, в случае, если пользовательский обработчик не закончит работу.

Для случая с «user mode» (т.е., когда программа пользователя вызывает INT 2D):

  1. Вначале проверяется отсутствие пользовательского отладчика (путем проверки DEBUG_PORT). Если его нет, отладочный сервис ядра KiDispatchException вызовет первый обработчик исключений.
  2. Затем, идет группа вложенных операторов if-else, которые используют DbgkForwardException для перенаправления исключений пользовательскому отладчику. (К сожалению, по этим функциям, используемым в этих операторах, нет достаточной документации). Мы предполагаем, что DbgkForwardException должен вызывать пользовательский отладчик для обработки исключения, а KiUserDipsatchException вызывается для того, чтобы найти пользовательские обработчики (frame based user handlers), в тех случаях, когда отладчик не смог их обработать.
  3. Если атрибут «Search Frames» равен false, пункты выше (т.е. 1 и 2) – пропускаются. В этом случае KiDispatchException перенаправляет исключения пользовательскому отладчику (пытается сделать это дважды) и если они не обрабатываются, завершает пользовательский процесс.

Давайте снова вернемся к статье Ferrie [3]. Следующее описание – сложно. Мы проверим его в нашем последующем эксперименте (в части II). Здесь «exception address» является «значением регистра EIP из контекста (EIP value of the context)» (который будет скопирован обратно в пользовательский процесс), а «EIP register value» – это реально значение регистра EIP из пользовательского процесса, в тот момент, когда происходит исключение.

Но более важным является следующее описание из [3]: Это должно произойти еще до вызова KiDispatchException.

Согласно [3], в виду выше указанного поведения обработки исключений в Win32, это может привести к расщеплению байта (byte scission). Когда пользовательский отладчик (например, OllyDbg) решает возобновить выполнение, используя значение регистра (EIP register value), его поведение будет отличаться от нормального выполнения (т.е. поведения без отладчика). Мы проверим это утверждение в наших последующих экспериментах. В заключение мы бы хотели обсудить следующие факторы наших экспериментов:

  1. Влияет ли как-то debug mode (включенный через boot.ini ) на поведение пользовательского отладчика?
  2. То, как были определены пользовательские обработчики, влияет ли как-то на их поведение? (How would user defined handlers affect the behavior?)
  3. И в заключение, корректно ли поведение IMM по отношению к коду по адресу 0x413BD5?

Мы рассмотрим их в наших экспериментах в Части II этой серии уроков.

© Translated by Prosper-H from r0 Crew

Ссылки

[1] Guiseppe Bonfa, “Step-by-Step Reverse Engineering Malware: ZeroAccess / Max++ / Smiscer Crimeware Rootkit”

[2] Tyler Shields, “Anti-Debugging - A Developer’s View”

[3] P. Ferrie, “Anti-Unpacker Tricks - Part Three”, Virus Bulletin Feb 2009

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

[5] Intel, “Intel 64 and IA-32 Architectures for Software Developers Manual (5 Volume)”

[6] Microsoft, “Lesson 8 - Interrupt and Exception Handling”, MSDNAA

[7] A. Almeida, “Kernel and Remote Debuggers”, Developer Fusions

[8] Wikipedia, “Win32 Thread Information Block”

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

2.Tyler Shields, “Anti-Debugging - A Developer’s View”

Эта тема сейчас недоступан. Вы бы не могли дать пример программы на высокоуровневом языке c try-catch для определения debug режима?

Название статьи осталось же, можно загуглить. Обновил неработающую ссылку.

Спасибо. Собственно в дополнение, нашел сокращенный вариант этой статьи http://www.microsoft.com/msj/0197/Exception/Exception.aspx на РСДН:
https://rsdn.ru/article/baseserv/except.xml

В Мах++ как я понял использует способ с UnhandledExceptionFilter

Я ещё не писал плагинов под дебаггеры, но верно ли то что в данном случае мы можем сделать плагин, который при обнаружении опкода CD 2D во время выполнения будет уменьшать регистр EIP на единицу, и это позволит обойти все такие обманки?

Да. Только делать это нужно во время обработки прерывания.