CVE-2017-17558漏洞学习
简介
这是USB core中的一个拒绝服务漏洞。带有精心设计的描述符的恶意USB设备可以通过在配置描述符中设置过高的bNumInterfaces值来导致内核访问未分配的内存。虽然在解析期间调整了该值,但是在其中一个错误返回路径中跳过了该调整。该漏洞出现在4.15之前
补丁分析
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\n",
漏洞出在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\n", ncfg, USB_MAXCONFIG); dev->descriptor.bNumConfigurations = ncfg = USB_MAXCONFIG; } if (ncfg < 1) { dev_err(ddev, "no configurations\n"); 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\n", cfgno, "start", result); dev_err(ddev, "chopping to %d config(s)\n", cfgno); dev->descriptor.bNumConfigurations = cfgno; break; } else if (result < 4) { dev_err(ddev, "config index %d descriptor too short " "(expected %i, got %i)\n", 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\n", cfgno, "all"); kfree(bigbuffer); goto err; } if (result < length) { dev_warn(ddev, "config index %d descriptor too short " "(expected %i, got %i)\n", 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\n"); 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\n", 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\n", cfgno, nintf, USB_MAXINTERFACES); nintf = USB_MAXINTERFACES; }
所以最后的补丁就是将config->desc.bNumInterfaces先赋值成0,这样在错误退出的路径之上就不会有无效内存的访问出现,并且在最后的大for循环退出之后,会有如下语句将这个值恢复,所以正常运行退出的时候也不会有什么错误:
config->desc.bNumInterfaces = nintf = n;