Resumen de la vulnerabilidad
Esta vulnerabilidad afecta al subsistema USB del núcleo Linux, permitiendo una condición de denegación de servicio. Un dispositivo USB malicioso con descriptores manipulados puede provocar que el kernel acceda a memoria no asignada, mediante el establecimiento de un valor elevado en el campo bNumInterfaces del descriptor de configuración. Durante el análisis, se ajusta este valor, pero en una ruta de error específica dicho ajuste se omite. La vulnerabilidad está presente en versiones del kernel anteriores a la 4.15.
Análisis del parche correctivo
El parche aplicado se encuentra en el commit 48a4ff1c7bb5a32d2e396b03132d20d552c0eca7 del repositorio del núcleo Linux. La corrección modifica la función usb_parse_configuration en el archivo drivers/usb/core/config.c. A continuación, se muestra un fragmento de los cambios realizados:
diff --git a/drivers/usb/core/config.c b/drivers/usb/core/config.c
index 55b198b..78e92d2 100644
--- a/drivers/usb/core/config.c
+++ b/drivers/usb/core/config.c
@@ -555,6 +555,9 @@ static int usb_parse_configuration(struct usb_device *dev, int cfgidx,
unsigned contador_iad = 0;
memcpy(&config->desc, buffer, USB_DT_CONFIG_SIZE);
+ num_intf = num_intf_original = config->desc.bNumInterfaces;
+ config->desc.bNumInterfaces = 0; // Se ajustará posteriormente
+
if (config->desc.bDescriptorType != USB_DT_CONFIG ||
config->desc.bLength < USB_DT_CONFIG_SIZE ||
config->desc.bLength > size) {
@@ -568,7 +571,6 @@ static int usb_parse_configuration(struct usb_device *dev, int cfgidx,
buffer += config->desc.bLength;
size -= config->desc.bLength;
- num_intf = num_intf_original = config->desc.bNumInterfaces;
if (num_intf > USB_MAXINTERFACES) {
dev_warn(ddev, "configuración %d tiene demasiadas interfaces: %d, "
"usando el máximo permitido: %d\n",
El problema radicaba en la asignación de num_intf y num_intf_original, la cual se realizaba después de una posible ruta de error. El parche adelanta esta asignación y establece config->desc.bNumInterfaces a cero inmediatamente después de copiar el descriptor. Esto previene el acceso a memoria no válida en caso de que la validación falle. Al final del procesamiento, el valor se restaura mediante la asignación config->desc.bNumInterfaces = num_intf = n;.
Arquitectura del subsistema USB
El subsistema USB del núcleo Linux organiza los dispositivos en una jerarquía de capas para facilitar el desarrollo de controladores. Los elementos clave son:
- Dispositivo: Representa el hardware completo.
- Configuración: Define un conjunto funcional del dispositivo; se puede seleccionar una de varias durante la conexión.
- Interfaz: Cada configuración contiene una o más interfaces, las cuales pueden ser manejadas por controladores distintos.
- Punto final: Los puntos finales son canales de comunicación unidireccionales entre el host y el dispositivo.
En el archivo include/linux/usb/ch9.h se definen estructuras como usb_config_descriptor, que almacena información relevante de la configuración:
struct usb_config_descriptor {
__u8 bLength; // Longitud del descriptor, normalmente 9
__u8 bDescriptorType; // Tipo de descriptor
__le16 wTotalLength; // Tamaño total del paquete de configuración
__u8 bNumInterfaces; // Número de interfaces
__u8 bConfigurationValue; // Identificador de la configuración
__u8 iConfiguration; // Índice del string descriptivo
__u8 bmAttributes; // Atributos de la configuración
__u8 bMaxPower; // Consumo máximo de corriente
} __attribute__ ((packed));
La estructura usb_host_config encapsula el descriptor anterior y otros datos relacionados:
struct usb_host_config {
struct usb_config_descriptor desc;
char *string;
struct usb_interface_assoc_descriptor *intf_assoc[USB_MAXIADS];
struct usb_interface *interface[USB_MAXINTERFACES];
struct usb_interface_cache *intf_cache[USB_MAXINTERFACES];
unsigned char *extra;
int extralen;
};
Estados de los dispositivos USB
Un dispositivo USB pasa por varios estados durante su conexión:
- Attached: Conectado al hub.
- Powered: Alimentado eléctricamente.
- Default: Tras un reset, responde con la dirección predeterminada.
- Address: Se le asigna una dirección única por el host.
- Configured: Configurado para operar.
- Suspended: En modo de bajo consumo.
Funcionamiento de usb_parse_configuration
La función usb_parse_configuration es invocada por usb_get_configuration, la cual obtiene la información de configuración del dispositivo. El flujo general se ilustra en el siguiente código simplificado:
int obtener_configuracion_usb(struct usb_device *dispositivo)
{
struct device *ddev = &dispositivo->dev;
int num_configs = dispositivo->descriptor.bNumConfigurations;
int resultado = 0;
unsigned int indice_cfg, longitud;
unsigned char *buffer_grande;
struct usb_config_descriptor *desc;
if (dispositivo->autorizado == 0)
goto salida_no_autorizado;
if (num_configs > USB_MAXCONFIG) {
dev_warn(ddev, "demasiadas configuraciones: %d, usando el máximo: %d\n", num_configs, USB_MAXCONFIG);
dispositivo->descriptor.bNumConfigurations = num_configs = USB_MAXCONFIG;
}
dispositivo->config = kzalloc(num_configs * sizeof(struct usb_host_config), GFP_KERNEL);
// ... asignaciones y bucle de lectura ...
for (indice_cfg = 0; indice_cfg < num_configs; indice_cfg++) {
// Obtener descriptor inicial para conocer longitud total
// ... llamadas a usb_get_descriptor ...
resultado = analizar_configuracion_usb(dispositivo, indice_cfg,
&dispositivo->config[indice_cfg], buffer_grande, longitud);
if (resultado < 0) {
++indice_cfg;
goto error;
}
}
// ... limpieza y devolución ...
}
En analizar_configuracion_usb (análoga a usb_parse_configuration), el código vulnerable se mostraba así antes del parche:
memcpy(&conf->desc, buffer, USB_DT_CONFIG_SIZE);
if (conf->desc.bDescriptorType != USB_DT_CONFIG ||
conf->desc.bLength < USB_DT_CONFIG_SIZE) {
dev_err(ddev, "descriptor inválido para configuración %d\n", idx_cfg);
return -EINVAL;
}
// Otras validaciones...
num_interf = num_interf_orig = conf->desc.bNumInterfaces;
if (num_interf > USB_MAXINTERFACES) {
dev_warn(ddev, "ajustando número de interfaces al máximo\n");
num_interf = USB_MAXINTERFACES;
}
El error consistía en que si la validación del descriptor fallaba después de la copia inicial, conf->desc.bNumInterfaces conservaba el valor malicioso, conduciendo a accesos de memoria incorrectos. Con el parche, este campo se establece a cero de inmediato para evitar dicho escenario.