
CVE-2024-38063 - Explorando remotamente o kernel via IPv6
Houve exatamente uma alteração feita em todo o arquivo do driver, que, no final das contas, era realmente o bug.
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.

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á 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).
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.
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.
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])
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.
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.
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.

A entrada list->Next é NULL.