開源USB協議棧漏洞挖掘


文章首發於

https://forum.butian.net/share/169

起因

有一天打開 github 的 explore頁面,發現推送了一個 sboot_stm32 的項目,之前也一直對USB協議棧的實現感興趣,於是就分析了一下,分析完 sboot_stm32 后,然后花了 2 天在 google 上找了一些類似的嵌入式USB協議棧的源碼進行了分析。

下面對分析的一些思路和發現進行分享,發現的都是一些內存越界、溢出相關的漏洞,具體數目如下:

軟件 BUG數目
sboot_stm32 2
tinyusb 6
lufa 7
TeenyUSB 5
USBDevice 3
rt-thread 10
總計 33

不過大部分開發者還沒回復,最終可能會有偏差.

漏洞挖掘

sboot_stm32

issue 地址

https://github.com/dmitrystu/sboot_stm32/issues/35

sboot_stm32 是基於 libusb_stm32 開發的 usb 協議棧的上層應用,類似於 HTTP 和 TCP 之間的關系。我們首先來看一下 libusb_stm32 對 usb 協議數據的處理。

首先我們要明確 USB 本身就是一個收發USB數據的外設,那么在代碼中處理USB數據包的邏輯肯定是:

  1. 控制USB外設,從總線收包
  2. 調用處理函數對數據包進行解析、處理

為了快速定位數據處理函數,我們可以從協議規范入手,對於 USB 協議而言,通信的第一步是傳輸 USB control request,所以可以根據文件名、數據結構的定義和協議規范中對 USB control request 的數據結構進行對比就可以找到協議棧中的數據處理函數,最后再依次回溯,可以找到最上層的收包函數。

libusb_stm32 中描述 USB control request 數據包的結構體定義如下

/**\brief Represents generic USB control request.*/
typedef struct {
    uint8_t     bmRequestType;  /**<\brief This bitmapped field identifies the characteristics of
                                 * the specific request.*/
    uint8_t     bRequest;       /**<\brief This field specifies the particular request.*/
    uint16_t    wValue;         /**<\brief It is used to pass a parameter to the device, specific to
                                 * the request.*/
    uint16_t    wIndex;         /**<\brief It is used to pass a parameter to the device, specific to
                                 * the request.*/
    uint16_t    wLength;        /**<\brief This field specifies the length of the data transferred
                                 * during the second phase of the control transfer.*/
    uint8_t     data[];         /**<\brief Data payload.*/
} usbd_ctlreq;

處理 control request 的函數為 usbd_process_ep0 .

static void usbd_process_ep0 (usbd_device *dev, uint8_t event, uint8_t ep) {
    switch (event) {
    case usbd_evt_epsetup:
        /* force switch to setup state */
        dev->status.control_state = usbd_ctl_idle;
        dev->complete_callback = 0;
    case usbd_evt_eprx:
        usbd_process_eprx(dev, ep);
        break;
    case usbd_evt_eptx:
        usbd_process_eptx(dev, ep);
        break;
    default:
        break;
    }
}

在 USB 設備中,端點(endpoint)是主機和設備之間進行通訊的基本單元,USB 設備間的通信實際是就是端點之間的通信,所以在USB代碼中帶 endpoint/ep 的函數比較有可能會數據收發有關。

usbd_process_ep0 就是用於處理 0 號端點的數據交互,實際數據處理位於 usbd_process_eprx .

static void usbd_process_eprx(usbd_device *dev, uint8_t ep) {
    uint16_t _t;
    usbd_ctlreq *const req = dev->status.data_buf;

	......................
	......................
	// 驅動 USB外設收包
    _t = dev->driver->ep_read(ep, dev->status.data_buf, dev->status.data_count);
	
	// 處理收到的數據包
    switch (usbd_process_request(dev, req)) {

函數首先通過 ep_read 收取數據包,然后調用 usbd_process_request 進行解析,usbd_process_request里面就會根據數據包的內容進入具體的分支去解析。

static usbd_respond usbd_process_request(usbd_device *dev, usbd_ctlreq *req) {
    if (dev->control_callback) {
        usbd_respond r = dev->control_callback(dev, req, &(dev->complete_callback));
        if (r != usbd_fail) return r;
    }
    switch (req->bmRequestType & (USB_REQ_TYPE | USB_REQ_RECIPIENT)) {
    case USB_REQ_STANDARD | USB_REQ_DEVICE:
        return usbd_process_devrq(dev, req);
    case USB_REQ_STANDARD | USB_REQ_INTERFACE:
        return usbd_process_intrq(dev, req);
    case USB_REQ_STANDARD | USB_REQ_ENDPOINT:
        return usbd_process_eptrq(dev, req);
    default:
        break;
    }
    return usbd_fail;
}

如果用戶設置了 control_callback ,就會先調用 control_callback 去解析數據,否則就進入標准的USB協議解析流程。

sboot_stm32 里面的兩個漏洞就位於自己實現的 control_callback 函數中

inline static void usbd_reg_control(usbd_device *dev, usbd_ctl_callback callback) {
    dev->control_callback = callback;
}
usbd_reg_control(&dfu, dfu_control);

下面分析一下 dfu_control 函數,關鍵代碼如下

static usbd_respond dfu_control (usbd_device *dev, usbd_ctlreq *req, usbd_rqc_callback *callback) {

    if ((req->bmRequestType & (USB_REQ_TYPE | USB_REQ_RECIPIENT)) == (USB_REQ_CLASS | USB_REQ_INTERFACE)) {
        switch (req->bRequest) {

        case USB_DFU_DNLOAD:
            return dfu_dnload(req->data, req->wLength);
        case USB_DFU_UPLOAD:
            return dfu_upload(dev, req->wLength);
        case USB_DFU_GETSTATUS:
            return dfu_getstatus(req->data);
        case USB_DFU_CLRSTATUS:
            return dfu_clrstatus();
        case USB_DFU_GETSTATE:
            return dfu_getstate(req->data);
        case USB_DFU_ABORT:
            return dfu_abort();
        default:
            return dfu_err_badreq();
        }
    }
    return usbd_fail;
}

函數入參中 req 是直接從 USB 總線上接收的,可以認為是有害的數據,函數根據 bmRequestType 和 bRequest 來決定下一步的處理。

漏洞位於下面兩個 case:

        case USB_DFU_DNLOAD:
            return dfu_dnload(req->data, req->wLength);
        case USB_DFU_UPLOAD:
            return dfu_upload(dev, req->wLength);

問題的根因都一樣,如果 req->wLength 過大,就會導致溢出。

tinyusb

issue 地址

https://github.com/hathach/tinyusb/issues/880

tinyusb 處理USB數據包的入口函數為 tuh_task,關鍵代碼如下

void tuh_task(void)
{
  while (1)
  {
    if ( !osal_queue_receive(_usbd_q, &event) ) return;

    switch (event.event_id)
    {
      case DCD_EVENT_SETUP_RECEIVED:
        // Process control request
        if ( !process_control_request(event.rhport, &event.setup_received) )
        {
      break;

      case HCD_EVENT_XFER_COMPLETE:
      {
        usbh_device_t* dev = &_usbh_devices[event.dev_addr];
        uint8_t const ep_addr = event.xfer_complete.ep_addr;
        uint8_t const epnum   = tu_edpt_number(ep_addr);
        uint8_t const ep_dir  = tu_edpt_dir(ep_addr);

        if ( 0 == epnum )
        {
          usbh_control_xfer_cb(event.dev_addr, ep_addr, event.xfer_complete.result, event.xfer_complete.len);
        }else
        {
          uint8_t drv_id = dev->ep2drv[epnum][ep_dir];
          usbh_class_drivers[drv_id].xfer_cb(event.dev_addr, ep_addr, event.xfer_complete.result, event.xfer_complete.len);
        }
      }
      break;

其實主要就是根據收到的事件類型來調用特定函數對端點的數據進行解析:

  1. process_control_request: 處理 setup 數據包
  2. usbh_control_xfer_cb: 處理 0 號端點的數據
  3. usbh_class_drivers[drv_id].xfer_cb: 處理其他端點的數據

process_control_request 的關鍵代碼如下

static bool process_control_request(uint8_t rhport, tusb_control_request_t const * p_request)
{
  
  switch ( p_request->bmRequestType_bit.recipient )
  {
    case TUSB_REQ_RCPT_DEVICE:
      if ( TUSB_REQ_TYPE_CLASS == p_request->bmRequestType_bit.type )
      {

        usbd_class_driver_t const * driver = get_driver(_usbd_dev.itf2drv[itf]);

        // forward to class driver: "non-STD request to Interface"
        return invoke_class_control(rhport, driver, p_request);
      }
     case ...................
       ................
       ................

這里就是常規的利用 switch 根據請求的類型進行相應的處理,其中 invoke_class_control 用於調用其他代碼注冊的處理函數,對數據包進行處理

static bool invoke_class_control(uint8_t rhport, usbd_class_driver_t const * driver, tusb_control_request_t const * request)
{
  return driver->control_xfer_cb(rhport, CONTROL_STAGE_SETUP, request);
}

可以看到實際是調用 control_xfer_cb 回調函數進行解析,於是我們搜索 .control_xfer_cb 就可以找到所有注冊的 control 請求的處理函數,比如

  {
    DRIVER_NAME("CDC")
    .init             = cdcd_init,
    .reset            = cdcd_reset,
    .open             = cdcd_open,
    .control_xfer_cb  = cdcd_control_xfer_cb,
    .xfer_cb          = cdcd_xfer_cb,
    .sof              = NULL
  },

dfu_moded_control_xfer_cb 越界訪問

bool dfu_moded_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const * request)
{
  switch (request->bRequest)
  {
  
    case DFU_REQUEST_DETACH:
    case DFU_REQUEST_UPLOAD:
    case DFU_REQUEST_GETSTATUS:
    case DFU_REQUEST_CLRSTATUS:
    case DFU_REQUEST_GETSTATE:
    case DFU_REQUEST_ABORT:
    {
      if(stage == CONTROL_STAGE_SETUP)
      {
        return dfu_state_machine(rhport, request);
      }
    }

其中 request 從 usb 總線直接收上來,從該函數開始一個一個處理函數進行分析,就可以發現一些問題,比如 dfu_req_dnload_setup

static void dfu_req_dnload_setup(uint8_t rhport, tusb_control_request_t const * request)
{
  tud_control_xfer(rhport, request, _dfu_state_ctx.transfer_buf, request->wLength);
}

如果 request->wLength 比較大,就會導致越界讀。

netd_xfer_cb 整數溢出導致堆溢出漏洞

函數代碼如下

bool netd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes)
{
  /* new packet received */
  if ( ep_addr == _netd_itf.ep_out )
  {
    handle_incoming_packet(xferred_bytes);
  }

問題出在 handle_incoming_packet 函數

static void handle_incoming_packet(uint32_t len)
{
  uint8_t *pnt = received;
  uint32_t size = 0;

  if (_netd_itf.ecm_mode)
  {
    size = len;
  }
  else
  {
    rndis_data_packet_t *r = (rndis_data_packet_t *) ((void*) pnt);
    if (len >= sizeof(rndis_data_packet_t))
      if ( (r->MessageType == REMOTE_NDIS_PACKET_MSG) && (r->MessageLength <= len))
        if ( (r->DataOffset + offsetof(rndis_data_packet_t, DataOffset) + r->DataLength) <= len)
        {
          pnt = &received[r->DataOffset + offsetof(rndis_data_packet_t, DataOffset)];
          size = r->DataLength;
        }
  }

  if (!tud_network_recv_cb(pnt, size))
  {

r->DataLengthr->DataOffset 的類型都是uint32_t ,且都是從 USB 總線上接收。

r->DataLength=0xFFFFFFFFr->DataOffset 是一個比較小的值, 就會導致整數溢出,從而通過下面的檢查.

if ( (r->DataOffset + offsetof(rndis_data_packet_t, DataOffset) + r->DataLength) <= len)

然后在 tud_network_recv_cb 時就會越界。

lufa

issue 鏈接

https://github.com/abcminiuser/lufa/issues/172

定位數據入口

結合代碼文件名、函數中使用到的結構體、以及USB的規范,可以快速定位到處理控制傳輸的代碼

void USB_Device_ProcessControlRequest(void)
{
	USB_ControlRequest.bmRequestType = Endpoint_Read_8();
	USB_ControlRequest.bRequest      = Endpoint_Read_8();
	USB_ControlRequest.wValue        = Endpoint_Read_16_LE();
	USB_ControlRequest.wIndex        = Endpoint_Read_16_LE();
	USB_ControlRequest.wLength       = Endpoint_Read_16_LE();


	EVENT_USB_Device_ControlRequest();
	.................
	.................

	if (Endpoint_IsSETUPReceived())
	{
		uint8_t bmRequestType = USB_ControlRequest.bmRequestType;

		switch (USB_ControlRequest.bRequest)
		{
			case ....
			......

lufa的代碼中會使用封裝好的 Endpoint_Read_xx 從端點中獲取數據,函數開頭首先獲取了 USB_ControlRequest 的各個字段,然后根據 USB_ControlRequest.bRequest 進行相應的處理。

對於源碼審計而言,找到入口后跟數據流即可,簡單跟蹤了USB_Device_ProcessControlRequest里面的分支,沒有發現什么問題,其實在 device 側的 控制傳輸 的處理邏輯都相對比較簡單,一般不容易出現問題,在device 側更容易出現問題的是那些USB應用層的協議,比如 CDC、RNDIS等。

於是又翻了翻代碼的目錄,發現一些路徑下存在一些USB應用層協議的實現,比如

E:\data\USB_VULN\lufa-master\Demos\Device\ClassDriver
λ ls
AudioInput/   DualVirtualSerial/  KeyboardMouse/             MassStorageKeyboard/  VirtualSerial/
AudioOutput/  GenericHID/         KeyboardMouseMultiReport/  MIDI/                 VirtualSerialMassStorage/
CCID/         Joystick/           makefile                   Mouse/                VirtualSerialMouse/
DualMIDI/     Keyboard/           MassStorage/               RNDISEthernet/

或者我們可以通過全局搜索 Endpoint_Read_ 來找到代碼中會處理 USB 數據的位置,進而找到審計的入口。

代碼思維導圖

下面直接介紹幾個典型的漏洞

RNDISEthernet 控制請求處理溢出

在 USB_Device_ProcessControlRequest 里面,會先調用 EVENT_USB_Device_ControlRequest 對數據進行處理,用戶可以自己實現 EVENT_USB_Device_ControlRequest 來實現自定義的控制傳輸

void USB_Device_ProcessControlRequest(void)
{
	.........
 	EVENT_USB_Device_ControlRequest(); // 調用回調函數進行處理

RNDISEthernet 實現的 EVENT_USB_Device_ControlRequest 如下

void EVENT_USB_Device_ControlRequest(void)
{
	CheckIfMSCompatibilityDes criptorRequest();

	switch (USB_ControlRequest.bRequest)
	{
		case RNDIS_REQ_SendEncapsulatedCommand:
			if (USB_ControlRequest.bmRequestType == (REQDIR_HOSTTODEVICE | REQTYPE_CLASS | REQREC_INTERFACE))
			{
				Endpoint_ClearSETUP();

				/* Read in the RNDIS message into the message buffer */
				Endpoint_Read_Control_Stream_LE(RNDISMessageBuffer, USB_ControlRequest.wLength);

漏洞在於沒有檢查 USB_ControlRequest.wLength ,如果 USB_ControlRequest.wLength 大於 RNDISMessageBuffer 的大小就會導致全局變量的溢出。

CCID_Task 棧溢出漏洞

根據函數名可以大概猜測,CCID_Task 應該是負責處理 CCID 協議數據的應用

void CCID_Task(void)
{
	Endpoint_SelectEndpoint(CCID_OUT_EPADDR);

	uint8_t RequestBuffer[CCID_EPSIZE - sizeof(USB_CCID_BulkMessage_Header_t)];
	uint8_t ResponseBuffer[CCID_EPSIZE];
	Aborted = false;
	AbortedSeq = -1;

	if (Endpoint_IsOUTReceived())
	{
		USB_CCID_BulkMessage_Header_t CCIDHeader;
		CCIDHeader.MessageType = Endpoint_Read_8();
		CCIDHeader.Length      = Endpoint_Read_32_LE();
		CCIDHeader.Slot        = Endpoint_Read_8();
		CCIDHeader.Seq         = Endpoint_Read_8();

		switch (CCIDHeader.MessageType)
		{
			........
			........

函數首先切換端點,用於后續的數據傳輸,然后調用 Endpoint_Read 從端點中獲取 CCIDHeader, 最后根據 CCIDHeader.MessageType 來決定處理的方式,漏洞位於處理 CCID_PC_to_RDR_XfrBlock 請求時

			case CCID_PC_to_RDR_XfrBlock:
			{
				uint8_t  Bwi            = Endpoint_Read_8();
				uint16_t LevelParameter = Endpoint_Read_16_LE();
				
				Endpoint_Read_Stream_LE(RequestBuffer, CCIDHeader.Length * sizeof(uint8_t), NULL);

這里直接使用 CCIDHeader.Length 來讀取數據到 RequestBuffer 中, RequestBuffer是一個棧數組,大小為 64 字節,只要 CCIDHeader.Length 大於 64 就會棧溢出。

IP_ProcessIPPacket 越界訪問

RNDIS 設備通過 USB 讀取到以太報文后會調用 Ethernet_ProcessPacket 對報文進行解析

RNDIS_Device_ReadPacket(&Ethernet_RNDIS_Interface, &F rameIN.F rameData, &F rameIN.F rameLength);
Ethernet_ProcessPacket(&F rameIN, &F rameOUT);

最終會進入 IP_ProcessIPPacket 解析 IP 數據包,關鍵代碼如下:

int16_t IP_ProcessIPPacket(Ethernet_F rame_Info_t* const F rameIN,
                           void* InDataStart,
                           void* OutDataStart)
{

	IP_Header_t* IPHeaderIN  = (IP_Header_t*)InDataStart;
	uint16_t HeaderLengthBytes = (IPHeaderIN->HeaderLength * sizeof(uint32_t));


	switch (IPHeaderIN->Protocol)
	{
		case PROTOCOL_ICMP:
			RetSize = ICMP_ProcessICMPPacket(F rameIN,
			                                 &((uint8_t*)InDataStart)[HeaderLengthBytes],
			                                 &((uint8_t*)OutDataStart)[sizeof(IP_Header_t)]);
			break;

問題在於沒有校驗 IPHeaderIN->HeaderLength, 從而會在后面使用 HeaderLengthBytes 時導致越界。

TeenyUSB

issue 鏈接

https://github.com/xtoolbox/TeenyUSB/issues/18

定位數據入口

TeenyUSB 代碼邏輯還是比較清晰的,USB_LP_CAN_RX0_IRQHandler 為 USB 中斷的處理函數

void USB_LP_CAN_RX0_IRQHandler(void)
{
    while ((wIstr = GetUSB(drv)->ISTR) & USB_ISTR_CTR)
    {
        GetUSB(drv)->ISTR = (uint16_t)(USB_CLR_CTR);
        tusb_ep_handler(drv, wIstr & USB_ISTR_EP_ID);  // 處理端點的數據
    }

如果 USB 相關的中斷就會進入 tusb_ep_handler 進行處理,關鍵代碼如下

void tusb_ep_handler(tusb_device_driver_t *drv, uint8_t EPn)
{
    uint16_t EP = PCD_GET_ENDPOINT(GetUSB(drv), EPn);
    if (EP & USB_EP_CTR_RX)
    {
        if (EPn == 0)
        {
            if (EP & USB_EP_SETUP)
            {
                // Handle setup packet
                uint8_t temp[8];
                tusb_read_ep0(drv, temp);
                tusb_device_ep_xfer_done(drv, EPn, temp, 8, 1);
            }
            else
            {
                // Handle ep 0 data packet
                tusb_recv_data(drv, EPn);
            }
        }
        else
        {
            tusb_recv_data(drv, EPn);
        }
    }

主要就是根據觸發中斷的端點號、數據傳輸類型調用相應的函數進行解析:

  1. tusb_device_ep_xfer_done:處理 setup 請求
  2. tusb_recv_data: 處理端點的數據請求,包括 ep0.

下面看 tusb_recv_data 的實現,關鍵的代碼如下:

// called by the ep data interrupt handler when got data
void tusb_recv_data(tusb_device_driver_t *drv, uint8_t EPn)
{
    tusb_ep_data *ep = &drv->Ep[EPn];
    uint16_t EP = PCD_GET_ENDPOINT(GetUSB(drv), EPn);
   	...............
   	...............
    if (ep->rx_buf && pma)
    {
        uint32_t len = tusb_pma_rx(drv, pma, ep->rx_buf + ep->rx_count);
        pma->cnt = 0;
        ep->rx_count += len;
        
        // 判斷數據包是否以及傳輸完畢
        if (len != GetOutMaxPacket(drv, EPn) || ep->rx_count >= ep->rx_size)
        {
            if (tusb_device_ep_xfer_done(drv, EPn, ep->rx_buf, ep->rx_count, 0) == 0)
            {
                ep->rx_count = 0;
            }
            else
            {
                // of rx done not return success, change rx_count to rx_size, this will block
                // the data recieve
                ep->rx_count = ep->rx_size;
            }
        }
    }

tusb_recv_data 會接收端點的數據包,同時也會處理分包傳輸的場景,數據包傳輸完畢后進入 tusb_device_ep_xfer_done 對數據進行解析。

int tusb_device_ep_xfer_done(tusb_device_driver_t *drv, uint8_t EPn, const void *data, int len, uint8_t isSetup)
{
    tusb_device_t *dev = (tusb_device_t *)tusb_dev_drv_get_context(drv);
    if (dev)
    {
        if (EPn == 0x00)
        {
            if (isSetup)
            {
                // endpoint 0, setup data out
                memcpy(&dev->setup, data, len);
                tusb_setup_handler(dev);
            }else if (dev->ep0_rx_done)
            {
                dev->ep0_rx_done(dev, data, len);
                dev->ep0_rx_done = NULL;
            }
        }
        else if (EPn & 0x80)
        {
            tusb_on_tx_done(dev, EPn & 0x7f, data, len);
        }
        else
        {
            return tusb_on_rx_done(dev, EPn, data, len);
        }
    }

主要就是根據端點的信息決定下一步的解析方式:

  1. tusb_setup_handler:處理 setup 請求
  2. ep0_rx_done: 處理從 ep0 收到的數據
  3. tusb_on_rx_done: 解析其他端點收到的數據,實際調用 backend->device_recv_done 函數進行解析。

tusb_setup_handler 代碼如下

void tusb_setup_handler(tusb_device_t *dev)
{
    tusb_setup_packet *setup_req = &dev->setup;
    // we pass all request to tusb_class_request, not only class request
    if (tusb_class_request(dev, setup_req))
    {
        return;
    }
    if ((setup_req->bmRequestType & USB_REQ_TYPE_MASK) == USB_REQ_TYPE_VENDOR)
    {
        if (tusb_vendor_request(dev, setup_req))
        {
            return;
        }
    }
    else if ((setup_req->bmRequestType & USB_REQ_TYPE_MASK) == USB_REQ_TYPE_STANDARD)
    {
        if (!tusb_standard_request(dev, setup_req))
        {

首先會通過 tusb_class_request 調用其他應用注冊的回調函數對數據進行解析

int tusb_class_request(tusb_device_t* dev, tusb_setup_packet* setup_req)
{
    tusb_device_config_t* dev_config = dev->user_data;
    if(dev_config && 
      (setup_req->bmRequestType & USB_REQ_RECIPIENT_MASK) == USB_REQ_RECIPIENT_INTERFACE ){
        uint16_t iInterfce = setup_req->wIndex;
        if(iInterfce<dev_config->if_count){
            tusb_device_interface_t* itf = dev_config->interfaces[iInterfce];
            if(itf && itf->backend && itf->backend->device_request){
                return itf->backend->device_request(itf, setup_req);
            }
        }
    }
    // the setup packet will be processed in Teeny USB stack
    return 0;
}

比如

const tusb_device_backend_t cdc_device_backend = {
    .device_init = (int(*)(tusb_device_interface_t*))tusb_cdc_device_init,
    .device_request = (int(*)(tusb_device_interface_t*, tusb_setup_packet*))tusb_cdc_device_request,
    .device_send_done = (int(*)(tusb_device_interface_t*, uint8_t, const void*, int))tusb_cdc_device_send_done,
    .device_recv_done = (int(*)(tusb_device_interface_t*, uint8_t, const void*, int))tusb_cdc_device_recv_done,
};

其中 device_recv_donetusb_on_rx_done 中被調用。

至此我們弄清楚了程序的收包邏輯,所以程序中解析數據的入口有以下幾種:

  1. tusb_setup_handler 解析標准的 setup 請求
  2. tusb_device_backend_t 結構體中注冊的 device_requestdevice_recv_done.
  3. dev->ep0_rx_done 回調函數

代碼思維導圖

下面介紹幾個典型漏洞

tusb_rndis_device_request 溢出漏洞

關鍵代碼如下

static int tusb_rndis_device_request(tusb_rndis_device_t* cdc, tusb_setup_packet* setup_req)
{
	...................
	...................
    }else if(setup_req->bRequest == CDC_SEND_ENCAPSULATED_COMMAND){
        dev->ep0_rx_done = rndis_dataout_request;
        tusb_set_recv_buffer(dev, 0, cdc->encapsulated_buffer, setup_req->wLength);
        tusb_set_rx_valid(dev, 0);
        return 1;
    }

入參 setup_req 是從 USB 總線中收上來的,問題在於沒有校驗 setup_req->wLength ,直接通過 tusb_set_recv_buffer 讓 USB 外設往 cdc->encapsulated_buffer 收數據,如果 wLength 過大,就會導致 encapsulated_buffer 溢出。

msc_scsi_write_10 越界讀

msc 的收包和處理函數為 msc_data_out

static void msc_data_out(tusb_msc_device_t* msc)
{
  switch(msc->state.stage){
    case MSC_STAGE_CMD:
      if(msc->state.cbw.signature != MSC_CBW_SIGNATURE || msc->state.data_out_length != BOT_CBW_LENGTH){
        // Got an error command
        msc_scsi_sense(msc, msc->state.cbw.lun, SCSI_SENSE_ILLEGAL_REQUEST, INVALID_CDB, 0);
        msc_bot_abort(msc);
        return;
      }
      msc->state.csw.signature = MSC_CSW_SIGNATURE;
      msc->state.csw.tag = msc->state.cbw.tag;
      msc->state.csw.data_residue = msc->state.cbw.total_bytes;
      handle_msc_scsi_command(msc);
      break;
    case MSC_STAGE_DATA:
      handle_msc_scsi_command(msc);
      break;
    default:
      msc->state.stage = MSC_STAGE_CMD;
      msc_prepare_rx(msc, &msc->state.cbw, BOT_CBW_LENGTH);
      break;
  }
}

這個函數會被循環調用,函數的大概邏輯是 首先 msc_prepare_rxmsc->state.cbw 里面收數據,然后解析 msc->state.cbw 里面的數據進行后續的處理,整個狀態機通過 msc->state.stage 控制。

漏洞點

static int msc_scsi_write_10(tusb_msc_device_t* msc)
{
  tusb_msc_cbw_t * cbw = &msc->state.cbw;
  tusb_msc_csw_t       * csw = &msc->state.csw;
  scsi_read_10_cmd_t* cmd = (scsi_read_10_cmd_t*)cbw->command;
 	......
 	......
    uint32_t block_addr = GET_BE32(cmd->logical_block_addr);
    uint32_t block_size = cbw->total_bytes/ GET_BE16(cmd->transfer_length);
    int length = msc->state.data_out_length;
    uint16_t block_count = length/block_size;
    
    if(msc->block_write){
      // TODO: support data buffer length less than the block size
      int xferred_length = cbw->total_bytes - csw->data_residue;
      
      // vuln !!! 
      length = msc->block_write(msc, cbw->lun, msc->state.data_buffer, block_addr + xferred_length/block_size, block_count);

問題在於沒有檢查 block_count ,導致 write 的時候會越界。

USBDevice

issue 鏈接

https://github.com/IntergatedCircuits/USBDevice/issues/28

定位數據入口

USBDevice 的收包的入口函數為 USB_vDevIRQHandler ,關鍵代碼如下

void USB_vDevIRQHandler(USB_HandleType * pxUSB)
{
    uint32_t ulGINT = pxUSB->Inst->GINTSTS.w & pxUSB->Inst->GINTMSK.w;

        // 從USB外設接收數據
        ......................
        ......................

        /* OUT endpoint interrupts */
        if ((ulGINT & USB_OTG_GINTSTS_OEPINT) != 0)
        {
            uint8_t ucEpNum;

            /* Handle individual endpoint interrupts */
            for (ucEpNum = 0; xDAINT.b.OEPINT != 0; ucEpNum++, xDAINT.b.OEPINT >>= 1)
            {
                if ((xDAINT.b.OEPINT & 1) != 0)
                {
                    // 處理 out 端點接收到數據
                    USB_prvOutEpEventHandler(pxUSB, ucEpNum);
                }
            }
        }

函數首先從USB外設接收數據,數據接收完后調用 USB_prvOutEpEventHandler 對收到的數據進行處理

static void USB_prvOutEpEventHandler(USB_HandleType * pxUSB, uint8_t ucEpNum)
{
    if ((ulEpFlags & USB_OTG_DOEPINT_STUP) != 0)
    {
        /* Process SETUP Packet */
        USB_vSetupCallback(pxUSB);
    }
    else if ((ulEpFlags & USB_OTG_DOEPINT_XFRC) != 0)
    {

        if ((ucEpNum > 0) || (pxEP->Transfer.Progress == pxEP->Transfer.Length))
        {
            /* 處理 data 包 */
            USB_vDataOutCallback(pxUSB, pxEP);
        }

主要就是根據硬件上報的狀態決定是 setup 包還是 data 包的處理。

  1. USB_vSetupCallback: 處理 setup 包
  2. USB_vDataOutCallback: 處理 data 包

USB_vSetupCallback 實際為 USBD_SetupCallback 函數

void USBD_SetupCallback(USBD_HandleType *dev)
{
    USBD_ReturnType retval = USBD_E_INVALID;

    dev->EP.OUT[0].State = USB_EP_STATE_SETUP;

    /* Route the request to the recipient */
    switch (dev->Setup.RequestType.Recipient)
    {
        case USB_REQ_RECIPIENT_DEVICE:
            retval = USBD_DevRequest(dev);
            break;

        case USB_REQ_RECIPIENT_INTERFACE:
            retval = USBD_IfRequest(dev);
            break;

        case USB_REQ_RECIPIENT_ENDPOINT:
            retval = USBD_EpRequest(dev);
            break;

        default:
            break;
    }

代碼流程非常清晰,就是根據 RequestType 來分發請求,這里需要注意的是 USBD_IfRequest 里面會調用 Class->SetupStage 來對請求進行處理,USB應用可以自己實現相應的回調函數

static inline USBD_ReturnType USBD_IfClass_SetupStage(
        USBD_IfHandleType *itf)
{
    if (itf->Class->SetupStage == NULL)
    {   return USBD_E_INVALID; }
    else
    {   return itf->Class->SetupStage(itf); }
}

因此 SetupStage 回調函數也是一個處理數據的點。

處理 data 包實際進入 USBD_EpOutCallback

void USBD_EpOutCallback(USBD_HandleType *dev, USBD_EpHandleType *ep)
{
    ep->State = USB_EP_STATE_IDLE;

    if (ep == &dev->EP.OUT[0])
    {
        USBD_CtrlOutCallback(dev);
    }
    else
    {
        USBD_IfClass_OutData(dev->IF[ep->IfNum], ep);
    }
}

函數根據端點的類型,來決定調用對應的回調函數

一個注冊回調函數的示例如下:

/* NCM interface class callbacks structure */
static const USBD_ClassType ncm_cbks = {
    .GetDes criptor  = (USBD_IfDescCbkType)  ncm_getDesc,
    .GetString      = (USBD_IfStrCbkType)   ncm_getString,
    .Init           = (USBD_IfCbkType)      ncm_init,
    .Deinit         = (USBD_IfCbkType)      ncm_deinit,
    .SetupStage     = (USBD_IfSetupCbkType) ncm_setupStage,
    .DataStage      = (USBD_IfCbkType)      ncm_dataStage,
    .OutData        = (USBD_IfEpCbkType)    ncm_outData,
    .InData         = (USBD_IfEpCbkType)    ncm_inData,
};

代碼的思維導圖

下面介紹幾個典型漏洞

USBD_CtrlReceiveData 溢出漏洞

函數的定義如下

USBD_ReturnType USBD_CtrlReceiveData(USBD_HandleType *dev, void *data)
{
    USBD_ReturnType retval = USBD_E_ERROR;

    /* Sanity check */
    if (dev->EP.OUT[0].State == USB_EP_STATE_SETUP)
    {
        uint16_t len = dev->Setup.Length;

        dev->EP.OUT[0].State = USB_EP_STATE_DATA;
        USBD_PD_EpReceive(dev, 0x00, (uint8_t*)data, len);

        retval = USBD_E_OK;
    }
    return retval;
}

函數入參中的 data 用於存放從端點讀取的數據,函數內部會調用 USBD_PD_EpReceive 從端點接收數據,接收的長度為 dev->Setup.Length, 但是由於 dev->Setup.Length 是直接從 USB 總線中接收的,所以如果上層沒有校驗就會導致溢出。

示例

static USBD_ReturnType cdc_setupStage(USBD_CDC_IfHandleType *itf)
{
    USBD_ReturnType retval = USBD_E_INVALID;
    USBD_HandleType *dev = itf->Base.Device;

    switch (dev->Setup.RequestType.Type)
    {
        case USB_REQ_TYPE_CLASS:
        {
            switch (dev->Setup.Request)
            {
                case CDC_REQ_SET_LINE_CODING:
                    cdc_deinit(itf);

                    retval = USBD_CtrlReceiveData(dev, &itf->LineCoding);  // 沒有檢查 dev->Setup.Length

dfu_upload 溢出漏洞

調用路徑

dfu_setupStage -> dfu_upload

漏洞代碼

static USBD_ReturnType dfu_upload(USBD_DFU_IfHandleType *itf)
{
    USBD_ReturnType retval = USBD_E_INVALID;
    USBD_HandleType *dev = itf->Base.Device;

    if ((dev->Setup.Length > 0) && (DFU_APP(itf)->Read != NULL))
    {
        uint8_t *data = dev->CtrlData;
        itf->BlockNum = dev->Setup.Value;

        else if (itf->BlockNum > 1)
        {
            itf->DevStatus.State = DFU_STATE_UPLOAD_IDLE;

            DFU_APP(itf)->Read(
                    DFUSE_GETADDRESS(itf, &dfu_desc),
                    data,
                    dev->Setup.Length);

問題在於沒有檢查 dev->Setup.Length, 導致 DFU_APP(itf)->Read 時會溢出。

rt-thread USB 協議棧

issue 鏈接

https://github.com/RT-Thread/rt-thread/issues/4776

定位數據入口

device側協議棧

在 RT-Thread 中,USB協議棧作為一個 task 運行, 其入口函數為 rt_usbd_thread_entry

    /* init usb device thread */
    rt_thread_init(&usb_thread,
                   "usbd",
                   rt_usbd_thread_entry, RT_NULL,
                   usb_thread_stack, RT_USBD_THREAD_STACK_SZ,
                   RT_USBD_THREAD_PRIO, 20);

函數的關鍵代碼如下

static void rt_usbd_thread_entry(void* parameter)
{
    while(1)
    {
        if(rt_mq_recv(&usb_mq, &msg, sizeof(struct udev_msg),
                    RT_WAITING_FOREVER) != RT_EOK )
            continue;

        switch (msg.type)
        {
 
        case USB_MSG_DATA_NOTIFY:
            _data_notify(device, &msg.content.ep_msg);
            break;
        case USB_MSG_SETUP_NOTIFY:
            _setup_request(device, &msg.content.setup);
            break;
        case USB_MSG_EP0_OUT:
            _ep0_out_notify(device, &msg.content.ep_msg);
            break;
        ..................
        ..................

        }
    }
}

主要就是事件驅動,然后根據事件的類型來進行相應的處理

  1. _setup_request: 處理 setup 數據
  2. _ep0_out_notify:處理 ep0 的 data 包
  3. _data_notify: 處理其他端點的 data 包

相關的代碼邏輯如下

其中比較重要的時圖中標黃的部分,這些涉及到回調函數的調用,以 _function_request 為例

static rt_err_t _function_request(udevice_t device, ureq_t setup)
{

    switch(setup->request_type & USB_REQ_TYPE_RECIPIENT_MASK)
    {
    case USB_REQ_TYPE_INTERFACE:
        intf = rt_usbd_find_interface(device, setup->wIndex & 0xFF, &func);
        .............
        .............
        intf->handler(func, setup);

參數中的 setup 是從 USB 總線上收到的數據包,然后根據 setup->wIndex 找到 intf , 最后調用 intf->handler 完成數據的解析。

這里是調用點,用戶可以 rt_usbd_interface_new 注冊一個 interface 和相應的回調函數。

rt_usbd_interface_new(device, _interface_as_handler);

下面再看一下 _data_notify 處理 USB 分包的邏輯, USB 的數據傳輸和以太網的數據傳輸比較類似,也有類似 MTU的概念,USB 一次傳輸的最大長度一般為 64 或者 128,當需要傳輸的數據大於最大傳輸長度時就需要進行分包傳輸,所以對於 USB 協議棧來說需要對分包的情況進行處理。

static rt_err_t _data_notify(udevice_t device, struct ep_msg* ep_msg)
{
    
    {
        size = ep_msg->size;
 
        ep->request.remain_size -= size;
        ep->request.buffer += size;

        if(ep->request.req_type == UIO_REQUEST_READ_BEST)
        {
            EP_HANDLER(ep, func, size);
        }
        else if(ep->request.remain_size == 0)
        {
            EP_HANDLER(ep, func, ep->request.size);
        }
        else
        {
            dcd_ep_read_prepare(device->dcd, EP_ADDRESS(ep), ep->request.buffer, ep->request.remain_size > EP_MAXPACKET(ep) ? EP_MAXPACKET(ep) : ep->request.remain_size);
        }
    }

    return RT_EOK;
}

每當usb設備的OUT端點接收完一個數據包,就會進入 _data_notify,這里面會根據收到數據包的長度對 ep->request.remain_size 進行修改,當 ep->request.remain_size 為 0 就表示數據包接收完畢。

remain_size 大於最大包長度時就會涉及到分包傳輸,這時如果 req_type == UIO_REQUEST_READ_BEST,就表示分包的處理由應用程序自己實現,協議棧每收到一個包就調用回調函數讓應用程序去處理。

否則的話就由協議棧處理分包,當數據包接收完畢之后在調用回調函數,應用程序處理分包的場景示例:

if(ecm_eth_dev->func->device->state == USB_STATE_CONFIGURED)
{
    ecm_eth_dev->rx_size = 0;
    ecm_eth_dev->rx_offset = 0;
    ecm_eth_dev->eps.ep_out->request.buffer = ecm_eth_dev->eps.ep_out->buffer;
    ecm_eth_dev->eps.ep_out->request.size = EP_MAXPACKET(ecm_eth_dev->eps.ep_out);
    ecm_eth_dev->eps.ep_out->request.req_type = UIO_REQUEST_READ_BEST;
    rt_usbd_io_request(ecm_eth_dev->func->device, ecm_eth_dev->eps.ep_out, &ecm_eth_dev->eps.ep_out->request);
}

經過對數據流的梳理,可以知道涉及到數據處理的函數如下:

  1. _setup_request: 處理 setup 請求
  2. 通過 rt_usbd_ep0_read 注冊的 ep0 收包結束函數 ep0->rx_indicate
  3. 通過 rt_usbd_interface_new 注冊的 interface 數據處理函數
  4. 通過 rt_usbd_endpoint_new 注冊的端點數據處理函數

代碼導圖

host 側協議棧

當有USB 設備插上時會進入 rt_usbh_attatch_instance ,主要是獲取一些 USB 設備的基本信息,比如設備類型、支持功能等。

host 側主要是通過封裝的 pipe 來完成和 USB 設備的通信

    /* alloc true address ep0 pipe*/
    rt_usb_hcd_alloc_pipe(device->hcd, &device->pipe_ep0_out, device, &ep0_out_desc);
    rt_usb_hcd_alloc_pipe(device->hcd, &device->pipe_ep0_in, device, &ep0_in_desc);

rt_usbh_get_des criptor 為例看一下 host 收包的方式

rt_err_t rt_usbh_get_descriptor(uinst_t device, rt_uint8_t type, void* buffer,
    int nbytes)
{
    struct urequest setup;
    int timeout = USB_TIMEOUT_BASIC;

    RT_ASSERT(device != RT_NULL);

    setup.request_type = USB_REQ_TYPE_DIR_IN | USB_REQ_TYPE_STANDARD |
        USB_REQ_TYPE_DEVICE;
    setup.bRequest = USB_REQ_GET_DESCRIPTOR;
    setup.wIndex = 0;
    setup.wLength = nbytes;
    setup.wValue = type << 8;

    if(rt_usb_hcd_setup_xfer(device->hcd, device->pipe_ep0_out, &setup, timeout) == 8)
    {
        if(rt_usb_hcd_pipe_xfer(device->hcd, device->pipe_ep0_in, buffer, nbytes, timeout) == nbytes)
        {
            if(rt_usb_hcd_pipe_xfer(device->hcd, device->pipe_ep0_out, RT_NULL, 0, timeout) == 0)
            {
                return RT_EOK;
            }
        }
    }
    return RT_ERROR;
}

首先發送 setup 請求通知 usb device 發送 des criptorhost , 然后用 rt_usb_hcd_pipe_xfer 接收數據,該函數內部會處理分包的情況。

下面介紹幾個典型的漏洞

ecm模塊 _ep_out_handler 溢出漏洞

rt_usbd_function_ecm_create 中會注冊 out 端點的回調函數


    /* create a bulk in and a bulk out endpoint */
    data_desc = (ucdc_data_desc_t)data_setting->desc;
    eps->ep_out = rt_usbd_endpoint_new(&data_desc->ep_out_desc, _ep_out_handler);
    eps->ep_in = rt_usbd_endpoint_new(&data_desc->ep_in_desc, _ep_in_handler);

然后在 _function_enable 里面會調用 rt_usbd_io_request 准備后續收包,req_typeUIO_REQUEST_READ_BEST ,表示USB的分包由 ecm 模塊自己處理。

static rt_err_t _function_enable(ufunction_t func)
{
    cdc_eps_t eps;
    rt_ecm_eth_t ecm_device = (rt_ecm_eth_t)func->user_data;

    eps = (cdc_eps_t)&ecm_device->eps;
    eps->ep_out->buffer = ecm_device->rx_pool;

    eps->ep_out->request.buffer = (void *)eps->ep_out->buffer;
    eps->ep_out->request.size = EP_MAXPACKET(eps->ep_out);
    eps->ep_out->request.req_type = UIO_REQUEST_READ_BEST;
    rt_usbd_io_request(func->device, eps->ep_out, &eps->ep_out->request);
    return RT_EOK;
}

下面看看 _ep_out_handler 的代碼

static rt_err_t _ep_out_handler(ufunction_t func, rt_size_t size)
{
    rt_ecm_eth_t ecm_device = (rt_ecm_eth_t)func->user_data;
    rt_memcpy((void *)(ecm_device->rx_buffer + ecm_device->rx_offset),ecm_device->rx_pool,size);
    ecm_device->rx_offset += size;
    if(size < EP_MAXPACKET(ecm_device->eps.ep_out))
    {
        ecm_device->rx_size = ecm_device->rx_offset;
        ecm_device->rx_offset = 0;
        eth_device_ready(&ecm_device->parent);

    }else
    {
        ecm_device->eps.ep_out->request.buffer = ecm_device->eps.ep_out->buffer;
        ecm_device->eps.ep_out->request.size = EP_MAXPACKET(ecm_device->eps.ep_out);
        ecm_device->eps.ep_out->request.req_type = UIO_REQUEST_READ_BEST;
        rt_usbd_io_request(ecm_device->func->device, ecm_device->eps.ep_out, &ecm_device->eps.ep_out->request);
    }

    return RT_EOK;
}

首先將收到的數據拷貝到 ecm_device->rx_buffer ,然后把收到數據包的 sizeEP_MAXPACKET進行比較,不相等就表示收包結束,否則再次請求收包。

漏洞在於沒有校驗 ecm_device->rx_offset 是否會越界,如果惡意的 USB 設備不斷發送 size == EP_MAXPACKET 的數據包就會導致溢出。

rt_usbh_attatch_instance 堆溢出漏洞

相關代碼如下

    /* get device descriptor head */
    ret = rt_usbh_get_descriptor(device, USB_DESC_TYPE_DEVICE, (void*)dev_desc, 8);

    /* get full device descriptor again */
    ret = rt_usbh_get_descriptor(device, USB_DESC_TYPE_DEVICE, (void*)dev_desc, dev_desc->bLength);
  1. 首先通過 rt_usbh_get_des criptor 獲取 usb 設備的 des criptor, 保存到 dev_desc.
  2. 然后再次使用 rt_usbh_get_des criptor 往往 dev_desc 里面寫數據,長度為 dev_desc->bLength

問題在於如果 dev_desc->bLength 過大,就會溢出 dev_desc.

總結

漏洞大部分出現在基於USB協議棧的上層應用,比如 dfu、RNDIS 等,而且盡管 setup 請求只有簡單的8個字節,但是對 setup 請求中的 size 字段解析也出現了很多的問題。

大部分漏洞都是由於解析變長數據結構時導致的,這也是解析二進制數據格式時經常出現漏洞的地方。

文中涉及的漏洞,基本用 libfuzzer 適配一下都能發現,主要是識別出數據的入口。

由於漏洞成因都不復雜,感覺用黑盒fuzz工具也能快速發現。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM