Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2023-28252 — Análise técnica e exploit de prova de conceito para CVE-2023-28252, uma vulnerabilidade de escalonamento de privilégio no driver do sistema de arquivos de log comum (CLFS) do Windows, usada em ataques de ransomware Nokoyawa. | Kitploit
Ferramentas/GitHubGitHub/fortra/cve-2023-28252
Escalada de PrivilégiosForensia de MemóriaAnálise de VulnerabilidadesExploraçãoEngenharia ReversaDepuradoresExploração de Binários
GitHubfortra/cve-2023-28252

CVE-2023-28252

Análise técnica e exploit de prova de conceito para CVE-2023-28252, uma vulnerabilidade de escalonamento de privilégio no driver do sistema de arquivos de log comum (CLFS) do Windows, usada em ataques de ransomware Nokoyawa.

Ver Repositório
18444há 3 anosRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Desde fevereiro de 2022 foi relatado um novo ransomware que parece estar usando uma vulnerabilidade de dia zero no Windows, de acordo com a pesquisa conduzida pela Trend Micro.
Mais informações sobre este ransomware podem ser encontradas neste link.
De acordo com a análise da Kaspersky, o grupo de ransomware Nokoyawa usou outras explorações direcionadas ao driver do Common Log File System (CLFS) desde junho de 2022, com características semelhantes, mas distintas, todas ligadas a um único desenvolvedor de explorações.
Em abril de 2023, quando a Microsoft lançou a correção, a CVE-2023-28252 foi atribuída.
Anteriormente, em 2022, um bug semelhante no mesmo componente foi pesquisado por nós e documentado neste artigo de blog

Formato de arquivo do Common Log File System (CLFS):

Para enfrentar a análise, é necessário conhecer o formato de arquivo .blf, que é manipulado pelo driver vulnerável Common Log File System chamado CLFS.sys e que está na pasta de drivers dentro de system32.

Mais informações sobre este tipo de arquivo podem ser encontradas nos links abaixo:

https://www.zscaler.com/blogs/security-research/technical-analysis-windows-clfs-zero-day-vulnerability-cve-2022-37969-part

https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/introduction-to-the-common-log-file-system

https://github.com/ionescu007/clfs-docs/blob/main/README.md

https://www.coresecurity.com/core-labs/articles/understanding-cve-2022-37969-windows-clfs-lpe

A vulnerabilidade:

Esta análise é feita para Windows 11 21H2, clfs.sys versão 10.0.22000.1574, embora também funcione em Windows 10 21H2, Windows 10 22H2, Windows 11 22H2 e Windows Server 2022.

Em versões anteriores do Windows, é necessário ajustar alguns valores, caso contrário, produziríamos um BSOD.

Microsoft Patch Tuesday de abril de 2023.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança médiaVocê pode verificar a versão do driver conforme mostrado

Quando a vulnerabilidade foi publicada, em abril de 2023, comecei com Esteban Kazimirow a realizar a engenharia reversa do driver CLFS.sys, embora neste caso, apenas analisar o patch foi muito difícil de deduzir onde estava o bug e como acioná-lo, já que a exploração é muito complexa.

Mais tarde, saiu um artigo de blog cujo autor, a partir de uma amostra de um malware, mostrou algumas partes do código descompilado pelo HexRays e algumas informações que orientaram onde a exploração deveria ser enfrentada.

Obviamente, as informações fornecidas não estavam completas, mas sem essa ajuda teria sido improvável ter conseguido construir o PoC e posteriormente um exploit funcional.

Para facilitar a compreensão, primeiro explicaremos como construir o PoC e depois faremos a análise da vulnerabilidade.

Este artigo de blog contém duas seções:

Construindo o PoC:

1- Obter os endereços do kernel que precisamos para a exploração

2- Preparando o caminho para criar os arquivos .blf:

3- Criar o arquivo "trigger blf" usando a função CreateLogFile()

4- Criar o arquivo "trigger blf"

5- Obter o endereço do kernel do BASE BLOCK do trigger blf

6- Chamar AddLogContainer com o handle do trigger blf

7- Preparando os arquivos spray blf

8- Preparando a memória para realizar o spray

9- Acionando o bug

Depuração:

1- Verificando o spray de memória

2- Observando o RecordOffset[12] do trigger blf

3- Observando o valor iFlushBlock no arquivo spray blf

4- Por que ele lê do BLOCK 1 SHADOW em vez do BLOCK 0 CONTROL?

5- Por que o checksum é igual a zero nos arquivos spray blf?

6- Finalizando a exploração.

7- O patch real

Construindo o PoC:

1- Obter os endereços do kernel que precisamos para a exploração

Vou criar uma função chamada InitEnvironment para obter alguns endereços necessários do kernel.

Obter o endereço EPROCESS do meu processo e armazená-lo na variável g_EProcessAddress, depois o endereço EPROCESS do processo SYSTEM e armazená-lo em system_EPROCESS, depois o endereço EHTREAD da thread principal do meu processo e armazená-lo em g_EThreadAddress e finalmente o endereço do PREVIOUS MODE que nesta versão do PoC não será usado.

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

Este método é bem conhecido, a função GetObjectKernelAddress chama NtQuerySystemInformation duas vezes com o primeiro argumento SystemExtendedHandleInformation, a primeira chamada é passada com um tamanho incorreto e retorna erro, mas também retorna o tamanho correto que é usado na segunda chamada e obtém as informações de todos os handles, então percorrendo em um loop as informações de cada handle e no campo Object do handleinfo correto obtém o endereço pesquisado no kernel.

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamente

Também preciso dos endereços do kernel das seguintes funções exportadas por CLFS.sys:

• ClfsEarlierLsn

• ClfsMgmtDeregisterManagedClient

E as funções exportadas de NTOSKRNL.exe

• RtlClearBit/PoFxProcessorNotification

• SeSetAccessStateGenericMapping

Para obter esses endereços, usa um método semelhante ao usado para obter a base do kernel de ambos os módulos, chamando NtQuerySystemInformation duas vezes, mas neste caso o primeiro argumento será SYSTEM_INFORMATION_CLASS (no PoC usamos a função FindKernelModulesBase para esse propósito).

Uma imagem contendo texto, fonte, captura de tela, linha Descrição gerada automaticamenteEm seguida, carrega CLFS.sys e NTOSKRNL.exe como módulos normais no modo de usuário chamando LoadLibrary, obtém os endereços no modo de usuário com GetProcAddress e então subtrai a imagebase de cada um, o que obtém o offset da função e finalmente adiciona cada offset às bases do kernel correspondentes e, com isso, obtém os endereços do kernel de todas as funções necessárias.

Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

2- Preparando o caminho para criar os arquivos .blf:

Crio uma função chamada createInitialTriggerBlfFile que irá gerar e escrever um arquivo .blf.

O caminho que é usado como argumento no CreateLogFile é diferente de um caminho normal, por exemplo, para abrir o arquivo 1280.blf localizado na pasta C:\Users\Public, devemos definir o caminho LOG:C:\Users\Public\1280. Isso será salvo na variável stored_name_CreateLog.

Faço isso usando wsprintfW() já que stored_env armazena o caminho C:\Users\Public, obtido anteriormente das variáveis de ambiente. A esta string vou preceder a string LOG: e um nome aleatório no final, sem a extensão .blf.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Este será o caminho para meu arquivo inicial que chamarei de "trigger blf". Claro, também devo salvar o caminho normal para o mesmo arquivo sem o LOG: na frente e com a extensão BLF para abri-lo e modificá-lo com CreateFile(), WriteFile() como qualquer outro arquivo, este caminho será, por exemplo: C:\Users\Public\1280.blf, e será armazenado na variável stored_name_fopen.

Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

Claro, ambos os caminhos correspondem ao mesmo arquivo, e devo usar um ou outro conforme apropriado.

3- Criar o arquivo "trigger blf" usando a função CreateLogFile().

A função CreateLogFile cumpre uma função bastante semelhante a CreateFile() (cria novos arquivos ou abre arquivos existentes e obtém seus handles), até mesmo alguns argumentos são semelhantes, mas CreateLogFile() funciona apenas com arquivos blf.

Além disso, quando abre um arquivo existente, verifica se o formato está ok, mesmo que cada bloco tenha um checksum e se este não estiver correto, retornará um erro.

Vou criar 2 tipos de arquivos BLF:

  1. O trigger blf

  2. O spray blf

Ambos são arquivos blf, mas modificados de forma diferente.

Um close-up de um código de computador Descrição gerada automaticamente com baixa confiançaDesta forma, o PoC primeiro cria o arquivo "trigger blf", usando CreateLogFile, com o caminho por exemplo: LOG:C:\Users\Public\1280 que configurei antes e foi armazenado na variável stored_name_CreateLog.

O quinto argumento fCreateDisposition, como em CreateFileA(), pode assumir os seguintes valores:

Uma imagem contendo texto, fonte, linha, recibo Descrição gerada automaticamente

Neste caso, usarei o argumento OPEN_ALWAYS, então o arquivo será criado se não existir e, se existir, será aberto. Como o arquivo ainda não existe, será criado com um nome aleatório.

logFile = CreateLogFile(stored_name_CreateLog, GENERIC_READ | GENERIC_WRITE, 1, 0, 4, 0);

CreateLogFile() criará nosso arquivo "trigger blf" com seus 6 blocos e seus checksums correspondentes e retornará o handle que será armazenado na variável logFile.

Uma imagem contendo texto, captura de tela, fonte, número Descrição gerada automaticamente

Cada bloco terá a partir do offset mostrado na coluna esquerda, um cabeçalho cujo tamanho é 0x70 bytes.

Assim, por exemplo, o cabeçalho do CONTROL BLOCK vai do offset 0x0 a 0x70.

Uma captura de tela de um computador Descrição gerada automaticamente com baixa confiança

Todos os cabeçalhos de todos os blocos têm a mesma estrutura chamada _CLFS_LOG_BLOCK_HEADER.

Esta é a estrutura do cabeçalho:

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

No offset 0xC do cabeçalho, posso encontrar o checksum, então como o CONTROL BLOCK começa no offset 0, o checksum estará no offset 0xC do arquivo e assim cada bloco terá seu checksum em 0xC a partir do início de seu bloco.

Uma captura de tela de um computador Descrição gerada automaticamente com baixa confiança

4- Criando o arquivo "trigger blf":

Para modificar o arquivo trigger blf, devo abri-lo como um arquivo normal com CreateFileA ou fopen e depois modificá-lo com WriteFile ou fwrite respectivamente, faço isso no início da função fun_prepare do PoC.

Lembre-se de que o caminho normal está armazenado na variável stored_name_fopen, então eu o uso para abrir o arquivo com wfopen_s (que é uma variante de fopen que suporta strings Unicode).

O arquivo é modificado na função craftTriggerBlfFile chamada de fun_prepare.

Uma imagem contendo texto, linha, fonte, captura de tela Descrição gerada automaticamente

Então chamo fseek para apontar para o offset a ser alterado e depois com fwrite o arquivo é modificado.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança médiaAs alterações a serem feitas no arquivo "trigger blf" são as seguintes:

Após fazer essas alterações, FixCRCFile é chamado para calcular o novo checksum e corrigir os checksums dos primeiros 4 blocos. Os próximos dois blocos não têm nenhuma alteração, portanto não é necessário recalcular seus checksums.

Uma imagem contendo texto, fonte, captura de tela, número Descrição gerada automaticamente

5- Obtendo o endereço do kernel do BASE BLOCK do trigger blf:

O driver CLFS.sys lê os seis blocos do arquivo e, para armazenar seu conteúdo, faz uma alocação no pool do Kernel.

Uma imagem contendo texto, captura de tela, fonte, número Descrição gerada automaticamente

Há uma estrutura muito importante de tamanho 0x90 que no artigo de blog anterior do CVE-2022-37969, através de engenharia reversa encontrei alguns campos e chamei de pool_0x90. Após muito mais engenharia reversa, agora sei que seu nome real é m_rgBlocks e, à medida que o controlador aloca memória para copiar do arquivo o conteúdo de cada bloco, ele salva o tamanho de cada bloco, o offset inicial e o endereço do kernel onde foi armazenado.

Uma imagem contendo texto, captura de tela, fonte Descrição gerada automaticamente

Ele possui seis CLFS_METADATA_BLOCK que correspondem a cada bloco pelo seu número.

Cada estrutura CLFS_METADATA_BLOCK tem 0x18 bytes de comprimento. (0x18*6=0x90)

Uma imagem contendo texto, fonte, linha, número Descrição gerada automaticamenteNo offset 0 há uma união, mas pelo menos neste exploit apenas o campo pbImage é usado, então simplificando seria:

A alocação dessa estrutura pode ser feita a partir de dois lugares diferentes do driver CLFS.sys, de acordo com a criação de um novo arquivo ou se um existente é aberto. No caso de quando um novo arquivo é criado, o driver aloca os 0x90 bytes a partir de CClfsBaseFilePersisted::CreateImage+28A, enquanto no caso de um arquivo existente, aloca a partir de CClfsBaseFilePersisted::ReadImage+6E.

Depois disso, vou obter o endereço inicial do bloco 2 que corresponde ao arquivo trigger blf, chamado BASE BLOCK que começa no offset 0x800 e tem comprimento 0x7a00.

Uma imagem contendo texto, captura de tela, fonte, número Descrição gerada automaticamente

Dentro da função fun_prepare, abaixo, este endereço será encontrado no kernel usando este trecho de código.

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

Primeiro, a função getBigPoolInfo encontra todas as alocações no pool que têm a tag "Clfs" e um tamanho de 0x7a00, então as armazena em um array.

Depois disso, abre novamente o arquivo trigger blf previamente modificado usando CreateLogFile com o argumento OPEN_EXISTING, então abre um arquivo existente, isso realizará a alocação de seu BASE BLOCK.

Quando getBigPoolInfo é chamado novamente, haverá um novo pool "Clfs" de tamanho 0x7a00, e seu endereço é recuperado chamando NtQuerySystemInformation duas vezes.

O endereço do BASE BLOCK do arquivo trigger blf é armazenado na variável CLFS_kernelAddrArray.

Observe que se o arquivo trigger blf modificado não tiver o checksum correto, a função CreateLogFile() falhará.

6- Chamando AddLogContainer com o handle do trigger blf:

A última parte da função fun_prepare chama a api AddLogContainer usando o handle do arquivo trigger blf.

Um close-up de um código de computador Descrição gerada automaticamente com baixa confiança

7- Preparando os arquivos spray blf:

Na última função do PoC chamada to_trigger, um segundo tipo de arquivo blf será criado,

Vou nomeá-lo spray blf.

Este tipo de arquivo será usado para preencher um espaço de memória (spray), são necessários 10 iguais a este, mas inicialmente apenas um é criado. Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

Três arrays serão criados para armazenar os nomes aleatórios desses arquivos:

stored_log_arrays: armazena dez novos nomes aleatórios de arquivos .blf que serão usados com CreateLogFile.

stored_container_arrays: armazena nomes aleatórios para criar dez novos arquivos de contêiner.

stored_fopen_arrays: armazena os nomes dos arquivos de log do primeiro array (variável stored_log_arrays), mas com seu caminho normal (sem a string "LOG:") e com a extensão .blf.

Uma captura de tela de um código de computador Descrição gerada automaticamente com confiança média

Em cada iteração, o arquivo blf é copiado usando CopyFileW, os nomes armazenados nos arrays são atribuídos.

A função fun_trigger chama craftSprayBlfFile onde são feitas modificações em cada arquivo e FixCRCFile corrigirá os CRCs.

Uma captura de tela de um programa de computador Descrição gerada automaticamente com confiança média

Resumindo, criei 10 arquivos semelhantes (spray blf) com nomes aleatórios com as seguintes modificações:

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

A última alteração é copiar todo o bloco 0 (CONTROL BLOCK) para o bloco 1 (CONTROL BLOCK SHADOW)

Uma captura de tela de um computador Descrição gerada automaticamente com baixa confiança

O efeito dessas alterações, juntamente com as feitas no arquivo trigger blf, será explicado mais tarde no capítulo de depuração.

Algumas dessas alterações são aquelas que produzem a vulnerabilidade, enquanto outras são necessárias apenas para contornar as verificações do driver.

Neste ponto, os arquivos já estão criados e modificados, prontos para realizar o spray, então quando forem abertos com CreateLogFile, eles serão localizados na área de memória que desejamos, como será mostrado mais tarde.

8- Preparando a memória para realizar o spray

Na função to_trigger, um array de 12 elementos é criado, contendo o endereço do BASE BLOCK do arquivo trigger blf mais 0x30.

Então, na função fun_pipeSpray, a memória é preenchida com um spray de pipes, dentro dela há um loop que chama CreatePipe e cria o número de pipes passado como primeiro argumento, o segundo argumento é um array que armazenará os handles de todos os pipes criados.

Dentro de um loop, ele chama CreatePipe criando pipes de leitura-escrita.

Desta forma, primeiro 0x5000 pipes serão criados e depois chama novamente para criar outros 0x4000 pipes.

Em seguida, usa WriteFile para escrever nos primeiros 5000 pipes o array recém-criado com os endereços de BASE BLOCK + 0x30 do arquivo trigger blf.

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

Agora já tem um bloco compacto criado na memória, ele liberará 0x667 pipes do número 0x2000 até o 0x2667, já que na memória os pipes não estão na mesma ordem em que foram criados, o que acontecerá é que haverá espaços livres neste bloco de memória.

Uma imagem contendo texto, captura de tela, fonte, software Descrição gerada automaticamenteObserve que as alocações dos pipes têm como tamanho de usuário 0x90 bytes, portanto, quando forem liberados, teremos

Ele libera os espaços de memória de tamanho 0x90 entre a memória cheia de pipes.
Em seguida, faz um loop para chamar CreateLogFile com os 10 arquivos spray blf.
Quando CreateLogFile é chamado para abrir arquivos existentes, a alocação de 0x90 bytes é realizada para o m_rgBlocks, um para cada arquivo spray blf, então essas alocações ocuparão as lacunas que foram deixadas ao liberar os pipes, pois são do mesmo tamanho.

Uma captura de tela de um código de computador Descrição gerada automaticamente com confiança média

Em seguida, repete o processo de escrever nos 0x4000 pipes finais o array que tem o endereço de BASE BLOCK +0x30 do trigger blf.

9- Acionando o bug

Todas essas manipulações criam um espaço de memória controlado. Mostrarei como ele está quando está sendo depurado, mas a ideia é que o m_rgBlocks de cada arquivo spray blf ocupe as lacunas de 0x90 bytes que foram liberadas.

Então, já na parte final, o bug é acionado dentro de um while( 1 ) usando uma chamada a AddLogContainer para os arquivos spray blf.Uma imagem contendo texto, fonte, linha, número Descrição gerada automaticamente

Dentro desse while o bug é acionado:

Uma captura de tela de um programa de computador Descrição gerada automaticamente com baixa confiança

Esse while sairá quando encontrar o token do Sistema, usando a função NtFsControlFile que lerá os atributos dos pipes.

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

Então, usando CreateLogFile, novamente sobrescreve o token do nosso processo com o Token do Sistema encontrado recentemente e, dessa forma, conseguimos a elevação de privilégio.

Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

Em seguida, restaura alguns valores, fecha os handles dos pipes e dos arquivos blf, e executa um Notepad como Sistema para verificar que elevamos corretamente.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Observe os arquivos blf criados na pasta PUBLIC. Lembre-se de que, se quiser tentar novamente, primeiro deve excluir os arquivos criados. Alguns estarão travados e não poderão ser excluídos, mas o PoC ainda funcionará.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Depuração:

1- Verificando o memory spray

Antes de começar com o efeito das alterações em trigger blf e spray blf para realizar a exploração, devo verificar se m_rgBlocks dos spray blf files estão localizados nos buracos que ocorrem na distribuição de memória, após realizar o pipe spray e a subsequente liberação de um número fixo de pipes.

Quando esse procedimento termina, um pipe deve estar localizado sob os 0x90 bytes de m_rgBlocks, então, quando m_rgBlocks é usado, ocorrerá um OUT OF BOUNDS e ele lerá desse pipe que está abaixo.

O PoC tem um ponto ideal para colocar um breakpoint:

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

Neste ponto, a abertura dos spray blf files está completa e a função AddLogContainer ainda não foi chamada.

Para depurar em modo de usuário, usarei x64dbg e, para modo kernel, IDA com o plugin Windbg.

Uma captura de tela de um computador Descrição gerada automaticamente com baixa confiança

Neste ponto, a memória já deve estar preparada, e posso ver a distribuição.

Vou pausar o IDA para encontrar um ponto interessante para colocar um breakpoint.

Vou configurar um breakpoint em CClfsBaseFilePersisted::AddContainer, que é chamado a partir de AddLogContainer e, no início, tem o registrador RCX apontando para a estrutura CClfsBaseFilePersisted e no offset 0x30 há um ponteiro para m_rgBlocks.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Quando o breakpoint é atingido, verifico na call stack que AddLogContainer está sendo chamado a partir do meu PoC.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

O registrador RCX aponta para:

Uma captura de tela de um computador Descrição gerada automaticamente com baixa confiança Uma captura de tela de um computador Descrição gerada automaticamente com baixa confiança

O primeiro campo é o ponteiro para uma vtable (CLFS! CClfsBaseFilePersisted::'vftable') e no offset 0x30 está o ponteiro para m_rgBlocks.

Uma captura de tela de um código de computador Descrição gerada automaticamente com confiança média

Os blocos 0, 1, 4 e 5 ainda não salvaram o pbImage, enquanto os blocos 2 (BASE BLOCK) e 3 (SHADOW BLOCK) já salvaram.

Cada bloco na tabela m_rgBlocks tem seu cbOffset que é o offset onde o bloco começa no arquivo, cbImage é o tamanho do bloco e eBlockType é o tipo do bloco.

Se o spray estiver correto, abaixo do m_rgBlocks deve haver um pipe e, dentro dele, os ponteiros para BASE BLOCK + 0x30 do trigger blf.

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

O comando "!pool" no windbg exibe a distribuição de memória:

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Cada m_rgBlocks tem uma tag “Clfs” e seu tamanho é 0xa0 porque é o tamanho de usuário 0x90 mais o cabeçalho 0x10, e abaixo há um pipe com a tag “NpFr” que tem o mesmo tamanho de usuário 0x90 + cabeçalho 0x10.

Como a distribuição não é uma ciência exata, alguns “Clfs” foram colocados continuamente, o que é indesejável, mas aquele com o qual estou trabalhando está corretamente posicionado, seguido por um pipe.

2-Observando o RecordOffset[12] do trigger blf

Uma das primeiras alterações que afetam é a feita no arquivo trigger blf no offset 0x858, onde o valor 0x369 é armazenado.

Um close-up de um sinal Descrição gerada automaticamente com baixa confiança

O BASE BLOCK começa no offset 0x800 do arquivo.

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamente

Dentro do _CLFS_LOG_BLOCK_HEADER no offset 0x800+0x58 (0x58 do início do cabeçalho do BASE BLOCK).

Uma imagem contendo texto, captura de tela, fonte, exibição Descrição gerada automaticamente

No offset 0x28 começa o array RecordOffsets (DWORD).

Avançando 0x30 bytes, no offset 0x58 (0x828+0x30=0x858 do início), está o campo 12 de RecordOffsets.

Uma imagem contendo texto, fonte, captura de tela, gráficos Descrição gerada automaticamente

Executo o PoC até CreateLogFile como mostrado na imagem abaixo:

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

Uma captura de tela de um computador Descrição gerada automaticamente

Antes de entrar em CreateLogFile, vou colocar um breakpoint em um lugar onde o valor 0x369 ainda não foi usado.

Em um caso em que CreateLogFile abre um arquivo existente, a estrutura m_rgBlocks é alocada aqui:

CClfsBaseFilePersisted::ReadImage+6E

Então, vou configurar um breakpoint no IDA exatamente aqui:

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Quando o breakpoint é acionado:

Uma imagem contendo texto, captura de tela, fonte, número Descrição gerada automaticamente

Em m_rgBlocks ainda há lixo porque ainda não foi inicializado, mas assim que pbImage do bloco 2 for alocado, o endereço será salvo no offset 0x30 do início, já que o primeiro campo dentro de cada CLFS_METADATA_BLOCK é pbImage.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamente

Agora configuro um hardware breakpoint de escrita: ba w1 ffffd003'7f5bea30

Depois de inicializar para zero, ele para quando salva pbImage.

A análise diz que corresponde ao block0, porque não considera a constante r14*8 que é 0x30 depois, como resultado, está realmente escrevendo o pbImage do block 2.

Uma imagem contendo texto, captura de tela, fonte Descrição gerada automaticamente

Observe que CClfsBaseFilePersisted::ReadMetadataBlock é usado para alocar qualquer um dos blocos, usando o tamanho passado como argumento.

Uma imagem contendo texto, fonte, captura de tela, linha Descrição gerada automaticamente

Agora configuro um breakpoint de leitura/escrita em 0x58 a partir do base block, para ver quando usa o valor 0x369.

ba r1 FFFF978A'16ECF000+0x58

Quando o breakpoint é atingido, lê o valor 0x369 localizado no RecordOffset[12], adiciona a um ponteiro estranho em r14 e incrementa o conteúdo de RAX+r14.

Algumas linhas acima no código, ESI tem o valor 0x13 e multiplica por 0x18, que é o tamanho de cada bloco em m_rgBlocks.

WINDBG>? 0x18*0x13

Evaluate expression: 456 = 00000000'000001c8

Se eu adicionar o valor de r8= 0x1c8 que é maior que 0x90, ao endereço inicial de m_rgBlocks, estará lendo OUT OF BOUNDS.

Uma captura de tela de um computador Descrição gerada automaticamente

Abaixo de m_rgBlocks, está o pipe com o ponteiro para BASE BLOCK + 0x30, ele lê esse ponteiro que foi estrategicamente colocado dentro do pipe.

Uma captura de tela de um programa de computador Descrição gerada automaticamente com baixa confiançaA posição atual no código foi chamada a partir da instrução while(1) do módulo principal.

Dentro do arquivo spray blf, coloquei estrategicamente o valor 0x13 no offset 0x48a (iFlushBlock).

Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

3-Observando o valor iFlushBlock no arquivo spray blf.

No offset 0x8a do arquivo spray blf, está localizado iFlushBlock do BLOCK 0, cujo valor é 4, enquanto o offset 0x48a pertence a iFlushBlock do BLOCK 1, e seu valor é 0x13

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Agora preciso descobrir por que ele lê iFlushBlock = 0x13 do BLOCK 1 em vez de iFlushBlock = 4 do BLOCK 0.

4-Por que ele lê do BLOCK 1 SHADOW em vez do BLOCK 0 CONTROL?

Se eu olhar para trás para descobrir de onde veio o 0x13, vejo na call stack que WriteMetadataBlock é chamado a partir de CClfsBaseFilePersisted::ExtendMetadataBlock+416, lá o segundo argumento iFlushBlock é EDX=0x13, que vem de r9w.

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamente

Uma captura de tela de um programa de computador Descrição gerada automaticamente com baixa confiançaAlgumas linhas antes, CClfsBaseFile::GetControlRecord foi chamado para recuperar o endereço do BLOCK 0, talvez o problema esteja aqui, então vou reiniciar e colocar um breakpoint nele.

GetControlRecord chama CClfsBaseFile::AcquireMetadataBlock que deve preencher a tabela m_rgBlocks com o endereço do block 0, quando passo por cima dessa função, ela obtém o endereço do block 1, então o problema ocorre dentro de CClfsBaseFile::AcquireMetadataBlock.

Adicionando 0x8A ao endereço recuperado, posso confirmar que o valor 0x13 que pertence ao BLOCK 1 está presente.

Uma captura de tela de um código de computador Descrição gerada automaticamente com confiança média

Vou reiniciar e colocar um breakpoint lá:

Uma captura de tela de um programa de computador Descrição gerada automaticamente com baixa confiança

CClfsBaseFile::GetControlRecord+27 call CClfsBaseFile::AcquireMetadataBlock

Uma captura de tela de um computador Descrição gerada automaticamente

O segundo argumento passado para AcquireMetadataBlock é zero, corresponde ao block 0, ele vai copiar do arquivo e armazenar seu endereço em m_rgBlocks Uma captura de tela de um computador Descrição gerada automaticamente com confiança média.

Na enumeração _CLFS_METADATA_BLOCK_TYPE do tipo de bloco, eles têm nomes diferentes dos que usei, mas são os mesmos 6 blocos.

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamente

Depois de verificar que o tipo de bloco é menor que o máximo m_cBlocks=6, ele salva um valor de referência para evitar ler o mesmo bloco duas vezes.

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

ReadMetadataBlock é chamado, o problema de ler block 1 em vez de block 0 estaria dentro dessa função.

Uma imagem contendo texto, fonte, número, linha Descrição gerada automaticamente

Se tudo estiver bem, ele aloca usando cbImage como tamanho e armazena o endereço no campo block 0-> pbImage em m_rgBlocks.

Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

O comando !pool exibe a tag e o tamanho alocados.

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamente

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamentePortanto, já tenho o endereço de pbImage do block 0 armazenado em m_rgBlocks, então preciso ver por que ele copia os bytes do block 1 lá em vez dos bytes do block 0.

Chego a uma chamada para CClfsContainer::ReadSector onde um ponteiro para uma variável contendo pbImage é passado, para escrever os bytes.

Observe as alterações feitas no conteúdo de pbimage ao passar por cima de ReadSector.

Adicionando 0x8a a pbImage, posso encontrar o valor 4 que é o valor correto, em vez de 0x13, então o problema deve ocorrer depois.

Após chamar ClfsDecodeBlock, ele retorna um erro 0x0C01A000A.

CClfsBaseFilePersisted::ReadMetadataBlock+153 chama ClfsDecodeBlock

Após esse erro, ele adiciona 1 ao tipo e chama CClfsBaseFilePersisted::ReadMetadataBlock novamente, mas com tipo 1 para ler o bloco 1.

Uma captura de tela de um computador Descrição gerada automaticamente

Em CClfsBaseFilePersisted::ReadMetadataBlock, aloca e armazena um novo pbImage em m_rgBlocks para o block 1.

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

Os Blocks 0 e 1 têm endereços diferentes, agora, se eu adicionar 0x8a ao endereço do block 1, seu valor é 0x13.

Talvez, como block 0 retornou um erro, ele use block 1 e o retorne para GetControlRecord como Control Block.

Conforme mostrado anteriormente, quando ele usa o valor 0x13 em vez de 4, sai dos limites de m_rgBlocks e lê os valores do pipe spray controlados por mim.

Uma imagem contendo texto, captura de tela, fonte Descrição gerada automaticamenteEm seguida, libera o pbImage do block 0 e copia o ponteiro do block 1 para o block 0.

Uma captura de tela de um computador Descrição gerada automaticamente

Seria necessário encontrar o valor que causa o erro 0x0C01A000A dentro de ClfsDecodeBlock.

Dentro de ClfsDecodeBlock, o checksum do primeiro bloco é zero, este é o erro 0xC01A000A.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

5-Por que o checksum é igual a zero nos arquivos blf spray?

Antes de chamar AddLogContainer, ao abrir qualquer arquivo spray blf com um editor hexadecimal, o checksum foi alterado para zero.

Uma captura de tela de um computador Descrição gerada automaticamente

Ele deveria ter sido alterado antes, quando foi aberto com CreateLogFile.

Uma imagem contendo texto, captura de tela, exibição, fonte Descrição gerada automaticamente

Por algum motivo, os arquivos spray blf terminam, após sair de CreateLogFile, com checksum do block 0 igual a 0 e retornam um handle válido, vamos ver por que isso acontece.

Paro em CreateLogFile antes de abrir algum arquivo spray blf.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Observe que, antes de chamar CreateLogFile, os spray files têm o checksum correto no block 0 e, após concluir a função, o valor do checksum muda para zero.

Uma captura de tela de um código de computador Descrição gerada automaticamente com baixa confiança

Então, coloco um breakpoint em CClfsBaseFile::GetControlRecord, para olhar dentro.

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamenteApós passar por CClfsContainer::ReadSector, o checksum não é zero.

Antes de entrar para calcular o CRC32, ele coloca o campo checksum como zero na memória para calcular o CRC, e o resultado está correto.

Uma imagem contendo texto, fonte, captura de tela Descrição gerada automaticamente

Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

Em seguida, verifica o valor de eExtendState =2 e vai para WriteMetadataBlock.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Aqui o checksum ainda é zero na memória, só preciso ver quando esse valor é escrito no arquivo.

Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

Ele verifica alguns valores que são manipulados no arquivo blf spray para alcançar CClfsBaseFilePersisted::ExtendMetadataBlock.

Uma captura de tela de um programa de computador Descrição gerada automaticamente com confiança média

Após um loop para ler blocos que ainda não foram lidos, o block 0 continua com checksum = 0.

Um retângulo branco com texto preto Descrição gerada automaticamente com baixa confiança

Chegando em WriteMetadataBlock.

Como estou executando antes de ele substituir o bloco 0 pelo 1, o valor iFlushBlock do arquivo blf spray ainda é 4, o valor correto.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Agora está trabalhando com o block 4, e escreverá o bloco 4 no arquivo, aqui ainda não é o problema.

Então chega a CClfsBaseFilePersisted::FlushControlRecord

Uma captura de tela de um programa de computador Descrição gerada automaticamente com confiança média

Dentro dele, atinge WriteMetadataBlock, mas com argumento 0, para escrever o block 0 no arquivo.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Então, ClfsEncodeBlock retorna erro 0xC01A000A, embora ele vá escrever o arquivo com o block 0 ruim em CClfsContainer::WriteSector, logo abaixo.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

A variável var_54 armazena o valor de erro 0xC01A000A e será verificada antes de sair da função.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Mas, após chamar CClfsContainer::WriteSector que não retorna erro, o conteúdo de var_54 é sobrescrito com zero.

Portanto, a função retorna zero sem erro e continua funcionando, já que CreateLogFile retornará um handle em vez de um valor de erro.

Uma captura de tela de um computador Descrição gerada automaticamente

6-Finalizando a exploração.

O valor 0x13 em iFlushBlock faz com que saia fora dos limites e lerá o ponteiro que está nos pipes e que aponta para o Base Block +30 do trigger blf.

Uma captura de tela de um computador Descrição gerada automaticamenteEm seguida, adiciona 0x28 a esse ponteiro, ( 0x58 do início do bloco base do trigger blf ) que tem o valor 0x369.

Uma captura de tela de um programa de computador Descrição gerada automaticamente com confiança média

A instrução INC aumentará o valor 0x14 em 1 e se repete 4 vezes, então 0x14 termina em 0x18.

WINDBG>db r14+369

ffffcb82'091e7397 14 00 00 00

Depois disso, CreateLogFile é chamado e lê o valor 0x1858.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Um close-up de um cartão Descrição gerada automaticamente com baixa confiançaGetSymbol verifica se o bloco falso criado anteriormente em trigger blf, apontado pelo offset 0x1858, tem os valores corretos.

Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

se o ponteiro não tivesse sido incrementado várias vezes, ele teria o valor original 0x1458 e apontaria para o bloco correto.

Após sair de GetSymbol, ele usará esse bloco falso aqui.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Então ele lerá o valor do offset 0x18 do bloco falso onde coloco 0x05000000 e saltará para o conteúdo do que está lá.

WINDBG>dps 0x5000000

00000000'05000000 00000000'05001000

Ele lê o conteúdo de 0x05000000 e seu 0x05001000 e lá está ClfsEarlierLsn.

Uma captura de tela de um programa de computador Descrição gerada automaticamente com baixa confiança

Esta função é usada para retornar o valor 0xFFFFFFFF em RDX, embora desta primeira vez esse valor não seja usado.

A segunda chamada ocorre aqui, ela chama PoFxProcessorNotification que estava em 0x501000 +8

Uma imagem contendo texto, fonte, captura de tela, linha Descrição gerada automaticamente Uma imagem contendo texto, fonte, captura de tela, linha Descrição gerada automaticamente

WINDBG>dps 00000000**'05001000**

00000000'05001000 fffff805'7ab13220 CLFS! ClfsEarlierLsn

00000000'05001008 fffff805'769dc3b0 nt! PoFxProcessorNotification

Uma captura de tela de uma tela de computador Descrição gerada automaticamente com baixa confiança

nesta função RCX = 0x05000000 , verifica que 0x40 bytes depois deve ser diferente de zero

WINDBG>dps rcx+40

00000000'05000040 00000000'05000000

O endereço para saltar será 0x68 depois.

WINDBG>dps rcx+68

00000000'05000068 fffff805'7ab2bfb0 CLFS! ClfsMgmtDeregisterManagedClient

E o argumento será 0x48 bytes depois.

WINDBG>dps rcx+48

00000000'05000048 00000000'05000400

A ClfsMgmtDeregisterManagedClient, é uma função conveniente porque posso controlar o argumento e também tenho dois saltos para funções controladas por mim.

Uma captura de tela de um computador Descrição gerada automaticamente

A primeira chamada é novamente para ClfsEarlierLsn que retornou em RDX=0xFFFFFFFF.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

ele pegará a origem para escrever a partir do conteúdo de RDX=0xFFFFFFFF.

WINDBG>dps rdx

00000000'ffffffff ffff8005'3a4ee000

No endereço 0xFFFFFFFF eu tinha armazenado o system_EPROCESS & 0xfffffffffffff000.

Uma imagem contendo texto, fonte, linha, número Descrição gerada automaticamente

O destino é o ponteiro localizado em 0x5000400 +0x48

*(UINT64*)0x5000448 = para_PipeAttributeobjInkernel + 0x18;

Uma captura de tela de um computador Descrição gerada automaticamente

O ponteiro PipeAttribute no kernel que aponta para um buffer preenchido com “A” será sobrescrito com a parte alta do ponteiro SYSTEM EPROCESS.

Este ponteiro foi criado quando chamei anteriormente _NtFsControlFile com um buffer cheio de “A” .

Uma captura de tela de um programa de computador Descrição gerada automaticamente com confiança média

O conteúdo desse atributo pode ser lido usando NtFsControlFile.

Uma captura de tela de uma tela de computador Descrição gerada automaticamente com confiança média

Agora o atributo pipe não aponta mais para o buffer com “A” mas para system_EPROCESS & 0xffffffffffffff000.

Uma captura de tela de um programa de computador Descrição gerada automaticamente com baixa confiança

Este código será repetido até que o token do sistema seja recuperado.

Uma captura de tela de um código de computador Descrição gerada automaticamente com confiança média

Uma captura de tela de um computador Descrição gerada automaticamente

No Windows 11, o token do sistema está no offset 0x4b8 da estrutura EPROCESS lida recentemente.

Uma captura de tela de um computador Descrição gerada automaticamente

Preciso apenas escrever esse token do sistema no meu processo chamando CreateLogFile.

Uma imagem contendo texto, fonte, linha, captura de tela Descrição gerada automaticamente

Para fazer este trabalho, basta repetir o passo usado para ler o token do sistema.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Na chamada dupla, primeiro chama ClfsEarlierLsn para retornar 0xFFFFFFFF em RDX e depois chama nt_SeSetAccessStateGenericMapping.

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamente

Verifico que o valor apontado por RDX é o System Token.

Uma imagem contendo texto, captura de tela, fonte Descrição gerada automaticamente

O token do meu processo é:

Vai escrever lá.

WINDBG>dps rax+8

ffff9b8b'fc446578 ffffc402'f601c06c

WINDBG>dps rax+8

ffff9b8b'fc446578 ffffc402'ef841919

Agora meu processo é System posso executar um Notepad para verificar.

Uma imagem contendo texto, captura de tela, fonte, linha Descrição gerada automaticamente

Uma captura de tela de um computador Descrição gerada automaticamente

Uma captura de tela de um computador Descrição gerada automaticamente

7-O patch real

BINDIFF mostra muitas funções alteradas

Uma captura de tela de um computador Descrição gerada automaticamente

A função vulnerável está aqui:

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Uma captura de tela de um computador Descrição gerada automaticamente

O primário é a versão corrigida, o secundário é a versão vulnerável.

O patch testa o valor de retorno de CflsEncodeBlock, que é 0xC01A000A, armazena-o na variável var_54, e como é negativo, verifica e evita o WriteSector.

O patch, além de não escrever o arquivo, a função retorna corretamente 0xc01a000a, com o qual CreateLogFile não retorna nenhum handle e a exploração não pode continuar.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança média

Uma captura de tela de um computador Descrição gerada automaticamente

Somente se ClfsDecodeBlock não for negativo, ele vai para WriteSector mas deixa retornar o valor negativo 0xC01A000A.

Uma captura de tela de um computador Descrição gerada automaticamente com confiança médiaEste é o patch real que realmente impede a exploração usando o PoC que acabei de anexar.

Neste ponto, explicamos como o bug foi explorado, leva ao controle das funções que nos permitem ler o token SYSTEM e escrevê-lo em nosso próprio processo para alcançar a elevação de privilégio local. Você pode encontrar o PoC funcional no GitHub da Fortra.

Esperamos que ache útil, se tiver alguma dúvida pode nos contatar:

[email protected] 
@ricnar456

 [email protected]
@solidclt

Baixar ferramenta