CVE-2017-17558漏洞学习

简介

这是USB core中的一个拒绝服务漏洞。带有精心设计的描述符的恶意USB设备可以通过在配置描述符中设置过高的bNumInterfaces值来导致内核访问未分配的内存。虽然在解析期间调整了该值,但是在其中一个错误返回路径中跳过了该调整。该漏洞出现在4.15之前

补丁分析

看一下补丁怎么写的:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=48a4ff1c7bb5a32d2e396b03132d20d552c0eca7

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 iad_num = 0;
 
     memcpy(&config->desc, buffer, USB_DT_CONFIG_SIZE);
+    nintf = nintf_orig = config->desc.bNumInterfaces;
+    config->desc.bNumInterfaces = 0;    // Adjusted later
+
     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;
 
-    nintf = nintf_orig = config->desc.bNumInterfaces;
     if (nintf > USB_MAXINTERFACES) {
         dev_warn(ddev, "config %d has too many interfaces: %d, "
             "using maximum allowed: %d
",

漏洞出在usb_parse_configuration函数中,而usb_parse_configuration是驱动中解析设备配置的函数。补丁将下面这一行提前了几行

nintf = nintf_orig = config->desc.bNumInterfaces;

然后再讲config->desc.bNumInterfaces置为0了

下面,先介绍USB的驱动的整体架构,然后给出一个调usb_parse_configuration的例子,解析usb_parse_configuration函数

USB驱动架构

四个重要的描述符

为了方便USB设备驱动的编写,USB核心将USB设备抽象成4个层次,自顶向下依次是

1、设备:代表整个设备,一个设备会有相对应一个文件等

2、配置:一个设备有一个或是多个配置,不同的配置使设备表现出不同的功能组合,在连接期间必须从中选择一个。比如说带话筒的耳机可以可以有3个配置,让硬件有3中工作模式:耳机工作、话筒工作、耳机和话筒同时工作。

3、接口:一个配置包含多个接口。比如一个USB音响可以有音频接口和开关按钮的接口,一个配置可以使所有的接口同时有效,并且被不同的驱动同时连接

4、端点:一个接口可以有多个端点。端点是主机与硬件设备通信的最基本形式,每个端点有属于自己的地址,并且数据的传输是单向的,每一个USB设备在主机看来就是多个端点的集合

在include/linux/usb/ch9.h中,定义了几个结构体:usb_device_descriptor、usb_config_descriptor 、usb_interface_descriptor、usb_endpoint_descriptor。分别表示上面这些含义的描述符。每个设备均会分配一个或是多个。

看一个usb_config_descriptor函数的内容,总而言之就是和配置相关的信息:

struct usb_config_descriptor {
    __u8  bLength;//描述符的长度,值为USB_DT_CONFIG_SIZE=9
    __u8  bDescriptorType;//描述符类型

    __le16 wTotalLength;//向设备请求配置描述时获得数据包的长度
    __u8  bNumInterfaces;//接口数
    __u8  bConfigurationValue;//用这个值表示将要激活的配置
    __u8  iConfiguration;//配置描述信息的字符串描述符的索引值
    __u8  bmAttributes;//一些属性
    __u8  bMaxPower;//最大的电流
} __attribute__ ((packed));

而usb_parse_configuration函数中参数之一是usb_host_config结构,定义如下,可以知道主要的结构就是上面的usb_config_descriptor内容

struct usb_host_config {
    struct usb_config_descriptor    desc;

    char *string;        /* iConfiguration string, if present */

    /* List of any Interface Association Descriptors in this
     * configuration. */
    struct usb_interface_assoc_descriptor *intf_assoc[USB_MAXIADS];

    /* the interfaces associated with this configuration,
     * stored in no particular order */
    struct usb_interface *interface[USB_MAXINTERFACES];

    /* Interface information available even when this is not the
     * active configuration */
    struct usb_interface_cache *intf_cache[USB_MAXINTERFACES];

    unsigned char *extra;   /* Extra descriptors */
    int extralen;
};

USB设备中的几个重要概念

USB驱动整体的架构如下

在硬件层面,USB设备以主机控制器为根形成一颗树,主机控制器连接在PCI总线上作为一个PCI设备,而连接实际物理设备的节点又叫Hub

USB设备状态

当一个USB设备插入接口,USB设备需要经过几个步骤才能正常开始运作,比如说要让主机和设备相互认识一下,主机给设备分配个地址什么的。在USB协议中,定义了几个状态来描述主机和设备的交互的状态:

  • Attached:设备连接上Hub,表示初始状态
  • Powered:,连接上设备之后,加电
  • Default:加电后,设备收到复位信号,才能使用默认的地址回应主机发送过来的设备和配置描述符的请求
  • Address:主机分配了一个唯一地址给设备
  • Configured:表示设备以及被主机给配置过了
  • Suspended:为了省电,设备可以被挂起

usb_parse_configuration函数解读

usb_parse_configuration完全由usb_get_configuration调用,从名字就可以看,usb_get_configuration就是获取USB设备的配置信息(参考前面USB设备的状态)。当一个设备被插入Hub的时候,内核最终就会调用这个函数,为了保持逻辑完整性,从usb_get_configuration函数开始看起,这个函数的参数只有一个,就是struct usb_device *dev,usb_device结构就是内核中对于一个usb设备的抽象,这个函数就是获取这个设备的配置

int usb_get_configuration(struct usb_device *dev)
{
    struct device *ddev = &dev->dev;
    int ncfg = dev->descriptor.bNumConfigurations;
    int result = 0;
    unsigned int cfgno, length;
    unsigned char *bigbuffer;
    struct usb_config_descriptor *desc;

    cfgno = 0;
    if (dev->authorized == 0)    /* Not really an error */
        goto out_not_authorized;
    result = -ENOMEM;
    if (ncfg > USB_MAXCONFIG) {
        dev_warn(ddev, "too many configurations: %d, "
            "using maximum allowed: %d
", ncfg, USB_MAXCONFIG);
        dev->descriptor.bNumConfigurations = ncfg = USB_MAXCONFIG;
    }

    if (ncfg < 1) {
        dev_err(ddev, "no configurations
");
        return -EINVAL;
    }

    length = ncfg * sizeof(struct usb_host_config);
    dev->config = kzalloc(length, GFP_KERNEL);
    if (!dev->config)
        goto err2;

    length = ncfg * sizeof(char *);
    dev->rawdescriptors = kzalloc(length, GFP_KERNEL);
    if (!dev->rawdescriptors)
        goto err2;

    desc = kmalloc(USB_DT_CONFIG_SIZE, GFP_KERNEL);
    if (!desc)
        goto err2;

    result = 0;
    for (; cfgno < ncfg; cfgno++) {
        /* We grab just the first descriptor so we know how long
         * the whole configuration is */
        result = usb_get_descriptor(dev, USB_DT_CONFIG, cfgno,
            desc, USB_DT_CONFIG_SIZE);
        if (result < 0) {
            dev_err(ddev, "unable to read config index %d "
                "descriptor/%s: %d
", cfgno, "start", result);
            dev_err(ddev, "chopping to %d config(s)
", cfgno);
            dev->descriptor.bNumConfigurations = cfgno;
            break;
        } else if (result < 4) {
            dev_err(ddev, "config index %d descriptor too short "
                "(expected %i, got %i)
", cfgno,
                USB_DT_CONFIG_SIZE, result);
            result = -EINVAL;
            goto err;
        }
        length = max((int) le16_to_cpu(desc->wTotalLength),
            USB_DT_CONFIG_SIZE);

        /* Now that we know the length, get the whole thing */
        bigbuffer = kmalloc(length, GFP_KERNEL);
        if (!bigbuffer) {
            result = -ENOMEM;
            goto err;
        }
        result = usb_get_descriptor(dev, USB_DT_CONFIG, cfgno,
            bigbuffer, length);
        if (result < 0) {
            dev_err(ddev, "unable to read config index %d "
                "descriptor/%s
", cfgno, "all");
            kfree(bigbuffer);
            goto err;
        }
        if (result < length) {
            dev_warn(ddev, "config index %d descriptor too short "
                "(expected %i, got %i)
", cfgno, length, result);
            length = result;
        }

        dev->rawdescriptors[cfgno] = bigbuffer;

        result = usb_parse_configuration(dev, cfgno,
            &dev->config[cfgno], bigbuffer, length);
        if (result < 0) {
            ++cfgno;
            goto err;
        }
    }
    result = 0;

err:
    kfree(desc);
out_not_authorized:
    dev->descriptor.bNumConfigurations = cfgno;
err2:
    if (result == -ENOMEM)
        dev_err(ddev, "out of memory
");
    return result;
}

上面的函数逻辑也比较的简单,不断的获取配置,然后调用usb_parse_configuration去解析,其他部分的实现就不具体看了,这里主要是弄清楚,传递给usb_parse_configuration函数的几个参数的含义,dev指usb设备,cfgno指当前第几个配置,&dev->config[cfgno]指需要解析的配置,后面是buffer和长度

在usb_parse_configuration就是解析从USB设备读取过来的配置信息,判断数据正确性。

先看for循环前面的一部分,memcpy函数将buffer里面的值放入描述符内,紧接着的一个if条件判断如果失败,则会造成这个函数的结束。然而这个config->desc里面的值却没有被更改,所以可以利用恶意设备来操纵,造成前面所说的访问无效内存

    memcpy(&config->desc, buffer, USB_DT_CONFIG_SIZE);
    if (config->desc.bDescriptorType != USB_DT_CONFIG ||
        config->desc.bLength < USB_DT_CONFIG_SIZE) {
        dev_err(ddev, "invalid descriptor for config index %d: "
            "type = 0x%X, length = %d
", cfgidx,
            config->desc.bDescriptorType, config->desc.bLength);
        return -EINVAL;
    }
    cfgno = config->desc.bConfigurationValue;

    buffer += config->desc.bLength;
    size -= config->desc.bLength;

    nintf = nintf_orig = config->desc.bNumInterfaces;
    if (nintf > USB_MAXINTERFACES) {
        dev_warn(ddev, "config %d has too many interfaces: %d, "
            "using maximum allowed: %d
",
            cfgno, nintf, USB_MAXINTERFACES);
        nintf = USB_MAXINTERFACES;
    }

所以最后的补丁就是将config->desc.bNumInterfaces先赋值成0,这样在错误退出的路径之上就不会有无效内存的访问出现,并且在最后的大for循环退出之后,会有如下语句将这个值恢复,所以正常运行退出的时候也不会有什么错误:

    config->desc.bNumInterfaces = nintf = n;
原文地址:https://www.cnblogs.com/likaiming/p/10863079.html