- Статус
- Оффлайн
- Регистрация
- 21 Июн 2025
- Сообщения
- 137
- Реакции
- 28
Сразу скажу что статья моя, только перезаливается на югейм с левого форума т.к тут много единомышленников которым интересна тема разработки защитных механизмов, а еще мне нужна обоснованная критика :)
Всех читателей приветствую на этой великолепной статье, в ней разложены основы разработки
Пожалуйста, авторизуйтесь для просмотра ссылки.
.Я планирую реализовать 5 объемных частей. Наша цель - разработать полноценный EDR с корреляционным движком, телеметрией и кучей других разных функций для детектирования современной продвинутой малвари. Наша EDR будет детектировать известные в современности техники обхода антивирусных защитных обеспечений.
edr:
EDR (Endpoint Detection and Response) — это
технология кибербезопасности, которая постоянно отслеживает конечные точки (компьютеры, смартфоны, серверы), обнаруживает угрозы, анализирует их в реальном времени и предоставляет инструменты для автоматического реагирования, блокировки и расследования инцидентов, выходя за рамки традиционных антивирусов
Наша EDR будет функционировать на уровне UserMode, возможно, в будущем я реализую статью с переходом на Kernel-Mode. Не 'возможно', а стопроцентно, мы будем обязаны перейти на уровень ядра.
Стоит помнить, что EDR-разработка это нетривиальная задача, которая требует рационального мышления, дедуктивного мышления и построения логических связей, вы должны понимать что будет делать малварь на два шага вперед и опережать его.
Не буду затягивать с предисловием, без воды, начинаю.
Основа
P.S - Изначально функция называлась set_score, но ее логика была странной я ее переделал, на некоторых скринах вы можете видеть использование set_score вместо add_score. Главное поймите принцип, логика похожая только сделана правильнее.
Наша EDR использует систему накопления баллов угроз. Каждая подозрительная операция добавляет определенное количество баллов к общему счету. Когда сумма баллов достигает или превышает пороговое значение (100), EDR принимает решение о завершении вредоносного процесса с помощью функции TerminateProcess().
Помните, что это учебный код, просто введение, чтобы вы поняли принцип разработки, EDR никогда не убивает процесс, в который заинжектирован, а лишь целевой процесс.
Первая статья это введение, просто изучите код и попытайтесь понять принцип. Попробуйте сами поработать с хуками и так далее. Настоящая движуха начнется именно во 2 части.
Первая статья это введение, просто изучите код и попытайтесь понять принцип. Попробуйте сами поработать с хуками и так далее. Настоящая движуха начнется именно во 2 части.
add_score:
void add_score(Corecial* x, int delta) {
LONG new_score = InterlockedAdd(&x->score, delta);
if (new_score >= 100) {
TerminateProcess(GetCurrentProcess(), 0);
}
}
Для этого у меня есть функция add_score, она реализует эту логику. Она принимает указатель на структуру Corecial, содержащую счетчик баллов, и значение delta, которое нужно добавить. Важной особенностью является использование функции InterlockedAdd(), которая обеспечивает атомарность операции сложения в многопоточной среде - это критически важно, поскольку разные хуки могут вызываться параллельно из разных потоков.
В общем, после того как мы объявили структуру, мы создаем функцию типа void. В параметрах мы создаем указатель на структуру - x, также создаем переменную типа int delta.
Знак -> используется для доступа к полям структуры через указатель. Это равносильно разыменованию указателя и обращению к полю: сначала мы получаем саму структуру через *x, затем обращаемся к её полю (*x).score. Оператор -> просто объединяет эти две операции в одну.
Когда новое значение счетчика достигает или превышает 100 баллов, система немедленно завершает текущий процесс. В будущих версиях мы заменим эту простую логику на более логический корреляционный движок, который будет анализировать цепочки событий и выявлять сложные атаки. В общем, будет анализ основываясь на логических связях.
Если A и B и C вызвались в течение N времени, то принимаем решение.
Хуки.
Хук - это перехватывание функции для изменения ее работоспособности. В нашем же случае, хук - это способ перехватить функцию, для анализа ее аргументов с целью вынести вердикт, какова цель вызывания этой функции, в случае если функция является подозрительной, мы задаем определенное значение с обращением в нашу структуру Corecial к score через *x.
Давайте включим аналитическое мышление, представьте, что вы - разработчик малвари, к примеру, вы разработали дроппер. Задача дроппера - доставить малварь на ПК жертвы, запустить ее от имени администратора и удалить все следы, но при этом, реализовать это нужно так, чтобы жертва даже и малейших подозрений не имела касаемо того, что ее ПК заражен.
Задайте себе вопрос: Самый известный способ обхода UAC который мы должны пофиксить в первую очередь?
Ответ проявляется перед глазами - fodhelper.
После доставки малварь на ПК, программа дроппер обязана запустить ее с полным обходом UAC.
UAC (User Account Control) Windows — это функция безопасности, которая защищает систему от несанкционированных изменений, запрашивая подтверждение пользователя перед выполнением действий, требующих прав администратора, и позволяя обычным пользователям работать с минимальными привилегиями, что снижает риск заражения вирусами и вредоносными программами.
Дроппер через ShellExecuteA вызывает системную утилиту fodhelper.exe(компонент для установки дополнительных функций Windows). В контексте UAC-обхода, малварь использует ее, т.к fodhelper.exe имеет AEM - он автоматический запускается с правами администратора без запроса UAC, если вызван из определенных условий.
Принцип прост:
1) Малварь запускается fodhelper.exe;
2) fodhelper.exe автоматически получает высокие привилегии;
3) Далее через некоторые механизмы которые показаны в коде, малварь заставляет fodhelper.exe выполнить вредоносный код(малварь)
Реализовывать мы будем именно в этой части IAT-хуки.
Давайте реализуем хук на функцию ShellExecuteA с проверкой аргумента.
Структура ShellExecuteA:
structure:
HINSTANCE ShellExecuteA(
[in, optional] HWND hwnd,
[in, optional] LPCSTR lpOperation,
[in] LPCSTR lpFile,
[in, optional] LPCSTR lpParameters,
[in, optional] LPCSTR lpDirectory,
[in] INT nShowCmd
);
Точно также, мы в коде после функции add_score объявляем прототип этой функции, с указателем на нее через PShellExecuteA, также, с OriginalPShellExecuteA которая хранит в себе оригинальную функцию, ее мы будем возвращать, в случае если код прошел анализ.
c:
#include <stdio.h>
#include <windows.h>
#include <stdint.h>
#include <string.h>
#include <winternl.h>
typedef HINSTANCE(WINAPI* PShellExecuteA)(HWND, LPCSTR, LPCSTR, LPCSTR, LPCSTR, INT);
PShellExecuteA OriginalPShellExecuteA = NULL;
После чего сразу же объявляем функцию HookShellExecuteA, передаем ей параметры из структуры, подготавливаем возврат оригинала:
Пока что никакого условия здесь нет. Нет проверки, вообще ничего, просто функция которая возвращает оригинальную функцию.
Реализуем проверку с использованием условной конструкции:
Мы создаем условную конструкцию, где с помощью _stricmp сравниваем.
То есть:
Если IpFile содержит в себе строку fodhelper.exe, передать в структуру Corecial, параметру score значение + 35. Вычисляем: 100 - 35 = 65 баллов еще осталось передать, до завершения процесса.
Если все нормально, этой строки нет - вызов оригинальной функции, анализ пройден.
Это максимально простая проверка на fodhelper.
Теперь стоит поговорить про Process Hollowing.
Это техника, при которой малварь создает легитимный процесс в приостановленном состоянии (CREATE_SUSPENDED), затем выгружает его оригинальный код из памяти и заменяет своим вредоносным кодом.
Нам стоит реализовать хук на функцию ZwUnmapViewOfSection, прервав ее - прервутся и другие функции. Функция ZwUnmapViewOfSection отвечает за выгрузку оригинального образа из памяти.
Ее структура:
c:
NTSTATUS ZwUnmapViewOfSection(
HANDLE ProcessHandle, // хэндл процесса
PVOID BaseAddress // базовый адрес для выгрузки кода
);
Точно в такой же последовательности как с ShellExecuteA объявляем ее:
hookzw:
typedef NTSTATUS(NTAPI* PZwUnmapViewOfSection)(HANDLE, PVOID);
PZwUnmapViewOfSection OriginalPZwUnmapViewOfSection = NULL;
Точно в такой же последовательности объявляем хук и смотрим - выгружается ли код из памяти своего процесса или нет:
hook:
NTSTATUS HookZwUnmapViewOfSection(HANDLE ProcessHandle, PVOID BaseAddress)
{
if (ProcessHandle != GetCurrentProcess() && BaseAddress != NULL)
{
add_score(&global_x, 50);
}
return OriginalPZwUnmapViewOfSection(ProcessHandle, BaseAddress);
}
Это самая минимальная проверка на Process Hollowing, плюс нашей функции add_score в том, что с ней мы не сразу убиваем процесс, мы проводим поведенческий анализ.
Завершать процесс основываясь только на выгрузке кода не из памяти своего процесса, - было бы нерациональным решением.
Стоит немного поговорить про W^X Bypass.
Это техника, при которой, малварь сначала пишет код в память как данные (write), а затем через WinAPI функцию VirtualProtect меняет права на исполнение (execute).
Или же когда малварь сначала пишет код как R/X, но потом меняет права на R/W/X.
Малварь подобным методом пытается снизить подозрительность, избегая регионы R/W/X, которые очень легко детектируется.
Типичный паттерн использования этой функции:
c:
VirtualAlloc(PAGE_READWRITE) -> Write shellcode -> VirtualProtect(PAGE_EXECUTE_READ)
Запись шеллкода с правами PAGE_READWRITE -> изменение прав через VirtualProtect -> PAGE_EXECUTE_READ чтобы снизить подозрительность и избежать детекты.
Точно с такой же последовательностью, как и с другими функциями, реализовываем хук на NtProtectVirtualMemory что является оберткой VirtualProtectMemory:
c:
typedef NTSTATUS(NTAPI* PNtProtectVirtualMemory)(
_In_ HANDLE ProcessHandle,
_Inout_ PVOID* BaseAddress,
_Inout_ PSIZE_T RegionSize,
_In_ ULONG NewProtection,
_Out_ PULONG OldProtection
);
PNtProtectVirtualMemory OriginalNtProtectVirtualMemory = NULL;
Обратите внимание на первую условную конструкцию. В ней мы проверяем, производится ли изменение прав доступа памяти в нашем процессе или в чужом. Если права меняются в чужом процессе, мы добавляем 15 баллов к score.
Вторая условная конструкция немного сложнее. Здесь мы проверяем, устанавливаются ли новые права доступа PAGE_EXECUTE_READ или PAGE_EXECUTE_READWRITE. Если условие выполняется, мы переходим внутрь блока.
В этом блоке создается переменная указатель типа void | returnAddress. Эта переменная нужна для функции
Пожалуйста, авторизуйтесь для просмотра ссылки.
. С помощью этой функции мы определяем, откуда был вызван наш код. Мы проверяем, вызвана ли функция из зарегистрированного модуля системы или нет.Дело в том, что малварь часто работает из памяти, которая не принадлежит никакому модулю. Например, когда вредоносный код выделяет память через VirtualAllocEx, записывает туда шеллкод, а затем вызывает его. Такой код находится в куче, а не в секции .text какого-либо модуля, что является фактическим доказательством того, что это шеллкод.
Если функция CaptureStackBackTrace показывает, что вызов произошел не из зарегистрированного модуля, мы добавляем еще 20 баллов к нашей переменной score.
Полный код хука:
C:
NTSTATUS HookNtProtectVirtualMemory(_In_ HANDLE ProcessHandle, _Inout_ PVOID* BaseAddress, _Inout_ PSIZE_T RegionSize, _In_ ULONG NewProtection, _Out_ PULONG OldProtection)
{
if (ProcessHandle != (HANDLE)-1 && GetProcessId(ProcessHandle) != GetCurrentProcessId())
{
add_score(&global_x, 15);
}
if (NewProtection == PAGE_EXECUTE_READ || NewProtection == PAGE_EXECUTE_READWRITE)
{
void* returnAddress = NULL;
if (CaptureStackBackTrace(1, 1, &returnAddress, NULL) > 0)
{
HMODULE hMod = NULL;
if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)returnAddress, &hMod))
{
add_score(&global_x, 20);
}
}
}
return OriginalNtProtectVirtualMemory(ProcessHandle, BaseAddress, RegionSize, NewProtection, OldProtection);
}
NtWriteVirtualMemory
Также давайте сразу же поставим хук на эту функцию, которая выполняет запись кода в чужой процесс, ну, либо в свой.
Проверка будет максимально проста, мы будем смотреть, выполняется ли запись кода в чужой процесс или же нет, если да - запись определенное количество байт в Corecial -> score.
Точно такой же последовательностью как и в прошлые разы, объявляем структуру и хук:
c:
typedef NTSTATUS(NTAPI* PNtWriteVirtualMemory)(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN ULONG NumberOfBytesToWrite,
OUT PULONG NumberOfBytesWritten OPTIONAL
);
PNtWriteVirtualMemory OriginalNtWriteVirtualMemory = NULL;
NTSTATUS HookNtWriteVirtualMemory(IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN PVOID Buffer, IN ULONG NumberOfBytesToWrite, OUT PULONG NumberOfBytesWritten OPTIONAL)
{
if (ProcessHandle != (HANDLE)-1)
{
DWORD targetPid = GetProcessId(ProcessHandle);
if (targetPid != 0 && targetPid != GetCurrentProcessId())
{
add_score(&global_x, 50);
}
}
return OriginalNtWriteVirtualMemory(ProcessHandle, BaseAddress, Buffer, NumberOfBytesToWrite, NumberOfBytesWritten);
}
Значение -1 в WinAPI обозначает псевдо-хэндл текущего процесса. То-есть, если ProcessHandle равен -1, это значит, что операция выполняется над памятью текущего процесса.
В случае если условие выполняется, то мы переходим непосредственно в блок, где проверяем, равен ли полученный PID процесса нулю и не совпадает ли он с PID текущего процесса. Таким способом мы проверяем, выполняется ли запись кода в чужой процесс либо же нет. В случае если проверка пройдена успешно, возвращается оригинальная функция. Прошу обратить внимание, многие могут закритиковать тем, что мол зачем 50, запись кода может использовать отладчик/JIT компилятор и так далее. Я это прекрасно понимаю, как раз таки поэтому я и реализовал функцию add_score. Отладчик может вызвать функцию записи кода в процесс, но вызовет ли отладчик параллельно с этим выгрузку кода из системного процесса для записи своего? Не думаю.
Немного поговорим про HWDB-хуки
Пожалуйста, авторизуйтесь для просмотра ссылки.
-хуки это - технология детектирования малварь, использующая аппаратные возможности процессора.В нашем же случае, мы будем использовать аппаратные регистры отладки(dr0/dr7), первые три dr0/dr3 будут хранить в себе адреса функций. Мы говорим процессору: "Когда процесс вызовет адрес, который я тебе передал, вызови исключение #DB и передай управление моему VEH-обработчику". После того, как процессор передаст управление нашему VEH-обработчику, там будут реализовываться непосредственно хуки на эти функции и проверки аргументов функций.
Плюс HWDB хуков в том, что мы вообще не трогаем память, мы ничего там не меняем, абсолютно ничего, все происходит на уровне процессора. Мы просто перехватываем исключение и обрабатываем его через наш VEH-обработчик, все максимально просто.
Память девственно чиста, в отличие от IAT-хуков.
Проинициализируем функцию SetHardwareBreakpoint типа void, в параметры передаем ей переменную address типа PVOID и переменную index типа int.
address используется для отслеживания, а переменнная index используется для хранения номера аппаратного регистра процесса, используем мы именно те регистры, в которых можно хранить адреса(dr0/dr3)
А сама функция будет устанавливать аппаратный брейкпоинт на указанный адрес в памяти, в нашем случае, этот адрес будет адрес функции, которую мы хукаем.
Реализация функции с использованием switch/case выглядит вот так:
с:
void SetHardwareBreakpoint(PVOID address, int index)
{
if (index < 0 || index > 3) return;
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
HANDLE hThread = GetCurrentThread();
HANDLE hThreadCopy;
if (!DuplicateHandle(GetCurrentProcess(), hThread, GetCurrentProcess(), &hThreadCopy, 0, FALSE, DUPLICATE_SAME_ACCESS))
{
return;
}
if (GetThreadContext(hThreadCopy, &ctx))
{
switch (index)
{
case 0: ctx.Dr0 = (DWORD64)address; break;
case 1: ctx.Dr1 = (DWORD64)address; break;
case 2: ctx.Dr2 = (DWORD64)address; break;
case 3: ctx.Dr3 = (DWORD64)address; break;
}
ctx.Dr7 |= (1ULL << (index * 2));
ctx.Dr7 &= ~(0xFULL << (16 + (index * 4)));
SetThreadContext(hThreadCopy, &ctx);
}
CloseHandle(hThreadCopy);
}
Немного про код. Сначала функция проверяет, что индекс в допустимом диапазоне. Затем она получает контекст текущего потока и дублирует его хэндл для безопасной работы. В зависимости от выбранного индекса, адрес брейкпоинта записывается в соответствующий регистр (dr0/dr1/dr2/dr3 и т.д).
Далее настраивается регистр управления dr7: включается брейкпоинт соответствующего индекса и задаются его параметры.
Обратите внимание на эти две строчки кода:
c:
ctx.Dr7 |= (1ULL << (index * 2));
ctx.Dr7 &= ~(0xFULL << (16 + (index * 4)));
Эти две строки настраивают регистр управления аппаратными брейкпоинтами dr7.
Так, первая строка включает брейкпоинт с указанным индексом, устанавливая соответствующий бит в регистре dr7, который отвечает за включение.
Вторая строка просто берет и сбрасывает настройки типа брейкпоинта и его размера для выбранного индекса. Если еще проще, то мы просто очищаем старые параметры, что затем установить новые. Сначала мы смотрим параметры через
Пожалуйста, авторизуйтесь для просмотра ссылки.
, а потом задаем эти параметры через
Пожалуйста, авторизуйтесь для просмотра ссылки.
.Кстати, стоит помнить, что в будущем мы реализуем хуки на эти функции, дело в том, что малварь может попытаться обнулить наши регистры, которые хранят в себе адреса хукнутых функций. Далее.
Но, пока переменная adress ничего не хранит, нам нужно реализовать VEH-обработчик который будет перехватывать #DB исключение от процессора и устанавливать хуки.
Перед тем как трогать обработчик, мы должны объявить структуру функций, которые мы хотим хукнуть.
Я выбрал: NtCreateThreadEx/NtAllocateVirtualMemoryNtProtectVirtualMemory.
Многие могут задаться вопросом: зачем ставить HWDB-хук на третью функцию, если мы уже поставили IAT-хук на неё?
Друзья, это момент, когда ты понимаешь, что не всё так просто, IAT-хуки обойти очень просто, банально загрузить функции по хэшу из EAT(EAT-хуки будут разбираться в следующих статьях).
Но обойти HWDB-хуки намного сложнее, учитывая то, что мы пофиксим все дыры в безопасности этих хуков.
Структура NtProtectVirtualMemory у нас уже объявляна, так что объвляем другие две функции:
c:
typedef NTSTATUS(NTAPI* PNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
PNtAllocateVirtualMemory OriginalNtAllocateVirtualMemory = NULL;
typedef NTSTATUS(NTAPI* PNtCreateThreadEx)(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
IN PVOID StartRoutine,
IN PVOID Argument OPTIONAL,
IN ULONG CreateFlags,
IN SIZE_T ZeroBits,
IN SIZE_T StackSize,
IN SIZE_T MaximumStackSize,
IN PVOID AttributeList OPTIONAL
);
PNtCreateThreadEx OriginalNtCreateThreadEx = NULL;
Также объявите переменные которые обращаются к каждой структуре функции:
c:
PVOID addrNtProtect = NULL;
PVOID addrNtAllocate = NULL;
PVOID addrNtCreateThread = NULL;
__declspec(thread) PHANDLE g_pThreadHandleAddr = NULL;
Объявляем обработчик:
В первую очередь, в обработчике мы также реализуем функцию, которая будет проверять, вызывается ли код из секции .text или нет, чтобы понять, выполняется ли шеллкод:
heap:
LONG WINAPI HardwareBreakpointHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
PCONTEXT ctx = ExceptionInfo->ContextRecord;
PVOID faultAddr = ExceptionInfo->ExceptionRecord->ExceptionAddress;
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{
void* returnAddress = NULL;
if (CaptureStackBackTrace(2, 1, &returnAddress, NULL) > 0)
{
HMODULE hMod = NULL;
if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCSTR)returnAddress, &hMod))
{
add_score(&global_x, 20);
}
}
}
Сначала мы получаем указатель на структуру CONTEXT из параметра ExceptionInfo.
После чего реализуем условную конструкцию:
с:
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{
void* returnAddress = NULL;
if (CaptureStackBackTrace(2, 1, &returnAddress, NULL) > 0)
{
HMODULE hMod = NULL;
if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCSTR)returnAddress, &hMod))
{
add_score(&global_x, 20);
}
}
ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP
То-есть, если код исключения равен EXCEPTION_SINGLE_STEP, значит сработал наш аппаратный брейкпоинт, а если еще проще, наш хук.
Далее, после того как мы проверили, вызывается ли код секции .text или нет, реализовали проверку EXCEPTION_SINGLE_STEP, можем переходить к непосредственному реализацию наших хуков.
Кстати, не забудьте вернуть EXCEPTION_CONTINUE_SEARCH. Это возвращаемое значение, которое указывает системе продолжить поиск других обработчиков исключений в цепочке. Двигаемся дальше.
Установка HWDB-хуков
Поставим сначала на NtCreateThreadEx:
Обратите внимание на условную конструкцию if (faultAddr == addrNtCreateThread) она проверяет, сработал ли наш аппаратный брейкпоинт на функции NtCreateThreadEx. Если да, мы начинаем анализировать её параметры. Через ctx->R9 мы получаем параметр ProcessHandle который отвечает за хэндл процесса:
В x64 архитектуре первые 4 параметра передаются через регистры: RCX, RDX, R8, R9, остальное через стек:
asm:
mov qword ptr, [rsp+28h], ЗНАЧЕНИЕ
Далее, если поток создается в чужом процессе (hTargetProc != -1 и PID не наш процесс), возвращаем STATUS_ACCESS_DENIED которая вызывается для любых NtAPI функций, она просто наглухо заблокирует инжектирование. Если же поток создается в текущем процессе, мы сохраняем указатель на хэндл потока в g_pThreadHandleAddr и устанавливаем дополнительный брейкпоинт на адрес возврата через ctx->Dr2, чтобы отследить завершение создания потока.
Далее, обратите внимание на строчки:
с:
ctx->Dr2 = retAddr;
ctx->Dr7 |= (1ULL << 4);
Во второй строчке, мы включаем брейкпоинт, бит 4 в регистре dr7 управляет брейкпоинтом номер 2, то есть каждый брейкпоинт имеет свой бит включения: 0, 2, 4, 6 и т.д. Подробнее можете прчитать на форумах по типу Хабра.
Таким методом можно отслеживать создание нового потока, как я уже говорил, малварь может создать новый поток, в котором будет выполнять свои вредоносные действия
Также важно прописать ctx->EFlags |= (1 << 16); потому-что без этого процессор не будет генерировать следующие single-step исключения.
Делаем абсолютно тоже самое, в той же последовательности, с разными условиями проверки функций, с другими функциями:
c:
if (faultAddr == addrNtAllocate)
{
ULONG Protect = *(ULONG*)(ctx->Rsp + 48);
if (Protect == PAGE_EXECUTE_READWRITE)
{
return 0xC0000022;
}
ctx->EFlags |= (1 << 16);
return EXCEPTION_CONTINUE_EXECUTION;
}
if (faultAddr == addrNtProtect)
{
ULONG NewProtection = (ULONG)(ctx->R9);
if (NewProtection == PAGE_EXECUTE_READ || NewProtection == PAGE_EXECUTE_READWRITE)
{
void* returnsAddress = NULL;
if (CaptureStackBackTrace(1, 1, &returnsAddress, NULL) > 0)
{
HMODULE hMods = NULL;
if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)returnsAddress, &hMods))
{
add_score(&global_x, 20);
}
}
}
ctx->EFlags |= (1 << 16);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
Наша условная конструкция проверяет, сработал ли наш брейкпоинт на функцию NtAllocateVirtualMemory или нет. Далее, буквально вторая строчка внутри блока функции, там мы получаем параметр Protect, как уже было сказано выше, мы уже проверяли этот параметр в IAT-хуке, тут будет совершенно тоже самое. Касаемо ctx->Rsp+48, дело в том, что этот параметр находится по смещению rsp+48 в стеке, таким методом мы обращаемся к нему, точнее на его адрес.
Далее самая важная проверка, мы смотрим, равен ли параметр Protect PAGE_EXECUTE_READWRITE, запрашивается ли он с правами R/W/X или нет. Если да - вызываем STATUS_ACCESS_DENIED.
Я все еще прекрасно помню, что R/W/X могут использовать отладчики и JIT компиляторы, в будущих статьях мы немного поменяем логику.
Далее на разборе NtProtect:
Такая же условная конструкция что и в предыдущем хуке.
Проверяем, сработал брейкпоинт на функцию или нет, если да, переходим в блок.
Обратите внимание ctx->R9, таким способом мы получаем 4-й параметр, отвечающие за новые права доступа(NewProtection).
4 параметр это регистр r9 согласно x64 соглашению.
Обратите на условную конструкцию if (NewProtection == PAGE_EXECUTE_READ || NewProtection == PAGE_EXECUTE_READWRITE), мы уже реализовывали подобное в IAT хуки, таким способом мы детектируем попытку установки исполняемых прав, думаю вы помните.
Далее наш любимый CaptureStackBackTrace, с которым уже сталкиваемся 3 раз, все также проверяем вызывается ли функция из .text или нет. Если нет - шеллкод, передаем 20 баллов в структуру Corecial.
Работа HWDB-хуков в новом потоке.
Помните, я говорил вам про новый поток, где малварь может выполнять свои операции? Так вот, наши хуки будут работать и в нем.
У нас есть переменная g_pThreadHandleAddr, я про нее объяснял, напомню, что - это указатель на хэндл вновь созданного потока, то-есть указатель на уже НОВЫЙ поток. Когда срабатывает брейкпоинт на адресе возврата из NtCreateThreadEx.
с:
__declspec(thread) PHANDLE g_pThreadHandleAddr = NULL;
Я реализовал если так можно назвать многопоточные хуки, я уже объяснял что малварь может создать новый поток где будет выполнять свои зловредные действия, в то время, как в основном потоке будет, к примеру, работать калькулятор.
c:
if (faultAddr == (PVOID)ctx->Dr2)
{
if (g_pThreadHandleAddr != NULL)
{
HANDLE hNewThread = *g_pThreadHandleAddr;
if (hNewThread != NULL)
{
CONTEXT ThreadCtxNew = { 0 };
ThreadCtxNew.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (GetThreadContext(hNewThread, &ThreadCtxNew))
{
ThreadCtxNew.Dr0 = (DWORD64)addrNtCreateThread;
ThreadCtxNew.Dr1 = (DWORD64)addrNtAllocate;
ThreadCtxNew.Dr3 = (DWORD64)addrNtProtect;
ThreadCtxNew.Dr7 = (1ULL << 0) | (1ULL << 2) | (1ULL << 6);
SetThreadContext(hNewThread, &ThreadCtxNew);
}
}
g_pThreadHandleAddr = NULL;
}
ctx->Dr2 = 0;
ctx->Dr7 &= ~(1ULL << 4);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
Думаю, принцип работы теперь понятен.
Вообще тема HWDB-хуков довольно трудная для новичков, так что если вы, что-то не поняли, пожалуйста, гуглите, я пытаюсь объяснять максимально простым языком.
Обратите внимание на эту строчку:
с:
ThreadCtxNew.Dr7 = (1ULL << 0) | (1ULL << 2) | (1ULL << 6);
0 - dr0
2 - dr1
6 - dr3
А если еще проще: включаем их.
Далее:
c:
ctx->Dr2 = 0;
ctx->Dr7 &= ~(1ULL << 4);
Теперь, обратите внимание обратно на функцию SetHardwareBreakpoint которую мы уже реализовали, теперь регистры содержат в себе адреса, эти адреса мы передаем в переменную address, которая их теперь уже хранит в себе:
с:
switch (index)
{
case 0: ctx.Dr0 = (DWORD64)address; break;
case 1: ctx.Dr1 = (DWORD64)address; break;
case 2: ctx.Dr2 = (DWORD64)address; break;
case 3: ctx.Dr3 = (DWORD64)address; break;
}
Полный код нашего VEH-обработчика:
c:
LONG WINAPI HardwareBreakpointHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
PCONTEXT ctx = ExceptionInfo->ContextRecord;
PVOID faultAddr = ExceptionInfo->ExceptionRecord->ExceptionAddress;
if (ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP)
{
void* returnAddress = NULL;
if (CaptureStackBackTrace(2, 1, &returnAddress, NULL) > 0)
{
HMODULE hMod = NULL;
if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCSTR)returnAddress, &hMod))
{
add_score(&global_x, 20);
}
}
if (faultAddr == addrNtCreateThread)
{
HANDLE hTargetProc = (HANDLE)ctx->R9;
PVOID startRountine = *(PVOID*)(ctx->Rsp + 40);
if (hTargetProc != (HANDLE)-1 && GetProcessId(hTargetProc) != GetCurrentProcessId())
{
return 0xC0000022;
}
if (hTargetProc == (HANDLE)-1 || GetProcessId(hTargetProc) == GetCurrentProcessId())
{
g_pThreadHandleAddr = (PHANDLE)ctx->Rcx;
DWORD64 retAddr = *(DWORD64*)(ctx->Rsp);
ctx->Dr2 = retAddr;
ctx->Dr7 |= (1ULL << 4);
}
}
ctx->EFlags |= (1 << 16);
return EXCEPTION_CONTINUE_EXECUTION;
}
if (faultAddr == addrNtProtect)
{
BOOL bs = FALSE;
ULONG NewProtection = (ULONG)(ctx->R9);
if (NewProtection == PAGE_EXECUTE_READ || NewProtection == PAGE_EXECUTE_READWRITE)
{
void* returnsAddress = NULL;
if (CaptureStackBackTrace(1, 1, &returnsAddress, NULL) > 0)
{
HMODULE hMods = NULL;
if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
(LPCSTR)returnsAddress, &hMods))
{
add_score(&global_x, 20);
}
}
}
ctx->EFlags |= (1 << 16);
return EXCEPTION_CONTINUE_EXECUTION;
}
if (faultAddr == (PVOID)ctx->Dr2)
{
if (g_pThreadHandleAddr != NULL)
{
HANDLE hNewThread = *g_pThreadHandleAddr;
if (hNewThread != NULL)
{
CONTEXT ThreadCtxNew = { 0 };
ThreadCtxNew.ContextFlags = CONTEXT_DEBUG_REGISTERS;
if (GetThreadContext(hNewThread, &ThreadCtxNew))
{
ThreadCtxNew.Dr0 = (DWORD64)addrNtCreateThread;
ThreadCtxNew.Dr1 = (DWORD64)addrNtAllocate;
ThreadCtxNew.Dr3 = (DWORD64)addrNtProtect;
ThreadCtxNew.Dr7 = (1ULL << 0) | (1ULL << 2) | (1ULL << 6);
SetThreadContext(hNewThread, &ThreadCtxNew);
}
}
g_pThreadHandleAddr = NULL;
}
ctx->Dr2 = 0;
ctx->Dr7 &= ~(1ULL << 4);
return EXCEPTION_CONTINUE_EXECUTION;
}
return EXCEPTION_CONTINUE_SEARCH;
}
С:
typedef BOOL(WINAPI* PGetThreadContext)(
HANDLE hThread,
LPCONTEXT lpContext
);
PGetThreadContext OriginalGetThreadContext = NULL;
c:
BOOL WINAPI HookGetThreadContext(HANDLE hThread, LPCONTEXT lpContext)
{
BOOL result = OriginalGetThreadContext(hThread, lpContext);
if (result && lpContext != NULL)
{
if ((lpContext->ContextFlags & CONTEXT_DEBUG_REGISTERS) == CONTEXT_DEBUG_REGISTERS)
{
lpContext->Dr0 = 0;
lpContext->Dr1 = 0;
lpContext->Dr2 = 0;
lpContext->Dr3 = 0;
lpContext->Dr6 = 0;
lpContext->Dr7 = 0;
}
}
return result;
}
Важно помнить, что хук применяется только если в ContextFlags установлен CONTEXT_DEBUG_REGISTERS.
Тоже самое реализовываем с SetThreadContext, когда малварь увидит, что регистры пустые, она может подумать что это обманка и на всякий случай занулить их через SetThreadContext, но не тут то было, на ней тоже хук:
c:
BOOL WINAPI HookSetThreadContext(HANDLE hThread, const CONTEXT* lpContext)
{
if ((lpContext->ContextFlags & CONTEXT_DEBUG_REGISTERS) == CONTEXT_DEBUG_REGISTERS)
{
if (lpContext->Dr0 == 0 && lpContext->Dr1 == 0 && lpContext->Dr2 == 0)
{
add_score(&global_x, 50);
}
}
return OriginalSetThreadContext(hThread, lpContext);
}
Реализация защиты VEH-обработчика будет во 2 части.