Содержание статьи
Точки останова служат для контроля выполнения программы и, конечно же, их остановки в определенный момент. Глобально существует два вида брейк‑пойнтов: software breakpoints и hardware breakpoints.
Software breakpoint — точка останова, которая ставится с помощью отладчика или IDE. Чтобы поставить такую точку останова, можно, например, просто кликнуть на нужную строку программы в Visual Studio.
Такие точки останова можно ставить где угодно и сколько угодно. Никаких ограничений нет.
Hardware breakpoint — уже более сложная штука, которую мы сегодня и будем изучать. Эти бряки ставятся путем заполнения специальных отладочных регистров процессора (DR0–DR7). Согласно документации, Dr0–3 должны хранить адрес, по которому установлен breakpoint, но у меня бряк срабатывал, только если адрес заполнялся в DR0.
Первые три регистра называются регистрами с отладочными адресами (Debug Address Registers). Регистры с номерами 4 и 5 не используются и называются зарезервированными отладочными регистрами (Reserved Debug Registers). DR6 содержит различную информацию о сработавшем исключении. Исключение — это событие, возникающее, когда компьютер пытается выполнить инструкцию, адрес которой расположен в DR0. DR7 содержит биты управления отладкой. Если значение равно единице, то точка останова должна сработать, если нулю, то не должна.
Hardware breakpoints, как ты понимаешь, через красивый GUI не ставятся. Нам потребуется взаимодействовать с регистрами напрямую, используя, конечно же, наш любимый WinAPI. И само собой, только хардверные брейки позволят хукать, обходить AMSI и получать сисколы. Софтверные, к сожалению, для этого не подходят.
Обработка исключений
Итак, исключение возникает при попытке выполнить инструкцию, на которой стоит точка останова. По своей натуре оно при этом точно такое же, как, к примеру, при попытке деления на ноль.
Любые исключения могут быть обработаны. Здесь есть два пути — VEH (Vectored Exception Handling) и SEH (Structured Exception Handling). Отдельно я выделю еще UEH (Unhandled Exception Handling). Начнем с SEH. SEH — стандартный блок __try
, __try
.
#include <iostream>#include <Windows.h>int main() { int a = 2 - 2; int b = 3; __try { std::cout << b / a << std::endl; } __except (EXCEPTION_EXECUTE_HANDLER) { std::cout << "EXCEPTION" << std::endl; } return 0;}
SEH можно считать надстройкой над конструкцией try — except из С++. В SEH в блок __except
добавляются специальные значения, в зависимости от которых может меняться поведение обработчика исключений:
-
EXCEPTION_EXECUTE_HANDLER
— система передает управление в обработчик исключения. То есть будет поведение, как в коде выше; -
EXCEPTION_CONTINUE_SEARCH
— эта конструкция заставляет систему перейти к предыдущему блокуtry
, которому соответствует блокexcept
, и обработать этот блок. То есть система игнорирует текущий обработчик исключений и пытается найти обработчик исключений в охватывающем блоке (или блоках); -
EXCEPTION_CONTINUE_EXECUTION
— обнаружив такое значение, система возвращается к инструкции, вызвавшей исключение, и пытается выполнить ее снова.
Ниже — пример EXCEPTION_CONTINUE_EXECUTION
.
#include <iostream>#include <cstddef>#include <Windows.h>char g_szBuffer[100];LONG Filter(char** ppchBuffer) { if (*ppchBuffer == NULL) { *ppchBuffer = g_szBuffer; return(EXCEPTION_CONTINUE_EXECUTION); } return(EXCEPTION_EXECUTE_HANDLER);}int main() { int x = 0; char* pchBuffer = NULL; __try { *pchBuffer = 'J'; x = 5 / x; } __except (Filter(&pchBuffer)) { MessageBox(NULL, L"An exception occurred", NULL, MB_OK); } MessageBox(NULL, L"Function completed", NULL, MB_OK); return 0;}
Программы могут быть сложные, страшные, большие, нужно предусматривать корректный выход из всех блоков, изучать возможные исключения. Вдруг потребуется функция уведомления пользователя о сработавшем исключении? В общем, SEH хорош, но, помимо него, появился и VEH. VEH можно считать эдакой надстройкой над SEH. Работает она, само собой, только в Windows.
Если в программе возникает исключение, то первыми вызываются именно векторные обработчики и лишь затем система начнет разворачивать стек. С помощью VEH прога может, например, зарегистрировать функцию для просмотра или обработки всех исключений приложения. Причем в программу можно добавить несколько VEH-обработчиков, и они будут вызваны в том порядке, в котором были добавлены. Первый — первым, второй — вторым и так далее. SEH после VEH вызывается только в том случае, если VEH вернул EXCEPTION_CONTINUE_SEARCH
.
С помощью VEH можно ловить исключения, возникающие при хардверных брейках. Добавить обработчик можно с помощью функции AddVectoredExceptionHandler().
PVOID AddVectoredExceptionHandler( ULONG FirstHandler, PVECTORED_EXCEPTION_HANDLER VectoredHandler)
-
FirstHandler
— вызывать обработчик раньше всех ранее зарегистрированных обработчиков (значениеCALL_FIRST
) или после всех (значениеCALL_LAST
); -
VectoredHandler
— адрес функции обработчика. Эта функция должна возвращатьEXCEPTION_CONTINUE_EXECUTION
. Обработчики далее не выполняются, обработка средствами SEH не производится, управление передается в ту точку программы, из которой было вызвано исключение илиEXCEPTION_CONTINUE_SEARCH
(выполняется следующий векторный обработчик, а если таких нет, то разворачивается SEH).
Зарегистрируем обработчик и проверим работу VEH. Исключением пока будет стандартный Null-Pointer
. То есть обращение к указателю, который имеет значение nullptr
.
#include <iostream>#include <windows.h>#include <errhandlingapi.h>LONG WINAPI MyVectoredExceptionHandler(PEXCEPTION_POINTERS exceptionInfo){ std::cout << "Exception occurred!" << std::endl; std::cout << "Exception Code: " << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl; std::cout << "Exception Address: " << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl; return EXCEPTION_CONTINUE_SEARCH;}int main(){ if (AddVectoredExceptionHandler(1, MyVectoredExceptionHandler) == nullptr) { std::cout << "Failed to add the exception handler!" << std::endl; return 1; } int* p = nullptr; *p = 42; // Исключение возникает тут return 0;}
Видим, что обработчик успешно срабатывает и вызывается, затем возвращает EXCEPTION_CONTINUE_SEARCH
. Это, в свою очередь, дергает SEH, SEH в программе нет, поэтому Visual Studio включается и выдает нам исключение. Если будем возвращать EXCEPTION_CONTINUE_EXECTION
, то получим бесконечный вызов обработчика, так как каждый раз будет срабатывать строка *p
.
Точно такое же исключение будет срабатывать и при хардверных бряках.
Наконец, последний тип обработчиков — Unhandled Exception Filter. Он редко когда используется, но изначально задумывался как обработчик для исключений, которые вообще никто не обрабатывает. Ни VEH (если отсутствует или вернул EXCEPTION_CONTINUE_SEARCH
), ни SEH (если тоже отсутствует или указано EXCEPTION_CONTINUE_SEARCH
). Устанавливаются такие обработчики через функцию SetUnhandledExceptionFilter().
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( [in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter);
Функция принимает один‑единственный параметр — адрес функции‑обработчика, которая должна вызываться при возникновении необработанного исключения. С помощью UEF также ловятся исключения, возникающие при бряках.
Возьмем прошлый код и переделаем его под UEF.
#include <iostream>#include <windows.h>LONG WINAPI MyUnhandledExceptionHandler(PEXCEPTION_POINTERS exceptionInfo){ std::cout << "Unhandled exception occurred!" << std::endl; std::cout << "Exception Code: " << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl; std::cout << "Exception Address: " << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl; return EXCEPTION_CONTINUE_SEARCH;}int main(){ if (SetUnhandledExceptionFilter(MyUnhandledExceptionHandler) == nullptr) { std::cout << "Failed to set the unhandled exception filter!" << std::endl; return 1; } int* p = nullptr; *p = 42; return 0;}
Обрати внимание, что если ты запустишь этот код в Visual Studio, то она выдаст ошибку до UEF.
Это связано с тем, что исключение в данном случае обрабатывает Visual Studio. Если же файл будет запущен за пределами IDE, то мы получим успешный вызов обработчика.
Установка hardware breakpoint
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»