Приветствую, камрады! Меня зовут Кирилл Сотников, я работаю в Центре Кибербезопасности и Защиты Ростелекома. И сегодня я хочу поделиться небольшим исследованием по отключению/обходу антивирусов и EDR в операционной системе Windows.
Введение
Исследование было бы невозможным без следующих материалов:
Blinding EDR On Windows – описание и внутреннее устройство системы нотификации о создании/завершении процессов и потоков и ее отключение.
-
Part 1: Fs Minifilter Hooking – описание и внутреннее устройство минифильтров файловой системы и их перехват.
Мое почтение!
Билли установил единые правила как для средств защиты, так и для вредоносного программного обеспечения на игровом поле ядра Windows, и все должны их соблюдать. А кто не соблюдает, тот получит BS0D на экране от Patchguard (конечно, его можно обойти, но это отдельная тема для обсуждения). Средства защиты точно будут соблюдать эти правила в режиме ядра.
Правил же для пользовательского режима не существует. Обладая соответствующими привилегиями (используя неправильные настройки безопасности или эксплуатируя ошибки), можно делать что угодно. Исключения составляют защищенные процессы, до которых тяжеловато дотянуться (но все же можно).
Рассмотрим, как работают средства защиты.
Принципы работы средств защиты
Все современные средства защиты работают в двух режимах: режиме ядра и режиме пользователя.
Режим ядра
Для контроля и обнаружения работы вредоносного программного обеспечения и аномальной активности операционная система предоставляет возможности регистрировать функции обработки событий, которые происходят в системе. Основные типы используемых событий:
события создания/завершения процессов;
события создания/завершения потоков;
события работы файловой системы: создание, удаление файлов, обращение к ним, получение списков файлов и директорий и т.д., так называемые минифильтры ФС.
Режим пользователя
В режиме пользователя средство защиты инжектирует свою DLL в недоверенный контролируемый процесс. Эта DLL осуществляет перехват опасных API функций, и на основании результата анализа принимается решение о вредоносности процесса.
События файловой системы используются для статического анализа файлов и принятия решения об их вредоносности, а события, связанные с созданием/завершением потоков и процессов, используются для динамического/поведенческого анализа процесса.
В данной статье я остановился на самой важной части средства защиты – его драйвере, который осуществляет регистрацию и обработку событий безопасности. Отключение от оповещения на эти события, позволяет заключить средство защиты в “песочницу”, в которой оно не будет видеть никаких событий происходящих в системе.
Итак, поехали!
События создания/завершения процессов и потоков
За подписку на события создания/завершения процессов отвечают следующие функции NTAPI:
NTSTATUS PsSetCreateProcessNotifyRoutine(
[in] PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
[in] BOOLEAN Remove
);
NTSTATUS PsSetCreateProcessNotifyRoutineEx(
[in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
[in] BOOLEAN Remove
);
NTSTATUS PsSetCreateProcessNotifyRoutineEx2(
[in] PSCREATEPROCESSNOTIFYTYPE NotifyType,
[in] PVOID NotifyInformation,
[in] BOOLEAN Remove
);
Важным параметром этих функций является NotifyRoutine
(у первых двух функций) и NotifyInformation
(у последней функции), который является адресом функции обратного вызова (callback-функции) обработчика событий.
Последний параметр функций указывает на добавление нового обработчика или удаление старого (уже зарегистрированного).
После регистрации драйвером средства защиты callback-функции, отвечающей за обработку событий, всякий каждый раз, когда создается/завершается процесс, ему приходит уведомление. И драйвер может принять решение о разрешении его создания и инжекту своей DLL в этот процесс для динамического/поведенческого анализа.
В памяти ядра Windows существуют специальная таблица, в которой находятся адреса всех зарегистрированных обработчиков этих событий.
Все три эти функции вызывают одну функцию – PspSetCreateProcessNotifyRoutine
, которая осуществляет добавление/удаление из этой таблицы обработчика событий (рисунок 1).
На рисунке 1 выделен фрагмент кода, который получает адрес этой таблицы. По этому адресу памяти мы видим массив указателей на callback-функции – обработчики событий создания/завершения процессов (рисунок 2).
Зная, где находится этот массив, мы сможем повлиять на обработчики событий:
удалять их из списка путем обнуления указателя;
изменять указатель в списке;
отключить нотификацию легальным способом через NTAPI;
пропатчить саму функцию обработчика (например, инструкцией ret).
Аналогично происходит подписка/удаление подписки на события о создании/завершении потоков с помощью NTAPI:
NTSTATUS PsSetCreateThreadNotifyRoutine(
[in] PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);
NTSTATUS PsRemoveCreateThreadNotifyRoutine(
[in] PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
);
Их параметрами являются адреса callback-функций обработки событий создания/завершения потоков в системе.
Минифильтры файловой системы
Регистрация драйвера минифильтра файловой системы выполняется с помощью следующей NTAPI:
NTSTATUS FLTNTAPI FltRegisterFilter(
[in] PDRIVER_OBJECT Driver,
[in] const FLT_REGISTRATION *Registration,
[out] PFLT_FILTER *RetFilter
);
Параметр const FLT_REGISTRATION *Registration
, является указателем на структуру регистрации драйвера минифильтра.
В данной структуре находится поле структуры – const FLT_OPERATION_REGISTRATION *OperationRegistration
.
Данное поле структуры является массивом еще одних структур FLT_OPERATION_REGISTRATION
, каждый элемент которого описывает две callback-функции обработчика событий. А именно PreOperation
– функции, которая выполняется перед выполнением этой операции на файловой системе и PostOperation
– функции, которая выполняется после выполнения операции на файловой системе.
Именно эти callback-функции регистрируются средством защиты для мониторинга действий на файловой системе, действие которых далее и рассмотрим.
Из проведенного исследования 2, стало известно, что в ядре эти функции обработчики представлены в виде двусвязного списка (LIST_ENTRY
) структурCALLBACK_NODE
в недокументированной структуре FLT_INSTANCE
(рисунок 3).
Одним из полей этой структуры является CallbackNodes
– массив структур CALLBACK_NODE
. Этот массив содержит адреса всех функций обратных вызовов, принадлежащие экземпляру минифильтра.
Для перечисления экземпляров драйверов минифильтров, т.е. для получения FLT_INSTANCE
используется функция FltEnumerateInstances
.
Рассмотрим структуру CALLBACK_NODE
(рисунок 4):
Поля данной структуры:
PreOperation
иPostOperation
– указывают, вызываются ли обратные вызовы до или после операции.CallbackLinks
– это двусвязный список структурCALLBACK_NODE
(рисунок 5 и 6).
Взаимосвязь между структурами минифильтра, представлена на рисунке 5.
Или в обобщенном виде рисунок 6.
Поскольку структура FLT_INSTANCE
является недокументированной, то соответственно тяжело найти смещение массива CALLBACK_NODE
. Для получения этого смещения мы можем воспользоваться следующим методом.
Зарегистрируем собственный минифильтр с набором callback-функций, чтобы динамически найти смещение CallbackNodes
. После нахождения массива CALLBACK_NODE
мы можем просто перехватить PreOperation
и PostOperation
функции.
Практика
Для своего проекта я использовал два репозитория на GitHub, которые указаны в статьях 1 и 2. В них по отдельности реализован почти весь наш функционал:
https://github.com/uf0o/windows-ps-callbacks-experiments – проект по отключению нотификации о создания/удаления процессов и потоков.
https://github.com/SHA-MRIZ/FsMinfilterHooking – проект по перехвату минифильтров файловой системы.
Нам осталось их объединить. За основу я взял первый проект и добавил в него файлы из второго проекта. Дополнительно пришлось заменить INF файл из второго проекта. Поскольку данный проект является минифильтром файловой системы, в нем находятся дополнительные секции описывающие параметры минифильтра и зависимости от драйвера FltMgr.
Соответственно для объединенного драйвера нужны одни точки входа для следующих функций:
DriverEntry
– функция начальной инициализации драйвера;DobroUnload
– функция вызываемая при выгрузке драйвера;DobroDeviceControl
– функция обработки запросов из режима пользователя через функциюDeviceIoControl
.
Дополнительно для выполнения нашего функционала я реализовал следующие функции:
FSH_GetListFilters
– получение всех установленных минифильтров в файловой системе (возвращается пользователю по запросу в виде списке для отображения).
FSH_GetFSMinifilterByIndex
– получение имени минифильтра по его индексу, когда пользователь выбирает индекс минифильтра, который нужно перехватить.
Для отключения нотификации минифильтра файловой системы средства защиты после перехвата PreOperation
, функция перехватчик должна возвращать FLT_PREOP_SUCCESS_NO_CALLBACK
и имеет следующий вид:
FLT_PREOP_CALLBACK_STATUS HookPreOperationFunction(
PFLT_CALLBACK_DATA Data,
PCFLT_RELATED_OBJECTS FltObjects,
PVOID* CompletionContext)
{
// Входные параметры не используются
UNREFERENCED_PARAMETER(Data);
UNREFERENCED_PARAMETER(CompletionContext);
UNREFERENCED_PARAMETER(FltObjects);
// Disable FS minifilter :-)
return FLT_PREOP_SUCCESS_NO_CALLBACK; // Возвращаем значение означающее, что постобработчик не нужен.
}
А для перехвата PostOperation
, функция перехватчик имеет следующий вид:
FLT_POSTOP_CALLBACK_STATUS HookPostOperationFunction(
PFLT_CALLBACK_DATA Data,
PCFLT_RELATED_OBJECTS FltObjects,
PVOID CompletionContext,
FLT_POST_OPERATION_FLAGS Flags)
{
// Входные параметры не используются
UNREFERENCED_PARAMETER(CompletionContext);
UNREFERENCED_PARAMETER(Flags);
UNREFERENCED_PARAMETER(Data);
UNREFERENCED_PARAMETER(FltObjects);
// Disable FS minifilter :-)
return FLT_POSTOP_FINISHED_PROCESSING; // Просто выход из функции
}
Поскольку добавление нотификации о создании/завершении процесса, может быть выполнено одной из трех функций (PsSetCreateProcessNotifyRoutine, PsSetCreateProcessNotifyRoutineEx, PsSetCreateProcessNotifyRoutineEx2
), а у нас имеется только адрес функции, полученный из PspSetCreateProcessNotifyRoutine
, по этому адресу невозможно определить тип функции. Для его удаления я просто выполнил перебор всех трех типов функций, и та, которая выполняется без ошибки, и есть наш тип нотификации:
status =
PsSetCreateProcessNotifyRoutineEx2(PsCreateProcessNotifySubsystems, (PVOID)NotifyAddr, TRUE);
if (NT_SUCCESS(status)) // Если успех, то этот тип функции
{
…
}
status =
PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)NotifyAddr, TRUE);
if (NT_SUCCESS(status)) // Если успех, то этот тип функции
{
…
}
status =
PsSetCreateProcessNotifyRoutine((PCREATE_PROCESS_NOTIFY_ROUTINE)NotifyAddr, TRUE);
if (NT_SUCCESS(status)) // Если успех, то этот тип функции
{
…
}
После сборки проекта у нас должен получиться драйвер и клиентское приложение, которое управляет драйвером.
Установку и запуск драйвера можно выполнить с помощью следующих команд:
InfDefaultInstall.exe Dobro.inf
– регистрация драйвера в SCM.
REG ADD "HKLM\SYSTEM\CurrentControlSet\Services\Dobro" /v "ImagePath" /t REG_EXPAND_SZ /d "\??\<full_path_to_driver>\Dobro.sys" /f
– изменение пути до драйвера в реестре.
Sc start dobro
– запуск драйвера.
Для тех, кто пользуется мышкой:
Правой клавишей на inf файл и в контекстном меню выбрать установить.
Затем в regedit.exe
меняем значение ключа ImagePath
, указывающего на путь до файла драйвера, находящегося в разделе реестра: HKLM\SYSTEM\CurrentControlSet\Services\Dobro
.
Исследования я проводил на одном из антивирусов (разглашать название не буду). После проведения тестирования, дабы выяснить реакцию антивируса на снятие установленной им нотификации, выяснилось следующее.
Перехват нотификации минифильтра файловой системы позволил отключить антивирус от статического анализа файлов, т.е. теперь можно спокойно делать все что угодно на файловой системе, антивирус ничего не проверяет. Проверялось путем копирования на файловую систему импланта (т.н. Grunt) Covenant С2 – фреймворка для проведения тестирования на проникновение.
После удаления нотификации о создании/завершении процесса, стало невозможно ничего запустить, система впадает в "ступор" – это лечится перезагрузкой. Чтобы все было хорошо, необходимо сначала удалить нотификацию о создании/завершении потоков, а затем нотификацию о создании/завершении процессов. После этого антивирус работает как ни в чем не бывало, но уже не проверяя вновь запускаемые процессы и потоки путем инжектирования своей DLL для динамического – поведенческого анализа программ, позволяя запускать вредоносное программное обеспечение "без шума и пыли".
Проводя дальнейшее тестирование, выяснилось, что антивирус перестает обновляться, паникуя, что не может установить доверенное соединение с сервером. Видимо, при запуске процесса обновления антивирус должен получить нотификацию о создании своего процесса обновления, чтобы проконтролировать доверенность процесса обновления.
Чтобы все было стерильно, я решил добавить дополнительный функционал драйвера по перехвату функции нотификации о создании/завершении процессов и потоков и фильтрации по имени процесса. Когда создается новый процесс, мы проверяем его имя, если он принадлежит средству защите, то переправим нотификацию ему, если нет, ничего не делаем.
Я реализовал это так:
Удаляем нотификацию с помощью NTAPI
PsSetCreateProcessNotifyRoutine
илиPsSetCreateProcessNotifyRoutineEx
, илиPsSetCreateProcessNotifyRoutineEx2
, в зависимости от типа, создаваемого средством защиты обработчика нотификации о создании/завершении процесса.Создаем свой собственный обработчик нотификации о создании/завершении процесса соответствующей NTAPI (
PsSetCreateProcessNotifyRoutine
илиPsSetCreateProcessNotifyRoutineEx
, илиPsSetCreateProcessNotifyRoutineEx2
).В самом обработчике нотификации мы добавляем проверку на имя создаваемого процесса. Если процесс принадлежит средству защиты, то мы отправляем ему уведомление напрямую, путем вызова его функции по соответствующему указателю, иначе просто выходим из функции, ничего не делая.
BOOLEAN IsFiltered = FALSE;
…
IsFiltered = HlpPsIsFilteredPSByName(CreateInfo->ImageFileName, g_filteredPsName); // Проверка фильтрации по имени процесса
if (!IsFiltered)
{
((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)g_OrigPcreateProcessNotifyRoutine)(Process, ProcessId, CreateInfo); // Если фильтровать не надо, вызываем оригинальный обработчик
}
…
return;
После того как я реализовал перехват нотификации о создании/завершения процессов и потоков с фильтрацией по его имени, антивирус спокойно обновился – "в Багдаде все спокойно".
Таким образом, мы заключили антивирус в "песочницу", в которой он будет видеть только события связанные с собственной работой, а все остальные события не будут к нему приходить, путем:
отключения нотификации минифильтра файловой системы;
перехвата с фильтром по имени процесса нотификации о создании/завершении потоков;
перехвата с фильтром по имени процесса нотификации о создании/завершении процессов.
Основные функции драйвера, которые мы можем вызвать через клиентское приложение, представлены на рисунке 6.
В клиенте драйвера, помимо основных функций по снятию/перехвату/удалению нотификаций, реализована функция, тестирующая средство защиты путем создания нового процесса nslookup.exe и инжектирования в него шеллкода, который запускает notepad.exe.
Чтобы средство защиты сразу не "голосило" на этапе копирования в файловую систему, инжектирование осуществляется путем динамического получения адресов функций NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx из библиотеки NTDLL.DLL, при этом имена функций и имя библиотеки предварительно зашифрованы методом XOR ключом ‘$’. Ключ для расшифровки пользователь должен ввести вручную.
Результирующий проект моего исследования представлен на GitHub по адресу: https://github.com/Rostelecom-Red-Team/GoodbyeEDR
Что с запуском кода в ядре
В настоящее время проблем с подписью драйверов и выполнению кода в режиме ядра нет. Используем утекшие сертификаты или уязвимые злые драйвера.
Если вас заинтересовала эта тема, то советую почитать:
Windows: достучаться до железа (https://habr.com/ru/post/527006/);
Опасные 3rd-party драйверы в вашей системе или LOLDrivers (https://habr.com/ru/company/dsec/blog/468691/).
Лично я использовал утекшие сертификаты (если пошукать интернет, особенно китайские форумы, можно найти несколько сертификатов) для подписи драйвера с помощью утилиты TrustAsia.
Данную тему в настоящей статье развивать не будем.
Выводы
Какие я для себя сделал выводы:
Единые правила для ядра операционной системы Windows упрощают понимание внутреннего устройства и работы средств защиты и загоняют их в определенные рамки, которые можно обойти. Вредоносное программное обеспечение не скованно этими правилами, поэтому у него более широкая свобода действий.
То, что попало в ядро и выполняется там, считается доверенным и не проверяется.
На хорошем заканчиваю, всем спасибо.
Автор статьи: https://habr.com/ru/users/Kosmonit/
K36
Для загрузки драйвера нужны права администратора, что ограничивает применение.
Утекшие сертификаты могут быть отозваны.
С уровня ядра можно делать куда более интересные вещи, включая скрытую виртуальную файловую систему, когда файлы в ней находящиеся вообще невидимы для АВ.