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 — CVE-2024-38063 - Explorando remotamente o kernel via IPv6 | Kitploit
Ferramentas/GitHubGitHub/faizan-khanx/cve-2024-38063
Forensia de MemóriaAnálise de VulnerabilidadesExploraçãoEngenharia ReversaSegurança de RedePapers e PesquisaAprendizado e EducaçãoExploração de Binários
GitHubfaizan-khanx/cve-2024-38063

CVE-2024-38063

CVE-2024-38063 - Explorando remotamente o kernel via IPv6

Ver Repositório
13há 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 Via IPv6

  • Desde que o patch mais recente do Windows foi lançado em 13 de agosto, estive mergulhado nos detalhes de 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. Eu nunca tinha realmente olhado para IPv6 antes (ou 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.

A Análise de Patch Mais Fácil de Todos os Tempos

  • Normalmente, até mesmo apenas fazer engenharia reversa do patch para descobrir qual alteração de código corresponde à vulnerabilidade pode levar dias ou até semanas, mas neste caso foi instantâneo. Na verdade, foi tão fácil 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 depois perdi um dia inteiro fazendo engenharia reversa do driver errado? Talvez nunca saibamos.

Houve exatamente uma alteração feita em todo o arquivo do driver, que, no final das contas, era realmente o bug. image Uma visão geral do bindiff de 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 20 ou mais alterações de funções só para descobrir qual é a que devo observar, mas não desta vez. image

Ipv6pProcessOptions() antes do patch.

image 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á a função retornar false, resultando na execução do código original em vez da versão corrigida.

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

  • Levando isso em conta, fica claro que todo este patch faz é substituir uma chamada a IppSendErrorList() por IppSendError(), dando-nos uma pista de que o problema está com algum tipo de lista. A diff de patch mais fácil de todos os tempos (ou assim eu pensava).

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 de partes suficientes da base de código para entender o que está acontecendo, descobrir que tipo de vulnerabilidade foi corrigida, como criar uma solicitação para alcançar o código-alvo e qual estado resulta em uma condição explorável.

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

image O layout do cabeçalho de opções de destino da Wikipédia.

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 exige que o script Python seja executado como root.

root@kitploit:~
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. Então tentei adicionar algumas opções inválidas à estrutura para ver se conseguia alcançar a chamada a IppSendErrorList().

Uma breve revisão de código indicou que quase qualquer formatação inválida de opção poderia acionar a chamada a 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. image A função IppSendErrorList inteira.

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

Ele está fazendo uma lista, conferindo-a 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 uma das minhas duas células cerebrais disponíveis estar ocupada lutando contra uma infecção ruim 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.

  • Olhando para as funções e a estrutura que Axel fez engenharia reversa, 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.

image

A entrada list->Next é NULL.

  • Toda vez que meu breakpoint era atingido, a lista continha apenas um pacote. Passei muito mais tempo do que gostaria de admitir tentando descobrir por que e como fazer minha lista ser realmente uma lista. Meu primeiro pensamento foi fragmentação IPv6: o IPv6 permite que os remetentes dividam pacotes grandes em pacotes menores separados, o que faria sentido manter 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 à que estamos lidando aqui.

  • Acabei encontrando a resposta totalmente por acidente. Ocasionalmente, a lista era preenchida, mas o motivo não estava claro. Depois de muito andar em círculos, percebi que quando meu breakpoint de kernel é acionado, ele pausa o kernel inteiro, fazendo o adaptador de rede acumular pacotes. Quando o kernel é retomado, esses pacotes são passados pela pilha para o tcpip.sys em uma lista organizada. Isso só ocorria se os pacotes fossem enviados enquanto o kernel estava pausado, mas não processados antes do próximo breakpoint ser atingido.

  • Esse comportamento é provavelmente uma otimização de desempenho, onde em baixa taxa de transferência o kernel processa pacotes individualmente, mas em volumes maiores os pacotes são organizados em listas e processados em lotes. 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.

  • 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. Ironicamente, nosso PoC de DoS vai ter que usar DoS para acionar a condição de DoS. Se inundarmos o sistema com rajadas de pacotes IPv6, devemos conseguir uma boa lista grande passada para IppSendErrorList().

  • No início, não importa quantos pacotes eu enviasse, só conseguia fazer a lista ter 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 o VM-ception acontecendo no meu sistema de ataque, decidi simplesmente reconfigurar a VM alvo para usar apenas um único núcleo de CPU.

image

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

  • Então, ao que parece, uma VM dentro de uma 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 em que parte está o problema.

Mais Engenharia Reversa, Novamente, Para Sempre.

  • Após uma extensa engenharia reversa, ficou muito mais claro o que IppSendError faz. Em circunstâncias normais, ele simplesmente desativa o pacote definindo net_buffer_list->Status como 0xC000021B (STATUS_DATA_NOT_ACCEPTED). Em seguida, transmite um erro ICMP contendo informações sobre o pacote com erro de volta ao remetente.

    image Duas partes relevantes de IppSendError.

  • Meu primeiro passo foi ver se havia alguma função no tcpip.sys que ignorasse o valor de net_buffer_list->Status. Isso resultaria no driver processando pacotes que estão em estados indefinidos ou inesperados, esperançosamente levando a uma condição explorável.

image

O loop principal responsável por processar pacotes.

  • Como o loop responsável por chamar todas as funções de análise está envolvido em uma verificação de erro (ou seja, não podemos ir a lugar nenhum depois que o código de erro é definido), imaginei que esse era o buraco de coelho errado para seguir. Em vez disso, decidi voltar ao IppSendError e ver se há algum caminho de código que modifique 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.

image

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

  • Quando IppSendErrorList, e portanto IppSendError, é chamada com o argumento always_send_icmp definido como true, parece que ela tenta enviar o erro ICMP para cada pacote da lista.

  • Então, por razões que provavelmente só Deus conhece, ela alcança um bloco de código onde o campo packet->packet_size é definido como zero.

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

root@kitploit:~
def build_malicious_option(next_header, header_length, option_type, option_length):
    dest_options_header = 60
	
    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 parser?

image

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

  • O manipulador de pacotes simplesmente chama uma função da VTable com base no valor de packet->next_header, que permanece inalterado desde quando foi definido durante o pré-processamento. Isso permite que o processamento de pacotes 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 para qualquer valor de cabeçalho IPv6 válido, e o loop chamará o parser correspondente. Isso nos dá uma grande superfície de ataque potencial. image

    O formato do pacote IPv6.

  • Tudo o que resta fazer é encontrar uma parte alcançável do parser IPv6 que faça algo estranho com o campo packet_size.

De Volta à Fragmentação

  • O primeiro lugar que decidi investigar foi o parser de fragmentos IPv6, porque foi lá que a antiga vulnerabilidade cve-2021-24086 estava, então parecia um bom lugar para encontrar mais código maluco. image

Eh… está 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ê o decrementar abaixo de seu valor mínimo possível, ele volta ao valor máximo possível. Isso é chamado de integer overflow e integer underflow, 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:

    image

    O código ASM calculando o tamanho do fragmento.

  • AX são os 16 bits inferiores do registrador EAX. Embora o registrador EAX tenha 32 bits, AX opera como se fosse seu próprio registrador de 16 bits; portanto, quaisquer overflows ou underflows ficam confinados a AX e não afetam o restante do registrador EAX. Isso é incrivelmente conveniente porque um underflow no registrador EAX resultaria em um valor de 4 bilhões, o que levaria a uma tentativa de alocar 4GB de memória, que provavelmente falharia.

  • Como o valor de packet->packet_size é zero, este código define ax como 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á o registrador voltar ao valor máximo possível de inteiro de 16 bits (0xFFFF). Como estamos subtraindo 0x30, o valor de AX sofrerá underflow 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 temos um buffer overflow. Acredito que também realiza verificação de limites no buffer de origem, então também não temos nem mesmo uma leitura fora dos limites. No entanto, não saímos completamente de mãos vazias.

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

  • Nós bagunçamos nossa fragmentação gravemente o suficiente para o sistema dizer que está na hora de parar.
  • Enviamos um fragmento com o campo ‘More’ definido como 0, o que indica que este é o último fragmento, e o sistema iniciará a remontagem.
  • Não enviamos o último fragmento antes que o período de timeout (60 segundos) expire, e o sistema descarta os fragmentos.
  • Ipv6pReassemblyTimeout() é chamada na condição 3, então vamos examinar como isso pode ser explorado.

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

image

Isso é exatamente o que precisamos!

  • Anteriormente, nosso problema era que o código usava exatamente o mesmo cálculo tanto para a alocação de memória quanto para a operação de cópia. Este código, por outro lado, não usa. Vamos analisar mais a fundo o ASM para ver como ele é explorável

image

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, fizemos reassembly->packet_length sofrer underflow para 0xFFD0. Então o registrador DX, após adicionar os 8 bytes, fica 0xFFD8. Se fragment_list->net_buffer_length for maior que 0x27 (39 bytes), DX sofrerá overflow e será zerado.

  • fragment_list->net_buffer_length deve ser em torno de 0x38 bytes, então isso resultará em overflow 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 a memmove() usam apenas o valor inalterado de reassembly->packet_length como tamanho, isso 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 temos um buffer overflow baseado em kernel pool bastante controlável.

  • 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 é chamada. No entanto, pelos meus testes, isso não parece garantir a exploração. Acredito que também existam algumas 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 temos que vencer uma condição de corrida.

Redes Sociais

Estatísticas do GitHub do Faizan

instagram twitter linkedin github

Baixar ferramenta
RtlCopyMdlToBuffer()
  • Como ExAllocatePoolWithTagPriority() não zera a memória alocada, e RtlCopyMdlToBuffer() apenas copia 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 esteja preenchido com o que quer que estivesse armazenado no endereço antes da realocação. Se pudermos usar a fragmentação para construir um pacote que seja enviado de volta para nós, como uma solicitação ICMP Echo, poderíamos potencialmente vazar memória aleatória do kernel, levando a um bypass de ASLR.

  • Além disso, o código também define reassembly->fragment_size como o inteiro de 16 bits com underflow (65,488), então agora temos duas variáveis separadas que podemos potencialmente usar para causar um buffer overflow.

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