文章首發於
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數據包的邏輯肯定是:
- 控制USB外設,從總線收包
- 調用處理函數對數據包進行解析、處理
為了快速定位數據處理函數,我們可以從協議規范入手,對於 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;
其實主要就是根據收到的事件類型來調用特定函數對端點的數據進行解析:
- process_control_request: 處理 setup 數據包
- usbh_control_xfer_cb: 處理 0 號端點的數據
- 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->DataLength
和 r->DataOffset
的類型都是uint32_t
,且都是從 USB 總線上接收。
當r->DataLength=0xFFFFFFFF
且 r->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);
}
}
主要就是根據觸發中斷的端點號、數據傳輸類型調用相應的函數進行解析:
- tusb_device_ep_xfer_done:處理 setup 請求
- 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);
}
}
主要就是根據端點的信息決定下一步的解析方式:
- tusb_setup_handler:處理 setup 請求
- ep0_rx_done: 處理從 ep0 收到的數據
- 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_done
在 tusb_on_rx_done
中被調用。
至此我們弄清楚了程序的收包邏輯,所以程序中解析數據的入口有以下幾種:
tusb_setup_handler
解析標准的setup
請求tusb_device_backend_t
結構體中注冊的device_request
和device_recv_done
.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_rx
往 msc->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 包的處理。
- USB_vSetupCallback: 處理 setup 包
- 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;
..................
..................
}
}
}
主要就是事件驅動,然后根據事件的類型來進行相應的處理
- _setup_request: 處理 setup 數據
- _ep0_out_notify:處理 ep0 的 data 包
- _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);
}
經過對數據流的梳理,可以知道涉及到數據處理的函數如下:
_setup_request
: 處理 setup 請求- 通過
rt_usbd_ep0_read
注冊的 ep0 收包結束函數ep0->rx_indicate
- 通過
rt_usbd_interface_new
注冊的 interface 數據處理函數 - 通過
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 criptor
給 host
, 然后用 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_type
為 UIO_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
,然后把收到數據包的 size
和 EP_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);
- 首先通過
rt_usbh_get_des criptor
獲取 usb 設備的des criptor
, 保存到dev_desc
. - 然后再次使用
rt_usbh_get_des criptor
往往dev_desc
里面寫數據,長度為dev_desc->bLength
。
問題在於如果 dev_desc->bLength
過大,就會溢出 dev_desc
.
總結
漏洞大部分出現在基於USB協議棧的上層應用,比如 dfu、RNDIS 等,而且盡管 setup 請求只有簡單的8個字節,但是對 setup 請求中的 size 字段解析也出現了很多的問題。
大部分漏洞都是由於解析變長數據結構時導致的,這也是解析二進制數據格式時經常出現漏洞的地方。
文中涉及的漏洞,基本用 libfuzzer 適配一下都能發現,主要是識別出數據的入口。
由於漏洞成因都不復雜,感覺用黑盒fuzz工具也能快速發現。