
CVE-2024-38063 - Explotando el kernel de forma remota a través de IPv6
Hubo exactamente un cambio realizado en todo el archivo del controlador, que resulta ser, efectivamente, el bug después de todo.
Una vista bindiff de tcpip.sys antes y después de instalar el parche.
Solo se ha modificado una única función en todo el controlador. Normalmente, podría pasarme un día entero revisando más de 20 cambios de funciones diferentes solo para averiguar cuál es la que debería estar mirando, pero esta vez no.

Ipv6pProcessOptions() antes del parche.
Ipv6pProcessOptions() . Después del parche.
No solo se cambió una única función, sino una única línea de código.
La función de nombre extremadamente largo Feature_2660322619__private_IsEnabledDeviceUsage_3() es algo que Microsoft añade a veces para permitir reversiones parciales de parches. La llamada comprueba la presencia de una marca global o un ajuste de registro que, si está establecido, hará que la función devuelva false, lo que resulta en la ejecución del código original en lugar de la versión parcheada.
La razón por la que Microsoft hace esto es porque los parches de seguridad a veces rompen cosas sin querer, así que este ajuste permite a un administrador desparchear una única vulnerabilidad, sin desinstalar todo el paquete de parches mensual y debilitar drásticamente la seguridad del sistema.
Teniendo esto en cuenta, está claro que todo lo que hace este parche es sustituir una llamada a IppSendErrorList() por IppSendError(), lo que nos da una pista de que el problema está relacionado con algún tipo de lista. El diff de parche más fácil de la historia (o eso creía)
Aplicar ingeniería inversa al parche para encontrar el código alterado es solo la mitad del desafío (o en este caso, menos del 0.1%). El resto del proceso consiste en aplicar ingeniería inversa a suficiente parte del código base para entender qué está pasando, averiguar qué tipo de vulnerabilidad se parcheó, cómo construir una solicitud para llegar al código objetivo y qué estado resulta en una condición explotable.
La primera parte es bastante fácil. El cambio está en Ipv6pProcessOptions(), lo que nos dice que es IPv6 y que implica procesar opciones. Así que una consulta rápida al RFC nos dice exactamente qué es una opción de IPv6 y dónde podemos encontrar una.
El diseño de la cabecera de opciones de destino de Wikipedia.
Vale, genial. Lo que buscamos parece ser la cabecera de opciones de destino, que se sitúa justo después de la cabecera IPv6 principal. Usemos la biblioteca de Python ‘scapy’ para construir un paquete IPv6 de prueba.
Nota: Para mitigar los ataques DDoS que utilizan direcciones IP falsificadas, Windows restringe la capacidad de construir paquetes IP sin procesar. Por esta razón, opté por usar Linux para desarrollar mi prueba de concepto. Aunque Linux sí permite a los usuarios construir y enviar paquetes sin procesar de capa 2 y capa 3, requiere que el script de Python se ejecute 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 y ejecutar el script, quedó claro que todo lo que se necesitaba para llegar a la función vulnerable era enviar un paquete IPv6 con una estructura de opciones vacía. Entonces probé a añadir algunas opciones no válidas a la estructura para ver si podía llegar a la llamada a IppSendErrorList().Una breve revisión del código indicó que casi cualquier formato de opción no válido podría desencadenar la llamada a IppSendErrorList. Así que decidí usar la opción de paquete Jumbo con una longitud no válida (menos de 65535 bytes).
options_header = IPv6ExtHdrDestOpt(options=[Jumbo(jumboplen=0x1337)])
Entonces, ¿qué hace realmente IppSendErrorList()? Bueno, el código es bastante sencillo.
La función IppSendErrorList completa.
El código itera sobre una lista enlazada y llama a IppSendError() para cada elemento de la lista. Una vez más, los astros se han alineado y las cosas han sido fáciles hasta ahora. Si IppSendErrorList simplemente llama a IppSendError para cada elemento de una lista, y el parche sustituye la llamada a IppSendErrorList por IppSendError, entonces el problema ocurre cuando se llama a IppSendError sobre un elemento de la lista que no es el primero.
Aquí es donde las cosas pasaron de obvias a anormalmente difíciles, aunque creo que gran parte fue debido a que una de mis dos células cerebrales disponibles estaba ocupada luchando contra una mala infección de covid. Perdí un par de días entendiendo partes del código, quedándome dormido y luego olvidando lo que había descubierto. Todo el proceso requirió más de una semana de ingeniería inversa de partes de tcpip.sys para averiguar qué estaba pasando. Pero la entrada del blog de Axel fue extremadamente útil.
Al observar las funciones y la estructura a las que Axel aplicó ingeniería inversa, y a qué otras funciones se pasan, está claro que el único argumento pasado a Ipv6pProcessOptions() es la misma estructura packet_t definida en el artículo. En esencia, el puntero pasado a Ipv6pProcessOptions, e iterado por IppSendErrorList, es una lista enlazada de paquetes.
Así que establecí un punto de interrupción en Ipv6pProcessOptions() e inspeccioné la lista.

La entrada list->Next es NULL.