Análisis de la vulnerabilidad de denegación de servicio CVE-2017-17558 en el subsistema USB

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.

Etiquetas: CVE-2017-17558 USB kernel Linux denegación de servicio descriptores USB

Publicado el 6-22 23:02