USB提权之旅

作者: 360漏洞研究院 姚俊 刘永 分类: 安全研究,漏洞分析,漏洞复现 发布时间: 2025-06-09 10:40

一、概述

2025年2月国际特赦组织安全实验室发表技术报告,该报告声称塞尔维亚政府使用Cellebrite产品解锁目标对象的三星A32手机,事后该组织联合谷歌TAGThreat Analysis Group)进行调查分析,最终发现Cellebrite产品利用了多个USB驱动0day漏洞。

报告主要描述了发现的USB驱动漏洞以及Cellebrite产品模拟的USB外设序列,究竟如何利用这些漏洞,仍然是一个谜。我们对上述漏洞进行分析并尝试利用,遗憾的是,最终未能实现完整利用。虽然是一次失败的尝试,我们仍然获得了宝贵的经验:

  1. 我们对漏洞进行了分析,尤其是如何利用CVE-2024-50302实现信息泄露,目前没有公开资料;
  2. 漏洞利用的基本思路,与常见的ROOT提权场景不同,我们没有进程上下文,利用手段非常有限;
  3. 如何构造任意写;
  4. 这次尝试为什么会失败;

二、USB基础知识

USB协议将设备分为主机(Host)和外设(Device),一个设备的角色依赖于使用场景,例如当我们把手机插入电脑时,电脑是主机,手机是外设。而我们把一个U盘插入手机时,手机是主机,U盘是外设。平时常见的USB设备大多是外设,例如鼠标、键盘等。Hub是一种特殊的外设,它的功能是扩展主机可用的 USB端口数量,它是实现USB 分层星型拓扑(tiered-star topology) 的核心。外设在大部分场景下是被动的,不能主动发起请求。

当外设插入主机时,主机驱动尝试获取外设信息,这里涉及4个比较重要的描述符(Descriptor):设备描述符(Device Descriptor)、配置描述符(Configuration Descriptor)、接口描述符(Interface Descriptor)以及端点描述符(Endpoint Descriptor),它们的逻辑组织结构如下:

设备描述符用来描述外设的类型、生产厂家、型号和序列号等,除了这些基本信息外,设备描述符还包含了配置(Configuration)个数。配置代表了外设提供的功能组合,一个外设可以有多种功能组合,例如安卓手机有仅充电、传输文件等功能,某一时刻,只能有一种配置生效。配置描述符中包含了接口(Interface)个数,接口代表了一种具体的功能,例如USB 摄像头可以有两个接口:一个用于视频控制(Video Control Interface),一个用于视频流(Video Streaming Interface)。接口描述符中包含了端点(Endpoint)个数,端点是用来传输数据的管道,它有方向、类型等属性。

USB协议可以灵活地描述各种功能组合,例如有些键盘除了按键、LED灯外,还具有鼠标功能,这种外设属于复合设备(Composite Device),它的每一个接口逻辑上是一个单独的设备,所以针对上述键盘,我们可以使用两个接口来描述,一个用来描述键盘,另一个用来描述鼠标。

三、漏洞分析

TAG总共发现三个USB驱动漏洞,这些漏洞现在已经被修复:

CVE设备类型漏洞类型
CVE-2024-53197音频越界读
CVE-2024-53104视频越界写
CVE-2024-50302HID信息泄露

3.1 CVE-2024-53197

当我们把外设插入主机时,USB驱动会对外设进行枚举,它会依次发送USB_DT_DEVICEUSB_DT_CONFIG请求,外设需要分别响应设备描述符和配置描述符。驱动通过解析这些描述符来确认外设类型以及功能。USB驱动根据设备描述符中的idVendorVID)和idProductPID)等信息找到对应的驱动[1],然后调用该驱动来初始化外设[2]

static int usb_probe_interface(struct device *dev)
{
    id = usb_match_dynamic_id(intf, driver);            // [1]
    if (!id)
        id = usb_match_id(intf, driver->id_table);
    if (!id)
        return error;

    error = driver->probe(intf, id);                    // [2]
}

如果是extigy音频设备(VID: 0x041e,PID: 0x3000),内核会调用usb_audio_probe(),并最终调用snd_usb_extigy_boot_quirk()

usb_audio_probe()
|
|-> snd_usb_apply_boot_quirk()
    |
    |-> snd_usb_extigy_boot_quirk()

这个外设的特别之处在于它会再次请求设备描述符:

static int snd_usb_extigy_boot_quirk(struct usb_device *dev, struct usb_interface *intf)
{
    err = usb_get_descriptor(dev, USB_DT_DEVICE, 0, &dev->descriptor, sizeof(dev->descriptor));
}

usb_get_descriptor()会直接将外设响应的描述符写入dev->descriptor,该字段是一个usb_device_descriptor对象,其中包含了配置个数字段:

struct usb_device_descriptor {
    __u8  bLength;
    __u8  bDescriptorType;
    [...]
    __le16 idVendor;
    __le16 idProduct;
    [...]
    __u8  bNumConfigurations;       // 配置个数
} __attribute__ ((packed));

驱动不会再次请求配置描述符,这可能会导致新的配置个数与旧的配置数据不匹配。通过搜索bNumConfigurations,我们找到usb_destroy_configuration()

void usb_destroy_configuration(struct usb_device *dev)
{
    for (c = 0; c < dev->descriptor.bNumConfigurations; c++) {
        struct usb_host_config *cf = &dev->config[c];
        kfree(cf->string);
        for (i = 0; i < cf->desc.bNumInterfaces; i++) {
            if (cf->intf_cache[i])
                kref_put(&cf->intf_cache[i]->ref, usb_release_interface_cache);
        }
    }
    kfree(dev->config);
    dev->config = NULL;
}

当设备被拔出时,内核会调用该函数来销毁配置对象。它根据描述符中的配置个数,依次销毁usb_host_config对象,当第二次传入的bNumConfigurations偏大时,该函数会越界访问dev->config指向的内存。

这个漏洞的修复方式很简单,使用一个临时对象接收新的设备描述符,然后判断配置个数是否大于当前个数即可。

3.2 CVE-2024-53104

当视频设备插入主机时,UVC驱动需要确认外设支持的视频流格式、分辨率以及帧率等信息。uvc_parse_streaming()用于解析流格式:

static int uvc_parse_streaming(struct uvc_device *dev, struct usb_interface *intf)
{   
    case UVC_VS_FORMAT_DV:
        pr_info("[%s] calculate UVC_VS_FORMAT_DV format", __func__);
        /* DV format has no frame descriptor. We will create a
         * dummy frame descriptor with a dummy frame interval.
         */
        nformats++;
        nframes++;
        nintervals++;
        break;
}

当视频流格式为UVC_VS_FORMAT_DV时,驱动认为有一个frame和一个interval。正如注释所说明的那样,内核会伪造一个frame,这个frame的数据不是来自外设,而是由uvc_parse_format()构造:

static int uvc_parse_format(dev, streaming, format, intervals, buffer, buflen)
{
    switch (buffer[2]) {
    case UVC_VS_FORMAT_DV:
        ftype = 0;                                              // [1]
        /* Create a dummy frame descriptor. */                  // [2]
        frame = &format->frame[0];
        memset(&format->frame[0], 0, sizeof(format->frame[0]));
        frame->bFrameIntervalType = 1;
        frame->dwDefaultFrameInterval = 1;
        frame->dwFrameInterval = *intervals;
        *(*intervals)++ = 1;
        format->nframes = 1;
        break;
    }

该函数使用默认值填充了frame[2],需要注意的是:这里ftype被初始化为0[1]。接着该函数从外设传来的数据中解析frame

static int uvc_parse_format(dev, streaming, format, intervals, buffer, buflen)
{
    [ 构造假的frame,ftype为0 ]

    buflen -= buffer[0];
    buffer += buffer[0];

    /* Parse the frame descriptors. Only uncompressed, MJPEG and frame
     * based formats have frame descriptors.
     */
    while (buflen > 2 && buffer[1] == USB_DT_CS_INTERFACE &&    // [3]
           buffer[2] == ftype) {
        frame = &format->frame[format->nframes];
        frame->bFrameIndex = buffer[3];
        frame->bmCapabilities = buffer[4];
    }
}

实际上只有uncompressedMJPEGframe-based这三种format才有frame,但while循环并未对format的类型进行限制[3],使得UVC_VS_UNDEFINED(ftype=0)类型的frame会加入到UVC_VS_FORMAT_DV的格式中,导致内存使用量超出了预期,因为内存是从堆上分配的,因此是堆越界写漏洞。

这个漏洞的修复方式是对ftype的类型进行判断,只有不为0时才解析frame

3.3 CVE-2024-50302

HIDHuman Interface Device)外设用于人机交互,日常生活中我们见到的键盘、鼠标以及触摸板都属于此类外设。不同的HID外设其上报的数据(Report)格式有很大差异,为了使开发人员准确了解厂商的意图,正确使用物理设备,USB组织专门制定了相关规范,这里举一个例子来快速理解如何描述数据格式:

static uint8_t mouse_report_desc[] = {
    0x05, 0x01,        // Usage Page (Generic Desktop)
    0x09, 0x02,        // Usage (Mouse)
    0xa1, 0x01,        // Collection (Application)

    // Mouse Buttons
    0x09, 0x01,        // Usage (Button)
    0x19, 0x01,        // Usage Minimum (Button 1)
    0x29, 0x03,        // Usage Maximum (Button 3)
    0x15, 0x00,        // Logical Minimum (0)
    0x25, 0x01,        // Logical Maximum (1)
    0x95, 0x03,        // Report Count (3)
    0x75, 0x01,        // Report Size (1 bit)
    0x81, 0x02,        // Input (Data, Variable, Absolute)

    0x75, 0x05,         // Report Size (5)
    0x95, 0x01,         // Report Count (1)
    0x81, 0x03,         // Input (Cnst,Var,Abs)

    // X and Y Axes
    0x09, 0x30,        // Usage (X)
    0x09, 0x31,        // Usage (Y)
    0x15, 0x81,        // Logical Minimum (-127)
    0x25, 0x7f,        // Logical Maximum (127)
    0x75, 0x08,        // Report Size (8 bits)
    0x95, 0x02,        // Report Count (2)
    0x81, 0x06,        // Input (Data, Variable, Relative)

    // Scroll Wheel
    0x09, 0x38,        // Usage (Wheel)
    0x15, 0x81,        // Logical Minimum (-127)
    0x25, 0x7f,        // Logical Maximum (127)
    0x75, 0x08,        // Report Size (8 bits)
    0x95, 0x01,        // Report Count (1)
    0x81, 0x06,        // Input (Data, Variable, Relative)

    0xC0                // End Collection
};

这是一个鼠标(Usage (Mouse))的数据描述,从描述可以得知:这个鼠标包含按键(Usage (Button))、移动(Usage (X),Usage (Y))以及滚轮(Usage (Wheel))功能,这里仅介绍下按键相关的数据描述:

    // Mouse Buttons
    0x09, 0x01,        // Usage (Button)
    0x19, 0x01,        // Usage Minimum (Button 1)
    0x29, 0x03,        // Usage Maximum (Button 3)
    0x15, 0x00,        // Logical Minimum (0)
    0x25, 0x01,        // Logical Maximum (1)
    0x95, 0x03,        // Report Count (3)
    0x75, 0x01,        // Report Size (1 bit)
    0x81, 0x02,        // Input (Data, Variable, Absolute)

这个鼠标有3个按键(Usage Minimum (Button 1),Usage Maximum (Button 3)),每个按键产生1比特(Report Size (1 bit))的数据(是否被按下),开发人员可以根据该描述来解析键盘上报的数据。HID驱动在probe阶段会请求HID外设的数据描述符,函数调用路径如下:

usbhid_probe()
|
|-> hid_add_device() -> hdev->ll_driver->parse
    |
    |-> usbhid_parse()
        |
        |-> usb_get_extra_descriptor(interface, HID_DT_HID, &hdesc) // [1]
        |
        |-> rdesc = kmalloc(rsize, GFP_KERNEL); // [2]
        |
        |-> hid_get_class_descriptor(HID_DT_REPORT, rdesc, rsize); // [3]
        |
        |-> hid_parse_report(hid, rdesc, rsize) // [4]

hid_descriptor对象作为消息头简要描述了真正的描述符信息,包括个数(bNumDescriptors)和长度(wDescriptorLength[1]。驱动知道描述符长度后,调用usbhid_parse()分配内存空间[2],然后从设备请求描述符[3],再复制一份到hid_device[4],这里没有解析描述符,因为usbhid不是一个具体外设的驱动。当外设是触摸板时,MULTITOUCH驱动会解析描述符,函数调用流程如下:

mt_probe()
|
|-> hid_parse(hdev) // 解析描述符
|
|-> hid_hw_start -> hid_connect() -> hidinput_connect()
|   |
|   |-> report_features() // 漏洞点
|
|-> mt_set_modes(hdev) // 信息泄露

hid_parse()最终会调用hid_open_report()来解析描述符,规范规定描述符由item组成,其分为4类:maingloballocallong(保留),对应的解析函数分别是hid_parser_main()hid_parser_global()hid_parse_local()hid_parse_reserved()。我们发送的描述符如下:

static const uint8_t hid_report_desc[] = {
    0x05, 0x0D, // Usage Page (Digitizers)
    0x09, 0x05, // Usage (Touchpad)
    0xA1, 0x01, // Collection (Application)

    // Finger-1
    0x09, 0x22, // Usage (Finger)
    0xA1, 0x02, // Collection (Logical)

    0x09, 0x42, // Usage (Tip Switch)
    0x15, 0x00, // Logical Minimum (0)
    0x25, 0x01, // Logical Maximum (1)
    0x75, 0x01, // Report Size (1)
    0x95, 0x01, // Report Count (1)
    0x81, 0x02, // Input (Data,Var,Abs)

    0x75, 0x07, // Report Size (7)
    0x95, 0x01, // Report Count (1)
    0x81, 0x03, // Input (Cnst,Var,Abs)

    0x09, 0x51, // Usage (Contact ID)
    0x25, 0x7F, // Logical Maximum (127)
    0x75, 0x08, // Report Size (8)
    0x95, 0x01, // Report Count (1)
    0x81, 0x02, // Input (Data,Var,Abs)

    0x05, 0x01, // Usage Page (Generic Desktop)
    0x09, 0x30, // Usage (X)
    0x09, 0x31, // Usage (Y)
    0x16, 0x00, 0x00, // Logical Minimum (0)
    0x26, 0xFF, 0x7F, // Logical Maximum (32767)
    0x75, 0x10,       // Report Size (16)
    0x95, 0x02,       // Report Count (2)
    0x81, 0x02,       // Input (Data,Var,Abs)
    0xC0,       // End Collection

    /**
     * #define HID_DG_INPUTMODE     0x000d0052
     * #define HID_DG_CONTACTMAX    0x000d0055
     */
    0x0B, 0x55, 0x00, 0x0D, 0x00, //   Usage (HID_DG_CONTACTMAX)
    0x0B, 0x52, 0x00, 0x0D, 0x00, //   Usage (HID_DG_INPUTMODE)
    0x15, 0x00,       //   Logical Minimum (0)
    0x26, 0xFF, 0x00, //   Logical Maximum (255)
    0x75, 0x08,       //   Report Size (8)
    0x96, 0x00, 0x01, //   Report Count (256)
    0xB1, 0x02,       //   Feature (Data,Var,Abs)

    0xC0        // End Collection
};

它描述了数字化器(Digitizers)大类下的触摸板(Touchpad)具有的功能(Application),它的数据中包含一个手指触摸信息:

触摸状态
0x09, 0x42, // Usage (Tip Switch)               是否触摸触摸板
0x15, 0x00, // Logical Minimum (0)              未触摸
0x25, 0x01, // Logical Maximum (1)              触摸
0x75, 0x01, // Report Size (1)                  1bit数据
0x95, 0x01, // Report Count (1)                 1个report
0x81, 0x02, // Input (Data,Var,Abs)             输入

padding位
0x75, 0x07, // Report Size (7)                  将report padding到8bit
0x95, 0x01, // Report Count (1)
0x81, 0x03, // Input (Cnst,Var,Abs)

ID位
0x09, 0x51, // Usage (Contact ID)               标识ID
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x02, // Input (Data,Var,Abs)

二维轨迹
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)                        先是X轴数据
0x09, 0x31, // Usage (Y)                        后是Y轴数据
0x16, 0x00, 0x00, // Logical Minimum (0)        坐标最小值
0x26, 0xFF, 0x7F, // Logical Maximum (32767)    坐标最大值
0x75, 0x10,       // Report Size (16)           16bits数据
0x95, 0x02,       // Report Count (2)           每个轴一个report
0x81, 0x02,       // Input (Data,Var,Abs)       输入数据

虽然这段数据跟漏洞没有直接关系,但只有包含该数据,才能使驱动认为这是一个触摸板。漏洞相关的数据如下:

/**
 *
 * #define HID_DG_INPUTMODE     0x000d0052
 * #define HID_DG_CONTACTMAX    0x000d0055
 */
0x0B, 0x55, 0x00, 0x0D, 0x00, //   Usage (HID_DG_CONTACTMAX)
0x0B, 0x52, 0x00, 0x0D, 0x00, //   Usage (HID_DG_INPUTMODE)
0x15, 0x00,       //   Logical Minimum (0)
0x26, 0xFF, 0x00, //   Logical Maximum (255)
0x75, 0x08,       //   Report Size (8)
0x96, 0x00, 0x01, //   Report Count (256)
0xB1, 0x02,       //   Feature (Data,Var,Abs)

这里描述了一个外设的feature,用于主机与外设间同步状态或配置。feature属于main类型的item,因此由hid_parse_main()解析。解析完描述符后,驱动开始连接外设,report_features()会调用mt_feature_mapping(),因为我们的usageHID_DG_CONTACTMAX,因此驱动会调用mt_get_feature(),从而触发漏洞点:

static void mt_get_feature(struct hid_device *hdev, struct hid_report *report)
{
    buf = hid_alloc_report_buf(report, GFP_KERNEL);                     // [1]
    ret = hid_hw_raw_request(hdev, report->id, buf, size,
                            HID_FEATURE_REPORT, HID_REQ_GET_REPORT);    // [2]
    if (ret < 0) {
    } else {
        ret = hid_report_raw_event(hdev, HID_FEATURE_REPORT, buf, size, 0); // [3]
    }
}

hid_alloc_report_buf()通过kmalloc()分配内存,在没有缓解措施(INIT_ON_ALLOC_DEFAULT_ON)的内核上,堆内存没有进行初始化[1]。然后驱动向外设请求feature report[2],恶意的外设可以发送比它之前声明的长度短的数据:

0x75, 0x08,       //   Report Size (8)      8bit数据
0x96, 0x00, 0x01, //   Report Count (256)   256个,声明共计256字节


handle_get_report(struct usb_raw_control_event *event,
            struct usb_raw_control_io *io)
{
    memset(io->data, 0xcc, len);
    io->inner.length = 1;           // 实际只有1字节
    return 0;
}

内存遗留的历史数据被当成外设数据写入相关字段[3],之后mt_set_modes()对外设传入的数据进行更新,如果存在更新,驱动会调用hid_hw_request()将数据回传给外设,以实现同步:

static bool mt_need_to_apply_feature(struct hid_device *hdev)
{
    unsigned int index = usage->usage_index;

    switch (usage->hid) {
    case HID_DG_INPUTMODE:
        field->value[index] = td->inputmode_value;
        *inputmode_found = true;
        return true;
    }
}

所以HID_DG_INPUTMODE的作用是迫使驱动将数据重新发送回来,从而实现信息泄露。

该漏洞的修复方式是使用kzalloc()分配堆内存,确保堆上的历史数据被清空。

四、利用思路

我们分析了报告中披露的Cellebrite模拟的外设序列,除了上述设备,Cellebrite还模拟了HID MouseFastTrackPro,看上去它先利用了UVC越界写漏洞,再利用了extigy越界读漏洞,造成一个设备类型的混淆,具体利用方式无法确认,所以我们决定自己构造利用思路。

我们大致的思路和ROOT类似:

  1. 通过信息泄露获得内核符号地址和堆地址;
  2. 通过操纵堆来构造读写原语;
  3. 特权执行命令;

4.1 环境搭建

为了方便快速验证思路,我们使用QEMU模拟了手机环境,使用USB_RAW_GADGET+USB_DUMMY_HCD驱动来模拟设备;真实场景下,我们使用树莓派的Zero 2W搭配特制的板子,可以方便地插入手机:

然后通过串口或者wifi接入到树莓派,此时我们使用USB_RAW_GADGET+DWC2驱动来模拟设备,这样做的好处是:两种环境下的代码是一致的,只需切换USB_RAW_GADGET使用的UDCUSB Device Controller)驱动即可。

4.2 目标对象选择

我们有两个越界访问的漏洞,看上去UVC驱动越界写漏洞更好利用,因此,我们根据该漏洞的特点选择目标对象。目标对象最好有以下特点:

  1. 占用的堆不能太小,小堆使用比较频繁,不利于堆布局;
  2. 可以泄露内核符号地址;
  3. 可以泄露堆地址;
  4. 生命周期可控;

根据上述需要,我们选择hid_device作为目标对象,它使用8K的堆,且包含符号和堆地址信息:

struct hid_device {
    [...]
    struct list_head inputs;
    [...]
    int (*hiddev_connect)(struct hid_device *, unsigned int);
    [...]
}

链表初始化时指向自身,可以从链表指针获得堆地址,hiddev_connect是一个函数指针,可以获得内核符号地址。我们可以通过模拟HID设备插拔来控制hid_device的生命周期。

4.3 构造写原语(已验证)

HID外设发送数据时,驱动会根据report描述解析数据,并将数据保存在hid_device->report_enum数组中,它有3个元素:

 #define HID_INPUT_REPORT    0
 #define HID_OUTPUT_REPORT   1
 #define HID_FEATURE_REPORT  2
 #define HID_REPORT_TYPES    3

如果我们模拟的是键盘,那么驱动只会频繁使用HID_INPUT_REPORT,我们可以利用剩余元素的空间来构造其他对象。report_enum数组中保存的是hid_report_enum对象,它是顶级对象,数据最终会保存在hid_field中:

struct hid_field {
    [...]
    unsigned  report_size;      /* size of this field in the report */
    unsigned  report_count;     /* number of this field in the report */
    unsigned  report_type;      /* (input,output,feature) */
    __s32    *value;
}

report_size表示数据长度,report_count表示数据个数,value指向保存数据的内存地址。我们可以通过越界写漏洞将A对象的value指向B对象,从而实现对B对象的操控,然后通过篡改B对象的value来构造写原语。

4.3 堆布局

理想情况下,我们通过模拟一个Hub,可以方便地模拟其他设备的插拔,从而实现堆布局。但深入研究后发现,目前常见的UDC都不能模拟Hub,即使在虚拟环境下,USB_RAW_GADGET+USB_DUMMY_HCD组合也无法模拟Hub。但TAG分析出的设备序列中包含了Hub(证据1),我们认为Cellebrite使用了定制化的硬件(参见Turbo link)以及软件,从而支持更加灵活的利用方式。

在软硬件受限的条件下,我们只能选择使用复合设备,它的每个接口都是一个设备,这种方式有以下缺点:

  1. 受限于硬件,模拟的设备数量有限;
  2. 只能模拟通用设备,不能模拟具体厂商生产的设备(证据2);
  3. 所有的设备必须同时插拔,不能单独插拔指定设备(证据3),即无法实现更加灵活的堆布局;

这些缺点几乎使我们丧失了堆布局的能力,结果就是堆布局成功率非常低。

五、总结

看上去USB是一个比较好的攻击面,攻击者可以在手机锁屏状态下触达内核逻辑,对内核进行攻击。但实际利用有较高的难度,一是较新内核默认开启一些防护措施,这些措施可以有效避免漏洞被利用;二是USB场景下与内核交互的面有限,无法像进程那样灵活的控制内核对象;出乎我们意料的是:目前没有合适的硬件可以模拟Hub,只能定制化开发相关硬件和软件,利用的成本大大提高。