Аппаратные брейкпоинты в основном используются для отладки. В отличие от обычных точек останова, они не требуют модификации кода и более универсальны. Из-за этого они часто используются при отладке целей, которые используют тактику защиты от отладки. В этой статье подробно описывается внутренняя работа аппаратных брейкпоинтов в Windows, а также рассматриваются некоторые общие способы использования и методы обнаружения.
0.0 Предисловие
Исследование в этом блоге проводилось на 64-битной Windows 10 20H1. Возможно, что некоторые методы могут быть похожи на 32-битную Windows, однако это не является основной темой этого сообщения. Более того, эти методы, вероятно, будут сильно отличаться в других операционных системах, таких как Linux и OSX, из-за архитектурных различий.
1.0 Краткое руководство по регистрам отладки
Читатели, знакомые с регистрами отладки, могут сразу перейти к разделу 2.0.
Аппаратные брейкпоинты доступны как на x86, так и на x64. Они реализованы с помощью 8 регистров отладки, с именами от DR0 до DR7. Эти регистры имеют длину 32 и 64 бита на x86 и x64 соответственно. Расположение регистров на архитектуре x64 можно увидеть на рисунке ниже. Не волнуйтесь, если рисунок кажется запутанным, мы рассмотрим каждый регистр более подробно. Если вы хотите узнать больше о более тонких деталях регистров отладки, Intel SDM и AMD APM - отличные ресурсы.
1.3 DR6
Когда срабатывает аппаратная точка останова, статус отладки сохраняется в регистре отладки DR6. Вот почему этот регистр называется «Регистром состояния отладки». Он содержит биты для быстрой проверки, были ли запущены определенные события.
Биты с 0 по 3 устанавливаются в зависимости от того, какая аппаратная точка останова запускается. Это используется для быстрой проверки сработавшей точки останова.
Бит 13 называется BD и устанавливается, если текущее исключение запускается из-за доступа к регистру отладки. Бит GD должен быть активирован в DR7 для запуска этого типа исключения.
Бит 14 называется BS и устанавливается, если текущее исключение запускается из-за одиночного шага. Флаг TF должен быть включен в регистре EFLAGS для запуска этого типа исключения.
Бит 15 называется TS и устанавливается, если текущее исключение запускается из-за того, что текущая задача переключилась на задачу, для которой включен флаг отладочной ловушки.
Биты 8 и 9 называются LE и GE и представляют собой устаревшие функции, которые ничего не делают в современных процессорах. Эти биты использовались для указания процессору определить точную инструкцию, на которой возникла точка останова. Все условия точки останова на современных процессорах точны. Для совместимости со старым оборудованием рекомендуется всегда устанавливать оба бита в 1.
Бит 13 называется GD и очень интересен. Если этот бит включен, исключение отладки будет генерироваться всякий раз, когда инструкция пытается получить доступ к регистру отладки. Чтобы отличить этот тип исключения от обычного исключения аппаратной точки останова, в регистре отладки DR6 включен флаг BD. Этот бит обычно используется для предотвращения вмешательства программ в регистры отладки. Важно помнить, что исключение происходит до выполнения инструкции, и этот флаг автоматически удаляется процессором при вводе обработчика исключения отладки. Однако это решение не идеально, поскольку оно работает только с использованием инструкций MOV для доступа к регистру отладки. Они недоступны в пользовательском режиме, и, судя по моему тестированию, функции GetThreadContext и SetThreadContext не запускают это событие. Это делает невозможным использование этого обнаружения в пользовательском режиме.
Биты с 16 по 31 используются для управления условиями и размером каждой аппаратной точки останова. Каждый регистр имеет 4 бита, которые разделены на 2 2-битных поля. Первые 2 бита используются для определения типа аппаратной точки останова. Исключение отладки можно генерировать только при выполнении инструкции, записи данных, чтении и записи ввода-вывода, чтении и записи данных. Чтение и запись ввода-вывода разрешены только в том случае, если включено поле DE в регистре управления CR4, в противном случае это условие является неопределенным поведением. Размером можно управлять, используя последние 2 бита, и он используется для указания размера ячейки памяти по указанному адресу. Доступные размеры: 1 байт, 2 байта, 4 байта и 8 байтов.
1.5 Использование
Использование регистров отладки довольно просто. Существуют специальные инструкции для перемещения содержимого из регистра общего назначения в регистр отладки или наоборот. Однако эти инструкции могут быть выполнены только на уровне привилегий 0, в противном случае будет сгенерировано исключение #GP (0). Чтобы разрешить приложениям пользовательского режима изменять регистр отладки, Windows добавила поддержку изменения этих регистров с помощью SetThreadContext и GetThreadContext API. Пример использования этих функций показан в следующем фрагменте кода.
2.0 Windows и исключения
Теперь, когда мы знаем, как использовать аппаратные точки останова, пора посмотреть, как Windows справляется с ними.
Когда срабатывает аппаратная точка останова, независимо от причины запускается исключение #DB. Это соответствует прерыванию №1, что означает, что выполнение будет перенаправлено обработчику прерывания 1. Для получения дополнительной информации о том, как обрабатываются исключения, я рекомендую прочитать это сообщение, написанное Daax.
В Windows каждый обработчик прерывания инициализируется во время загрузки. Как именно это будет сделано, пока не важно. Каждый обработчик прерывания можно найти в таблице
Если исключение пришло из режима ядра, будет вызвано исключение
Вернувшись в
Помните тот адрес
4.0 Общие векторы обнаружения
В последнем разделе мы рассмотрим некоторые общие векторы обнаружения аппаратных точек останова. Для простоты к примерам не будут применяться какие-либо методы запутывания, и это оставлено в качестве упражнения для читателя. Вы можете быть настолько безумным, насколько хотите.
Это обнаружение очень легко реализовать, но его также легко обойти. Например, злоумышленник может просто перехватить
4.2 Обработчик исключений
Альтернативный способ получения структуры CONTEXT, включая регистры отладки, - это регистрация обработчика исключений. Первый и единственный аргумент в обработчике исключений VEH - это указатель на структуру EXCEPTION_POINTERS. Эта структура содержит информацию о текущем исключении, а также указатель на структуру CONTEXT. Оттуда мы можем легко проверить, заполнен ли какой-либо из регистров отладки. Есть несколько способов реализовать это обнаружение, самый простой - использовать
Определенные исключения отладки могут очищать биты 0–3. Оставшееся содержимое регистра DR6 никогда не очищается процессором. Чтобы избежать путаницы при идентификации исключений отладки, обработчики отладки должны очистить регистр (кроме бита 16, который они должны установить) перед возвратом к прерванной задаче.
Если вы уверены, что программа не использует аппаратные точки останова, можно проверить значение DR6, используя любой из ранее упомянутых методов, поскольку злоумышленник мог не очистить регистр.
4.5 Использование всех регистров отладки
Один из самых простых способов - просто использовать все доступные регистры отладки для себя. Этот метод ограничен только вашим творчеством и позволяет как обнаруживать, так и давать сбой при использовании оборудования. Простая реализация этого метода - установить все аппаратные точки останова на важных функциях. После вызова точки останова вы манипулируете некоторыми данными, прежде чем вернуться к исходной функции. Если злоумышленник перезапишет любой из регистров отладки, манипуляции с данными не произойдет, и программа выйдет из строя. Пример ниже изменяет сборку и восстанавливает ее прямо перед выполнением. Это можно повторить для всех 4 регистров отладки, поэтому удаление одного из них приведет к сбою программы.
Заключение
Некоторые люди, чьи предыдущие исследования мне очень помогли, перечислены в произвольном порядке.
Intel Software Developer Manual
AMD Architecture Programmer’s Manual
Applied Reverse Engineering: Exceptions and Interrupts
Detecting debuggers by abusing a bad assumption within Windows
ByePg: Defeating Patchguard using Exception-hooking
От ТС
Давно хотел перевести эту статью, но собрался только сейчас.
В последнее время я много работаю (потому что хочется кушать), и много времени уделять статьям и переводам не получается.
По возможности буду делать столько, сколько успеваю.
Спасибо всем, кто хвалит мои статьи и переводы. Отдельное спасибо команде форума, и администратору.
Перевод:
Azrv3l cпециально для xss.pro
0.0 Предисловие
Исследование в этом блоге проводилось на 64-битной Windows 10 20H1. Возможно, что некоторые методы могут быть похожи на 32-битную Windows, однако это не является основной темой этого сообщения. Более того, эти методы, вероятно, будут сильно отличаться в других операционных системах, таких как Linux и OSX, из-за архитектурных различий.
1.0 Краткое руководство по регистрам отладки
Читатели, знакомые с регистрами отладки, могут сразу перейти к разделу 2.0.
Аппаратные брейкпоинты доступны как на x86, так и на x64. Они реализованы с помощью 8 регистров отладки, с именами от DR0 до DR7. Эти регистры имеют длину 32 и 64 бита на x86 и x64 соответственно. Расположение регистров на архитектуре x64 можно увидеть на рисунке ниже. Не волнуйтесь, если рисунок кажется запутанным, мы рассмотрим каждый регистр более подробно. Если вы хотите узнать больше о более тонких деталях регистров отладки, Intel SDM и AMD APM - отличные ресурсы.
1.1 DR0 - DR3
От DR0 до DR3 упоминаются как «Регистры адреса отладки» или «Регистры адреса точки останова». Они очень просты, поскольку содержат только линейный адрес точки останова. Когда этот адрес совпадает с инструкцией или ссылкой на данные, возникает остановка. Регистр отладки DR7 можно использовать для более детального управления условиями каждой точки останова. Поскольку регистры должны быть заполнены линейным адресом, они будут работать, даже если пейджинг отключён. В этом случае линейный адрес будет таким же, как физический адрес.1.2 DR4 - DR5
DR4 и DR5 и «Зарезервированные регистры отладки». Несмотря на то, что можно предположить из их названия, они не всегда зарезервированы и могут использоваться. Их функциональность зависит от значения поля DE в регистре управления CR4. Когда этот бит включен, точки останова ввода-вывода включены, и попытка доступа к одному из регистров приводит к генерации исключения #UD. Однако, когда бит DE не активирован, регистры отладки DR4 и DR5 отображаются в DR6 и DR7 соответственно. Это сделано для совместимости с ПО для старых процессоров.1.3 DR6
Когда срабатывает аппаратная точка останова, статус отладки сохраняется в регистре отладки DR6. Вот почему этот регистр называется «Регистром состояния отладки». Он содержит биты для быстрой проверки, были ли запущены определенные события.
Биты с 0 по 3 устанавливаются в зависимости от того, какая аппаратная точка останова запускается. Это используется для быстрой проверки сработавшей точки останова.
Бит 13 называется BD и устанавливается, если текущее исключение запускается из-за доступа к регистру отладки. Бит GD должен быть активирован в DR7 для запуска этого типа исключения.
Бит 14 называется BS и устанавливается, если текущее исключение запускается из-за одиночного шага. Флаг TF должен быть включен в регистре EFLAGS для запуска этого типа исключения.
Бит 15 называется TS и устанавливается, если текущее исключение запускается из-за того, что текущая задача переключилась на задачу, для которой включен флаг отладочной ловушки.
1.4 DR7
DR7 называется «Регистром управления отладкой» и позволяет детально контролировать каждую точку останова. Первые 8 битов определяют, включена ли конкретная аппаратная точка останова. Четные биты (0, 2, 4 и 6), называемые L0 - L3, включают точку останова локально, то есть она срабатывает только при обнаружении исключения точки останова в текущей задаче. Нечетные биты (1, 3, 5, 7), называемые G0 - G3, активируют точку останова глобально, то есть она срабатывает при обнаружении исключения точки останова в любой задаче. Когда точка останова включена локально, соответствующие биты удаляются при переключении аппаратной задачи, чтобы избежать нежелательных точек останова в новой задаче. Биты не сбрасываются, когда он включен глобально.Биты 8 и 9 называются LE и GE и представляют собой устаревшие функции, которые ничего не делают в современных процессорах. Эти биты использовались для указания процессору определить точную инструкцию, на которой возникла точка останова. Все условия точки останова на современных процессорах точны. Для совместимости со старым оборудованием рекомендуется всегда устанавливать оба бита в 1.
Бит 13 называется GD и очень интересен. Если этот бит включен, исключение отладки будет генерироваться всякий раз, когда инструкция пытается получить доступ к регистру отладки. Чтобы отличить этот тип исключения от обычного исключения аппаратной точки останова, в регистре отладки DR6 включен флаг BD. Этот бит обычно используется для предотвращения вмешательства программ в регистры отладки. Важно помнить, что исключение происходит до выполнения инструкции, и этот флаг автоматически удаляется процессором при вводе обработчика исключения отладки. Однако это решение не идеально, поскольку оно работает только с использованием инструкций MOV для доступа к регистру отладки. Они недоступны в пользовательском режиме, и, судя по моему тестированию, функции GetThreadContext и SetThreadContext не запускают это событие. Это делает невозможным использование этого обнаружения в пользовательском режиме.
Биты с 16 по 31 используются для управления условиями и размером каждой аппаратной точки останова. Каждый регистр имеет 4 бита, которые разделены на 2 2-битных поля. Первые 2 бита используются для определения типа аппаратной точки останова. Исключение отладки можно генерировать только при выполнении инструкции, записи данных, чтении и записи ввода-вывода, чтении и записи данных. Чтение и запись ввода-вывода разрешены только в том случае, если включено поле DE в регистре управления CR4, в противном случае это условие является неопределенным поведением. Размером можно управлять, используя последние 2 бита, и он используется для указания размера ячейки памяти по указанному адресу. Доступные размеры: 1 байт, 2 байта, 4 байта и 8 байтов.
1.5 Использование
Использование регистров отладки довольно просто. Существуют специальные инструкции для перемещения содержимого из регистра общего назначения в регистр отладки или наоборот. Однако эти инструкции могут быть выполнены только на уровне привилегий 0, в противном случае будет сгенерировано исключение #GP (0). Чтобы разрешить приложениям пользовательского режима изменять регистр отладки, Windows добавила поддержку изменения этих регистров с помощью SetThreadContext и GetThreadContext API. Пример использования этих функций показан в следующем фрагменте кода.
C++:
/* Инициализация контекстной структуры */
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_ALL;
/* Заполнить контекстную структуру, контекстом текущего потока */
GetThreadContext(GetCurrentThread(), &context);
/* Установить локальную 1-байтовую аппаратную точку останова на test_func */
context.Dr0 = (DWORD64)&test_func;
context.Dr7 = 1 << 0;
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
/* Установить контекст */
SetThreadContext(GetCurrentThread(), &context);
2.0 Windows и исключения
Теперь, когда мы знаем, как использовать аппаратные точки останова, пора посмотреть, как Windows справляется с ними.
Когда срабатывает аппаратная точка останова, независимо от причины запускается исключение #DB. Это соответствует прерыванию №1, что означает, что выполнение будет перенаправлено обработчику прерывания 1. Для получения дополнительной информации о том, как обрабатываются исключения, я рекомендую прочитать это сообщение, написанное Daax.
В Windows каждый обработчик прерывания инициализируется во время загрузки. Как именно это будет сделано, пока не важно. Каждый обработчик прерывания можно найти в таблице
KiInterruptInitTable в ntoskrnl.exe. Это показывает нам, что KiDebugTrapOrFault является обработчиком прерывания для прерывания №1. Вторую функцию каждой записи пока можно игнорировать, она связана с смягчением последствий Meltdown, которое было добавлено в Windows.KiDebugTrapOrFault начинает с выполнения некоторых проверок работоспособности, чтобы убедиться в правильности GS. Эти проверки были добавлены для смягчения последствий CVE-2018-88974. Если все верно, вызывается KxDebugTrapOrFault. Эта функция эквивалентна KiDebugTrapOrFault до добавления защиты. Функция начинается с сохранения определенных регистров в TrapFrame. Остальная часть функции не очень полезна для нас, но она проверяет некоторые вещи, такие как SMAP. В конце функции вызывается KiExceptionDispatch.KiExceptionDispatch немного интереснее предыдущих функций. Он начинается с выделения ExceptionFrame в стеке и его заполнения. После этого он сохраняет несколько энергонезависимых регистров. Как только это будет сделано, функция создаст ExceptionRecord и заполнит его информацией о текущем исключении. После этого вызывается KiDispatchException.
Код:
.text:00000001403EF940 KiExceptionDispatch proc near
.text:00000001403EF940
.text:00000001403EF940 ExceptionFrame = _KEXCEPTION_FRAME ptr -1D8h
.text:00000001403EF940 ExceptionRecord = _EXCEPTION_RECORD ptr -98h
.text:00000001403EF940
.text:00000001403EF940 sub rsp, 1D8h
.text:00000001403EF947 lea rax, [rsp+1D8h+ExceptionFrame._Rbx]
.text:00000001403EF94F movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm6.Low], xmm6
.text:00000001403EF954 movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm7.Low], xmm7
.text:00000001403EF959 movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm8.Low], xmm8
.text:00000001403EF95F movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm9.Low], xmm9
.text:00000001403EF965 movaps xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm10.Low], xmm10
.text:00000001403EF96B movaps xmmword ptr [rax-80h], xmm11
.text:00000001403EF970 movaps xmmword ptr [rax-70h], xmm12
.text:00000001403EF975 movaps xmmword ptr [rax-60h], xmm13
.text:00000001403EF97A movaps xmmword ptr [rax-50h], xmm14
.text:00000001403EF97F movaps xmmword ptr [rax-40h], xmm15
.text:00000001403EF984 mov [rax], rbx
.text:00000001403EF987 mov [rax+8], rdi
.text:00000001403EF98B mov [rax+10h], rsi
.text:00000001403EF98F mov [rax+18h], r12
.text:00000001403EF993 mov [rax+20h], r13
.text:00000001403EF997 mov [rax+28h], r14
.text:00000001403EF99B mov [rax+30h], r15
[...]
.text:00000001403EF9BD lea rax, [rsp+1D8h+ExceptionFrame.Return]
.text:00000001403EF9C5 mov [rax], ecx
.text:00000001403EF9C7 xor ecx, ecx
.text:00000001403EF9C9 mov [rax+4], ecx
.text:00000001403EF9CC mov [rax+8], rcx
.text:00000001403EF9D0 mov [rax+10h], r8
.text:00000001403EF9D4 mov [rax+18h], edx
.text:00000001403EF9D7 mov [rax+20h], r9
.text:00000001403EF9DB mov [rax+28h], r10
.text:00000001403EF9DF mov [rax+30h], r11
.text:00000001403EF9E3 mov r9b, [rbp+0F0h]
.text:00000001403EF9EA and r9b, 1 ; PreviousMode
.text:00000001403EF9EE mov byte ptr [rsp+1D8h+ExceptionFrame.P5], 1 ; FirstChance
.text:00000001403EF9F3 lea r8, [rbp-80h] ; TrapFrame
.text:00000001403EF9F7 mov rdx, rsp ; ExceptionFrame
.text:00000001403EF9FA mov rcx, rax ; ExceptionRecord
[...]
.text:00000001403EFA67 SkipExceptionStack:
.text:00000001403EFA67 call KiDispatchException
KiDispatchException - это довольно длинная функция, в которой исключение наконец отправляется обработчику исключений. Ну, почти. Короче говоря, эта функция применит некоторые преобразования к коду исключения, объединит TrapFrame и ExceptionFrame в ContextRecord и предварительно обработает исключение, вызвав KiPreprocessFault. Что происходит здесь, зависит от того, произошло ли исключение из режима пользователя или режима ядра. В обоих случаях это позволит отладчику обработать это как первый и второй шанс.Если исключение пришло из режима ядра, будет вызвано исключение
RtlDispatchException, которое будет искать любые обработчики SEH и вызывать их. Если он не может найти обработчик SEH или если исключение не обрабатывается правильно, система выполнит проверку ошибок, вызвав KeBugCheckEx. Если исключение возникло из пользовательского режима, некоторые поля в TrapFrame будут исправлены, например указатель стека. Наконец, указатель инструкции в TrapFrame будет перезаписан адресом KeUserExceptionDispatcher. Мы скоро узнаем, что делает эта функция. ExceptionRecord и ContextRecord копируются в стек пользователя, и функция вернется.Вернувшись в
KiExceptionDispatch, мы просто очистим стек, восстановим изменчивое состояние, которое мы сохранили ранее, и вернемся в пользовательский режим с помощью iretq. Поскольку мы ранее перезаписали стек пользователя, поток выполнения возобновляется из KeUserExceptionDispatcher.
Код:
.text:00000001403EFA6C lea rcx, [rsp+1D8h+ExceptionFrame._Rbx] ; rcx = _KTRAP_FRAME
.text:00000001403EFA74 movaps xmm6, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm6.Low]
.text:00000001403EFA79 movaps xmm7, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm7.Low]
.text:00000001403EFA7E movaps xmm8, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm8.Low]
.text:00000001403EFA84 movaps xmm9, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm9.Low]
.text:00000001403EFA8A movaps xmm10, xmmword ptr [rsp+1D8h+ExceptionFrame._Xmm10.Low]
.text:00000001403EFA90 movaps xmm11, xmmword ptr [rcx-80h]
.text:00000001403EFA95 movaps xmm12, xmmword ptr [rcx-70h]
.text:00000001403EFA9A movaps xmm13, xmmword ptr [rcx-60h]
.text:00000001403EFA9F movaps xmm14, xmmword ptr [rcx-50h]
.text:00000001403EFAA4 movaps xmm15, xmmword ptr [rcx-40h]
.text:00000001403EFAA9 mov rbx, [rcx]
.text:00000001403EFAAC mov rdi, [rcx+8]
.text:00000001403EFAB0 mov rsi, [rcx+10h]
.text:00000001403EFAB4 mov r12, [rcx+18h]
.text:00000001403EFAB8 mov r13, [rcx+20h]
.text:00000001403EFABC mov r14, [rcx+28h]
.text:00000001403EFAC0 mov r15, [rcx+30h]
[...]
.text:00000001403EFBEC mov rdx, [rbp-40h]
.text:00000001403EFBF0 mov rcx, [rbp-48h]
.text:00000001403EFBF4 mov rax, [rbp-50h]
.text:00000001403EFBF8 mov rsp, rbp
.text:00000001403EFBFB mov rbp, [rbp+0D8h]
.text:00000001403EFC02 add rsp, 0E8h
[...]
.text:00000001403EFC17 swapgs
.text:00000001403EFC1A iretq
Помните тот адрес
KeUserExceptionDispatcher, который мы установили ранее? На самом деле это KiUserExceptionDispatcher, который находится в ntdll.dll. Эта функция отвечает за обработку исключений в пользовательском режиме. Он получит ExceptionRecord и Context из исключения и передаст выполнение в RtlDispatchException. Я не буду вдаваться в подробности здесь, но в конечном итоге он проверит обработчики исключений SEH и VEH и вызовет их, если они есть.
Код:
.text:000000018009EBF0 KiUserExceptionDispatcher proc near
.text:000000018009EBF0 cld
.text:000000018009EBF1 mov rax, cs:Wow64PrepareForException
.text:000000018009EBF8 test rax, rax
.text:000000018009EBFB jz short loc_18009EC0C
.text:000000018009EBFD mov rcx, rsp
.text:000000018009EC00 add rcx, 4F0h
.text:000000018009EC07 mov rdx, rsp
.text:000000018009EC0A call rax ; Wow64PrepareForException
.text:000000018009EC0C
.text:000000018009EC0C loc_18009EC0C:
.text:000000018009EC0C mov rcx, rsp
.text:000000018009EC0F add rcx, 4F0h
.text:000000018009EC16 mov rdx, rsp
.text:000000018009EC19 call RtlDispatchException
.text:000000018009EC1E test al, al
.text:000000018009EC20 jz short loc_18009EC2E
.text:000000018009EC22 mov rcx, rsp
.text:000000018009EC25 xor edx, edx
.text:000000018009EC27 call RtlGuardRestoreContext
.text:000000018009EC2C jmp short loc_18009EC43
.text:000000018009EC2E ; ---------------------------------------------------------------------------
.text:000000018009EC2E
.text:000000018009EC2E loc_18009EC2E:
.text:000000018009EC2E mov rcx, rsp
.text:000000018009EC31 add rcx, 4F0h
.text:000000018009EC38 mov rdx, rsp
.text:000000018009EC3B xor r8b, r8b
.text:000000018009EC3E call ZwRaiseException
.text:000000018009EC43
.text:000000018009EC43 loc_18009EC43:
.text:000000018009EC43 mov ecx, eax
.text:000000018009EC45 call RtlRaiseStatus
.text:000000018009EC45 KiUserExceptionDispatcher endp
3.0 (Вредоносное) Использование
3.1 Отладка
Как упоминается в их названии, регистры отладки в основном используются для целей отладки. В то время как обычные точки останова требуют редактирования сборки для добавления инструкции точки останова, аппаратные точки останова можно использовать без изменения какой-либо сборки. Это особенно полезно при работе с самомодифицирующимся кодом или проверками целостности.3.2 Malware
Благодаря скрытому использованию и встроенным средствам управления безопасностью (см. DR7, бит 13) они также являются любимым инструментом авторов вредоносных программ, особенно руткитов. Они позволяют вредоносному ПО незаметно перехватить функцию. Это можно использовать для перехвата важных системных процедур, таких какKiSystemCall64 в Windows или do_debug в Linux5.3.3 Читинг
Конечно, эти методы также используются читами, которые хотят оставаться скрытыми от античитов. Регистры отладки можно использовать для перехвата важных игровых функций и реализации пользовательской логики. Хорошим примером этого является ловушка Outlines VEH, выпущенная EBFE для Overwatch. Регистр отладки помещается в функцию, отвечающую за рисование контуров проигрывателя, а обработчик исключений регистрируется с помощьюAddVectoredExceptionHandler. Когда игра вызывает функцию outlines, аппаратная точка останова срабатывает и перенаправляет поток управления зарегистрированному обработчику исключений. Здесь он проверяет, исходит ли исключение из функции контуров, и редактирует некоторые данные, чтобы игра рисовала контур для всех игроков. Похоже, эта техника довольно хороша, поскольку Blizzard, похоже, не может ее обнаружить.4.0 Общие векторы обнаружения
В последнем разделе мы рассмотрим некоторые общие векторы обнаружения аппаратных точек останова. Для простоты к примерам не будут применяться какие-либо методы запутывания, и это оставлено в качестве упражнения для читателя. Вы можете быть настолько безумным, насколько хотите.
4.1 GetThreadContext
Один из простейших способов обнаружения аппаратных точек останова - использование WinAPI GetThreadContext. Эта функция просто возвращает структуру CONTEXT для данного потока. Эта структура включает значение каждого регистра отладки, что позволяет нам легко проверить, заполнен ли какой-либо из регистров.Это обнаружение очень легко реализовать, но его также легко обойти. Например, злоумышленник может просто перехватить
GetThreadContext, чтобы вернуть фальшивую структуру с удаленными полями регистра отладки.
C++:
/* Подготовка контекстной структуры */
CONTEXT context = { 0 };
/* CONTEXT_ALL заполнит все поля в структуре, это можно изменить в зависимости от ваших потребностей. */
context.ContextFlags = CONTEXT_ALL;
/* Вызов GetThreadContext с текущим потоком */
BOOL result = GetThreadContext(GetCurrentThread(), &context);
if (!result)
{
/* GetThreadContext failed, use GetLastError to find out why */
return;
}
/* Проверка каждого поля регистра отладки */
if (context.Dr0 != 0 /* ... */)
{
/* Debug register detected */
}
4.2 Обработчик исключений
Альтернативный способ получения структуры CONTEXT, включая регистры отладки, - это регистрация обработчика исключений. Первый и единственный аргумент в обработчике исключений VEH - это указатель на структуру EXCEPTION_POINTERS. Эта структура содержит информацию о текущем исключении, а также указатель на структуру CONTEXT. Оттуда мы можем легко проверить, заполнен ли какой-либо из регистров отладки. Есть несколько способов реализовать это обнаружение, самый простой - использовать
AddVectoredExceptionHandler и RaiseException.
C++:
/* Наш обработчик исключений */
long debug_veh(struct _EXCEPTION_POINTERS* ExceptionInfo)
{
/* Проверка исключения */
if (ExceptionInfo->ExceptionRecord->ExceptionCode == 0x1337)
{
/* Проверьте каждое поле регистра отладки */
if (ExceptionInfo->ContextRecord->Dr0 != 0 /* ... */)
{
/* Обнаружен регистр отладки */
}
/* Исправляет ошибку деления на ноль (см. Ниже). Второй аргумент должен быть сохранен в rcx, просто измените его на 100/10, прежде чем продолжить. */
/* ExceptionInfo->ContextRecord->Rcx = 10; */
/* Исключение обработано, мы можем продолжить нормальное выполнение */
return EXCEPTION_CONTINUE_EXECUTION;
}
/* Попробуйте следующий обработчик исключения, если это не то исключение */
return EXCEPTION_CONTINUE_SEARCH;
}
[...]
/* Где-нибудь в функции инициализации зарегистрируйте наш обработчик исключений */
AddVectoredExceptionHandler(1, debug_veh);
[...]
/* Обнаружение может быть запущено, когда вы захотите, вызвав исключение */
RaiseException(0x1337, 0, 0, nullptr);
/* В качестве альтернативы, если вышеуказанное не работает должным образом, просто активируйте ошибку деления на ноль.
Обязательно измените код исключения и исправьте ошибку (см. Выше) */
volatile int b = 0;
volatile int a = 100 / b;
4.3 MOV DRx инструкции
Это обнаружение возможно только при выполнении в режиме ядра, так как используемые инструкции MOV нигде не доступны. Используя __readdr и __writedr, можно напрямую управлять содержимым регистров отладки. Мы можем использовать эти встроенные функции, чтобы проверить, установлен ли какой-либо из регистров отладки. Важно помнить, что злоумышленник мог включить общий бит обнаружения в DR7. Это приводит к генерации исключения #DB каждый раз при доступе к регистру отладки. Это можно использовать для быстрой очистки регистров, когда вы пытаетесь их проверить.
C++:
/* Проверьте каждое поле регистра отладки */
if (__readdr(0) != 0)
{
/* Обнаружен регистр отладки */
}
4.4 Проверка DR6
Когда срабатывает аппаратная точка останова, DR6 заполняется информацией о событии. Это можно использовать для принятия более обоснованных решений в текущей ситуации. Важно отметить, что DR6 не очищается автоматически после обработки аппаратной точки останова. Следующий абзац Intel SDM описывает это более подробно.Определенные исключения отладки могут очищать биты 0–3. Оставшееся содержимое регистра DR6 никогда не очищается процессором. Чтобы избежать путаницы при идентификации исключений отладки, обработчики отладки должны очистить регистр (кроме бита 16, который они должны установить) перед возвратом к прерванной задаче.
Если вы уверены, что программа не использует аппаратные точки останова, можно проверить значение DR6, используя любой из ранее упомянутых методов, поскольку злоумышленник мог не очистить регистр.
4.5 Использование всех регистров отладки
Один из самых простых способов - просто использовать все доступные регистры отладки для себя. Этот метод ограничен только вашим творчеством и позволяет как обнаруживать, так и давать сбой при использовании оборудования. Простая реализация этого метода - установить все аппаратные точки останова на важных функциях. После вызова точки останова вы манипулируете некоторыми данными, прежде чем вернуться к исходной функции. Если злоумышленник перезапишет любой из регистров отладки, манипуляции с данными не произойдет, и программа выйдет из строя. Пример ниже изменяет сборку и восстанавливает ее прямо перед выполнением. Это можно повторить для всех 4 регистров отладки, поэтому удаление одного из них приведет к сбою программы.
C++:
/* Измените права доступа к странице на RWX, чтобы мы могли изменить ассемблер */
DWORD old_protect = 0;
BOOL result = VirtualProtect((void*)test_func, 0x1000, PAGE_EXECUTE_READWRITE, &old_protect);
if (!result)
{
/* Ошибка VirtualProtect, вызовите GetLastError, чтобы узнать, почему */
return;
}
/* Заменяем ассемблер мусором */
*(byte*)test_func ^= 0x42;
/* Зарегистрируйте наш VEH */
AddVectoredExceptionHandler(1, debug_veh);
/* Установите аппаратную точку останова для нашей функции */
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_ALL;
GetThreadContext(GetCurrentThread(), &context);
context.Dr0 = (DWORD64)test_func;
context.Dr7 = 1 << 0;
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(GetCurrentThread(), &context);
[...]
long debug_veh(struct _EXCEPTION_POINTERS* ExceptionInfo)
{
/* Проверьте, исходит ли исключение от нас */
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
{
/* Восстановите ассемблер перед его выполнением.
Мы не превращаем его обратно в мусор, поэтому при последующих вызовах произойдет сбой.
Это может быть достигнуто во второй аппаратной точке останова. */
*(byte*)test_func ^= 0x42;
/* Установите флаг возобновления (RF), чтобы мы не застряли в бесконечном цикле */
ExceptionInfo->ContextRecord->EFlags |= 0x10000;
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
Заключение
Некоторые люди, чьи предыдущие исследования мне очень помогли, перечислены в произвольном порядке.
- Derek Rynd (@daax_rynd)
- Can Bölük (@_can1357)
- Nemi (@0xNemi)
Intel Software Developer Manual
AMD Architecture Programmer’s Manual
Applied Reverse Engineering: Exceptions and Interrupts
Detecting debuggers by abusing a bad assumption within Windows
ByePg: Defeating Patchguard using Exception-hooking
От ТС
Давно хотел перевести эту статью, но собрался только сейчас.
В последнее время я много работаю (потому что хочется кушать), и много времени уделять статьям и переводам не получается.
По возможности буду делать столько, сколько успеваю.
Спасибо всем, кто хвалит мои статьи и переводы. Отдельное спасибо команде форума, и администратору.
Перевод:
Azrv3l cпециально для xss.pro