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-2024-38063- — Explorando o kernel remotamente via IPv6 | Kitploit
Ferramentas/GitHubGitHub/adminpentester/cve-2024-38063-
Forensia de MemóriaAnálise de VulnerabilidadesExploraçãoEngenharia ReversaAprendizado e EducaçãoExploração de Binários
GitHubadminpentester/cve-2024-38063-

CVE-2024-38063-

Explorando o kernel remotamente via IPv6

Ver Repositório
21há 1 anoAinda 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

CVE-2024-38063-

Explorando Remotamente o Kernel Através do IPv6

CVE-2024-38063 - Explorando Remotamente o Kernel Através do IPv6 Marcus Hutchins

Desde a última atualização de segurança da Microsoft lançada em 13 de agosto, mergulhei fundo no tcpip.sys (o driver do kernel responsável por processar pacotes TCP/IP). Uma vulnerabilidade com pontuação 9,8 no CVSS na parte mais facilmente acessível do kernel do Windows era algo que eu simplesmente não podia deixar passar. Nunca havia realmente examinado o IPv6 antes (nem os drivers responsáveis por analisá-lo), então sabia que tentar fazer engenharia reversa dessa vulnerabilidade seria extremamente desafiador, mas uma boa experiência de aprendizado.

Na maior parte, o tcpip.sys é amplamente não documentado. Consegui encontrar alguns write-ups de exploração para bugs antigos: aqui, aqui e aqui, mas pouco mais. Quando o principal resultado da minha pesquisa no Google em inglês está escrito em chinês, imediatamente sei que estou muito além da minha profundidade e vou passar por maus bocados, mas aprender é necessário. Apesar do Google Tradutor ter feito um trabalho medíocre, o post forneceu uma visão incrivelmente detalhada sobre como a fragmentação IPv6 funciona e me deu um bom ponto de partida.

Mais tarde, enquanto pesquisava alguns nomes de funções no Google, me deparei com outra análise da mesma vulnerabilidade de 2021, escrita por Axel Souchet (também conhecido como 0vercl0k), que se aprofundou ainda mais nos detalhes internos do tcpip.sys e me deu informações suficientes para definir várias estruturas não documentadas. A análise de patch mais fácil de todos os tempos

Normalmente, até mesmo fazer engenharia reversa do patch para descobrir qual mudança no código corresponde à vulnerabilidade pode levar dias ou até semanas, mas neste caso foi instantâneo. Foi tão fácil, na verdade, que várias pessoas nas redes sociais me disseram que eu estava errado e que o bug estava em outro lugar. Será que eu realmente as ouvi e perdi um dia inteiro revertendo o driver errado? Talvez nunca saberemos.

Houve exatamente uma mudança feita em todo o arquivo do driver, que, no final das contas, era realmente o bug.

Uma visão geral do bindiff do tcpip.sys antes e depois da instalação do patch.

Apenas uma única função em todo o driver foi modificada. Normalmente, eu poderia passar um dia inteiro analisando mais de 20 mudanças de função diferentes apenas para descobrir qual delas deveria estar observando, mas desta vez não.

Ipv6pProcessOptions() antes do patch.

Ipv6pProcessOptions() depois do patch.

Não foi apenas uma única função que foi alterada, mas uma única linha de código.

A função de nome extremamente longo Feature_2660322619__private_IsEnabledDeviceUsage_3() é algo que a Microsoft às vezes adiciona para permitir reversões parciais de patches. A chamada verifica a presença de uma flag global ou configuração de registro que, se definida, fará com que a função retorne falso, resultando na execução do código original em vez da versão corrigida.

O motivo pelo qual a Microsoft faz isso é porque patches de segurança às vezes quebram coisas involuntariamente, então essa configuração permite que um administrador desfaça o patch de uma única vulnerabilidade sem desinstalar todo o pacote de patches mensal e enfraquecer drasticamente a segurança do sistema.

Levando isso em conta, fica claro que tudo o que esse patch faz é substituir uma chamada para IppSendErrorList() por IppSendError(), nos dando uma pista de que o problema está em algum tipo de lista. A diferença de patch mais fácil de todos os tempos (ou pelo menos foi o que pensei). Vulnerabilidades opcionais, exploração obrigatória

Fazer engenharia reversa do patch para encontrar o código alterado é apenas metade do desafio (ou neste caso, menos de 0,1%). O resto do processo consiste em fazer engenharia reversa o suficiente da base de código para entender o que está acontecendo, descobrir que tipo de vulnerabilidade foi corrigida, como criar uma requisição para alcançar o código alvo e qual estado resulta em uma condição explorável.

A primeira parte é fácil o suficiente. A mudança está em Ipv6pProcessOptions(), o que nos diz que é IPv6 e envolve processamento de opções. Então, uma rápida consulta ao RFC nos diz exatamente o que é uma opção IPv6 e onde podemos encontrar uma.

O layout do cabeçalho de opções de destino da Wikipedia.

Ok, legal. O que estamos procurando parece ser o cabeçalho de opções de destino, que fica diretamente após o cabeçalho IPv6 principal. Vamos usar a biblioteca Python 'scapy' para criar um pacote IPv6 de teste.

Nota: Para mitigar ataques DDoS usando endereços IP falsificados, o Windows restringe a capacidade de construir pacotes IP brutos. Por esse motivo, optei por usar Linux para desenvolver minha prova de conceito. Embora o Linux permita que os usuários construam e enviem pacotes brutos de camada 2 e camada 3, isso requer que o script Python seja executado como root.

import sys import struct from scapy.all import *

def send_ipv6_option_packet(dest_ip): ethernet_header = Ether() ip_header = IPv6(dst=dest_ip) options_header = IPv6ExtHdrDestOpt() sendp(ethernet_header / ip_header / options_header)

if len(sys.argv) < 2: print('Use: python3 script.py <target_ipv6_address>') exit(-1)

send_ipv6_option_packet(sys.argv[1])

Depois de definir um breakpoint em tcpip!Ipv6pProcessOptions e executar o script, ficou claro que tudo o que era necessário para alcançar a função vulnerável era enviar um pacote IPv6 com uma estrutura de opções vazia. Tentei então adicionar algumas opções inválidas à estrutura para ver se conseguia alcançar a chamada para IppSendErrorList().

Uma breve revisão do código indicou que quase qualquer formatação inválida de opção poderia acionar a chamada para IppSendErrorList. Então, decidi usar a opção Jumbo Packet com um comprimento inválido (menos de 65535 bytes).

options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])

Então, o que IppSendErrorList() realmente faz? Bem, o código é bastante simples.

A função IppSendErrorList inteira.

O código itera uma lista encadeada e chama IppSendError() em cada item da lista. Novamente, as estrelas se alinharam e as coisas têm sido fáceis até agora. Se IppSendErrorList apenas chama IppSendError para cada item em uma lista, e o patch substitui a chamada para IppSendErrorList por IppSendError, então o problema ocorre quando IppSendError é chamado em um item da lista que não seja o primeiro.

Então, do que é essa lista e como podemos fazer uma? Ele está fazendo uma lista, ele está verificando….52.567 vezes

Foi aqui que as coisas passaram de óbvias para anormalmente difíceis, embora eu ache que grande parte disso se deveu a um dos meus dois neurônios disponíveis estar preocupado em lutar contra uma infecção grave de covid. Perdi alguns dias tentando entender partes do código, adormecendo e depois esquecendo o que havia descoberto. Todo o processo exigiu mais de uma semana de engenharia reversa de partes do tcpip.sys para descobrir o que estava acontecendo. Mas o post do blog de Axel foi extremamente útil.

Observando as funções e estruturas que Axel inverteu e para quais outras funções elas são passadas, fica claro que o único argumento passado para Ipv6pProcessOptions() é a mesma estrutura packet_t definida no artigo. Essencialmente, o ponteiro passado para Ipv6pProcessOptions, e iterado por IppSendErrorList, é uma lista encadeada de pacotes.

Então, defini um breakpoint em Ipv6pProcessOptions() e inspecionei a lista.

A entrada list->Next é NULL.

Toda vez que meu breakpoint era acionado, a lista continha apenas um pacote. Passei muito mais tempo do que gostaria de admitir tentando descobrir por que e como fazer minha lista realmente ser uma lista. Meu primeiro pensamento foi fragmentação IPv6: o IPv6 permite que remetentes dividam pacotes grandes em pacotes menores separados, o que faria sentido mantê-los juntos em uma lista.

Após extensa engenharia reversa, confirmei que minhas suposições estavam corretas, embora a lista de fragmentos não esteja relacionada com a que estamos lidando aqui.

Na verdade, acabei encontrando a resposta totalmente por acidente. Ocasionalmente, a lista se preenchia, mas o motivo não estava claro. Depois de muito andar em círculos, percebi que quando meu breakpoint no kernel é acionado, ele pausa todo o kernel, fazendo com que o adaptador de rede acumule pacotes. Quando o kernel é retomado, esses pacotes são passados para baixo na pilha até o tcpip.sys em uma lista organizada. Isso só ocorreu se os pacotes fossem enviados enquanto o kernel estava pausado, mas não processados antes do próximo breakpoint ser acionado.

Esse comportamento é provavelmente uma otimização de desempenho, onde em baixa taxa de transferência, o kernel processa pacotes individualmente, mas em volumes mais altos, os pacotes são organizados em listas e processados em lote. Muito provavelmente, as listas são separadas com base em fatores como protocolo e endereço de origem para acelerar o processamento, então nossa lista deve conter apenas pacotes IPv6 que enviamos. Ei cara, ouvi dizer que você gosta de DoS

Agora que sabemos que os pacotes são coalescidos em listas durante alta taxa de transferência, fica claro qual seria a opção mais fácil. Nossa PoC de DoS, ironicamente, terá que usar DoS para acionar a condição de DoS. Se inundarmos o sistema com rajadas de pacotes IPv6, devemos conseguir obter uma lista bem grande passada para IppSendErrorList().

No início, não importa quantos pacotes eu enviasse, só conseguia obter a lista com n > 1 se pausasse o kernel. Mas… já que estamos usando Python (dolorosamente lento), em uma VM (duplamente dolorosamente lento), provavelmente vamos precisar ajustar algumas configurações. Para neutralizar a VM-ception que estava acontecendo no meu sistema de ataque, decidi simplesmente reconfigurar a VM alvo para usar apenas um único núcleo de CPU.

Legal! A lista de pacotes agora é uma lista contendo muitas entradas!

Então, descobriu-se que uma VM dentro de outra VM não é a melhor opção para DoS, quem diria? Mas conseguimos fazer funcionar no final. Agora, só precisamos descobrir o que IppSendError() faz e onde está o problema. Mais engenharia reversa…de novo…para sempre…

Depois de uma extensa engenharia reversa, ficou muito mais claro o que IppSendError faz. Em circunstâncias normais, ele simplesmente desabilita o pacote definindo net_buffer_list->Status como 0xC000021B (STATUS_DATA_NOT_ACCEPTED). Então, transmite um erro ICMP contendo informações sobre o pacote errôneo de volta ao remetente.

Duas partes relevantes de IppSendError.

Meu primeiro passo foi verificar se havia alguma função no tcpip.sys que ignora o valor de net_buffer_list->Status. Isso resultaria no driver processando pacotes em estados indefinidos ou inesperados, esperançosamente levando a uma condição de exploração.

O loop principal responsável por processar pacotes.

Como o loop responsável por chamar todas as funções de análise está envolto em uma verificação de erro (o que significa que não podemos ir a lugar nenhum uma vez que o código de erro é definido), pensei que essa era a toca de coelho errada para seguir. Em vez disso, decidi voltar ao IppSendError e verificar se existem caminhos de código que modificam o estado do pacote antes de definir o código de erro, o que poderia levar a uma condição de corrida.

Depois de muito mais engenharia reversa, encontrei o seguinte código perto do final de IppSendError.

Um caminho de código em IppSendError que define packet_size como zero.

Quando IppSendErrorList, e portanto IppSendError, é chamado com o argumento always_send_icmp definido como verdadeiro, parece que ele tenta enviar o erro ICMP para cada pacote na lista.

Então, por razões provavelmente conhecidas apenas por Deus, ele atinge um bloco de código onde o campo packet->packet_size é definido como zero.

Para definir always_send_icmp como verdadeiro, tudo o que precisamos fazer é causar um erro específico no processamento do cabeçalho de opções definindo o valor 'Option Type' para qualquer número maior que 0x80.

def build_malicious_option(next_header, header_length, option_type, option_length): dest_options_header = 60

root@kitploit:~
options_header = struct.pack('BBBB', next_header, header_length, option_type, option_length) + b'1337'
return Ether(dst=mac_addr) / IPv6(dst=ip_addr, nh=dest_options_header) / raw(options_header)

packet = build_malicious_option(next_header=59, header_length=0, option_type=0x81, option_length=0) sendp(packet)

Mas, definir packet_size como zero não deveria quebrar o analisador?

Um trecho do loop principal responsável por processar pacotes.

O tratador de pacotes simplesmente chama uma função VTable baseada no valor de packet->next_header, que permanece inalterado desde quando foi definido durante a pré-análise. Isso permite que o processamento do pacote continue e até nos dá controle sobre qual processamento ocorre.

Como o valor de packet->next_header é obtido do campo 'Next Header' do pacote IPv6, podemos defini-lo como qualquer valor de cabeçalho IPv6 válido, e o loop chamará o analisador correspondente. Isso nos dá uma grande superfície de ataque potencial.

O formato do pacote IPv6.

Tudo o que resta é encontrar uma parte acessível do analisador IPv6 que faça algo bobo com o campo packet_size. De volta à fragmentação

O primeiro lugar que decidi olhar foi o analisador de fragmentos IPv6, porque é onde estava a antiga vulnerabilidade CVE-2021-24086, então parecia um bom lugar para encontrar mais código maluco.

Eh… é tão perto, mas também tão longe.

Temos uma vulnerabilidade aqui, mas não é um RCE.

Essencialmente, na maioria das CPUs, os registradores são circulares. Se você incrementar um registrador além de seu valor máximo possível, ele volta a zero. Da mesma forma, se você decrementá-lo abaixo de seu valor mínimo possível, ele volta ao valor máximo possível. Isso é chamado de estouro de inteiro e estouro negativo de inteiro, respectivamente. Esse comportamento é ligeiramente diferente para inteiros com sinal, mas não estamos lidando com eles aqui.

A primeira linha, fragment_size = LOWORD(packet->packet_size) - 0x30, consiste no seguinte código ASM:

O código ASM calculando o tamanho do fragmento.

AX são os 16 bits baixos do registrador EAX. Embora o registrador EAX seja de 32 bits, AX opera como se fosse seu próprio registrador de 16 bits, portanto, quaisquer estouros ou estouros negativos estão confinados a AX e não afetarão o resto do registrador EAX. Isso é incrivelmente conveniente porque um estouro negativo no registrador EAX resultaria em um valor de 4 bilhões, o que resultaria em uma tentativa de alocar 4 GB de memória, o que provavelmente falharia.

Como o valor de packet->packet_size é zero, este código define ax para zero e então subtrai 0x30 dele.

Em condições normais, o cabeçalho do pacote tem 0x30 bytes, então packet_size - 0x30 é o tamanho dos dados do fragmento.

No nosso caso, packet->packet_size é 0, então subtrair até mesmo 1 fará com que o registrador volte ao valor máximo possível de inteiro de 16 bits (0xFFFF). Como estamos subtraindo 0x30, o valor de AX sofrerá estouro negativo e se tornará MAX_VALUE - 0x2F, ou 0xFFD0, que é 65.488.

Infelizmente, como o mesmo cálculo é usado tanto para alocação de memória quanto para cópia de dados, não conseguimos um estouro de buffer. Acredito que RtlCopyMdlToBuffer() também realiza verificação de limites no buffer de origem, então nem mesmo obtemos uma leitura fora dos limites. No entanto, não saímos completamente de mãos vazias.

Como ExAllocatePoolWithTagPriority() não zera a memória alocada, e RtlCopyMdlToBuffer() copia apenas a quantidade real de dados disponíveis, obtemos cerca de 65kb de memória do kernel não inicializada. Como os endereços de memória são reciclados após a desalocação, é provável que o buffer seja preenchido com o que quer que estivesse armazenado no endereço antes da realocação. Se conseguirmos usar fragmentação para construir um pacote que seja enviado de volta para nós, como uma solicitação de Echo ICMP, poderíamos potencialmente vazar memória aleatória do kernel, levando a uma bypass de ASLR.

Além disso, o código também define reassembly->fragment_size para o inteiro de 16 bits estourado negativamente (65.488), então agora temos duas variáveis separadas que poderíamos usar potencialmente para causar um estouro de buffer. Derrotado, mas não vencido

Infelizmente (ou felizmente, pois provavelmente me economizou muito tempo), alguém chegou na minha frente. Antes que eu pudesse encontrar um lugar para usar um dos inteiros estourados negativamente para acionar um estouro de buffer, @ynwarcs encontrou a resposta e publicou uma PoC. Isso resolve a peça final do meu quebra-cabeça.

A solução (ou pelo menos uma delas) é Ipv6pReassemblyTimeout(). Embora não possamos causar um estouro no tratamento inicial do fragmento, aparentemente podemos durante a limpeza.

Fragmentos IPv6 permanecerão na memória até que uma de três condições ocorra:

root@kitploit:~
Nós bagunçamos nossa fragmentação o suficiente para que o sistema nos diga que é hora de parar.
Enviamos um fragmento com o campo 'More' definido como 0, o que indica que este é o último fragmento, e o sistema começará a remontagem.
Não enviamos o último fragmento antes do período de tempo limite (60 segundos) expirar, e o sistema descarta os fragmentos.

Ipv6pReassemblyTimeout() é chamado na condição 3, então vamos examinar como isso pode ser explorado.

Isso é exatamente o que precisamos!

Anteriormente, nosso problema era que o código usava o mesmo cálculo exato tanto para a alocação de memória quanto para a operação de cópia. Este código, por outro lado, não faz isso. Vamos dar uma olhada mais aprofundada no ASM para ver como é explorável.

O código assembly responsável por calcular o tamanho da alocação.

Como você pode ver aqui, a primeira parte do cálculo (fragment_list->net_buffer_length + reassembly->packet_length + 8) é feita usando o registrador DX de 16 bits.

Se você se lembra de antes, causamos um estouro negativo em reassembly->packet_length para 0xFFD0. Portanto, o registrador DX, após adicionar os 8 bytes, é 0xFFD8. Se fragment_list->net_buffer_length for maior que 0x27 (39 bytes), DX sofrerá estouro e será resetado para zero.

fragment_list->net_buffer_length deve ter cerca de 0x38 bytes, então resultará em estouro do registrador DX para 8. Após a adição dos 0x28 bytes, obteremos uma alocação de memória de apenas 48 bytes.

Como as chamadas subsequentes de memmove() usam apenas o valor intacto de reassembly->packet_length para o tamanho, resultará em 65.488 bytes sendo copiados de reassembly->payload para um buffer de 30 bytes. Um ótimo bônus adicional é que grande parte dos dados copiados vem do payload do fragmento, que controlamos e pode ser dados arbitrários de qualquer formato, então obtemos um bom e razoavelmente controlável estouro de buffer baseado no pool do kernel.

Para ter uma chance de acionar a vulnerabilidade, precisamos ter um ou mais pacotes de fragmento localizados após o pacote de opção malformado na lista encadeada no momento em que IppSendErrorList é chamado. No entanto, a partir dos meus testes, isso não parece garantir a exploração. Acredito que também existam outras condições que precisam ser atendidas. Suspeito, mas não confirmei, que o código de sincronização em IppSendError significa que também precisamos vencer uma condição de corrida.

Baixar ferramenta