USB 協議核心概念與實踐

作者: evilpan
原文鏈接: https://mp.weixin.qq.com/s/ipQD4PWP6EPydmxD6vWbMA

USB,全稱是 Universal Serial Bus,即通用串行總線,既是一個針對電纜和連接器的工業標準,也指代其中使用的連接協議。本文不會過多介紹標準中的細節,而是從軟件工程師的角度出發,介紹一些重要的基本概念,以及實際的主機和從機應用。最後作爲實際案例,從 USB 協議實現的角度分析了checkm8漏洞的成因。

USB 101

首先要明確的一點,USB 協議是以主機爲中心的 (Host Centric),也就是說只有主機端向設備端請求數據後,設備端才能向主機發送數據。從數據的角度來看,開發者最直接接觸的就是端點 (Endpoint),端點可以看做是數據收發的管道。

當主機給設備發送數據時,通常流程是:

  • 調用用戶層 API,如 libusb_bulk_transfer
  • 對內核的 USB 驅動執行對應系統調用,添加發送隊列,如 ioctl(IOCTL_USBFS_SUBMITURB)
  • 內核驅動中通過 HCI 接口向 USB 設備發送請求+數據
  • 數據發送到設備端的 Controller -> HCI -> Host

設備給主機發送請求也是類似,只不過由於是主機中心,發送的數據會保存在緩存中,等待主機發送 IN TOKEN 之後才真正發送到主機。在介紹數據發送流程之前,我們先來看下描述符。

描述符

所有的 USB 設備端設備,都使用一系列層級的描述符 (Descriptors) 來向主機描述自身信息。這些描述符包括:

  • Device Descriptors: 設備描述
  • Configuration Descriptors: 配置描述
  • Interface Descriptors: 接口描述
  • Endpoint Descriptors: 端點描述
  • String Descriptors: 字符串描述

它們之間的層級結構關係如下: 每種描述符都有對應的數據結構,定義在標準中的第九章,俗稱 ch9。下面以 Linux 內核的實現爲例來簡要介紹各個描述符,主要參考頭文件 include/uapi/linux/usb/ch9.h

設備描述

每個 USB 設備只能有一個設備描述(Device Descriptor),該描述符中包括了設備的 USB 版本、廠商、產品 ID 以及包含的配置描述符個數等信息,如下所示:

/* USB_DT_DEVICE: Device descriptor */
struct usb_device_descriptor {
    __u8  bLength; // 18 字節
    __u8  bDescriptorType; // 0x01

    __le16 bcdUSB; // 設備所依從的 USB 版本號
    __u8  bDeviceClass; // 設備類型
    __u8  bDeviceSubClass; // 設備子類型
    __u8  bDeviceProtocol; // 設備協議
    __u8  bMaxPacketSize0; // ep0 的最大包長度,有效值爲 8,6,32,64
    __le16 idVendor; // 廠商號
    __le16 idProduct; // 產品號
    __le16 bcdDevice; // 設備版本號
    __u8  iManufacturer; // 產商字名稱
    __u8  iProduct; // 產品名稱
    __u8  iSerialNumber; // 序列號
    __u8  bNumConfigurations; // 配置描述符的個數
} __attribute__ ((packed));

#define USB_DT_DEVICE_SIZE      18

每個字段的含義都寫在註釋中了,其中有幾點值得一提。

  • 設備類型、子類型和協議碼,是由 USB 組織定義的;
  • 產商號也是由 USB 組織定義的,但是產品號可以由廠商自行定義;
  • 廠商、產品和序列號分別只有 1 字節,表示在字符串描述符中的索引;

BCD: binary- coded decimal

配置描述

每種不同的配置描述(Configuration Descriptor)中分別指定了 USB 設備所支持的配置,如功率等信息;一個 USB 設備可以包含多個配置,但同一時間只能有一個配置是激活狀態。實際上大部分的 USB 設備都只包含一個配置描述符。

/* USB_DT_CONFIG: Configuration descriptor information.
 *
 * USB_DT_OTHER_SPEED_CONFIG is the same descriptor, except that the
 * descriptor type is different.  Highspeed-capable devices can look
 * different depending on what speed they're currently running.  Only
 * devices with a USB_DT_DEVICE_QUALIFIER have any OTHER_SPEED_CONFIG
 * descriptors.
 */
struct usb_config_descriptor {
    __u8  bLength; // 9
    __u8  bDescriptorType; // 0x02

    __le16 wTotalLength; // 返回數據的總長度
    __u8  bNumInterfaces; // 接口描述符的個數
    __u8  bConfigurationValue; // 當前配置描述符的值 (用來選擇該配置)
    __u8  iConfiguration; // 該配置的字符串信息 (在字符串描述符中的索引)
    __u8  bmAttributes; // 屬性信息
    __u8  bMaxPower; // 最大功耗,以 2mA 爲單位
} __attribute__ ((packed));

#define USB_DT_CONFIG_SIZE      9

當主設備讀取配置描述的時候,從設備會返回該配置下所有的其他描述符,如接口、端點和字符串描述符,因此需要 wTotalLength 來表示返回數據的總長度。

bmAttributes 指定了該配置的電源參數信息,D6 表示是否爲自電源驅動;D5 表示是否支持遠程喚醒;D7 在 USB1.0 中曾用於表示是否爲總線供電的設備,但是在 USB2.0 中被 bMaxPower 字段取代了,該字段表示設備從總線上消耗的電壓最大值,以 2mA 爲單位,因此最大電流大約是 0xff * 2mA = 510mA

接口描述

一個配置下有多個接口,可以看成是一組相似功能的端點的集合,每個接口描述符的結構如下:

/* USB_DT_INTERFACE: Interface descriptor */
struct usb_interface_descriptor {
    __u8  bLength;
    __u8  bDescriptorType; // 0x04

    __u8  bInterfaceNumber; // 接口序號
    __u8  bAlternateSetting;
    __u8  bNumEndpoints;
    __u8  bInterfaceClass;
    __u8  bInterfaceSubClass;
    __u8  bInterfaceProtocol;
    __u8  iInterface; // 接口的字符串描述,同上
} __attribute__ ((packed));

#define USB_DT_INTERFACE_SIZE       9

其中接口類型、子類型和協議與前面遇到的類似,都是由 USB 組織定義的。在 Linux 內核中,每個接口封裝成一個高層級的功能,即邏輯鏈接(Logical Connection),例如對 USB 攝像頭而言,接口可以分爲視頻流、音頻流和鍵盤(攝像頭上的控制按鍵)等。

還有值得一提的是 bAlternateSetting,每個 USB 接口都可以有不同的參數設置,例如對於音頻接口可以有不同的帶寬設置。實際上 Alternate Settings 就是用來控制週期性的端點參數的,比如 isochronous endpoint。

端點描述

端點描述符用來描述除了零端點(ep0)之外的其他端點,零端點總是被假定爲控制端點,並且在開始請求任意描述符之前就已經被配置好了。端點(Endpoint),可以認爲是一個單向數據信道的抽象,因此端點描述符中包括傳輸的速率和帶寬等信息,如下所示:

/* USB_DT_ENDPOINT: Endpoint descriptor */
struct usb_endpoint_descriptor {
    __u8  bLength;
    __u8  bDescriptorType; // 0x05

    __u8  bEndpointAddress; // 端點地址
    __u8  bmAttributes; // 端點屬性
    __le16 wMaxPacketSize; // 該端點收發的最大包大小
    __u8  bInterval; // 輪詢間隔,只對 Isochronous 和 interrupt 傳輸類型的端點有效 (見下)

    /* NOTE:  these two are _only_ in audio endpoints. */
    /* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
    __u8  bRefresh;
    __u8  bSynchAddress;
} __attribute__ ((packed));

#define USB_DT_ENDPOINT_SIZE        7
#define USB_DT_ENDPOINT_AUDIO_SIZE  9   /* Audio extension */

bEndpointAddress 8位數據分別代表:

  • Bit 0-3: 端點號
  • Bit 4-6: 保留,值爲0
  • Bit 7: 數據方向,0 爲 OUT,1 爲 IN

bmAttributes 8位數據分別代表:

Bit 0-1: 傳輸類型
- 00: Control
- 01: Isochronous
- 10: Bulk
- 11: Interrupt
Bit 2-7: 對非 Isochronous 端點來說是保留位,對 Isochronous 端點而言表示 Synchronisation Type 和 Usage Type,不贅述;




每種端點類型對應一種傳輸類型,詳見後文。

字符串描述

字符串描述符(String Descriptor)中包含了可選的可讀字符串信息,如果沒提供,則前文所述的字符串索引應該都設置爲0,字符串表結構如下:

/* USB_DT_STRING: String descriptor */
struct usb_string_descriptor {
    __u8  bLength;
    __u8  bDescriptorType; // 0x03

    __le16 wData[1];        /* UTF-16LE encoded */
} __attribute__ ((packed));

/* note that "string" zero is special, it holds language codes that
 * the device supports, not Unicode characters.
 */

字符串表中的字符都以 Unicode 格式編碼,並且可以支持多種語言。0號字符串表較爲特殊,其中 wData 包含一組所支持的語言代碼,每個語言碼爲 2 字節,例如 0x0409 表示英文。

傳輸

不像 RS-232 和其他類似的串口協議,USB 實際上由多層協議構造而成,不過大部分底層的協議都在 Controller 端上的硬件或者固件進行處理了,最終開發者所要關心的只有上層協議。

USB Packet

在 HCI 之下,實際傳輸的數據包稱爲 Packet,每次上層 USB 傳輸都會涉及到 2-3 次底層的 Packet 傳輸,分別是:

Token Packet: 總是由主機發起,指示一次新的傳輸或者事件
- In: 告訴 USB 設備,主機我想要讀點信息
- Out: 告訴 USB 設備,主機我想要寫點信息
- Setup: 用於開始 Control Transfer
Data Packet: 可選,表示傳輸的數據,可以是主機發送到設備,也可以是設備發送到主機
- Data0
- Data1
Status Packet: 狀態包,用於響應傳輸,以及提供糾錯功能
- Handshake Packets: ACK/NAK/STALL
- Start of Frame Packets








Transfer

基於這些底層包,USB 協議定義了四種不同的傳輸類型,分別對應上節中的四種端點類型,分別是:

Control Transfers: 主要用來發送狀態和命令,比如用來請求設備、配置等描述以及選擇和設置指定的描述符。只有控制端點是雙向的。

Interrupt Transfers: 由於 USB 協議是主機主導的,設備端的中斷信息需要被及時響應,就要用到中斷傳輸,其提供了有保證的延遲以及錯誤檢測和重傳功能。中斷傳輸通常是非週期性的,並且傳輸過程保留部分帶寬,常用於時間敏感的數據,比如鍵盤、鼠標等 HID 設備。

Isochronous Transfers: 等時傳輸,如其名字所言,該類傳輸是連續和週期性的,通常包含時間敏感的信息,比如音頻或視頻流。因此這類傳輸不保證到達,即沒有 ACK 響應。

Bulk Transfers: 用於傳輸大塊的突發數據(小塊也可以),不保留帶寬。提供了錯誤校驗(CRC16)和重傳機制來保證傳輸數據的完整性。塊傳輸只支持高速/全速模式。

這裏以控制傳輸(Control Transfers)爲例,來看看底層 Packet 如何組成一次完整的傳輸。控制傳輸實際上又可能最多包含三個階段,每個階段在應用層可以看成是一次 “USB 傳輸” (在Wireshark中佔一行),分別是:

Setup Stage: 主機發送到設備的請求,包含三次底層數據傳輸

1.Setup Token Packet: 指定地址和端點號(應爲0)
2.Data0 Packet: 請求數據,假設是 8 字節的 Device Descriptor Request
3.STALL 或者 NAK 來響應 Setup Packet

Data Stage: 可選階段,包含一個或者多個 IN/OUT 傳輸,以 IN 爲例,也包含三次傳輸

1.IN Token Packet: 表示主機端要從設備端讀數據
2.Datax Packet: 如果上面 Setup Stage 是 Device Descriptor Request, 這裏返回 Device Descriptor Response (的前8字節,然後再根據實際長度再 IN 一次)。
3.ACK/STALL/NAK Status Packet

Status Stage: 報告本次請求的狀態,底層也是三次傳輸,但是和方向有關:
如果在 Data Stage 發送的是 IN Token,則該階段包括:
1.OUT Token
2.Data0 ZLP(zero length packet): 主機發送長度爲0的數據
3.ACK/NACK/STALL: 設備返回給主機



如果在 Data Stage 發送的是 OUT Token,則該階段包括:
1.IN Token
2.Data0 ZLP: 設備發送給主機,表示正常完成,否則發送 NACK/STALL
3.ACK: 如果是 ZLP,主機響應設備,雙向確認


每個階段的數據都有自己的格式,例如 Setup Stage 的 Request,即 Data0 部分發送的 8 字節數據結構如下:

struct usb_ctrlrequest {
    __u8 bRequestType; // 對應 USB 協議中的 bmRequestType,包含請求的方向、類型和指定接受者
    __u8 bRequest; // 決定所要執行的請求
    __le16 wValue; // 請求參數
    __le16 wIndex; // 同上
    __le16 wLength; // 如果請求包含 Data Stage,則指定數據的長度
} __attribute__ ((packed));

下面是一些標準請求的示例:

bmRequestType bRequest wValue wIndex wLength Data
1000 0000b GET_STATUS (0x00) Zero Zero Two Device Status
0000 0000b CLEAR_FEATURE (0x01) Feature Selector Zero Zero None
0000 0000b SET_FEATURE (0x03) Feature Selector Zero Zero None
0000 0000b SET_ADDRESS (0x05) Device Address Zero Zero None
1000 0000b GET_DESCRIPTOR (0x06) Descriptor Type & Index Zero or Language ID Descriptor Length Descriptor
0000 0000b SET_DESCRIPTOR (0x07) Descriptor Type & Index Zero or Language ID Descriptor Length Descriptor
1000 0000b GET_CONFIGURATION (0x08) Zero Zero 1 Configuration Value
0000 0000b SET_CONFIGURATION (0x09) Configuration Value Zero Zero None

ref: https://www.beyondlogic.org/usbnutshell/usb6.shtml

雖然 HCI 之下傳輸的數據包大部分情況下對應用開發者透明,但是瞭解底層協議發生了什麼也有助於加深我們對 USB 的理解,後文中介紹 checkm8 漏洞時候就用到了相關知識。

主機端

在主機端能做的事情相對有限,主要是分析和使用對應的 USB 設備。

抓包分析

使用 wireshark 可以分析 USB 流量,根據上面介紹的描述符字段以及 USB 傳輸過程進行對照,可以加深我們對 USB 協議的理解。如下是對某個安卓設備的 Device Descriptor Response 響應:

也就是所謂安卓變磚恢復時經常用到的高通 9008 模式。說個題外話,最近對於高通芯片 BootROM 的研究發現了一些有趣的東西,後面可能會另外分享,Stay Tune!

應用開發

對於應用開發者而言,通常是使用封裝好的庫,早期只有 libusb,後來更新了 libusb1.0,早期的版本變成 libusb0.1,然後又有了 OpenUSB 和其他的 USB 庫。但不管用哪個庫,調用的流程都是大同小異的。以 Python 的封裝 pyusb 爲例,官方給的示例如下:

import usb.core
import usb.util

# find our device
dev = usb.core.find(idVendor=0xfffe, idProduct=0x0001)

# was it found?
if dev is None:
    raise ValueError('Device not found')

# set the active configuration. With no arguments, the first
# configuration will be the active one
dev.set_configuration()

# get an endpoint instance
cfg = dev.get_active_configuration()
intf = cfg[(0,0)]

ep = usb.util.find_descriptor(
    intf,
    # match the first OUT endpoint
    custom_match = \
    lambda e: \
        usb.util.endpoint_direction(e.bEndpointAddress) == \
        usb.util.ENDPOINT_OUT)

assert ep is not None

# write the data
ep.write('test')

總的來說分爲幾步,

  1. 根據設備描述符查找到指定的設備
  2. 獲取該設備的配置描述符,選擇並激活其中一個
  3. 在指定的配置中查找接口和端點描述符
  4. 使用端點描述符進行數據傳輸

如果不清楚 USB 的工作原理,會覺得上面代碼的調用流程很奇怪,往 USB 上讀寫數據需要那麼複雜嗎?但正是因爲 USB 協議的高度拓展性,才得以支持這麼多種類的外設,從而流行至今。

設備端

對於想要開發設備端 USB 功能的開發者而言,使用最廣泛的要數樹莓派 Zero了,畢竟這是樹莓派系列中唯一支持 USB OTG 的型號。網上已經有很多資料教我們如何將樹莓派 Zero 配置成 USB 鍵盤、打印機、網卡等 USB 設備的教程。當然使用其他硬件也是可以的,配置自定義的 USB 設備端可以讓我們做很多有趣的事情,比如網卡中間人或者 Bad USB 這種近源滲透方式。後文中我們會使用 Zero 進行簡單測試。

一些相關的配置資料可以參考:

內核驅動

在介紹應用之間,我們先看看內核的實現。還是以 Linux 內核爲例,具體來說,我們想了解如何通過添加內核模塊的方式實現一個新的自定義 USB 設備。俗話說得好,添加 Linux 驅動的最好方式是參看現有的驅動,畢竟當前內核中大部分都是驅動代碼。

因爲 Linux 內核既能運行在主機端,也能運行在設備端,因此設備端的 USB 驅動有個不同的名字: gadget driver。對於不同設備,也提供不同的內核接口,即 Host-Side API 和 Gadget API。既然我們是想實現自己的設備,就需要從 gadget 驅動入手。

g_zero.ko 就是這麼一個驅動,代碼在 drivers/usb/gadget/legacy/zero.c。該驅動實現了一個簡單的 USB 設備,包含 2 個配置描述,各包含 1 個功能,分別是 sink 和 loopback,前者接收數據並返回 0,後者接收數據並原樣返回:

  • drivers/usb/gadget/function/f_sourcesink.c
  • drivers/usb/gadget/function/f_loopback.c

代碼量不多,感興趣的自行 RTFSC。另外值得一提的是,對於運行於 USB device 端的系統而言,內核中至少有三個層級處理 USB 協議,可能用戶層還有更多。gadget API 屬於三層的中間層。至底向上,三層分別是:

  1. USB Controller Driver: 這是軟件的最底層,通過寄存器、FIFO、DMA、IRQ 等其他手段直接和硬件打交道,通常稱爲 UDC (USB Device Controller) Driver。
  2. Gadget Driver: 作爲承上啓下的部分,通過調用抽象的 UDC 驅動接口,底層實現了硬件無關的 USB function。主要用於實現前面提到的 USB 功能,包括處理 setup packet (ep0)、返回各類描述符、處理各類修改配置情況、處理各類 USB 事件以及 IN/OUT 的傳輸等等。
  3. Upper Level: 通過 Gadget Driver 抽象的接口,實現基於 USB 協議的上層應用,比如 USB 網卡、聲卡、文件存儲、HID 設備等。

關於 Linux USB 子系統的詳細設計結構,可以參考源碼中的文檔: Linux USB API ,以及其他一些資料,如下所示:

GadgetFS/ConfigFS

參考現有的 Linux 驅動,依葫蘆畫瓢可以很容易實現一個自定義的 USB Gadget。但是這樣存在一些問題,如果我想實現一個八聲道的麥克風,還要重新寫一遍驅動、編譯、安裝,明明內核中麥克風的功能已經有了,複製粘貼就顯得很不優雅。

那麼,有沒有什麼辦法可以方便組合和複用現有的 gadget function 呢?在 Linux 3.11 中,引入了 USB Gadget ConfigFS,提供了用戶態的 API 來方便創建新的 USB 設備,並可以組合複用現有內核中的驅動。

前文提到的基於樹莓派 Zero 實現的各類 USB 設備,大部分都是基於 Gadget ConfigFS 接口實現的。基於 configfs 創建 USB gadget 的步驟一般如下:

CONFIGFS_HOME=/sys/kernel/config/usb_gadget

# 1. 新建一個 gadget,並寫入實際的設備描述
mkdir $CONFIGFS_HOME/mydev # 創建設備目錄後,該目錄下自動創建並初始化了一個設備模板
cd $CONFIGFS_HOME/mydev
echo 0x0100 > bcdDevice # Version 1.0.0
echo 0x0200 > bcdUSB # USB 2.0
echo 0x00 > bDeviceClass
echo 0x00 > bDeviceProtocol
echo 0x40 > bMaxPacketSize0
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x1d6b > idVendor # Linux Foundation

# 2. 新建一個配置,並寫入實際的配置描述
mkdir configs/c.1 # 創建一個配置實例: <config name>.<config number>
cd configs/c.1
echo 0x01 > MaxPower
echo 0x80 > bmAttributes

# 3. 新建一個接口(function),或者將已有接口鏈接到當前配置下
cd $CONFIGFS_HOME/mydev
mkdir functions/hid.usb0 # 創建一個 function 實例: <function type>.<instance name>
echo 1 > functions/hid.usb0/protocol
echo 8 > functions/hid.usb0/report_length # 8-byte reports
echo 1 > functions/hid.usb0/subclass
ln -s functions/hid.usb0 configs/c.1

# 4. 將當前 USB 設備綁定到 UDC 驅動中
echo ls /sys/class/udc > $CONFIGFS_HOME/mydev/UDC

這樣就實現了一個最簡單的 USB gadget,當然要完整實現的話還可以添加字符串描述,以及增加各個端點的功能。使用 configfs 實現一個 USB 鍵盤的示例可以參考網上其他文章,比如 [Using RPi Zero as a Keyboard][kb],或者 Github 上的開源項目,比如 P4wnP1

有些人覺得 ConfigFS 配置起來很繁瑣,所以開發了一些函數庫(如 [libusbgx][libusbgx]) 來通過調用創建 gadget;有人覺得通過函數操作也還是繁瑣,就創建了一些工具(如 gt) 來通過處理一個類似於 libconfig 的配置文件直接創建 gadget,不過筆者用得不多。

FunctionFS

FunctionFS 最初是對 GadgetFS 的重寫,用於支持實現用戶態的 gadget function,並組合到現有設備中。這裏說的 FunctionFS 實際上是新版基於 ConfigFS 的 GadgetFS 拓展。在上一節中說到創建設備 gadget 的第四步就是給對應的 configuration 添加 function,格式爲 function—type.instance-name,type 對應一個已有的內核驅動,比如上節中是 hid

如果要使用當前內核中沒有的 function 實現自定義的功能,那麼內核還提供了一個驅動可以方便在用戶態創建接口,該驅動就是 ffs 即 FunctionFS。使用 ffs 的方式也很簡單,將上面第三步替換爲:

cd $CONFIGFS_HOME/mydev
mkdir functions/ffs.usb0
ln -s functions/ffs.usb0 configs/c.1

創建一個類型爲 ffs,名稱爲 usb0 的function,然後掛載到任意目錄:

cd /mnt
mount usb0 ffs -t functionfs

掛載完後,/mnt/ffs/ 目錄下就已經有了一個 ep0 文件,如名字所言正是 USB 設備的零端點,用於收發 Controller Transfer 數據以及各類事件。在該目錄中可以創建其他的端點,並使用類似文件讀寫的操作去實現端點的讀寫,內核源碼中提供了一個用戶態應用示例,代碼在 tools/usb/ffs-test.c。如果嫌 C 代碼寫起來複雜,還可以使用 Python 編寫 ffs 實現,比如 python-functionfs

案例分析: checkm8 漏洞

checkm8 漏洞就不用過多介紹了,曾經的神洞,影響了一系列蘋果設備,存在於 BootROM 中,不可通過軟件更新來修復,一度 Make iOS Jailbreak Great Again。當然現在可以通過 SEP 的檢查來對該漏洞進行緩解,這是後話。

關於 checkm8 的分析已經有很多了,我們就不再鸚鵡學舌,更多是通過 checkm8 的成因,來從漏洞角度加深對 USB device 開發的理解。

checkm8 漏洞發生在蘋果的救磚模式 DFU (Device Firmware Upgrade),即通過 USB 向蘋果設備刷機的協議。該協議是基於 USB 協議的一個拓展,具體來說:

  • 基於 USB Control Transfer
  • bmRequestType[6:5] 爲 0x20,即 Type 爲 Class
  • bmRequestType[4:0] 爲 0x01,即 Recipient 爲 Interface
  • bRequest 爲 DFU 相關操作,比如 Detach、Download、Upload、GetStatus、Abort 等

DFU 接口初始化的代碼片段如下:

Control Transfer 主要是在 ep0 上傳輸,因此 ep0 的讀寫回調中就會根據收到的數據來派發到不同的 handler,對於 DFU 協議的分發僞代碼如下:

static byte *data_buf;
static size_t data_rcvd;
static size_t data_size;
static struct usb_ctrlrequest setup_request; 

void handle_ctr_transfer_recv(byte *buf, int len, int *p_stage, int is_setup) {
  *p_stage = 0;
  if (!is_setup) {
    handle_data_recv(buf, len, p_stage);
  }
  // handle control request
  memcpy(&setup_request, buf, 8);
  switch(setup_request.bRequestType & 0x60) {
    case STANDARD:
      // ...
    case VENDOR:
      // ...
    case CLASS:
      if (setup_request.bRequestType & 0x1f == INTERFACE) {
        int n = intf_handlers[setup_request.wIndex]->handle_request(&setup_request, &data_buf);
        if (n > 0) {
          data_size = n;
        }
      }
    default:
    // ...
  }
}

其中 intf_handlers 是 usb_core_regisger_interface 函數中添加到的的全局函數數組。handle_reuqest 中傳入的是一個指針的指針,並在處理函數中複製爲 io_buffer 的地址。而開頭的 data stage 階段,內部實現就是將收到的數據拷貝到 data_buf 即 io_buffer 中。

io_buffer 一直是有效的嗎?並不盡然,因爲 io_buffer 在 DFU 退出階段會被 free 釋放掉,此後 data_buf 仍然持有着無效指針,就構成了一個典型的 UAF 場景,這正是 checkm8 的漏洞所在。至於如何觸發以及如何構造利用,可以需要額外的篇幅去進行介紹,感興趣的朋友可以參考文末的文章。

從 checkm8 漏洞中我們可以看到出現漏洞的根本成因:

  • 大量使用全局變量
  • 在處理 USB 內部狀態機出現異常時,沒有充分清除全局變量的值,比如只將 io_buffer 置零而沒有將 data_buf 置零
  • 在重新進入狀態機時,全局變量仍然有殘留,導致進入異常狀態或者處理異常數據

網上有人評論說這麼簡單的漏洞爲什麼沒有通過自動化測試發現出來,個人感覺這其實涉及到模糊測試的兩大難題:

一是針對 stateful 的數據測試,每增加一種內部狀態,測試的分支就成指數級別增長,從而增加了控制流覆蓋到目標代碼的難度;

二是硬件依賴,要測試這個 USB 狀態機,需要 mock 出底層的驅動接口,工作量和寫一個新的 USB 驅動差不多,更不用說 DFU 本身還會涉及存儲設備的讀寫,這部分接口是不是也要模擬?

因此這類漏洞的更多是通過代碼審計發現出來,不過廠商又執着於 Security by Obsecurity,這就導致投入的更多是利益驅動的組織,對個人用戶安全而言並不算是件好事。如果 iBoot 開源,那麼估計這個漏洞早就被提交給蘋果 SRC,成本也就幾千歡樂豆的事,也不至於鬧出這麼大的輿情,甚至以 checkm8 爲跳板,把 SEPOS 也擼了個遍。

後記

本文是最近對 USB 相關的一些學習記錄,雖然文章是從前往後寫的,但實際研究卻是從後往前做的。即先看到了網上分析 checkm8 的文章,爲了復現去寫一個 USB 設備,然後再去學習 USB 協議的細節,可以算是個 Leaning By Hacking 的案例吧。個人感覺這種方式前期較爲痛苦,但後期將點連成線之後還是挺醍醐灌頂的,也算是一種值得推薦的研究方法。

參考資料


Paper 本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1467/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章