Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Windows предоставляет мощные инструменты для установки точек останова непосредственно в памяти. Но знаешь ли ты, что с их помощью можно ставить и снимать хуки, а также получать сисколы? В этой статье я в подробностях расскажу, как это делать.
Точки останова служат для контроля выполнения программы и, конечно же, их остановки в определенный момент. Глобально существует два вида брейк‑пойнтов: software breakpoints и hardware breakpoints.
Software breakpoint — точка останова, которая ставится с помощью отладчика или IDE. Чтобы поставить такую точку останова, можно, например, просто кликнуть на нужную строку программы в Visual Studio.
Установка software breakpoint в Visual Studio
Такие точки останова можно ставить где угодно и сколько угодно. Никаких ограничений нет.
Установка множества software breakpoint
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 — __finally
, __try — __except
.
#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
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;}
Пример EXCEPTION_CONTINUE_EXECUTION
Программы могут быть сложные, страшные, большие, нужно предусматривать корректный выход из всех блоков, изучать возможные исключения. Вдруг потребуется функция уведомления пользователя о сработавшем исключении? В общем, CEH хорош, но, помимо него, появился и VEH. VEH можно считать эдакой надстройкой над SEH. Работает она, само собой, только в Windows.
Если в программе возникает исключение, то первыми вызываются именно векторные обработчики и лишь затем система начнет разворачивать стек. С помощью VEH прога может, например, зарегистрировать функцию для просмотра или обработки всех исключений приложения. Причем в программу можно добавить несколько VEH-обработчиков, и они будут вызваны в том порядке, в котором были добавлены. Первый — первым, второй — вторым и так далее. CEH после 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 Reference
. То есть обращение к указателю, который имеет значение 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;}
Обработка исключения с помощью VEH
Видим, что обработчик успешно срабатывает и вызывается, затем возвращает EXCEPTION_CONTINUE_SEARCH
. Это, в свою очередь, дергает SEH, SEH в программе нет, поэтому Visual Studio включается и выдает нам исключение. Если будем возвращать EXCEPTION_CONTINUE_EXECTION
, то получим бесконечный вызов обработчика, так как каждый раз будет срабатывать строка *p = 42
.
Бесконечная обработка исключения
Точно такое же исключение будет срабатывать и при хардверных бряках.
Наконец, последний тип обработчиков — Unhandled Exception Filter. Он редко когда используется, но изначально задумывался как обработчик для исключений, которые вообще никто не обрабатывает. Ни VEH (если отсутствует или вернул EXCEPTION_CONTINUE_SEARCH
), ни CEH (если тоже отсутствует или указано 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.
Исключение от «Студии», а не от UEF
Это связано с тем, что исключение в данном случае обрабатывает Visual Studio. Если же файл будет запущен за пределами IDE, то мы получим успешный вызов обработчика.
Вызов обработчика
Источник: xakep.ru