
Exploit de prova de conceito para CVE-2022-37969, uma escalada de privilégio local do driver do Windows Common Log File System. Demonstra heap spray, roubo de token e gravação arbitrária no kernel para obter privilégios de SYSTEM.
autores: Ricardo Narvaja & Daniel Kazimirow (Solid)
Apenas para fins de demonstração. O exploit completo funciona em sistemas Windows 11 21H2 vulneráveis.
PoC funcional baseado em informações publicadas anteriormente pela Zscaler
Confira o writeup Understanding the CVE-2022-37969 Windows Common Log File System Driver Local Privilege Escalation.
Passo a passo da exploração:
O cenário utilizado aqui foi Windows 11 21H2 (OS Build 22000.918) clfs.sys v10.0.22000.918
O primeiro passo é criar um arquivo chamado MyLog.blf na pasta pública (%public%), usando a função CreateLogFile():



Em seguida, cria vários arquivos de log com nomes aleatórios usando um loop.
E dentro do loop, chama nossa função getBigPoolInfo():

Ele chama a função NtQuerySystemInformation(), com 0x42 (66 decimal) como primeiro argumento, isso retornará em v5 as informações sobre os raids feitos no bigpool, cuja estrutura é do tipo SYSTEM_BIGPOOL_INFORMATION.

Temos que chamar essa função duas vezes. A primeira retornará um erro, mas nos dará o tamanho correto do buffer para chamar a segunda vez e obter as informações desejadas.

v5 receberá as informações da estrutura SYSTEM_BIG_POOL_INFORMATION.

O número de alocações no bigpool é armazenado no primeiro campo chamado Count, no segundo campo há um array de estruturas SYSTEM_BIGPOOL_ENTRY.

Então percorremos todas as estruturas em busca da tag "Clfs" e do tamanho 0x7a00.

Armazena em um array chamado kernelAddrArray o VirtualAddress, que é o primeiro campo de cada estrutura que possui a tag CLFS e tamanho 0x7a00. A partir de agora, os pools que atendem ambas as condições serão chamados de “pools certos”.

Além de armazenar cada pool certo no array, armazena o último pool certo encontrado no conteúdo da variável a2, que é usada como argumento da função.

Dessa forma, a2 sempre aponta para o último pool certo com a tag CLFS e tamanho 0x7a00 criado.
A variável v26 sempre armazena o pool certo anterior encontrado, pois é igual a v24 (v26=v24), antes de chamar getBigPoolinfo(), mas v24 é atualizado ao sair dessa chamada com o último pool certo encontrado, e v26 permanece com o pool certo anterior encontrado.

Em seguida, subtrai ambos os endereços e, caso o resultado seja negativo, inverte os operandos para que seja sempre positivo.

Dessa forma, em v32 será armazenada a diferença entre o VirtualAddress dos dois últimos pools certos encontrados.
Depois faz algo semelhante; neste caso, v23 é inicialmente zero, então faz v23 = v32 na primeira vez.

Na próxima vez no loop, v23 ainda tem o mesmo valor e não é zero, então interrompe e vai para cá.

V32 tem a última diferença e v23 a anterior; se forem iguais, sai e incrementa um, mas redefine o contador para zero.
A ideia é encontrar 6 comparações consecutivas de tags CLFS e tamanho 0x7a00 cujas diferenças sejam iguais, e essa diferença será 0x11000. Veremos ao executar que quando encontrar 6 (pois começa do zero) consecutivos com distâncias iguais, dará esse valor de diferença entre eles.


Ali vemos que ele encontrou 6 consecutivos e saiu do loop de criação de arquivos de log.
Na pasta "public" podemos ver os arquivos criados

Nossa função craftFile() abre o arquivo original (MyLog.blf) e o modifica para acionar o bug.

Após modificar o arquivo, é necessário alterar o CRC32, caso contrário, receberemos um erro de arquivo corrompido.
Esse valor está localizado no offset 0x80C do arquivo.

Em seguida, realiza um HeapSpray, usando a função VirtualAlloc() para alocar memória, em endereços arbitrários 0x10000 e 0x5000000 respectivamente, e salvando na segunda alocação (0x10000), o valor 0x5000000, a cada 0x10 bytes.

Usa CreatePipe() para criar um pipe anônimo e chama NtFsControlFile() usando 0x11003c como argumento para adicionar um atributo; posteriormente, você pode chamar essa mesma função com o argumento 0x110038 para lê-lo.
Mais detalhes sobre este método podem ser encontrados AQUI

Ali vemos o buffer de entrada que é o atributo que estamos adicionando; se chamarmos NtFsControlFile() novamente com o argumento 0x11038 na saída, deve retornar este mesmo atributo.

Procurando no pool pela tag do atributo criado (NpAt)


E quando encontra, salva em v30.Pointer o VirtualAddress deste pool.
V30.pointer+24 aponta para o AttributeValueSize no pool do kernel e o salva em um dos HeapSprays que fizemos anteriormente.

A ideia é escrever nesse endereço de kernel+8, para sobrescrever o AttributeValue.


A estrutura PipeAttribute tem como primeiro campo uma LIST_ENTRY que possui 16 bytes, depois um ponteiro para o nome do atributo que tem 8 bytes e então vem em 0x18 (24 decimal) o campo AttributeValueSize, que é o que estamos armazenando no HeapSpray.
Depois disso, carregamos CLFS.sys e ntoskrnl no modo de usuário e, usando GetProcAddress(), encontramos os endereços das funções ClfsEarlierLsn() e SeSetAccessStateGenericMapping().

Em seguida, chamamos a função FindKernelModulesBase() que encontrará a base do kernel de ambos os módulos usando NtquerySystemInformation(), desta vez com o argumento SystemModuleInformation para retornar as informações sobre todos os módulos.

Dessa forma, podemos calcular o offset de cada função e, em seguida, obtê-los no kernel

A função pipeArbitraryWrite() é chamada duas vezes; há uma flag que inicialmente é zero para a primeira chamada e, quando na segunda chamada o valor é 1, alterará os valores do HeapSpray.

Na primeira chamada, no endereço de memória 0x5000000, os seguintes valores são localizados

Lembre-se de que esse valor, além de alocar nesse endereço, é armazenado em nosso HeapSpray.

Assim fica a memória após a primeira chamada, como dissemos, no endereço por volta de 0x5000000

E no HeapSpray, a partir da memória 0x10000, armazenará o ponteiro para AttributeValueSize a cada 0x10 bytes, além do ponteiro para 0x5000000.

Esta sequência acionará o bug:

CreateLogFile() é chamado novamente no arquivo manipulado e em outro com um nome aleatório.
Em seguida, AddLogContainer() é chamado usando os handles desses arquivos.

A função NtSetinformationFile() é chamada, e os handles são fechados, com os quais o ponteiro é corrompido (será explicado mais tarde)

O HeapSpray impede que ocorra um BSOD neste ponto:

Definindo um breakpoint lá, podemos ver que o ponteiro está corrompido e aponta para nosso HeapSpray, com o qual podemos manipular as próximas duas chamadas de função da vtable.


RAX assume o valor 0x5000000 e pula primeiro para a função localizada em 0x5000000+18 e depois para 0x5000000+8.


Então, primeiro salta para fnClfsEarlierLsn() e depois para fnSeSetAccessStateGenericMapping().
Traçamos a partir do breakpoint e vemos que ele atinge CLFS!ClfsEarlierLsn().

Esta função é chamada exclusivamente porque, ao retornar, define EDX para 0xFFFFFFFF

No endereço 0xFFFFFFFF, havíamos armazenado o resultado do SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000

Como mencionamos, ao retornar de CLFS!ClfsEarlierLsn(), o valor de RDX é 0x00000000FFFFFFFF

Chegamos à segunda função nt!SeSetAccessStateGenericMapping()

Esta função é útil, pois RCX aponta para nosso HeapSpray, e RDX vale 0xFFFFFFFF, cujo conteúdo controlamos


O conteúdo de RCX+0x48 possui o ponteiro para AttributeValueSize que foi armazenado em v30.Pointer+24



Esse valor do ponteiro de AttributeValueSize é movido para RAX, então lê o conteúdo do endereço 0xFFFFFFFF onde havíamos armazenado o endereço do SYSTEM _EPROCESS & 0xFFFFFFFFFFFFFFF000.

Em seguida, sobrescreve em RAX+8 o próximo campo que é o AttributeValue()


Claro, o AttributeValue normalmente apontaria no kernel para o atributo que adicionamos.

E agora sobrescreveremos com um ponteiro do resultado do system _EPROCESS & 0xFFFFFFFFFFFFFFF00.
Isso significará que, quando chamarmos a função NtFsControlFile() novamente, desta vez com o argumento 0x110038 para ler o atributo, em vez de retornar os "A" que eram apontados pelo ponteiro AttributeValue, agora lerá a partir de _EPRROCESS & 0xFFFFFFFFFFFFFFFFF000 o número de bytes solicitado e o retornará no buffer de saída, com o qual podemos obter na primeira chamada o valor do SYSTEM TOKEN.

v9b é o endereço inicial do Output Buffer onde o conteúdo do resultado do System EPROCESS & 0xFFFFFFFFFFFFFFF000 foi copiado.
A isso ele adiciona v14, que são os últimos 3 bytes do System EPROCESS, e então adiciona 0x4b8 que é o offset do Token para esta versão do Windows 11; então encontra o conteúdo desse endereço que terá salvo o valor do System Token.



Lembre-se de que os últimos 4 bits foram alterados, não é significativo, então o valor ainda corresponde.
Na segunda chamada, o valor da Flag é 1, pois foi incrementado ao final da primeira chamada.

Ali vemos a ordem em que os valores são armazenados

O endereço 0xFFFFFFFF com o valor que acabamos de encontrar do System Process Token.


E no HeapSpray está o valor do endereço do Token do meu processo ao qual subtraio 8. Esse valor mais oito será usado como alvo; lembre-se de que você escreveu no endereço apontado por RAX+8.


No endereço de memória começando em 0x5000000

Também vemos que ele usa o nome de outro container, pois o anterior está sendo usado pelo processo do sistema e não pode ser aberto novamente ou excluído.

Então o bug é acionado pela segunda vez da mesma forma que na primeira tentativa.

Ele chega novamente a CLFS!ClfsEarlierLsn().

definindo RDX para 0xFFFFFFFF

Em seguida, chega a nt!SeSetAccessStateGenericMapping()

Lê o endereço do Token do meu processo menos 8, onde irá escrever

Então lê o SYSTEM TOKEN

E escreve no endereço do Token do meu processo (adiciona 8), o System Token

E dessa forma meu processo fica com o System Token

Uma vez que o token é escrito, iniciamos um processo para verificar os privilégios; neste caso, lançamos o Notepad.exe



Lembre-se de que este PoC funciona apenas no Windows 11; no Windows 10, produzirá um BSOD, portanto, você deve fazer algumas modificações para funcionar corretamente, o que não é explicado neste blogpost.
Analisando as estruturas
As estruturas e a maior parte da documentação sobre o formato do arquivo CLFS foram retiradas do excelente trabalho de IONESCU sobre CLFS Internals.
Podemos ver que uma verificação foi adicionada na função ClfsBaseFilePersisted::LoadContainerQ

Os valores que realizam uma adição pertencem à estrutura _CLFS_BASE_RECORD_HEADER.

Observe que o Base Block começa no offset 0x800 do arquivo e termina no offset 0x71FF, correspondendo os primeiros 0x70 bytes ao Log Block Header
Como boa prática, podemos adicionar a estrutura _CLF_LOG_BLOCK_HEADER no IDA
struct _CLFS_LOG_BLOCK_HEADER
{
UCHAR MajorVersion;
UCHAR MinorVersion;
UCHAR Usn;
char ClientId;
USHORT TotalSectorCount;
USHORT ValidSectorCount;
ULONG Padding;
ULONG Checksum;
ULONG Flags;
CLFS_LSN CurrentLsn;
CLFS_LSN NextLsn;
ULONG RecordOffsets[16];
ULONG SignaturesOffset;
};
Em seguida, temos o Base Record Header (_CLFS_BASE_RECORD_HEADER) que começa no offset 0x870 do início do arquivo e tem 0x1338 bytes de comprimento.

Se você quiser importá-lo para o IDA, antes deve adicionar os seguintes tipos e estruturas faltantes
typedef GUID CLFS_LOG_ID;
typedef UCHAR CLFS_LOG_STATE;
struct _CLFS_METADATA_RECORD_HEADER
{
ULONGLONG ullDumpCount;
};
Agora está pronto para ser adicionado:
typedef struct _CLFS_BASE_RECORD_HEADER
{
CLFS_METADATA_RECORD_HEADER hdrBaseRecord;
CLFS_LOG_ID cidLog;
ULONGLONG rgClientSymTbl[0x0b];
ULONGLONG rgContainerSymTbl[0x0b];
ULONGLONG rgSecuritySymTbl[0x0b];
ULONG cNextContainer;
CLFS_CLIENT_ID cNextClient;
ULONG cFreeContainers;
ULONG cActiveContainers;
ULONG cbFreeContainers;
ULONG cbBusyContainers;
ULONG rgClients[0x7c];
ULONG rgContainers[0x400];
ULONG cbSymbolZone;
ULONG cbSector;
USHORT bUnused;
CLFS_LOG_STATE eLogState;
UCHAR cUsn;
UCHAR cClients;
} CLFS_BASE_RECORD_HEADER, *PCLFS_BASE_RECORD_HEADER;

Depois de incluir as estruturas, notamos que realiza uma adição entre o cbSymbolZone e o endereço onde termina a _CLFS_BASE_RECORD_HEADER. (start + 1338h)
Lembre-se que o cbSymbolZone foi modificado no ficheiro de log criado de 0x000000F8 para 0x0001114B.
(offset 0x1b98 do ficheiro)
0x800(offset do início do Bloco Base) + 0x70 (logBlockHeader) + 0x1328 (cbsymbolZone)
0x800+0x70+0x1328 = 0x1b98
cbsymbolZone manipulado no ficheiro MyLog.blf:


Como o patch está na função CClfsBaseFilePersisted::LoadContainerQ, temos que dar uma olhada no objeto CClfsBaseFilePersisted.
Colocando um breakpoint em CLFS!CClfsBaseFilePersisted::LoadContainerQ e quando CreateLogFile é chamado com o handle do ficheiro manipulado, ele irá parar.

Chame a função CClfsBaseFile::GetBaseLogRecord para obter o endereço do Base Log Record (_CLFS_BASE_RECORD_HEADER)

RAX apontará para o endereço do _CLFS_BASE_RECORD_HEADER

Observe a estrutura _CLFS_BASE_RECORD_HEADER na memória e o campo cbsymbolZone 0x1328
bytes à frente


r14 armazena a estrutura correspondente ao "this", que é CClfsBaseFilePersisted, uma vez que é o this da função CClfsBaseFilePersisted::LoadContainerQ.

A estrutura CClfsBaseFilePersisted na memória:

Então, vamos criar uma estrutura com comprimento 0x21c0 para completar seus campos enquanto fazemos engenharia reversa (é uma estrutura não documentada) e chamá-la de struct_CClfsBaseFilePersisted

Dentro da função CClfsBaseFile::GetBaseLogRecord() obtém o ponteiro para _CLFS_BASE_RECORD_HEADER. e sabemos que o "this" nessa função é a estrutura: struct_CClfsBaseFilePersisted.

Leia dois campos (offset 0x28 e 0x30)

O campo 0x28 é uma word e tem o valor 6, então mudamos o tipo para word na estrutura.



Por enquanto, renomeamos para constante 6 (const_6)


De acordo com a documentação, 6 seria o número de blocos CLFS_METADATA_BLOCK_COUNT. O campo pode se referir a este valor.
E esse ponteiro está no offset 0x30.

Observe que o tamanho mostrado lá inclui o cabeçalho com comprimento 0x10


Quando a função ExAllocatePoolWithTag é chamada, alguns bytes são solicitados, mas o cabeçalho não está incluído, portanto, 0x90 bytes (0xa0 – 0x10) serão solicitados na chamada.
Procurando por texto +30h], as instruções que escrevem no offset 0x30 encontramos uma longa lista, mas filtrando a lista pelo tipo de objeto CClfsBaseFilePersisted ficamos com poucos resultados e imediatamente encontramos onde esse tamanho é alocado, e a mesma tag. (Dica: Nomes de funções Create e Initialize, são sempre os primeiros a olhar)


Como ainda não sabemos o nome, vamos colocar pool_0x90, que é outra estrutura não documentada, e vamos criar uma estrutura desse tamanho.


A pool_0x90 na memória tem outro ponteiro em seu próprio offset 0x30.

Este outro ponteiro aponta para o bloco base no ficheiro (Bloco Base começa no offset 0x800)


Imagem retirada do blogpost da Zscaler:

A alocação é enorme, porque contém todo o bloco base.



Então, vamos criar uma nova estrutura de tamanho 0x7a00 e chamá-la de BASE_BLOCK

Os primeiros 70 bytes já sabemos que correspondem a _CLFS_LOG_BLOCK_HEADER e os seguintes 0x1338 a _CLFS_BASE_RECORD_HEADER.

Então, adicionando o início do Bloco Base com o offset para o próximo registo (que é 0x70), obtemos a _CLFS_BASE_RECORD_HEADER

A _CLFS_BASE_RECORD_HEADER na memória.

Olhando para outros métodos do mesmo objeto CClfsBaseFilePersisted, em CClfsBaseFilePersisted::AddContainer obtém-se com CClfsBaseFile::GetBaseLogRecord também o endereço de _CLFS_BASE_RECORD_HEADER.

Em seguida, chama CClfsBaseFile::OffsetToAddr usando cbOffset, obtém o endereço de _CLFS_CONTAINER_CONTEXT, e armazena cboffset no array rgbcontainers que está no offset 0x328 da _CLFS_BASE_RECORD_HEADER.

A função CClfsBaseFile::OffsetToAddr é usada para encontrar endereços de estruturas a partir do offset

Neste ponto, o offset do contentor que será armazenado em 0x328 ainda é 0, porque ainda não adicionámos um contentor.

o PoC chama CreateLogFile duas vezes, a primeira vez com o ficheiro malformado MyLog.blf e a segunda vez com o ficheiro normal MyLogxxx.blf, então devemos parar a depuração duas vezes em todos os lugares acima e anotar no bloco de notas os endereços das estruturas acima para ambos os ficheiros.

Vamos avançar um pouco para CLFS!CClfsLogFcbPhysical::AllocContainer definindo um breakpoint nela e executando até lá.
Quando a função AddLogContainer() é alcançada no POC, paramos no breakpoint.

Vamos também definir um breakpoint em CClfsBaseFilePersisted::AddContainer+176 onde vimos antes que encontrará o offset e o ponteiro para a estrutura _CLFS_CONTAINER_CONTEXT.


Quando o depurador para, podemos ver que o offset é 0x1468.

Em RAX retornará o endereço da estrutura _CLFS_CONTAINER_CONTEXT.

a estrutura ainda está vazia porque o contentor ainda não foi adicionado.

Observe que o valor SignatureOffset=0x50 que escrevemos no offset 0x868 do ficheiro malformado, subtraindo o 0x800 do início do bloco base, estará na estrutura _CLFS_LOG_BLOCK_HEADER no offset 0x68.


Quando o PoC chama a função AddLogContainer() usando o ficheiro malformado, no offset 0x68 de _CLFS_LOG_BLOCK_HEADER, em vez do valor 0x50 que escrevemos lá, atualmente está 0xFFFF0050 na memória.

Em algum momento, esse valor foi alterado pelo programa, para ver quando aconteceu, na próxima execução, vamos definir um memory breakpoint de escrita.
O offset está armazenado em r15 + 0x328 (r15 aponta para a estrutura _CLFS_BASE_RECORD_HEADER)


RBX armazena o offset 0x1468.

Então, no endereço do Bloco Base + 0x70 + o offset 0x1468 que descobrimos, estará o endereço do contentor CLFS_CONTAINER_CONTEXT.

Na estrutura CLFS_CONTAINER_CONTEXT no offset 0x18 estará o ponteiro pContainer que será armazenado lá, podemos definir um breakpoint de escrita e ver quando é escrito.


Este é o ponteiro que devemos corromper, pois na função onde está a vulnerabilidade, primeiro lê o CLFS_CONTAINER_CONTEXT, depois move para r15 e em seguida lê o valor de r15+18, que é este ponteiro que acabámos de definir o Breakpoint de escrita.


ele armazena o pContainer no offset 0x1c0 da estrutura struct_CClfsBaseFilePersisted.

Após várias vezes que para, alcançamos o momento em que é corrompido. O topo do endereço do ponteiro foi alterado de FFs para zero.

Isso acontece quando o segundo AddLogContainer() do ficheiro malformado é chamado, o ponteiro do MyLogxxx anterior é corrompido.
O problema ocorre porque o SignaturesOffset, que deveria ser 0x50, agora é 0xFFFF0050, então permite escrever fora dos limites no memset que se segue.


A função memset() vai corromper a estrutura _CLFS_CONTAINER_CONTEXT que está abaixo, esta estrutura corresponde ao ficheiro MyLogxxx, pois quando foram criados, localizava-os a 0x11000 bytes de distância um do outro.
Desta forma, calcula exatamente onde escrever na próxima estrutura e zera o topo do ponteiro, de modo que aponta para o heap do usuário onde o HeapSspray foi criado.
a estrutura do bloco base do ficheiro malformado está apenas 0x11000 antes da do ficheiro MyLogxxx.
Malformado:

MyLogxxx


RCX é menor que RDX pois 0xFFFF0050 foi adicionado a, em vez de 0x50 como deveria ser.

e chegamos à função memset(), para definir a quantidade de 0xb0 bytes com Zeroes, com RCX apontando para a estrutura CLFS_CONTAINER_CONTEXT do ficheiro MyLogxxx, especificamente para os cinco bytes altos do pContainer.

Este ponteiro será corrompido ao sobrescrever os primeiros bytes:

permanecendo a apontar para um endereço de memória previamente controlado por nós através do HeapSpray


Em seguida, o handle do ficheiro MyLogxxx será fechado, e chega-se a CClfsBaseFilePersisted::RemoveContainer, a vulnerabilidade é finalmente acionada.

Agora que temos mais informações, notamos que aqui lê o Base_Block.LOG_BLOCK_HEADER.SignaturesOffset e o Base_Block.LOG_BLOCK_HEADER.TotalSectorCount
Na primeira parte do patch, o SignaturesOffset não deve ser maior que 0x7a00, no nosso era originalmente 0x50, se chegasse com um valor maior que 0x7a00 nos expulsaria.

Executando o PoC na máquina corrigida, ele compara 0x50 com 0x7a00 e como é menor, continua.

No bloco seguinte, o cbSymbolZone malformado é adicionado ao valor do endereço final de _CLFS_BASE_RECORD_HEADER e esta soma é armazenada em result_1.

Em seguida, o endereço do Base_Block é adicionado ao valor SignatureOffset, que num ficheiro normal é 0x7980.

O endereço máximo do base_block é 0x7a00, agora a SymbolZone é permitida até 0x80 antes do limite.
Irá armazená-lo em result_2, ou seja, esse seria o limite máximo para a SymbolZone dentro do base block, então compara ambos os resultados se o primeiro for maior que o segundo, significa que saiu dos limites.


Obviamente o primeiro membro será maior que o segundo e não continuará, pois a primeira soma do cbSymbolZone + endereço final da _CLFS_BASE_RECORD_HEADER excede o limite (que é o result_2) e leva a um "out of bounds".

A última coisa que teríamos que descobrir é onde o valor SignatureOffset de 0x50 se torna 0xFFFF0050.Então, vamos recomeçar, reiniciar e parar em CLFS!CClfsBaseFilePersisted::LoadContainerQ onde o valor ainda não foi alterado na memória e ainda é 0x50.
Defina um breakpoint de acesso no offset 0x68 em SignatureOffset.

E após várias paradas, detectamos o momento certo em que ele modifica o valor, em ClfsEncodeBlockPrivate.

Esta função não está corrigida (patched), então pode ser um comportamento causado pelo baixo valor de 0x50 e pelo restante dos valores sendo manipulados.
Entre os valores manipulados, podemos ver o valor ccoffsetArray cujo nome na estrutura _CLFS_BASE_RECORD_HEADER é rgClients e representa o array de offsets que apontam para o objeto Client Context.
O campo rgClients está localizado no offset 0x138 (0x9a8-0x800-0x70) da estrutura _CLFS_BASE_RECORD_HEADER.


No PoC, este valor é malformado para apontar para um objeto de contexto de cliente falso, chamado FakeClientContext


Esta é a estrutura Client Context _CLFS_CLIENT_CONTEXT
struct _CLFS_CLIENT_CONTEXT
{
CLFS_NODE_ID cidNode;
CLFS_CLIENT_ID cidClient;
USHORT fAttributes;
ULONG cbFlushThreshold;
ULONG cShadowSectors;
ULONGLONG cbUndoCommitment;
LARGE_INTEGER llCreateTime;
LARGE_INTEGER llAccessTime;
LARGE_INTEGER llWriteTime;
CLFS_LSN lsnOwnerPage;
CLFS_LSN lsnArchiveTail;
CLFS_LSN lsnBase;
CLFS_LSN lsnLast;
CLFS_LSN lsnRestart;
CLFS_LSN lsnPhysicalBase;
CLFS_LSN lsnUnused1;
CLFS_LSN lsnUnused2;
CLFS_LOG_STATE eState;
union
{
HANDLE hSecurityContext;
ULONGLONG ullAlignment;
};
};
O valor eState está no offset 0x78 a partir do início da estrutura, no arquivo manipulado 0x23a0+0x78.


Este valor mostra o status do log.
typedef UCHAR CLFS_LOG_STATE, *PCLFS_LOG_STATE;
const CLFS_LOG_STATE CLFS_LOG_UNINITIALIZED = 0x01;
const CLFS_LOG_STATE CLFS_LOG_INITIALIZED = 0x02;
const CLFS_LOG_STATE CLFS_LOG_ACTIVE = 0x04;
const CLFS_LOG_STATE CLFS_LOG_PENDING_DELETE = 0x08;
const CLFS_LOG_STATE CLFS_LOG_PENDING_ARCHIVE = 0x10;
const CLFS_LOG_STATE CLFS_LOG_SHUTDOWN = 0x20;
const CLFS_LOG_STATE CLFS_LOG_MULTIPLEXED = 0x40;
const CLFS_LOG_STATE CLFS_LOG_SECURE = 0x80;
este valor é definido como CLFS_LOG_STATE CLFS_LOG_SHUTDOWN =0x20
O outro valor malformado é fAttributes que corresponde ao conjunto de flags FILE_ATTRIBUTE associadas ao arquivo de log base (como System e Hidden).


Como o campo começa um byte antes em 0xa e abrange dois bytes, o valor de fAttributes é 0x100.


Finalmente, há o valor blocknameoffset que aponta para o offset 0x1bb8, ou seja, somando 0x78 e 0x800 aponta para o offset 0x2428 do arquivo.


Note que o offset para o Client Context é 0x1b30

Então, o Client Context está no offset 0x23a0.


E apenas 0x10 antes, está o valor correspondente a blocknameoffset.


Que apontaria para a string com o nome
o último é o blockattributeoffset que está 0xC antes do Client Context em 0x2394.

Estes dois últimos valores pertencem a uma estrutura anterior ao Client Context de 0x30 bytes de comprimento, chamada _CLFSHASHSYM
typedef struct _CLFSHASHSYM
{
CLFS_NODE_ID cidNode;
ULONG ulHash;
ULONG cbHash;
ULONGLONG ulBelow;
ULONGLONG ulAbove;
LONG cbSymName;
LONG cbOffset;
BOOLEAN fDeleted;
} CLFSHASHSYM, *PCLFSHASHSYM;


eles estão a 0x20 e 0x24 bytes do início da estrutura _CLFSHASHSYM, então na estrutura _CLFSHASHSYM o valor chamado blockNameOffset no POC é o campo cbSymName e o blockAttributteoffset é o campo cbOffset.


Esses são os valores malformados, agora precisamos ver como eles afetam a mudança do nosso SignaturesOffset do valor 0x50 para 0xFFFF0050.
Vamos dar uma olhada na função CClfsBaseFile::AcquireClientContext(), que deve retornar o contexto do cliente.

ela chama CClfsBaseFile::GetSymbol com o quarto argumento que será _CLFS_CLIENT_CONTEXT ** onde armazenará o ponteiro para Client Context.

Dentro da função CClfsBaseFile::GetSymbol passamos o offset malformado ccoffsetArray para CClfsBaseFile::OffsetToAddr e obtemos o endereço do contexto do cliente, vamos definir um breakpoint lá para que pare ao chamar o arquivo criado com CreatelogFile .

Lá ele para com o argumento manipulado ccoffsetArray.


A função CClfsBaseFile::OffsetToAddr retorna o falso Client Context

E verifica que o valor de cbOffset não é zero, já que 0xC é encontrado antes da estrutura _CLFS_CLIENT_CONTEXT que está em RAX.


Então ele compara cbOffset com ccoffsetArray (que está em RSI), eles devem ser iguais, caso contrário, obteremos um erro.

Ele também verifica que cbSymName seja igual a cbOffset+0x88, caso contrário, também obteremos um erro.

E finalmente, compara o byte cidClient com zero

Se todas essas verificações forem bem-sucedidas, o client context será salvo.

A saída da função r14 aponta para Client Context

Ao sair de CClfsLogFcbPhysical::Initialize teremos o endereço de CLFS_CLIENT_CONTEXT.

Agora ele lê o valor de fAttributes (0x100)

esta função pertence à classe CClfsLogFcbPhysical


Que foi alocada aqui, e seu tamanho é 0x15d0 e sua tag é “ClfC”

Vamos criar uma estrutura para armazenar o que estamos fazendo engenharia reversa, vamos chamá-la de: struct_CClfsLogFcbPhysical.

Note que em 0x2b0 ele salva o endereço da estrutura CClfsBaseFilePersisted.

Após salvar muitos valores na estrutura, ele vai para uma parte importante, testa o eState com 0x20.


Como o valor manipulado era 0x20, o teste retornará 1.


Vemos que no construtor na vtable está

Ele verificará se o arquivo está multiplexado.

Então, ele segue o caminho desejado, alcançando CClfsLogFcbPhysical::ResetLog.


Vários campos são inicializados como zero, exceto um que é inicializado como 0xFFFFFFFF00000000.

Aqui recupera o Client Context

ele armazena o valor 0xFFFFFFFF00000000.



Ele escreve 0xFFFFFFFF no offset 0x5c que é a parte alta de CLFS_LSN lsnRestart.ullOffset



Agora executamos a função ClfsEncodeBlockPrivate(), que é a responsável por sobrescrever o 0x50 com 0xFFFF0050 como vimos antes.
Lá ele lê o valor de SignatureOffset = 0x50 que ainda está como colocamos no arquivo malformado e o adiciona ao início de CLFS_LOG_BLOCK_HEADER.

este é um loop que está escrevendo 2 bytes, como o SignatureOffset em vez de apontar para um valor correto que em um arquivo normal é um valor alto, por exemplo 0x3f8 que o faz escrever mais adiante, aqui ele escreverá no mesmo CLFS_LOG_BLOCK_HEADER
A ideia é alterar o destino da escrita para tentar corromper o valor SignatureOffset.
Arquivo Normal

Neste ponto, ele começará a fazer um loop e escrever dois bytes.

O contador deve atingir o valor 0x3d para sair do loop.

RCX está aumentando a partir de 0x200, já estamos no terceiro ciclo, e seu valor é 0x600

na iteração 0xe, RCX é 0x1a00


Foi aí que ele escreveu o 0xFFFFFFFF000000.


Ele está lendo os dois últimos bytes FFFF

E então copiará em R8


Como vimos, este valor é crítico, pois permite contornar a verificação e escrever fora dos limites para corromper o ponteiro pContainer do arquivo que segue o memset() e escrever zeros no topo e deixá-lo apontando para nossa memória controlada (HeapSpray).
Na CClfsBaseFilePersisted::AllocSymbol a mesma soma que vai obter o destino do memset que é cbSymbolZone + endereço final de CLFS_BASE_RECORD_HEADER compara antes com Base_block + 0xFFFF0050, então tem valores corrompidos em ambos os lados da equação.
CbSymbolZone= 0x1114B
É o valor malformado que somado ao endereço final de CLFS_BASE_RECORD_HEADER fará com que escreva fora dos limites e o outro membro da comparação que deveria ser o endereço do Base Block + SignatureOffset, permanece SignatureOffset =0xFFFF0050 o que permite que esta verificação passe e escreva fora dos limites no memset() e zere o topo do ponteiro que permanecerá apontando para nosso HeapSpray.

Como RCX é menor que RDX.

Como vimos antes. (Os valores podem diferir porque pertencem a uma execução anterior)
Ele corromperá o ponteiro, definindo os bytes mais altos como 0

Deixando-o apontando para uma área de memória que controlamos através de HeapSpray


Então, quando a vulnerabilidade é acionada, chegamos a CClfsBaseFilePersisted::RemoveContainer

Lá estará o ponteiro já corrompido e pode ser explorado como vimos anteriormente.

Neste ponto, temos o bug explorado, ele leva ao controle das funções que permitem ler o token SYSTEM e escrever em nosso próprio processo para alcançar a escalada de privilégio local.
Esperamos que ache útil, se tiver alguma dúvida, pode nos contactar em [email protected] e [email protected]
Aproveite!