• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Принцип разработки EDR. Разрабатываем свой собственный EDR | 1 часть.

nexslin

HDD-drive
Пользователь
Регистрация
05.01.2026
Сообщения
23
Реакции
8
Всех читателей приветствую на этой великолепной статье, в ней разложены основы разработки EDR.
Я планирую реализовать 5 объемных частей. Наша цель - разработать полноценный EDR с корреляционным движком, телеметрией и кучей других разных функций для детектирования современной продвинутой малвари. Наша EDR будет детектировать известные в современности техники обхода антивирусных защитных обеспечений.

Код:
EDR (Endpoint Detection and Response) — это
технология кибербезопасности, которая постоянно отслеживает конечные точки (компьютеры, смартфоны, серверы), обнаруживает угрозы, анализирует их в реальном времени и предоставляет инструменты для автоматического реагирования, блокировки и расследования инцидентов, выходя за рамки традиционных антивирусов

Наша EDR будет функционировать на уровне UserMode, возможно, в будущем я реализую статью с переходом на Kernel-Mode. Не 'возможно', а стопроцентно, мы будем обязаны перейти на уровень ядра, т.к на уровне UserMode есть проблема с реализацией хука на syscall. Дело в том, что даже если мы реализуем его через instrumentation callback, при реализации через instrumentation callback необходимо учитывать существенный недостаток данного метода, который заключается в том, что мы чаще всего не можем знать, с каким индексом вызывался syscall.
А вот в Kernel-mode появляется конечно больше простора, тот же обработчик KiSystemCall позволит обрабатывать syscall вызванные из UserMode, что дает намного больше возможностей.

Статья ориентирована на: разработчиков малвари/системных программистов/начинающих EDR-разработчиков. Я предполагаю, что у вас уже есть базовые знания в: Архитектуре Windows, знать базовый синтаксис языка программирования С, понимать ассемблер на уровне чтения кода, умение работать с WinAPI/NtAPI, понимать архитектуру x64.

Стоит помнить, что EDR-разработка это нетривиальная задача, которая требует рационального мышления, дедуктивного мышления и построения логических связей, вы должны понимать что будет делать малварь на два шага вперед и опережать его.

Не буду затягивать с предисловием, без воды, начинаю.


Основа
P.S - Изначально функция называлась set_score, но ее логика была странной я ее переделал, на некоторых скринах вы можете видеть использование set_score вместо add_score. Главное поймите принцип, логика похожая только сделана правильнее.

Наша EDR использует систему накопления баллов угроз. Каждая подозрительная операция добавляет определенное количество баллов к общему счету. Когда сумма баллов достигает или превышает пороговое значение (100), EDR принимает решение о завершении вредоносного процесса с помощью функции TerminateProcess().
Помните, что это учебный код, просто введение, чтобы вы поняли принцип разработки, EDR никогда не убивает процесс, в который заинжектирован, а лишь целевой процесс.
Первая статья это введение, просто изучите код и попытайтесь понять принцип. Попробуйте сами поработать с хуками и так далее. Настоящая движуха начнется именно во 2 части.
C:
void add_score(Corecial* x, int delta) {
    LONG new_score = InterlockedAdd(&x->score, delta);
    if (new_score >= 100) {
        TerminateProcess(GetCurrentProcess(), 0);
    }
}
1769013129499.png

Для этого у меня есть функция 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 — это функция безопасности, которая защищает систему от несанкционированных изменений, запрашивая подтверждение пользователя перед выполнением действий, требующих прав администратора, и позволяя обычным пользователям работать с минимальными привилегиями, что снижает риск заражения вирусами и вредоносными программами.
1768995357026.png

Дроппер через ShellExecuteA вызывает системную утилиту fodhelper.exe(компонент для установки дополнительных функций Windows). В контексте UAC-обхода, малварь использует ее, т.к fodhelper.exe имеет AEM - он автоматический запускается с правами администратора без запроса UAC, если вызван из определенных условий.
Принцип прост:
1) Малварь запускается fodhelper.exe;
2) fodhelper.exe автоматически получает высокие привилегии;
3) Далее через некоторые механизмы которые показаны в коде, малварь заставляет fodhelper.exe выполнить вредоносный код(малварь)

Реализовывать мы будем именно в этой части IAT-хуки.
Давайте реализуем хук на функцию ShellExecuteA с проверкой аргумента.
Структура ShellExecuteA:
C:
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, передаем ей параметры из структуры, подготавливаем возврат оригинала:
1768996046778.png

Пока что никакого условия здесь нет. Нет проверки, вообще ничего, просто функция которая возвращает оригинальную функцию.
Реализуем проверку с использованием условной конструкции:
1768996214576.png

Мы создаем условную конструкцию, где с помощью _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 объявляем ее:
C:
typedef NTSTATUS(NTAPI* PZwUnmapViewOfSection)(HANDLE, PVOID);
PZwUnmapViewOfSection OriginalPZwUnmapViewOfSection = NULL;

Точно в такой же последовательности объявляем хук и смотрим - выгружается ли код из памяти своего процесса или нет:
C:
NTSTATUS HookZwUnmapViewOfSection(HANDLE ProcessHandle, PVOID BaseAddress)
{
    if (ProcessHandle != GetCurrentProcess() && BaseAddress != NULL)
    {
        add_score(&global_x, 50);
    }
    return OriginalPZwUnmapViewOfSection(ProcessHandle, BaseAddress);
}
Если хэндл процесса не равен текущему процессу, И если базовый адрес не равен 0, - мы обращаемся к нашей структуре Corecial и задаем еще +50 баллов. В случае если функция не выгружает ничего из чужих процессов - вызов оригинала.
Это самая минимальная проверка на 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)
Сначала память выделяется с помощью VirtualAlloc, PAGE_READWRITE, потом, через функцию 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;
Реализуем хук:
1769006737786.png

Друзья, обратите внимание на первую условную конструкцию. В ней мы проверяем, производится ли изменение прав доступа памяти в нашем процессе или в чужом. Если права меняются в чужом процессе, мы добавляем 15 баллов к score.
Вторая условная конструкция немного сложнее. Здесь мы проверяем, устанавливаются ли новые права доступа PAGE_EXECUTE_READ или PAGE_EXECUTE_READWRITE. Если условие выполняется, мы переходим внутрь блока.
В этом блоке создается переменная указатель типа void | returnAddress. Эта переменная нужна для функции CaptureStackBackTrace. С помощью этой функции мы определяем, откуда был вызван наш код. Мы проверяем, вызвана ли функция из зарегистрированного модуля системы или нет.
Дело в том, что малварь часто работает из памяти, которая не принадлежит никакому модулю. Например, когда вредоносный код выделяет память через 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);
}
Объясняю. Мы создаем условную конструкцию if (ProcessHandle != (HANDLE)-1). Она проверяет, равен ли хэндл процесса значению -1.
Значение -1 в WinAPI обозначает псевдо-хэндл текущего процесса. То-есть, если ProcessHandle равен -1, это значит, что операция выполняется над памятью текущего процесса.
В случае если условие выполняется, то мы переходим непосредственно в блок, где проверяем, равен ли полученный PID процесса нулю и не совпадает ли он с PID текущего процесса. Таким способом мы проверяем, выполняется ли запись кода в чужой процесс либо же нет. В случае если проверка пройдена успешно, возвращается оригинальная функция. Прошу обратить внимание, многие могут закритиковать тем, что мол зачем 50, запись кода может использовать отладчик/JIT компилятор и так далее. Я это прекрасно понимаю, как раз таки поэтому я и реализовал функцию add_score. Отладчик может вызвать функцию записи кода в процесс, но вызовет ли отладчик параллельно с этим выгрузку кода из системного процесса для записи своего? Не думаю.

Немного поговорим про HWDB-хуки
HWDB-хуки это - технология детектирования малварь, использующая аппаратные возможности процессора.
В нашем же случае, мы будем использовать аппаратные регистры отладки(dr0/dr7), первые три dr0/dr3 будут хранить в себе адреса функций. Мы говорим процессору: "Когда процесс вызовет адрес, который я тебе передал, вызови исключение #DB и передай управление моему VEH-обработчику". После того, как процессор передаст управление нашему VEH-обработчику, там будут реализовываться непосредственно хуки на эти функции и проверки аргументов функций.

Плюс HWDB хуков в том, что мы вообще не трогаем память, мы ничего там не меняем, абсолютно ничего, все происходит на уровне процессора. Мы просто перехватываем исключение и обрабатываем его через наш VEH-обработчик, все максимально просто.
Память девственно чиста, в отличие от IAT-хуков.

Проинициализируем функцию SetHardwareBreakpoint типа void, в параметры передаем ей переменную address типа PVOID и переменную index типа int.
address используется для отслеживания, а переменнная index используется для хранения номера аппаратного регистра процесса, используем мы именно те регистры, в которых можно хранить адреса(dr0/dr3)
А сама функция будет устанавливать аппаратный брейкпоинт на указанный адрес в памяти, в нашем случае, этот адрес будет адрес функции, которую мы хукаем.

Реализация функции с использованием switch/case выглядит вот так:
C:
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, который отвечает за включение.
Вторая строка просто берет и сбрасывает настройки типа брейкпоинта и его размера для выбранного индекса. Если еще проще, то мы просто очищаем старые параметры, что затем установить новые. Сначала мы смотрим параметры через GetThreadContext, а потом задаем эти параметры через SetThreadContext.
Кстати, стоит помнить, что в будущем мы реализуем хуки на эти функции, дело в том, что малварь может попытаться обнулить наши регистры, которые хранят в себе адреса хукнутых функций. Далее.

Но, пока переменная 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;
Я знаю у многих появится вопрос по поводу __declspec(thread) g_pThreadHandleAddr, очень важный момент в обработчике. В общем, просто помните, что малварь может перейти в другой поток и уже там выполнять свои зловредные намерения, в нашем VEH-обработчике мы это продумаем.

Объявляем обработчик:
1769002304529.png

В первую очередь, в обработчике мы также реализуем функцию, которая будет проверять, вызывается ли код из секции .text или нет, чтобы понять, выполняется ли шеллкод.

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);
            }
        }
}
Сначала мы обращаемся к структуре CONTEXT, передаем из нее параметр ExceptionInfo. Она просто сохраняет полное состояние потока(регистры процессра(dr), флаги и так далее, хранит регистры для работы с FPU, но это другое).
1769002543625.png

Сначала мы получаем указатель на структуру CONTEXT из параметра ExceptionInfo.
После чего реализуем условную конструкцию:
C:
    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);
            }
        }
Касаемо CaptureStackBackTrace я уже объяснял выше, не хочу заливать водой статью, пролистайте и почитай. Обратите внимание на условие if.
ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP
То-есть, если код исключения равен EXCEPTION_SINGLE_STEP, значит сработал наш аппаратный брейкпоинт, а если еще проще, наш хук.
Далее, после того как мы проверили, вызывается ли код секции .text или нет, реализовали проверку EXCEPTION_SINGLE_STEP, можем переходить к непосредственному реализацию наших хуков.

Кстати, не забудьте после всех скобок вернуть EXCEPTION_CONTINUE_SEARCH:
1769002860300.png

Это возвращаемое значение, которое указывает системе продолжить поиск других обработчиков исключений в цепочке. Двигаемся дальше.

Установка HWDB-хуков
Поставим сначала на NtCreateThreadEx:
1769003645993.png

Обратите внимание на условную конструкцию if (faultAddr == addrNtCreateThread) она проверяет, сработал ли наш аппаратный брейкпоинт на функции NtCreateThreadEx. Если да, мы начинаем анализировать её параметры. Приступим.
Через ctx->R9 мы получаем параметр ProcessHandle который отвечает за хэндл процесса:
1769003871259.png

В x64 архитектуре первые 4 параметра передаются через регистры: RCX, RDX, R8, R9, остальное через стек:

Код:
 mov qword ptr, [rsp+28h], ЗНАЧЕНИЕ

NtCreateThreadEx имеет параметр hProcess (хэндл процесса, где создается поток), который как раз передается в R9.
Далее, если поток создается в чужом процессе (hTargetProc != -1 и PID не наш процесс), возвращаем STATUS_ACCESS_DENIED которая вызывается для любых NtAPI функций, она просто наглухо заблокирует инжектирование. Если же поток создается в текущем процессе, мы сохраняем указатель на хэндл потока в g_pThreadHandleAddr и устанавливаем дополнительный брейкпоинт на адрес возврата через ctx->Dr2, чтобы отследить завершение создания потока.

Далее, обратите внимание на строчки:
C:
                ctx->Dr2 = retAddr;
                ctx->Dr7 |= (1ULL << 4);
В первой строчке, мы сохраняем адреса возврата из стека в регистр dr2, это адрес куда функция NtCreateThreadEx вернет управление после своего выполнения
Во второй строчке, мы включаем брейкпоинт, бит 4 в регистре dr7 управляет брейкпоинтом номер 2, то есть каждый брейкпоинт имеет свой бит включения: 0, 2, 4, 6 и т.д. Подробнее можете прчитать на форумах по типу Хабра.
Таким методом можно отслеживать создание нового потока, как я уже говорил, малварь может создать новый поток, в котором будет выполнять свои вредоносные действия
1769004301485.png

Так очень важно прописать 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;
}

Обратите внимание, разберу для начала хук NtAllocate:
1769004640652.png

Наша условная конструкция проверяет, сработал ли наш брейкпоинт на функцию 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 соглашению.
1769004967467.png

Обратите на условную конструкцию if (NewProtection == PAGE_EXECUTE_READ || NewProtection == PAGE_EXECUTE_READWRITE), мы уже реализовывали подобное в IAT хуки, таким способом мы детектируем попытку установки исполняемых прав, думаю вы помните.
Далее наш любимый CaptureStackBackTrace, с которым уже сталкиваемся 3 раз, все также проверяем вызывается ли функция из .text или нет. Если нет - шеллкод, передаем 20 баллов в структуру Corecial.

Работа HWDB-хуков в новом потоке.
Помните, я говорил вам про новый поток, где малварь может выполнять свои операции? Так вот, наши хуки будут работать и в нем.
У нас есть переменная g_pThreadHandleAddr, я про нее объяснял, напомню, что - это указатель на хэндл вновь созданного потока, то-есть указатель на уже НОВЫЙ поток. Когда срабатывает брейкпоинт на адресе возврата из NtCreateThreadEx.
1769005228893.png

Я написал код, который устанавливает все те же хуки только уже в новом потоке:

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;
}
С помощью GetThreadContext мы проверяем, установлены ли хуки, заполнены ли регистры, если нет - мы заполняем их все теми же значениями уже в НОВОМ потоке, задавая их через SetThreadContext.
1769005618359.png


Думаю, принцип работы теперь понятен.
Вообще тема HWDB-хуков довольно трудная для новичков, так что если вы, что-то не поняли, пожалуйста, гуглите, я пытаюсь объяснять максимально простым языком.

Обратите внимание на эту строчку:
C:
ThreadCtxNew.Dr7 = (1ULL << 0) | (1ULL << 2) | (1ULL << 6);
Как уже было написано в статье ранее, через dr7 регистр мы активируем наши брейкпоинты
0 - dr0
2 - dr1
6 - dr3
А если еще проще: включаем их.

Далее:
C:
        ctx->Dr2 = 0;
        ctx->Dr7 &= ~(1ULL << 4);
Мы очищаем временные структуры, еще проще: просто сбрасываем 4 бит, т.к он больше не нужен.

Теперь, обратите внимание обратно на функцию SetHardwareBreakpoint которую мы уже реализовали, теперь регистры содержат в себе адреса, эти адреса мы передаем в переменную address, которая их теперь уже хранит в себе:
1769005840922.png


Фух, так.
Полный код нашего 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 == 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)
    {
        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;
}

Стоит помнить, что малварь может попытаться вызвать GetThreadContext, посмотреть состояние регистров, Для защиты от этого мы ставим хук на GetThreadContext и возвращаем фейковую информацию. Когда функция вызывается с флагом CONTEXT_DEBUG_REGISTERS, мы обнуляем все debug-регистры в возвращаемой структуре Вы должны помнить последовательность реализации IAT хука. Инициализируем структуру:
1769008005633.png

Создаем хук:

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;
}
Таким образом, малварь, проверяющая регистры через GetThreadContext, увидит, что аппаратные брейкпоинты не установлены, хотя на самом деле они активны и работают, все просто.
Важно помнить, что хук применяется только если в ContextFlags установлен CONTEXT_DEBUG_REGISTERS.

Тоже самое реализовываем с SetThreadContext, когда малварь увидит, что регистры пустые, она может подумать что это обманка и на всякий случай занулить их через SetThreadContext, но не тут то было, на ней тоже хук.
1769008208094.png

Ставим хук:

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);
}
Тут тоже все довольно просто, мы создаем условную конструкцию, где проверяем, пытается ли вызывающий код установить, debug-регистры через SetThreadContext. Если флаг CONTEXT_DEBUG_REGISTERS установлен и значения регистров dr0/dr1/dr2 равны нулю, это подозрительно, скорее всего малварь может пытаться сбросить наши брейкпоинты. В таком случае мы добавляем 50 баллов к счетчику угроз, но все же разрешаем вызов оригинальной функции, чтобы не прервать работу легитимных приложений.

Окей, первая защита реализовано, теперь про вторую.
Малварь может просто взять и поставить свой обработчик поверх нашего, только сделать это так, чтобы его обработчик был в приоритете, потом принять исключение #DB от процессора.
Тут тоже все очень просто, просто поставим хук на обработчик с проверкой параметра First. Инициализируем структуру:
1769008493930.png

Хукаем:
1769008607437.png

Тут все просто, если параметр First НЕ равен нулю, то-есть малварь пытается выставить свою обработчик в приоритет, то мы передаем в структуру Corecial 25 очков. Вообще, стоило бы принять более жесткие меры, но все же, пусть будет так.

Реализация функции поиска IAT.
Теперь нам нужно реализовать функцию InstallIATHook, в ней будет происходить установка непосредственного нашего обработчика, код:

C:
void InstallIATHook() {

    AddVectoredExceptionHandler(1, HardwareBreakpointHandler);

    addrNtCreateThread = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtCreateThreadEx");
    addrNtAllocate = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");
    addrNtProtect = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtProtectVirtualMemory");

    if (addrNtCreateThread)
    {
        SetHardwareBreakpoint(addrNtCreateThread, 0);
    }

    if (addrNtAllocate)
    {
        SetHardwareBreakpoint(addrNtAllocate, 1);
    }

    if (addrNtProtect)
    {
        SetHardwareBreakpoint(addrNtProtect, 3);
    }

}
Так, тут все просто
Эта функция выполняет настройку и установку всех хуков, как IAT, так и HWDB.
Но она пока-что не полная, мы должны будем в ней реализовать поиск FirstThunk модуля, где находится IAT процесса.
Объясню что делает функция на данный момент времени:
1) Получает оригинальные адреса функция через GetProcAdress;
2) Регистрирует VEH-обработчик через AddVectoredExceptionHandler, HardwareBreakpointHandler будет перехватывать исключения #DB от аппаратных брейкпоинтов;
3) Непосредственно установка HWDB-хуков через SetHardwareBreakpoint:
1769007390837.png

То-есть, NtCreareThreadEx -> адрес в dr0(по индексированию равен нулю), NtAllocateVirtualMemory -> адрес в dr1(по индексированию равен 1), NtProtectVirtualMemory -> адрес в dr3(по индексированию равен 3).

Двигаемся дальше, теперь нам нужно реализовать еще одну деталь, она будет искать нужные нам библиотеки для хуков, которые хранят в себе функции, в памяти.
Находится она в Firsthunk:
C:
    HMODULE hBase = GetModuleHandle(NULL);
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hBase;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hBase + dosHeader->e_lfanew);

    IMAGE_DATA_DIRECTORY importDir = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    if (importDir.VirtualAddress == 0) return;

    PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)hBase + importDir.VirtualAddress);

C:
HMODULE hBase = GetModuleHandle(NULL);
Первым делом я достаю хендл нашего модуля с помощью функции GetModuleHandle и указываю ей null(0), это означает что мы получаем хендл нашего основного exe файла процесса куда нас заинжектили. Кстати да, мы в этой же статье реализуем инжектор, который будет инжектировать нас в потенциально вредоносный процесс.

Дальше берем и парсим наш PE заголовок стандартным образом:
Сначала достаем DOS заголовок приводим наш хендл к структуре IMAGE_DOS_HEADER

C:
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hBase;
Дальше нам нужно достать NT заголовки стандартного смещения в e_lfanew.
1769007705372.png

Теперь нам нужно достать директорию импорта чтобы найти нашу таблицу IAT
Она находится в OptionalHeader в DataDirectory под индексом IMAGE_DIRECTORY_ENTRY_IMPORT:
1769007810026.png

C:
IMAGE_DATA_DIRECTORY importDir = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

Далее проверяем, что директория импорта вообще существует если VirtualAddress равен нулю то выходим:
C:
if (importDir.VirtualAddress == 0) return;
И после чего создаем указатель на массив структур IMAGE_IMPORT_DESCRIPTOR фактически это и есть наша IAT(таблица импорта)
C:
PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)hBase + importDir.VirtualAddress);

После того как мы спарсили PE-IAT, мы должны добавить все необходимые функции, точнее загрузить их и активировать IAT хуки:
C:
    OriginalPShellExecuteA = (PShellExecuteA)GetProcAddress(GetModuleHandleA("shell32.dll"), "ShellExecuteA");
    OriginalSetThreadContext = (PSetThreadContext)GetProcAddress(GetModuleHandleA("kernel32.dll"), "SetThreadContext");
    OriginalGetThreadContext = (PGetThreadContext)GetProcAddress(GetModuleHandleA("kernel32.dll"), "GetThreadContext");
    OriginalAddVectoredExeceptionHandler = (PAddVectoredExceptionHandler)GetProcAddress(GetModuleHandleA("kernel32.dll"), "AddVectoredExceptionHandler");

    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    OriginalNtProtectVirtualMemory = (PNtProtectVirtualMemory)GetProcAddress(hNtdll, "NtProtectVirtualMemory");
    OriginalNtWriteVirtualMemory = (PNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
    OriginalNtAllocateVirtualMemory = (PNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
    OriginalPZwUnmapViewOfSection = (PZwUnmapViewOfSection)GetProcAddress(hNtdll, "ZwUnmapViewOfSection");

    while (importDesc->Name) {
        char* dllName = (char*)((BYTE*)hBase + importDesc->Name);
        PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((BYTE*)hBase + importDesc->FirstThunk);

        if (_stricmp(dllName, "shell32.dll") == 0) {
            while (thunk->u1.Function) {
                if ((PVOID)thunk->u1.Function == (PVOID)OriginalPShellExecuteA) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)HookShellExecuteA;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                thunk++;
            }
        }
        else if (_stricmp(dllName, "kernel32.dll") == 0) {
            while (thunk->u1.Function) {
                if ((PVOID)thunk->u1.Function == (PVOID)OriginalSetThreadContext) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)HookSetThreadContext;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                else if ((PVOID)thunk->u1.Function == (PVOID)OriginalGetThreadContext) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)HookGetThreadContext;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                else if ((PVOID)thunk->u1.Function == (PVOID)OriginalAddVectoredExeceptionHandler) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)HookAddVectoredExceptionHandler;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                thunk++;
            }
        }
        else if (_stricmp(dllName, "ntdll.dll") == 0) {
            while (thunk->u1.Function) {
                PVOID hookAddr = NULL;
                if ((PVOID)thunk->u1.Function == (PVOID)OriginalNtWriteVirtualMemory)
                    hookAddr = (PVOID)HookNtWriteVirtualMemory;
                else if ((PVOID)thunk->u1.Function == (PVOID)OriginalNtProtectVirtualMemory)
                    hookAddr = (PVOID)HookNtProtectVirtualMemory;
                else if ((PVOID)thunk->u1.Function == (PVOID)OriginalPZwUnmapViewOfSection)
                    hookAddr = (PVOID)HookZwUnmapViewOfSection;

                if (hookAddr) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)hookAddr;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                thunk++;
            }
        }
        importDesc++;
    }
}
1769009327482.png


Тут все просто, мы для каждой dll проверяем ее имя и заменяем адреса функций в IAT.
Каждая замена выполняется с временным изменением прав доступа на странице памяти (PAGE_READWRITE), чтобы модифицировать IAT и установить хуки, после чего права восстанавливаются. Это классический подход IAT-хукам.

Мини анти-отладка
Реализовывать отладку мы будем вместо функции IsDebuggerPresent, вручную через поиск в PEB.
На языке ассемблера это выглядит так:

Код:
    mov rax, [gs:0x60]
    cmp byte [rax+2], 1
    je debugger_found_opcode
То-есть, по смещению 0x60 лежит указатель на peb
Потом, смещение на 0x02 байта где находится байт проверки на отладку
Послечего проверка установлена ли однерка(3 строчка) peb флаг устанавливает однерку если наш запущена под отладчиком.

На Си это выглядело бы так:

C:
void anti()
{
    PPEB pPeb = (PPEB)__readgsqword(0x60);
    if (pPeb->BeingDebugged)
    {
        Sleep(1400);
        TerminateProcess(GetCurrentProcess(), 0);
    }
}
Редко кто применяет анти-отладочные техники к .dll, но, я просто показал как это работает.
Не советую использовать эту функцию.

После чего вызываем все функции внутри DllMain:
1769009771508.png

Готово, основная .dll подготовлена, она будет инжектироваться в процесс и проводить анализ, выставлять хуки и наблюдать за поведение.
Исходный код:
C:
#include <stdio.h>
#include <windows.h>
#include <stdint.h>
#include <string.h>
#include <winternl.h>

typedef struct {
    LONG score;
} Corecial;
static Corecial global_x = { 0 };

void add_score(Corecial* x, int delta) {
    LONG new_score = InterlockedAdd(&x->score, delta);
    if (new_score >= 100) {
        TerminateProcess(GetCurrentProcess(), 0);
    }
}

typedef BOOL(WINAPI* PGetThreadContext)(
    HANDLE hThread,
    LPCONTEXT lpContext
    );
PGetThreadContext OriginalGetThreadContext = NULL;

typedef BOOL(WINAPI* PSetThreadContext)(
    HANDLE hThread,
    const CONTEXT* lpContext
    );
PSetThreadContext OriginalSetThreadContext = NULL;

typedef PVOID(WINAPI* PAddVectoredExceptionHandler)(
    ULONG First,
    PVECTORED_EXCEPTION_HANDLER Handler
    );
PAddVectoredExceptionHandler OriginalAddVectoredExeceptionHandler = NULL;


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;

PVOID addrNtProtect = NULL;
PVOID addrNtAllocate = NULL;
PVOID addrNtCreateThread = NULL;
__declspec(thread) PHANDLE g_pThreadHandleAddr = NULL;

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 == 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)
    {
        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;
}

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);
}

typedef HINSTANCE(WINAPI* PShellExecuteA)(HWND, LPCSTR, LPCSTR, LPCSTR, LPCSTR, INT);
PShellExecuteA OriginalPShellExecuteA = NULL;

typedef NTSTATUS(NTAPI* PZwUnmapViewOfSection)(HANDLE, PVOID);
PZwUnmapViewOfSection OriginalPZwUnmapViewOfSection = NULL;

typedef NTSTATUS(NTAPI* PNtProtectVirtualMemory)(
    _In_    HANDLE  ProcessHandle,
    _Inout_ PVOID* BaseAddress,
    _Inout_ PSIZE_T RegionSize,
    _In_    ULONG   NewProtection,
    _Out_   PULONG  OldProtection
    );
PNtProtectVirtualMemory OriginalNtProtectVirtualMemory = NULL;

typedef NTSTATUS(NTAPI* PNtWriteVirtualMemory)(
    IN HANDLE ProcessHandle,
    IN PVOID BaseAddress,
    IN PVOID  Buffer,
    IN ULONG  NumberOfBytesToWrite,
    OUT PULONG NumberOfBytesWritten OPTIONAL
    );
PNtWriteVirtualMemory OriginalNtWriteVirtualMemory = NULL;

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;
}
PVOID WINAPI HookAddVectoredExceptionHandler(ULONG First, PVECTORED_EXCEPTION_HANDLER Handler)
{
    if (First != 0)
    {
        add_score(&global_x, 25);
    }
    return OriginalAddVectoredExeceptionHandler(First, Handler);
}

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);
}



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);
}

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);
}

NTSTATUS HookZwUnmapViewOfSection(HANDLE ProcessHandle, PVOID BaseAddress)
{
    if (ProcessHandle != GetCurrentProcess() && BaseAddress != NULL)
    {
        add_score(&global_x, 50);
    }
    return OriginalPZwUnmapViewOfSection(ProcessHandle, BaseAddress);
}


HINSTANCE WINAPI HookShellExecuteA(HWND hwnd, LPCSTR lpOperation, LPCSTR lpFile, LPCSTR lpParameters, LPCSTR lpDirectory, INT nShowCmd) {

    if (lpFile != NULL)
    {
        if (_stricmp(lpFile, "fodhelper.exe") == 0)
        {
            add_score(&global_x, 35);
        }
    }
    return OriginalPShellExecuteA(hwnd, lpOperation, lpFile, lpParameters, lpDirectory, nShowCmd);
}

void InstallIATHook() {
    AddVectoredExceptionHandler(1, HardwareBreakpointHandler);

    addrNtCreateThread = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtCreateThreadEx");
    addrNtAllocate = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");
    addrNtProtect = GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtProtectVirtualMemory");

    if (addrNtCreateThread)
    {
        SetHardwareBreakpoint(addrNtCreateThread, 0);
    }

    if (addrNtAllocate)
    {
        SetHardwareBreakpoint(addrNtAllocate, 1);
    }

    if (addrNtProtect)
    {
        SetHardwareBreakpoint(addrNtProtect, 3);
    }

    HMODULE hBase = GetModuleHandle(NULL);
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hBase;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hBase + dosHeader->e_lfanew);

    IMAGE_DATA_DIRECTORY importDir = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    if (importDir.VirtualAddress == 0) return;

    PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)hBase + importDir.VirtualAddress);

    OriginalPShellExecuteA = (PShellExecuteA)GetProcAddress(GetModuleHandleA("shell32.dll"), "ShellExecuteA");
    OriginalSetThreadContext = (PSetThreadContext)GetProcAddress(GetModuleHandleA("kernel32.dll"), "SetThreadContext");
    OriginalGetThreadContext = (PGetThreadContext)GetProcAddress(GetModuleHandleA("kernel32.dll"), "GetThreadContext");
    OriginalAddVectoredExeceptionHandler = (PAddVectoredExceptionHandler)GetProcAddress(GetModuleHandleA("kernel32.dll"), "AddVectoredExceptionHandler");

    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    OriginalNtProtectVirtualMemory = (PNtProtectVirtualMemory)GetProcAddress(hNtdll, "NtProtectVirtualMemory");
    OriginalNtWriteVirtualMemory = (PNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
    OriginalPZwUnmapViewOfSection = (PZwUnmapViewOfSection)GetProcAddress(hNtdll, "ZwUnmapViewOfSection");

    while (importDesc->Name) {
        char* dllName = (char*)((BYTE*)hBase + importDesc->Name);
        PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((BYTE*)hBase + importDesc->FirstThunk);

        if (_stricmp(dllName, "shell32.dll") == 0) {
            while (thunk->u1.Function) {
                if ((PVOID)thunk->u1.Function == (PVOID)OriginalPShellExecuteA) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)HookShellExecuteA;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                thunk++;
            }
        }
        else if (_stricmp(dllName, "kernel32.dll") == 0) {
            while (thunk->u1.Function) {
                if ((PVOID)thunk->u1.Function == (PVOID)OriginalSetThreadContext) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)HookSetThreadContext;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                else if ((PVOID)thunk->u1.Function == (PVOID)OriginalGetThreadContext) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)HookGetThreadContext;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                else if ((PVOID)thunk->u1.Function == (PVOID)OriginalAddVectoredExeceptionHandler) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)HookAddVectoredExceptionHandler;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                thunk++;
            }
        }
        else if (_stricmp(dllName, "ntdll.dll") == 0) {
            while (thunk->u1.Function) {
                PVOID hookAddr = NULL;
                if ((PVOID)thunk->u1.Function == (PVOID)OriginalNtWriteVirtualMemory)
                    hookAddr = (PVOID)HookNtWriteVirtualMemory;
                else if ((PVOID)thunk->u1.Function == (PVOID)OriginalNtProtectVirtualMemory)
                    hookAddr = (PVOID)HookNtProtectVirtualMemory;
                else if ((PVOID)thunk->u1.Function == (PVOID)OriginalPZwUnmapViewOfSection)
                    hookAddr = (PVOID)HookZwUnmapViewOfSection;

                if (hookAddr) {
                    DWORD oldProtect;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), PAGE_READWRITE, &oldProtect);
                    thunk->u1.Function = (DWORD_PTR)hookAddr;
                    VirtualProtect(&thunk->u1.Function, sizeof(PVOID), oldProtect, &oldProtect);
                }
                thunk++;
            }
        }
        importDesc++;
    }
}
void anti()
{
    PPEB pPeb = (PPEB)__readgsqword(0x60);
    if (pPeb->BeingDebugged)
    {
        Sleep(1400);
        TerminateProcess(GetCurrentProcess(), 0);
    }
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
    if (reason == DLL_PROCESS_ATTACH) {
        add_score(&global_x, 1);
        anti();
        InstallIATHook();
    }
    return TRUE;
}
Компилируем программу и получаем путь к ней:
1769010164998.png

Этот путь нам пригодится для реализации инжектора.

Инжектор
Инжектор должен будет инжектировать наш .dll в процесс, чтобы тот начал анализ
Я реализовал максимально простой инжектор с путем до нашей .dll:

C:
#include <windows.h>
#include <stdio.h>
#include <locale.h>

int main() {
    setlocale(LC_ALL, "RU");
    char dllPath[] = "C:\\Users\\Admin\\source\\repos\\xss-forum.dll\\x64\Debug\\xss-forum.dll.dll";
    DWORD PID;
    printf("=== edr found started ===\n");

    printf("enter pid process: ");
    scanf_s("%d", &PID);


    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
    if (hProcess == NULL) {
        printf("error: code: %lu\n", GetLastError());
        return 1;
    }

    LPVOID pAmyat = VirtualAllocEx(hProcess, NULL, strlen(dllPath) + 1, MEM_COMMIT, PAGE_READWRITE);
    if (pAmyat == NULL) {
        printf("error.\n");
        CloseHandle(hProcess);
        return 1;
    }

    if (!WriteProcessMemory(hProcess, pAmyat, (LPVOID)dllPath, strlen(dllPath) + 1, NULL)) {
        printf("Error.\n");
        VirtualFreeEx(hProcess, pAmyat, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return 1;
    }

    HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
    LPTHREAD_START_ROUTINE pLoadLibrary = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryA");
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pLoadLibrary, pAmyat, 0, NULL);
    if (hThread == NULL) {
        printf("Error: %lu\n", GetLastError());
        VirtualFreeEx(hProcess, pAmyat, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return 1;
    }

    printf("injected sucessfuly\n");
    printf("monitoring started\n");
    while (1) {
        Sleep(1000);
    }

    CloseHandle(hThread);
    WaitForSingleObject(hThread, INFINITE);
    VirtualFreeEx(hProcess, pAmyat, 0, MEM_RELEASE);
    CloseHandle(hProcess);

    return 0;
}

Давайте проверим работоспособность инжектора:
1769010541851.png

Отлично, все прекрасно работает.

Теория про канал логирования(IPC)
IPC(именованный канал) - это общая область памяти в ядре Windows, задача которого выглядеть как обычный файл, но работать как канал.
1) У канала есть свой адрес;
2) Также есть сервер, в нашем случае это наш инжектор. Он создает канал и слушает его. Как диспетчер, который ждет звонка.
3) Клиент, клиент это наша .dll. Она подключается к уже созданному каналу, быстро вбрасывает сообщение, информацию касаемо процесса и отключается.
То-есть таким методом мы можем получать информацию от edr.dll всю информацию касательно процесса, в которую она была инжектирована.
Можно будет создать .txt файл на диске, а можно просто слать логи на другой ПК через сервер, но это труднее.
Подробнее про него в будущем, 2-3 частях.


Также принимаю обоснованную критику статьи.
Удачи.
 

Вложения

  • 1768993957198.png
    1768993957198.png
    40.2 КБ · Просмотры: 17
  • 1768994483566.png
    1768994483566.png
    14.3 КБ · Просмотры: 16
  • 1768999100709.png
    1768999100709.png
    32.3 КБ · Просмотры: 15
  • 1769005455678.png
    1769005455678.png
    23.9 КБ · Просмотры: 15
  • 1769011763322.png
    1769011763322.png
    36 КБ · Просмотры: 15
Последнее редактирование:
поясните, пожалуйста, в чем смысл сего стенда? я искренне не понимаю.
предварительное тестирование малвари?

а окончательное? самопал ведь не заменит Crowdstrike Falcon (с доп.)...
 
поясните, пожалуйста, в чем смысл сего стенда? я искренне не понимаю.
предварительное тестирование малвари?

а окончательное? самопал ведь не заменит Crowdstrike Falcon (с доп.)...
Обучение EDR-разработке.
Для многих подобная статья является очень полезной, в частности для тех, кто увлекается EDR-разработкой, или просто для тех, кому интересно что происходит под капотом.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Обучение EDR-разработке.
Для многих подобная статья является очень полезной, в частности для тех, кто увлекается EDR-разработкой, или просто для тех, кому интересно что происходит под капотом.
это рерайт с другого форума
 
это рерайт с другого форума
С дюти? Gemfory это мой акк если что, и я эту статью там не выкладывал, можешь проверить.
Писалась вручную мною именно для xss.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх