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-2022-37969PoC — Tutorial do CVE-2022-37969 com foco na metodologia de exploração do Kernel, não nas causas internas da CVE | Kitploit
Ferramentas/GitHubGitHub/emilc3978/cve-2022-37969poc
Escalada de PrivilégiosForensia de MemóriaAnálise de VulnerabilidadesExploraçãoEngenharia ReversaAprendizado e EducaçãoExploração de Binários
GitHubemilc3978/cve-2022-37969poc

CVE-2022-37969PoC

Tutorial do CVE-2022-37969 com foco na metodologia de exploração do Kernel, não nas causas internas da CVE

Ver Repositório
22há 9 mesesAinda não revisado

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

Conteúdo

Introdução Geral

Isto foi feito para esclarecer aspetos gerais relacionados à exploração no Windows. Explica conceitos básicos aplicados ao CVE-2022-37969. O resultado final é um PoC funcional. Não esclarece todos os aspetos sobre o CVE, mas fornece pedaços de código reutilizáveis e explica mecanismos que podem ser encontrados em muitos exploits genéricos.

O utilizador-alvo seria um engenheiro reverso iniciante, um desenvolvedor de exploits que procura um código-fonte de prova de conceito funcional para testar e entender os conceitos básicos de Windows Internals. Fornece um ponto de referência para aprendizagem adicional.

Requisitos: Depuração básica do kernel, Engenharia Reversa básica, conceitos básicos de Windows Internals, habilidades de programação em c/c++

Contexto

Um programa é um pedaço de código que está em execução numa máquina. Geralmente, um programa recebe dados (entrada), faz cálculos usando as entradas e gera dados (saída). A maioria dos programas são escritos por humanos e, portanto, têm bugs. Um bug é gerado por algum código-fonte que não foi escrito corretamente (o programador queria fazer algo com a entrada, o código resultante foi diferente do resultado pretendido). A maioria dos bugs é corrigida antes do lançamento do produto, mas alguns permanecem. Isto acontece porque existem diferentes tipos de bugs, alguns mais difíceis de detetar do que outros.

O Windows é um programa de computador, foi escrito por humanos e, portanto, tem bugs. Por que é importante? Porque os sistemas Windows podem executar programas que operam dados sensíveis, como contas bancárias, bases de dados de saúde e outros. Alguns bugs podem ser usados para ganhar acesso ilegal a dados restritos (este é um bom caso de uso para um exploit).

Existem vários tipos de bugs, alguns úteis, outros não. Geralmente, os bugs são gerados por entradas no programa que, em conjunto com as linhas de código que foram escritas incorretamente, geram um comportamento ou saída malformados. Encontrar essa entrada é trabalho do especialista em segurança (ou hacker). O próximo passo é avaliar o comportamento/saída malformada resultante e responder à pergunta: "Pode ser usado de forma útil?" É aqui que os bugs são classificados em diferentes categorias. Por exemplo, um bug pode gerar um comportamento que corrompe algumas estruturas de dados e faz com que o computador alvo reinicie. Sua utilidade é limitada. Um bug pode fazer com que a entrada seja escrita numa zona de memória que controla as permissões de acesso a ficheiros restritos. Este tipo de bug é mais útil.

Assim, a partir do conjunto de todos os bugs possíveis, o hacker procura o subconjunto mais útil para o seu propósito. De forma geral, o problema é: "Posso dar ao programa alvo uma entrada especialmente criada para que não quebre o sistema, mas possa elevar o meu nível de acesso e lucrar?"

Após esta introdução não técnica, o âmbito do tutorial pode ser formulado: Conseguimos encontrar um programa Windows que aceite uma entrada malformada e, como resultado de um código errado do desenvolvedor, possa elevar ilegalmente as nossas permissões de utilizador normal para administrador?

Programa alvo: Windows CLFS (Common Log File System Driver)

Nome do exploit: CVE-2022-37969

Tipo: Escalação de Privilégio Local

DOWNLOAD DO ISO VULNERÁVEL: Download Aqui

Teoria geral de escalação de privilégio no Windows

O espaço de endereçamento do Windows é aproximadamente dividido entre espaço do utilizador (execução de programas gerais) e espaço do kernel (execução do próprio sistema operativo e componentes de hardware – drivers). Um utilizador normal não deve aceder ao espaço do kernel, mas existem mecanismos pelos quais programas de utilizador normal podem aceder a partes do código do kernel (chamadas de sistema, procedimentos de driver). Por que precisamos de acesso? Para interagir com o SO de forma segura e controlada, conforme fornecido pelos designers do SO.

Alguns drivers usam entradas de dados fornecidas pelo utilizador para operar em estruturas de dados do espaço do kernel. Se a entrada gerar um bug, o kernel pode ser corrompido. Um caso é o Common Log File System Driver. Usando algumas entradas especiais, podemos forçar o driver a alterar estruturas de dados do kernel que detêm o nível de acesso de privilégio para o utilizador e sobrescrever utilizador normal com administrador.

O que precisa ser modificado para elevar o privilégio para administrador?

Começamos com o objetivo final em mente. O Windows armazena, dentro de uma estrutura de dados do kernel chamada _EPROCESS, informações para cada processo em execução no sistema. Exemplo de _Eprocess

Um campo importante é struct _EX_FAST_REF Token. Esta é outra estrutura de dados que aponta para dados que referenciam o nível de privilégio desse respetivo processo. Na imagem seguinte, o processo System tem um token de sistema e o processo Explorer tem um token de utilizador normal.

Tokens

Assim, para elevar o privilégio do Explorer.exe, precisaríamos de copiar o valor do Token do _EPROCESS do System para o Token do _EPROCESS do Explorer. Iremos conseguir algo semelhante copiando o Token do System para o Token do nosso próprio programa e lançando um Command Prompt a partir do processo elevado (os processos filhos herdam o token do processo pai).

Para completar estas ações, precisamos de mecanismos para:

  1. Obter o endereço da estrutura de dados _EPROCESS no Kernel
  2. Ler o valor do campo Token para o processo System
  3. Obter o endereço da estrutura de dados _EPROCESS para o Explorer
  4. Escrever o valor do token do System no offset do Token do Explorer na sua estrutura _EPROCESS

Localizando a estrutura de dados _EPROCESS para um processo alvo por PID

Introdução: A natureza do Windows ao longo dos anos: Com a descoberta de novas vulnerabilidades, o Windows precisou de patches para as mitigar. Além disso, com o surgimento de novas tecnologias, o Windows precisou de atualizações para se manter competitivo. Um requisito crucial era a retrocompatibilidade com versões anteriores. E, por vezes, a segurança era alcançada através da obscuridade. Estruturas de dados, definições de funções foram removidas dos manuais, mas a funcionalidade ainda permaneceu. Através de engenharia reversa, os investigadores conseguiram usar essas funcionalidades para vários fins.

Para encontrar o endereço do kernel do _EPROCESS, iremos usar uma função não documentada: NtQuerySystemInformation (Veja o link para parâmetros). Usando o parâmetro SystemInformationClass, podemos especificar que tipo de informação queremos obter. Iremos obter informações gerais de processo especificando o valor SystemExtendedHandleInformation (#define SystemExtendedHandleInformation 0x40).

Uma ressalva ao usar NtQuerySystemInformation é que não sabemos de antemão qual é o comprimento dos dados retornados, mas NtQuerySystemInformation tem um mecanismo que ajuda. Se for chamada com um array de tamanho errado para os dados necessários, retorna ERROR e o tamanho correto dos dados que deveriam ter sido solicitados. Isto pode ser usado para ler corretamente as Informações do Processo da seguinte forma:

  1. Chamar NtQuerySystemInformation com um parâmetro SystemInformationLength dummy
  2. Ler o valor do parâmetro ReturnLength retornado
  3. Chamar NtQuerySystemInformation novamente com o valor correto de SystemInformationLength retornado anteriormente

A estrutura de dados retornada é do tipo PSYSTEM_HANDLE_INFORMATION_EX. Esta é uma estrutura de dados não documentada. (Veja o link) que leva a uma estrutura de dados SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, que contém no campo Object o endereço do kernel da estrutura de dados _Eprocess para o processo correspondente.

Assim, a lógica seria: Iterar sobre todos os elementos PSYSTEM_HANDLE_INFORMATION_EX, comparar o campo UniqueProcessId de SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX com o PID do processo desejado e selecionar o campo Object correspondente para encontrar o seu endereço _Eprocess no kernel.

Um snippet de código:

proc_1 proc_2

NtQuerySystemInformation é declarada como um ponteiro para uma função e o seu endereço é obtido dinamicamente em tempo de execução através de loadlibrary e getprocaddress.

  1. typedef NTSTATUS(WINAPI* ptr_NtQuerySystemInformation)(int, PVOID, ULONG, PULONG);
  2. ntdll=LoadLibrary(L"ntdll.dll");
  3. MyNtQuerySystemInformation = (ptr_NtQuerySystemInformation)GetProcAddress(ntdll, "NtQuerySystemInformation");

Para ler e escrever o Token, teremos que confiar em vulnerabilidades dentro de clfsw32.sys e NamedPipes.

Leitura e escrita de dados no Kernel usando pipes

Isto é de certa forma uma caixa preta, detalhada noutros artigos, mas o conhecimento mínimo para esta funcionalidade será explicado para obter uma compreensão básica do processo de exploit. Pipes são mecanismos de comunicação entre processos. Os processos podem passar informações uns aos outros usando pipes. Os pipes são representados como estruturas de dados do Kernel que têm alguns campos que podem ser preenchidos a partir do espaço do utilizador. Um exemplo seriam os Atributos de Pipe.

(Mais tarde, ligaremos isto à vulnerabilidade CLSF para obter leitura/escrita arbitrária do espaço do utilizador no espaço do kernel)

Alocação de dados no kernel

Para leitura adicional, consulte fengshui-spraying-big-kids-pool. Uma explicação simplificada do mecanismo seria:

O Kernel tem 2 maneiras de alocar memória em relação ao tamanho que queremos alocar: small-pool para objetos <4KB + cabeçalho e big-pool para objetos >4KB + cabeçalho. As páginas big-pool são importantes porque podem ser enumeradas a partir do espaço do utilizador. Isto significa que um utilizador normal pode encontrar todos os endereços iniciais do kernel que contenham uma página Big-Pool.

Como? Cada página Big-Pool tem um campo chamado Tag (que pode ser usado para obter informações sobre o tipo de dados armazenados). Todas as páginas Big-Pool no sistema podem ser enumeradas usando NtQuerySystemInformation com SystemBigPoolInformation como valor para o parâmetro SystemInformationClass. Depois, de todas as páginas, podemos filtrar por Tag e obter o endereço das páginas Big-Pool nas quais estamos interessados. Por exemplo, o CLFS usa páginas Big-Pool com a tag 'Clfs'. Podemos obter o endereço de todas as páginas no Kernel onde os objetos CLFS são alocados.

proc_1

Voltando aos Pipes, podemos operar da mesma forma: alocar um pipe suficientemente grande para usar o mecanismo Big-Pool, enumerar as páginas big-pool, procurar a Tag específica do pipe e filtrar essas páginas.

Assim, podemos vazar para o espaço do utilizador o local onde o kernel alocou os nossos pipes. E quanto a controlar os dados que vão para o Kernel e lê-los?

Para isso, contamos com os Atributos de Pipe. Tal como a tag Big-Pool, o Atributo de Pipe é um array que pode conter informações que descrevem o pipe (preenchido pelo utilizador). Usando a função não documentada NtFsControlFile, podemos ler e escrever arbitrariamente na estrutura de dados do pipe-attribute. Sendo não documentada, apenas é fornecida uma prova de conceito que define um vetor PipeAttribute e depois o lê. A única modificação permitida às primitivas de leitura/escrita é controlar o conteúdo do buffer de entrada/saída e o seu tamanho.

Escrever e ler do atributo de pipe no kernel, ex.:

Aqui alocamos um pipe suficientemente grande para usar as páginas big-pool (0x2000), definimos um buffer de entrada e saída para um valor controlado. Cuidado: os primeiros 2 bytes do valor de entrada DEVEM ser 0x5a 0x00 para funcionar.

pipewr

Aqui lemos de volta o que escrevemos anteriormente no kernel, apenas alterando o buffer de saída.

peprd

E o resultado:

piperes

Como é que uma página pipe big-pool se parece na memória e por que a operação anterior nos é útil?

Aqui estão os conteúdos da página big-pool do pipe no kernel: Ao consultar as páginas big-pool, encontrámos o início da estrutura de dados do pipe no kernel. No endereço pipe_begin+0x20, encontramos um ponteiro para o nosso buffer de entrada+0x2.

pipe_kern

Quando chamamos a função de leitura do atributo do pipe, o SO fará o seguinte:

  1. Identificar a localização do pipe no kernel
  2. Adicionar 0x20, desreferenciar o ponteiro e despejar o conteúdo do espaço do kernel no nosso buffer do utilizador.

Por que é útil? Imagine se conseguirmos substituir o ponteiro em pipe_begin+0x20 pela localização do token de segurança do processo System e chamar a leitura do atributo do pipe. Isso vazaria o seu valor para o espaço do utilizador. A substituição é realizada através da vulnerabilidade CLFS.SYS.

Memory spraying

Compreender esta técnica é crucial para entender o exploit.

A maioria dos exploits não é determinística por natureza, mas probabilística. Mesmo que o código que aproveita a vulnerabilidade esteja correto, o exploit pode não funcionar. Ao colocar o programa alvo num estado instável, o desenvolvedor do exploit deve garantir que, após executar o exploit, o sistema não crasha. Imagine um programa que, quando explorado, dá a capacidade de ler de um endereço que depende de um valor de variável dentro do programa.

Ex:

Digamos que alvo=0x1000000+ var_1&0xff+ var_2&0xff00.

O hacker não pode controlar alvo, var_1 ou var_2.

Mas o exploit dá a capacidade de ler do endereço alvo.

Poderíamos ler de qualquer lugar entre 0x1000000 e 0x100FFFF, o que, em alguns casos, pode ser útil ou não.

Ex2:

Digamos que um exploit dá a capacidade de ler um QWORD de um endereço que corresponde a um certo padrão e colocar o conteúdo no valor do seu token de segurança. Digamos que obtivemos anteriormente o valor do token de segurança para System.exe. Como podemos aproveitar o exploit para elevar privilégios?

read_addr=0x1000000+alfa&0xFFFF00

Não podemos controlar o parâmetro alfa.

OFF TOPIC MAS MUITO IMPORTANTE: O kernel pode aceder ao espaço do utilizador correspondente ao processo que está a executar código do kernel naquele momento.

De que endereço podemos ler? Bem, 0x1000000, 0x1000100(alfa=1),0x1000200(alfa=2),....,0x1FFFF00(alfa=ffff00).

Para garantir que o exploit tenha sucesso, o programador deve fazer o seguinte:

  1. Alocar no espaço do utilizador um pedaço de memória de tamanho 0x1000000 (aproximadamente): memory=virtualalloc(dest=0x1000000,size=0x1000000,....)
  2. for (i=0x1000000;i<0x2000000;i+=0x100) ((QWORD*)i)[0]=system_token_value;
  3. O código em 2. preenche a memória em qualquer valor possível de alfa com o valor do token do sistema. Portanto, é garantido que, independentemente do valor do parâmetro alfa, o token do sistema estará presente no endereço.

Isto é, de facto, memory spraying. Manipulação de memória seguindo um padrão específico que corresponde a todos os valores possíveis de uma expressão probabilística que é o resultado de um exploit.

Um requisito para isto funcionar no nosso caso é que a memória possa ser alocada no endereço 0x1000000. Esta é uma limitação com a qual também teremos que trabalhar no caso do exploit real.

Common Log File System

Com alguns aspetos técnicos esclarecidos, agora é necessária uma compreensão básica do CLFS. Link da Microsoft. O CLFS é usado para Logs de aplicações, logs de base de dados, transações, etc.

Exploits geralmente requerem um certo layout de memória para funcionar. A forma do layout é ditada por valores de variáveis dentro do programa no momento exato da exploração.

Este tutorial não explicará em detalhe o código vulnerável, ou o formato do sistema de ficheiros de Log. Oferecerá uma compreensão básica dos processos que geram o exploit.

Os ficheiros de log são um tipo especial de ficheiros que têm um certo formato e podem ser interagidos através da API do driver e DLL do CLFS. Dentro deste sistema, existe também o conceito de contentores de log. Os contentores de log são também ficheiros de log, mas estão ligados em memória a um ficheiro de log principal (o ficheiro de log ao qual são adicionados).

Esta construção funciona da seguinte forma:

  1. Criar ou abrir um ficheiro de log principal
  2. Criar ou abrir ficheiros de log secundários
  3. Usar a API AddLogContainer para adicionar os ficheiros de log secundários como contentores ao ficheiro de log principal

Esta operação força a alocação de novo espaço de memória dentro do ficheiro de log principal e modificará diferentes objetos dentro do layout de memória do ficheiro de log principal. A modificação do layout de memória, conceptualmente, pareceria assim:

containet_concept

Visão geral de alto nível do mecanismo do exploit

Tal como acontece com todas as estruturas/ficheiros de dados importantes, antes de usar, o driver CLFS faz verificações de sanidade contra o formato do ficheiro de log:

  1. Antiviolação (integridade): O hash do ficheiro é armazenado num offset constante no ficheiro. Para abrir o ficheiro com sucesso, o driver calcula o hash do ficheiro e verifica-o contra o valor armazenado. Se os valores não forem iguais, significa que o ficheiro foi adulterado após a criação, e um erro é gerado.
  2. Verificações de intervalo e estrutura do ficheiro: Vários cabeçalhos e comprimentos são verificados para garantir que o formato do ficheiro é respeitado.

O segundo ponto é o ponto de partida da vulnerabilidade. O atacante é capaz de modificar um ficheiro de log previamente criado, recalcular o seu hash e editar o campo hash para fazer o driver passar no teste de integridade. As modificações estão relacionadas com comprimentos de cabeçalhos do ficheiro. Um conjunto cuidadosamente criado de valores determina uma aprovação numa verificação de intervalo que, de outra forma, falharia e geraria um erro. O driver aceita assim o comprimento falso como válido e continua a execução do código normalmente. O comprimento falso é usado para calcular um offset dentro do ficheiro onde um endereço hardcoded será escrito. Isto dá ao atacante a capacidade de controlar o endereço onde a operação de escrita anterior ocorrerá.

Esta vulnerabilidade deve ser encadeada com a leitura/escrita no kernel via pipe para completar o exploit. Prova visual:

range-check

Na figura anterior, vemos a função AllocSymbol dentro de CLFS.sys, que é responsável pela verificação de intervalo falsa. A verificação de comprimento vulnerável é aquela que retornaria o código de erro 0xC0000023. (BaseLogRecord + v9 + Size_1 + 0x1338 > v8 + *(0x68)) A sua entrada para a condição é em parte controlada pelo atacante. É possível injetar um valor na fórmula que faz com que a verificação passe, mesmo que normalmente falhasse. Ao controlar v8 e v9, podemos manipular o valor de verdade da condição para FALSE, impedindo o retorno de erro e levando ao cálculo arbitrário do valor da variável v10, usando o valor v9 controlado pelo atacante.

V10 é ainda usado para um memset com 0 como valor a ser definido. Assim, o atacante ganha a capacidade de definir arbitrariamente memória para 0. Isto não é tudo. Antes do retorno final, *a3=v10. A3 é um parâmetro enviado por endereço para a função AllocSymbol. O chamador de AllocSymbol recebe assim o valor de V10 após retornar de AllocSymbol.

O chamador de AllocSymbol é FindSymbol.

find-symbol

Dentro de FindSymbol, V33 é o parâmetro chamado a3 em AllocSymbol. Então, v33 recebe o valor de v10. Depois, o valor no endereço v33 é definido para uma constante (0xc1fdf006) e ainda prefixado com o valor 0x30.

constant-fix

Assim, o atacante ganha a capacidade de definir uma localização de memória arbitrária com uma constante.

Por que isso seria importante? Se conseguirmos sobrescrever o valor predefinido de um ponteiro de função com um endereço constante acessível tanto do espaço do utilizador como do espaço do kernel, e tivermos a garantia de que o respetivo ponteiro será chamado, ganhamos controlo da execução do código. Embora esta seja a ideia geral, existem contratempos técnicos que serão explicados mais adiante.

Corrupção de ponteiro CLFS, ou qual localização sobrescrever para sequestrar o fluxo de execuçãoAnteriormente falamos sobre contêineres CLFS. Quando um contêiner é adicionado a um arquivo, no arquivo pai uma estrutura é preenchida. A estrutura tem um ponteiro chamado CClfsContainer* pContainer;. Quando o respectivo contêiner é desalocado, no arquivo pai, operações de limpeza ocorrem e desreferenciam pContainer e usam os valores [[pContainer]+0x18] e [[pContainer]+0x8] como ponteiros de função.

Portanto, a capacidade de controlar pContainer e executar as APIs específicas do contêiner para adição e exclusão garante o redirecionamento do fluxo de controle.

Onde pContainer está localizado?

A estrutura do arquivo CLFS é não documentada, mas algumas tentativas individuais de reverter a estrutura do arquivo existem.

Uma visão geral da estrutura de um arquivo de log:

bird_eye_file_Structure

E uma visão detalhada do bloco base:

detailed_base_blok

Diferenças entre a estrutura e a visão de memória:

Os arquivos CLFS são alocados em páginas big-pool com um valor de tag Clfs. Quando o endereço de log de um CLFS é obtido no espaço do usuário (o mesmo método usado para obter o endereço do espaço do kernel de um objeto pipe), o kernel retornará O ENDEREÇO ONDE O BLOCO BASE COMEÇA. (no offset 0x800 na imagem anterior). No offset 0xb98 vemos uma estrutura de dados chamada regContainers. Este é um array de valores de 32 bits. Cada valor está relacionado a um contêiner e representa o offset (contando a partir de 0x870) onde as estruturas de dados internas de um contêiner estão localizadas no arquivo do bloco base.

O layout de memória de um arquivo de contêiner é o seguinte:

  1. Estrutura CLFS_CONTAINER_CONTEXT

container

No offset 0x18 da estrutura CLFS_CONTAINER_CONTEXT está nosso alvo pContainer.

Aparentemente o offset para pContainer é constante, ou seja, o primeiro elemento do array regContainers. E seu valor é 0x1468.

Prova de que pContainer é desreferenciado e usado como ponteiro de função:

Desreferenciar e acessar pContainer como um ponteiro de função é o cerne do exploit. Isso acontece dentro da função Remove container no driver CLFS.SYS. Portanto, o exploit será acionado quando o contêiner for removido.

Prova:

container_removal

Vamos revisar os passos de remoção de um contêiner para mostrar que de fato o ponteiro pContainer é acessado como um ponteiro para uma função

  1. Na linha 22 há uma chamada para a API GetBaseLogRecord que retorna o endereço do kernel do bloco base + 0x70 (ele move o ponteiro do arquivo além do cabeçalho)
  2. Na linha 23 uma cópia do resultado (BaseRecord_1) é inicializada
  3. Na linha 41 outra cópia de uma variável a4 é inicializada como v10
  4. Na linha 50 : Há uma construção interessante de indexação de vetor: LODWORD(a4) = *( (_DWORD*)BaseLogRecord_1 + StartingIndex_1 + 0xCA); Se houver um único contêiner adicionado ao arquivo, então StartingIndex_1 é zero. O valor retornado por GetBaseLogRecord é convertido para um vetor DWORD (32 bits) e indexado por 0xCA elementos. Observe que (_DWORD*)BaseLogRecord_1 + 0 + 0xCA não simplesmente adiciona o valor 0xCA a BaseLogRecord_1. Devido ao cast, isso é equivalente a BaseLogRecord_1[0xCA]. Isso é refletido no código desmontado correspondente a isto, mov eax, [rdi+r12*4+328h] onde rdi é a base, r12 é StartingIndex_1 (note que é multiplicado por 4 que é o comprimento de um elemento de 32 bits (DWORD)) e somado com 0x328 que é 0xCA*0x4.

O valor de a4 é no final BaseBlock+0x70+0+328=(BaseBlock+0x398) que nos levaria ao primeiro valor de RgConrainers (0x398+0x800=0xB98).

  1. Na linha 76, v13 é obtido de v10 (cópia de a4) via cast para vetor QWORD (tamanho de elemento 8) e indexação com 3 posições. Observe na linha 16 da descompilação que a4 é do tipo _CLFS_CONTAINER_CONTEXT. Portanto, v10 é o terceiro campo da estrutura que corresponde a pContainer.
  2. Nas linhas 82 e 83, pContainer é desreferenciado, somado com 0x18 e 0x8, interpretado como um ponteiro para uma função e executado. (call cs:__guard_dispatch_icall_fptr é na verdade uma chamada para uma instrução jmp eax)

Isso prova que ao reescrever o valor de pConainter podemos alterar o fluxo de controle do driver e apontá-lo para endereços controlados pelo atacante.

Pulverização de Kernel com objetos

Anteriormente foi explicado que às vezes você precisa preencher um pedaço de memória com um padrão predeterminado de valores para garantir a execução bem-sucedida de um exploit. Isso porque o hacker pode controlar apenas um subconjunto das variáveis que compõem o estado do programa no momento da exploração. No exemplo simples de pulverização de memória anterior, usamos apenas valores escritos em um vetor para satisfazer uma certa condição.

No caso real, as condições são mais complicadas. Precisamos organizar arquivos de Log na memória do kernel em uma certa ordem com um offset conhecido entre eles.

Primeiro de tudo, vamos criar muitos arquivos de Log em um loop e estudar como o sistema operacional aloca memória para eles. O experimento usará o seguinte padrão:

  1. Identificar arquivos CLFS presentes no sistema por padrão
  2. Alocar um novo arquivo CLFS
  3. Identificar o endereço do kernel do Bloco Base do novo arquivo
  4. Repetir para ~50 arquivos
  5. Estudar os endereços onde os arquivos foram alocados

O seguinte trecho de código realiza isso:

page_study

E o resultado:

result_addresses

Vamos ordenar os endereços:

ordered_addresses

Ao inspecionar o offset entre os endereços, um pseudo padrão em relação às alocações: Existem alocações contínuas que têm um offset constante entre elas. Por exemplo, de ffffd80faf444000 a ffffd80faf4ee000 o offset entre duas alocações consecutivas é 0x11000.

Essa suposição é crucial para o funcionamento do exploit. Será utilizada.

Outra suposição: Vamos pegar algumas páginas que estão separadas por 0x11000. Por exemplo: ffffd80faf444000,ffffd80faf455000,ffffd80faf466000,ffffd80faf477000,ffffd80faf488000,ffffd80faf499000. Todas as páginas correspondem a arquivos CLFS abertos. Se fecharmos um arquivo, a página será desalocada e a memória no respectivo endereço ficará livre. Isso se pareceria assim na memória (digamos que fechamos o arquivo correspondente à página em ffffd80faf466000): ffffd80faf444000,ffffd80faf455000,xxxxxxxxxxxxxxxx,ffffd80faf477000,ffffd80faf488000,

SE abrirmos o arquivo novamente, o SO irá, com alto grau de certeza, alocar uma página no mesmo endereço (ffffd80faf466000) para preencher o buraco e tornar a memória contínua. --> Isso também é crucial para a execução do exploit.

Esquema do exploit até agora

Agora apresentamos um esquema da estratégia que empregaremos para garantir que sobrescrevamos o ponteiro *pConainter com um endereço sob nosso controle, no espaço do usuário.

Esquema do exploit

  1. Alocar um monte de arquivos clfs resultando em um layout de memória como este:

first_allocation

  1. Identificar uma sequência (idealmente máxima) de arquivos que estão a 0x11000 de distância um do outro.

second_Allocation

Na imagem anterior start(aux1)+0x11000=start(A),start(A)+0x11000=start(B),start(B)+0x11000=start(aux2)

  1. Usaremos Logfile A para sobrescrever o *pcontainer pointer do Logfile B, e acionar o exploit fechando Logfile B usando um código especial de exclusão após fechamento.

  2. Adicionar um contêiner de log ao Logfile B, para alocar e atualizar os campos que sinalizam que B tem um arquivo de contêiner correto. Podemos adicionar aux2 como um contêiner para B.

  3. Fechar LogfileA para que possamos editá-lo em disco. É importante escolher A e B no meio de uma sequência máxima de arquivos espaçados por 0x11000, a fim de criar um buraco na memória que o SO priorizará preencher. Se não o fizer, o exploit falhará.

  4. Recalcular o hash de A, editar seu campo de hash para manter a integridade e abrir A novamente, esperando que o Kernel o coloque no mesmo endereço, caso contrário o exploit falhará.

  5. Chamar AddLogContainer em A com um arquivo de Log normal para acionar a sobrescrita do ponteiro *pContainer de B

  6. Excluir o arquivo B para que RemoveConainter seja chamado e a execução seja transferida para o *pContainer de B que agora está corrompido e aponta para código alocado pelo usuário.

A imagem a seguir representa os passos mencionados anteriormente.

steps_first

CÓDIGO - Obtendo um layout de memória correto

Começamos alocando 50 arquivos CLFS. Após cada alocação, consultamos todas as páginas CLFS na memória do Kernel e fazemos uma lista com todos os endereços que foram alocados para cada arquivo. Em seguida, identificamos 2 arquivos que estão a 0x11000 de distância um do outro (A e B no esquema). (e armazenamos nas variáveis first, second)

allocate_clfs

Após identificar a busca de endereço por dois arquivos com 0x11000 de distância A->first , B->second

find_tw_files

Em seguida, feche A (first) e edite-o em disco (malformar seus cabeçalhos) e abra-o novamente.

malform_first_file_o

Editando os cabeçalhos de A e recalculando seu hash

Este passo precisa de mais explicação porque precisamos calcular alguns valores exatos que, ao acionar o código explorável em AllocSymbol, levarão à sobrescrita do ponteiro *pConainter de B.

Pela análise anterior das funções AllocSymbol e FindSymbol sabemos que devemos modificar o arquivo A de tal forma que sobrescreva o valor lógico da condição IF para FALSE e injete um valor na variável v9 que levará a uma escrita no ponteiro da localização *pContainer de B.

Primeiro, qual valor v9 deve ser definido?

AllocSymbol calcula v10 (endereço alvo da escrita) como: v10=BaseLogRecord + v9 + 0x1338. Isso é calculado no contexto do espaço de endereço de A. Portanto, BaseLogRecord é: Endereço da Página Kernel de A + 0x70. v9 é controlado pelo atacante e 0x1338 é constante.

Onde está *pContainer de B em relação ao seu Endereço de Página Kernel?

Isso foi detalhado anteriormente, então, a partir do endereço de Página Kernel de B precisamos adicionar 0x398 para chegar ao vetor regContainter de B e indexar pelo primeiro elemento para obter o offset para a primeira estrutura CONTEINER_CONTEXT. Como afirmado anteriormente, para o primeiro contêiner o índice foi determinado como 0x1468 (contando a partir de B BaseBlock +0x70)

Portanto, a localização de *pContainer de B é B's Kernel_Address + 0x70 + 0x1468 + 0x18 (terceiro elemento na estrutura CONTEINER_CONTEXT)

B's Kernel_Address=A's Kernel Address+0x11000 (porque construímos a memória desta forma)

A's BaseRecordAddress=A's Kernel Address+0x70

V10=A's Kernel Address+0x70+v9+0x1338

v10 precisa sobrescrever *pContainer de B

V10 precisa ser: v10=B's Kernel_Address + 0x70 + 0x1468 + 0x18 (terceiro elemento na estrutura CONTEINER_CONTEXT)

Substituindo B's Kernel_Address: v10=A's Kernel Address+0x11000+0x70+0x1468+0x18

Reduzindo v10: A's Kernel Address+0x70+v9+0x1338=A's Kernel Address+0x11000+0x70+0x1468+0x18

Resolvendo para v9 = 0x11000+0x70+0x1468+0x18-0x1338-0x70=0x11148

Agora, quais campos em A precisam ser sobrescritos? --> Existem 2 categorias de campos:

  1. Campos para forçar a condição IF em AllocSymbol a ser avaliada como FALSE
  2. Campo correspondente a v9 para calcular a distância exata até *pContainer de B

Os campos da categoria 1 não serão detalhados. V9 corresponde ao campo no offset 0x1b98 no disco dentro do arquivo A. Não haverá mais detalhes relacionados ao porquê esses campos acionam o exploit, cabe ao leitor ler sobre isso, se interessado.

Aqui estão os valores que precisam ser modificados:

disk_a_modification

Observe que (little endian), o valor usado é 0x11149 em vez de 0x11148. Isso porque precisamos controlar os octetos de ordem superior em *pContainer. O octeto menos significativo terá a forma X0 (10 ou 20 ou 30...). Podemos fazer um spray de memória em cada um dos valores para levar em conta essa condição.

Observe que v10 também será usado para memset com 00 em uma região de memória de tamanho 0xa0

first_memset

Então, no mesmo endereço, a sobrescrita com constante

overwrite_constant

Observe o valor constante que tem a forma 0x30c1fdf006X0

Isso ocorre quando adicionamos um contêiner de log ao arquivo A, mas não cobrimos o processo de recálculo de hash após a modificação dos campos de A.

A visão do arquivo de log em disco é um pouco diferente da visão em memória. Estamos modificando apenas o conteúdo do bloco base que tem comprimento 0x7a00 e começa no disco no offset do arquivo 0x800. O algoritmo de hash usado para aplicar hash no bloco base é CRC32. O campo que armazena o valor do hash também está armazenado dentro do bloco base no offset 0x80c.

Procedimento para recalcular o hash:

  1. Zerar o valor antigo de CRC32 em 0x80c
  2. Editar os campos de acordo com os valores e offsets definidos
  3. Calcular CRC32 para o bloco base começando em 0x800 com comprimento 0x7a00.
  4. Substituir o valor zerado pelo novo valor CRC32

O código completo responsável por acionar o exploit:

Para obter o código de execução redirecionado para o espaço do usuário, precisamos:

  1. Obter o layout de memória dos arquivos CLFS A e B (já explicado)
  2. Fechar A, editá-lo em disco com cabeçalhos malformados e abri-lo novamente (explicado)
  3. Adicionar um Contêiner de Log a B (chamado second no código) para inicializar as estruturas do contêiner. O contêiner precisa ser um arquivo de log normal (um dos 50 alocados anteriormente)
  4. Preparar a memória do usuário que representará o caminho de execução alterado (SERÁ EXPLICADO A SEGUIR)
  5. Adicionar um contêiner de log a A (chamado first no código) com um arquivo de log normal como contêiner. Adicionar um contêiner à versão malformada de A acionará uma sobrescrita no valor de *pConainter de B (second) com um valor constante da forma 0x30c1fdf006X0.
  6. Configurar B (second) para ser excluído automaticamente após o fechamento usando NtSetInformationFile. É crucial usar esta API, pois simplesmente fechar seu handle não acionará a API RemoveContainer.

ntsetinfofile

  1. Fechar B e acionar o redirecionamento.

Spray de memória do usuário e layout

Existem algumas condições impostas à memória do usuário que é o alvo para redirecionar o fluxo de execução usando *pContainer. Não podemos simplesmente começar a executar instruções aleatoriamente do espaço do usuário com o programa em modo kernel.

O objetivo desta seção é vazar o valor do SystemToken no espaço do usuário, sem causar uma falha do sistema (BSOD).

Como você pode ler de um endereço no kernel e armazenar o resultado no espaço do usuário? Um lembrete rápido sobre a seção de Pipe no tutorial:

second_pipes

Aqui nós:

  1. Alocamos um pipe (objeto kernel) com um tamanho grande o suficiente para ser alocado dentro de um BigPool
  2. Escrevemos algumas informações na seção de atributos do pipe (a string do alfabeto BCDEFBBBB)
  3. Obtemos o endereço do objeto pipe no espaço do usuário usando o campo Tag das páginas BigPool
  4. Estudamos a estrutura do pipe no Kernel usando Windbg.
  5. Lemos os dados de volta do kernel no espaço do usuário usando NtFsControlFile PipeReadAttribute

A ideia de ligar o objeto Pipe ao nosso objetivo de obter o valor do token do sistema é usar NtFsControlFile PipeReadAttribute para ler do endereço do token do sistema em vez do início do buffer que armazena as informações que passamos ao kernel usando PipeWrite Attribute.

Isso significa que devemos modificar o valor do ponteiro no endereço PIPE_BEGIN+=0x20 para conter o endereço onde o token do sistema está localizado.

O endereço é conhecido, obtido nas etapas anteriores do tutorial, quando localizamos e analisamos a estrutura EPROCESS.

Aqui precisamos encontrar um mecanismo que nos permita escrever na estrutura do pipe em uma localização fixa.

Para isso, usamos o redirecionamento de código obtido via exploração da função CLFS AddLogConainer. Pode-se pensar que é suficiente escrever um shellcode que faça a substituição bruta, mas (embora não tenha sido testado) certamente pode não funcionar. Isso porque o driver executa em contexto Kernel e executa código de uma área de processo do usuário.

Para contornar essa limitação, devemos encontrar ROPs do Kernel que realizem uma escrita em um valor arbitrário. Ou seja, encontrar alguns pedaços de código do kernel que estão no final de uma função e terminam com uma instrução ret, e fornecer seus endereços como alvos para redirecionamento. Dessa forma, o código ainda é executado pelo Kernel.

Esta é a ideia principal, mas limitações surgem da forma como o driver CLFS chama código dentro de *pConainer:

mem_spray_user

Para criar um padrão de memória utilizável, devemos estudar a forma como a função RemoveContiner acessa o código de *pContainer.

Na imagem anterior, destacamos as porções de código que desreferenciam *pContainer e o usam como ponteiro de função.

RDI é a constante que foi escrita dentro de *pcontainer ao explorar AllocSymbol. Como você vê, a constante não é inteiramente fixa em valor, varia no primeiro byte (a forma é 6X0, onde X pode ser qualquer coisa).

mov rax, [rdi] desreferencia o valor constante. Para não causar uma leitura inválida (e BSOD), devemos garantir que rdi contenha um ponteiro para um endereço válido. Para fazer isso, devemos pulverizar a memória do usuário de 0x30C1FDF00000 até pelo menos 0x30C1FDF006FF. Isso é feito alocando um pedaço de memória com VirtualAlloc. Claro, se o sistema não puder alocar memória por qualquer motivo, o exploit falha.

LPVOID dirtySpray = VirtualAlloc((LPVOID)0x000030c1fdf006d0, 0x1000000, 0x3000, 0x4);

Então, para qualquer valor possível de X, armazenamos em 0x30C1FDF000X0 a 0x30C1FDF00FX0 outro valor que corresponde a memória de usuário válida, escolhendo um valor arbitrário, digamos 0x5000000. Desta forma, garantimos que rax será igual a 0x5000000 para qualquer rdi da forma 0x30C1FDF006X0

Após esta etapa, vemos mais duas operações de desreferência: mov rax, [rax+0x18] e mov rax, [rax+0x8]

Para garantir que a memória ainda esteja consistente nesses endereços, devemos armazenar em 0x5000008 e 0x5000018 ponteiros para funções do kernel (ROP1 e ROP2, serão calculados no futuro.)

Este é o algoritmo para gerar o padrão de spray.

algo_spray_first_stage

Nota: O padrão evoluirá quando introduzirmos os ROPs, pois eles também têm parâmetros que serão levados em consideração, mas este é o mínimo até agora para evitar endereçamento de memória inválido.

É assim que a memória pulverizada na primeira etapa se parece no depurador (observe onde o valor 0x5000000 está colocado --> alinhado a X0):

sprayed_mem_constant

É assim que a memória em 0x5000000 se parece (os ROPs que serão detalhados)

sprayed_mem_ROP

E o processo de redirecionamento de código no depurador do kernel:

debugger_redir

Observe o valor do registrador RDI e o ponteiro de instrução no depurador. RDI desreferenciado uma vez é 0x5000000 e armazenado em RAX. [RAX+0x18] é o segundo ROP, e mais abaixo [RAX+0x8] será o endereço do primeiro ROP.

ROPs do Kernel: o que são, por que úteis

Neste estágio, temos as seguintes partes do exploit que devemos ligar:

  1. O endereço do token do sistema
  2. Redirecionamento de código usando o exploit CLFS
  3. Um pipe com objeto de atributo

O escopo neste ponto é ligar o redirecionamento de código a um pedaço de código que irá sobrescrever o ponteiro do Pipe para o buffer de atributos com o endereço do token do sistema e usar o Pipe read attribute para obter as informações de volta no espaço do usuário.Como afirmamos, o código que é redirecionado também deve estar no kernel. Além disso, temos apenas 2 funções para usar. O tutorial não abordará como essas 2 funções específicas foram encontradas, mas presume-se que exista uma lista de candidatos ROP que são frequentemente usados.

Estudaremos 2 funções:

  1. SeSetAccessStateGenericMapping em ntoskrnl.exe Chamada em segundo lugar ([rax+0x8])
  2. ClfsEarlierLsn em CLFS.SYS Chamada primeiro ([rax+0x18])

Análise de ClfsEarlierLSn:

ealier

A única função desta função quando chamada com parâmetros incorretos é definir EDX como 0xFFFFFFFF e retornar. Por que isso é necessário ficará claro ao analisar o segundo ROP.

Análise de SeSetAccessStateGenericMapping:

sesetAccess

Isso precisa ser discutido linha por linha em detalhes.

Entradas: Ao entrar nesta função:

  1. O valor de RAX não importa porque é sobrescrito na primeira linha da função
  2. RCX é o valor de RDI, que é a constante 0x30C1FDF006X0
  3. [RCX+0x48] tem a forma 0x30C1FDF00YX8 sempre alinhado a 0x8 e não entrará em conflito com 0x30C1FDF006X0 que está alinhado a 0 e contém 0x5000000

Execução do código da função:

  1. mov rax, [rcx+48h] desreferenciará de 0x30C1FDF00YX8 e moverá o valor para RAX
  2. movups xmm0, xmmword ptr [rdx] moverá 16 bytes de RDX para o registrador XMM0. Lembre-se de que de ClfsEarlierLsn RDX é 0xFFFFFFFF
  3. movdqu xmmword ptr [rax+8], xmm0 moverá para [RAX+0x8] o valor de XMMO

Essencialmente, isso lê de um endereço e armazena dentro do Kernel. Pode ser interpretado assim:

mem_move

Então, para sobrescrever o ponteiro do buffer de atributo do pipe com um ponteiro para o token do sistema, devemos alocar 16 octetos em 0xFFFFFFFF e armazenar o endereço do token do sistema lá. Então, devemos pulverizar a memória em 0x30C1FDF00YX8 repetidamente com o valor do endereço de destino -0x8. Esse é o valor do deslocamento para o buffer de atributo do pipe -0x8. Isto é, PIPE_KERNEL_PAGE_ADDRESS+0x20-0x8.

Aqui está como isso é alcançado no código real:

mem_pattern

Outro aspecto que não foi explicado: Como obter os endereços do kernel dos ROPs no espaço do usuário?

É um truque. Primeiro, você pode obter o endereço base de qualquer módulo do kernel usando NtQuerySystemInformation com os seguintes parâmetros: NtQuerySystemInformation(SystemModuleInformation, (HANDLE)ModuleInfo, ModuleInfoSize, &retlen)

Em seguida, você filtra pelo valor de ModuleInfo->Modules[i].Name para corresponder à biblioteca necessária. Isso obtém o endereço base no kernel.

Então exploramos o fato de que o deslocamento entre o endereço base e o endereço de exportação é constante, independentemente do espaço de endereçamento onde o executável está carregado.

Carregamos o módulo no espaço do usuário (você pode fazer isso) usando LoadLibrary e obtemos o endereço da exportação usando Getprocaddress. Calculamos o delta entre Exportação e base do usuário e o adicionamos à base do kernel (encontrada usando NtQuerySystemInformation). Assim, obtemos no espaço do usuário o endereço da exportação que está carregada no KernelSpace.

Lendo o valor do token do sistema

Uma vez que o Ponteiro para o atributo foi corrompido e definido para apontar para a localização do token do sistema, uma chamada para MyNtFsControlFile com os parâmetros corretos deve ler do endereço e vazar o valor do token do sistema para o espaço do usuário.

read_token

O que vem a seguir?

Tendo o valor correto para o token do Sistema, para completar o exploit, devemos escrevê-lo em vez do nosso próprio processo. Ou seja, sobrescrever o token do processo que executa o exploit com o valor do token do sistema.

As etapas necessárias para fazer essa mudança não incluem novas técnicas ou metodologias, mas dependem do reuso de código anterior. O algoritmo para sobrescrever nosso próprio valor de token implica uma operação de escrita no kernel em um determinado endereço. Isso é alcançado pelas funções ROP SeSetAccessStateGenericMapping e ClfsEarlierLsn. Isso implica que PRECISAMOS ACIONAR O EXPLOIT DO CLFS UMA SEGUNDA VEZ. Isso está correto, devemos fazer a alocação do contêiner e a pulverização de memória uma segunda vez e esperar que o sistema operacional não trave.

Etapas para sobrescrever nosso próprio valor de token:

  1. Identificar onde no Kernel o nosso próprio token está armazenado. (semelhante à primeira vez em que identificamos o endereço para o token do sistema)
  2. Fazer a pulverização de memória do sistema e a pulverização de objetos com arquivos CLFS novamente
  3. Dentro da memória pulverizada pelo usuário, definir o conteúdo do buffer de modo que o DESTINATION_ADDRESS seja o endereço do nosso próprio token do sistema e o endereço de origem (no endereço 0xFFFFFFFF) seja o valor do token do sistema obtido na primeira execução do exploit.
  4. Acionar o exploit do CLFS uma segunda vez.
  5. Iniciar um cmd.exe e verificar os privilégios.

Como não precisamos de uma leitura do kernel na segunda vez, não temos uso do pipe desta vez.

Como as etapas não exigem conhecimento adicional, o tutorial pode terminar aqui com uma prova final de um cmd.exe com nt authority\system

final_proof

Sinopse

O foco principal deste tutorial não foram os internos do CVE em si, mas sim uma descrição amigável do processo de criação de um exemplo de escalada de privilégio no Windows. Muitos exploits compartilham os mesmos métodos e blocos de construção, como pulverização de memória ou trabalho com certas estruturas de dados do Windows. E na maioria das vezes há uma grande lacuna entre ter conhecimento teórico interno do Windows e efetivamente escrever um algoritmo que aproveita uma vulnerabilidade.

Baixar ferramenta
ffffd80faf499000