开源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数据包的逻辑肯定是:
- 控制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工具也能快速发现。
开源USB协议栈漏洞挖掘的更多相关文章
- QEMU漏洞挖掘
		
转载:https://www.tuicool.com/articles/MzqYbia qemu是一个开源的模拟处理器硬件设备的全虚拟化仿真器和虚拟器. KVM(kernel virtual mach ...
 - 关于PHP代码审计和漏洞挖掘的一点思考
		
这里对PHP的代码审计和漏洞挖掘的思路做一下总结,都是个人观点,有不对的地方请多多指出. PHP的漏洞有很大一部分是来自于程序员本身的经验不足,当然和服务器的配置有关,但那属于系统安全范畴了,我不太懂 ...
 - [网站安全] [实战分享]WEB漏洞挖掘的一些经验分享
		
WEB漏洞有很多种,比如SQL注入,比如XSS,比如文件包含,比如越权访问查看,比如目录遍历等等等等,漏洞带来的危害有很多,信息泄露,文件上传到GETSHELL,一直到内网渗透,这里我想分享的最主要的 ...
 - ref:【干货分享】PHP漏洞挖掘——进阶篇
		
ref:http://blog.nsfocus.net/php-vulnerability-mining/ [干货分享]PHP漏洞挖掘——进阶篇 王陶然 从常见的PHP风险点告诉你如何进行PH ...
 - SRC漏洞挖掘
		
SRC目标搜集 文章类的平台 https://www.anquanke.com/src 百度搜索 首先得知道SRC厂商的关键字,利用脚本搜集一波. 比如[应急响应中心]就可以作为一个关键字.通过搜索引 ...
 - xss漏洞挖掘小结
		
xss漏洞挖掘小结 最近,在挖掘xss的漏洞,感觉xss真的不是想象的那样简单,难怪会成为一类漏洞,我们从防的角度来讲讲xss漏洞的挖掘方法: 1.过滤 一般服务器端都是采用这种方式来防御xss攻击, ...
 - Metasploit是一款开源的安全漏洞检测工具,
		
Metasploit是一款开源的安全漏洞检测工具,可以帮助安全和IT专业人士识别安全性问题,验证漏洞的缓解措施,并管理专家驱动的安全性进行评估,适合于需要核实漏洞的安全专家,同时也适合于强大进攻能力的 ...
 - 小白日记38:kali渗透测试之Web渗透-手动漏洞挖掘(四)-文件上传漏洞
		
手动漏洞挖掘 文件上传漏洞[经典漏洞,本身为一个功能,根源:对上传文件的过滤机制不严谨] <?php echo shell_exec($_GET['cmd']);?> 直接上传webshe ...
 - 小白日记37:kali渗透测试之Web渗透-手动漏洞挖掘(三)-目录遍历、文件包含
		
手动漏洞挖掘 漏洞类型 #Directory traversal 目录遍历[本台机器操作系统上文件进行读取] 使用者可以通过浏览器/URL地址或者参数变量内容,可以读取web根目录[默认为:/var/ ...
 - 小白日记36:kali渗透测试之Web渗透-手动漏洞挖掘(二)-突破身份认证,操作系统任意命令执行漏洞
		
手动漏洞挖掘 ###################################################################################### 手动漏洞挖掘 ...
 
随机推荐
- Spring —— bean生命周期
			
bean生命周期 生命周期:从创建到消亡的完整过程 bean生命周期:bean从创建到销毁的整体过程 bean生命周期控制:在bean创建后到销毁前做一些事情 方式一:配置控制生命周期 <b ...
 - FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
			
EasyPusher是一款国产的RTSP直播录制推流客户端工具,它支持Windows.Linux.Android.iOS等操作系统.EasyPusher采用RTSP推流协议,其中安卓版EasyPus ...
 - OxyPlot公共属性一览
			
一.PlotModel 1.构造函数中设置的属性 public PlotModel() { this.Axes = new ElementCollection(this); //坐标轴集合; this ...
 - 《MongoDB游记之轻松入门到进阶》代码下载
			
<MongoDB游记之轻松入门到进阶>代码下载,看看有没有用 http://pan.baidu.com/s/1boKG28R https://item.jd.com/12236244.ht ...
 - cf2009 Codeforces Round 971 (Div. 4)
			
A. Minimize! 签到题.计算\((c-a)+(b-c)\)的最小值,其实值固定的,等于\(b-a\). int a, b; void solve() { cin >> a > ...
 - 2023年6月中国数据库排行榜:OceanBase 连续七月踞榜首,华为阿里谋定快动占先机
			
群雄逐鹿,酣战墨坛. 2023年6月的 墨天轮中国数据库流行度排行 火热出炉,本月共有273个数据库参与排名.本月排行榜前十变动不大,可以用一句话概括为:OTO 组合连续两月开局,传统厂商GBase南 ...
 - k8s的pod的理解
			
pod共享相同的IP地址和端口空间. 这意味着在同一 pod中的容器运行的 多个进程需要注意不能绑定到相同的端口号, 否则会导致端口冲突, 但这只涉及同一pod中的容器. 由于每个pod都有独立的端口 ...
 - 云原生的 WebAssembly 能取代 Docker 吗?
			
WebAssembly 是一个可移植.体积小.加载快并且兼容 Web 的全新格式.由于 WebAssembly 具有很高的安全性,可移植性,效率和轻量级功能,因此它是应用程序安全沙箱方案的理想选择.现 ...
 - ToDesk云电脑开启公测!支持AIGC、高性能渲染等场景,价格低至0.98元
			
在云计算和人工智能技术飞速发展的今天,云电脑作为一种新型的计算模式,正逐渐改变着传统电脑的使用方式.近日,ToDesk云电脑宣布开启公测,以其支持AIGC(人工智能.大数据.云计算等技术的融合应用). ...
 - RL 基础 | 如何使用 OpenAI Gym 接口,搭建自定义 RL 环境(详细版)
			
参考: 官方链接:Gym documentation | Make your own custom environment 腾讯云 | OpenAI Gym 中级教程--环境定制与创建 知乎 | 如何 ...