USB 協(xié)議核心概念與實踐
USB,全稱是?Universal Serial Bus[1],即通用串行總線,既是一個針對電纜和連接器的工業(yè)標準,也指代其中使用的連接協(xié)議。本文不會過多介紹標準中的細節(jié),而是從軟件工程師的角度出發(fā),介紹一些重要的基本概念,以及實際的主機和從機應用。最后作為實際案例,從 USB 協(xié)議實現(xiàn)的角度分析了checkm8漏洞的成因。
首先要明確的一點,USB 協(xié)議是以主機為中心的 (Host Centric),也就是說只有主機端向設備端請求數(shù)據(jù)后,設備端才能向主機發(fā)送數(shù)據(jù)。從數(shù)據(jù)的角度來看,開發(fā)者最直接接觸的就是端點 (Endpoint),端點可以看做是數(shù)據(jù)收發(fā)的管道。
當主機給設備發(fā)送數(shù)據(jù)時,通常流程是:
?調用用戶層 API,如?libusb_bulk_transfer?對內核的 USB 驅動執(zhí)行對應系統(tǒng)調用,添加發(fā)送隊列如?ioctl(IOCTL_USBFS_SUBMITURB)?內核驅動中通過 HCI 接口向 USB 設備發(fā)送請求+數(shù)據(jù)?數(shù)據(jù)發(fā)送到設備端的 Controller -> HCI -> Host
設備給主機發(fā)送請求也是類似,只不過由于是主機中心,發(fā)送的數(shù)據(jù)會保存在緩存中,等待主機發(fā)送 IN TOKEN 之后才真正發(fā)送到主機。在介紹數(shù)據(jù)發(fā)送流程之前,我們先來看下描述符。
描述符
所有的 USB 設備端設備,都使用一系列層級的描述符 (Descriptors) 來向主機描述自身信息。這些描述符包括:
?Device Descriptors: 設備描述?Configuration Descriptors: 配置描述?Interface Descriptors: 接口描述?Endpoint Descriptors: 端點描述?String Descriptors: 字符串描述
它們之間的層級結構關系如下:
des.png每種描述符都有對應的數(shù)據(jù)結構,定義在標準中的第九章,俗稱 ch9。下面以 Linux 內核的實現(xiàn)為例來簡要介紹各個描述符,主要參考頭文件?include/uapi/linux/usb/ch9.h。
設備描述
每個 USB 設備只能有一個設備描述(Device Descriptor),該描述符中包括了設備的 USB 版本、廠商、產品 ID 以及包含的配置描述符個數(shù)等信息,如下所示:
/* USB_DT_DEVICE: Device descriptor */struct usb_device_descriptor {__u8 bLength;// 18 字節(jié)__u8 bDescriptorType;// 0x01__le16 bcdUSB;// 設備所依從的 USB 版本號__u8 bDeviceClass;// 設備類型__u8 bDeviceSubClass;// 設備子類型__u8 bDeviceProtocol;// 設備協(xié)議__u8 bMaxPacketSize0;// ep0 的最大包長度,有效值為 8,6,32,64__le16 idVendor;// 廠商號__le16 idProduct;// 產品號__le16 bcdDevice;// 設備版本號__u8 iManufacturer;// 產商字名稱__u8 iProduct;// 產品名稱__u8 iSerialNumber;// 序列號__u8 bNumConfigurations;// 配置描述符的個數(shù)} __attribute__ ((packed));#define USB_DT_DEVICE_SIZE 18
每個字段的含義都寫在注釋中了,其中有幾點值得一提。
?設備類型、子類型和協(xié)議碼,是由 USB 組織定義的;?產商號也是由 USB 組織定義的,但是產品號可以由廠商自行定義;?廠商、產品和序列號分別只有 1 字節(jié),表示在字符串描述符中的索引;
BCD: binary- coded decimal
配置描述
每種不同的配置描述(Configuration Descriptor)中分別指定了 USB 設備所支持的配置,如功率等信息;一個 USB 設備可以包含多個配置,但同一時間只能有一個配置是激活狀態(tài)。實際上大部分的 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;// 返回數(shù)據(jù)的總長度__u8 bNumInterfaces;// 接口描述符的個數(shù)__u8 bConfigurationValue;// 當前配置描述符的值 (用來選擇該配置)__u8 iConfiguration;// 該配置的字符串信息 (在字符串描述符中的索引)__u8 bmAttributes;// 屬性信息__u8 bMaxPower;// 最大功耗,以 2mA 為單位} __attribute__ ((packed));#define USB_DT_CONFIG_SIZE 9
當主設備讀取配置描述的時候,從設備會返回該配置下所有的其他描述符,如接口、端點和字符串描述符,因此需要?wTotalLength?來表示返回數(shù)據(jù)的總長度。
bmAttributes?指定了該配置的電源參數(shù)信息,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
其中接口類型、子類型和協(xié)議與前面遇到的類似,都是由 USB 組織定義的。在 Linux 內核中,每個接口封裝成一個高層級的功能,即邏輯鏈接(Logical Connection),例如對 USB 攝像頭而言,接口可以分為視頻流、音頻流和鍵盤(攝像頭上的控制按鍵)等。
還有值得一提的是?bAlternateSetting,每個 USB 接口都可以有不同的參數(shù)設置,例如對于音頻接口可以有不同的帶寬設置。實際上 Alternate Settings 就是用來控制周期性的端點參數(shù)的,比如 isochronous endpoint。
端點描述
端點描述符用來描述除了零端點(ep0)之外的其他端點,零端點總是被假定為控制端點,并且在開始請求任意描述符之前就已經被配置好了。端點(Endpoint),可以認為是一個單向數(shù)據(jù)信道的抽象,因此端點描述符中包括傳輸?shù)乃俾屎蛶挼刃畔ⅲ缦滤?
/* USB_DT_ENDPOINT: Endpoint descriptor */struct usb_endpoint_descriptor {__u8 bLength;__u8 bDescriptorType;// 0x05__u8 bEndpointAddress;// 端點地址__u8 bmAttributes;// 端點屬性__le16 wMaxPacketSize;// 該端點收發(fā)的最大包大小__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位數(shù)據(jù)分別代表:
?Bit 0-3: 端點號?Bit 4-6: 保留,值為0?Bit 7: 數(shù)據(jù)方向,0 為 OUT,1 為 IN
bmAttributes?8位數(shù)據(jù)分別代表:
?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,notUnicode characters.*/
字符串表中的字符都以?Unicode[2]?格式編碼,并且可以支持多種語言。0號字符串表較為特殊,其中 wData 包含一組所支持的語言代碼,每個語言碼為 2 字節(jié),例如 0x0409 表示英文。
傳輸
不像 RS-232 和其他類似的串口協(xié)議,USB 實際上由多層協(xié)議構造而成,不過大部分底層的協(xié)議都在 Controller 端上的硬件或者固件進行處理了,最終開發(fā)者所要關心的只有上層協(xié)議。
USB Packet
在 HCI 之下,實際傳輸?shù)臄?shù)據(jù)包稱為 Packet,每次上層 USB 傳輸都會涉及到 2-3 次底層的 Packet 傳輸,分別是:
?Token Packet: 總是由主機發(fā)起,指示一次新的傳輸或者事件?In: 告訴 USB 設備,主機我想要讀點信息?Out: 告訴 USB 設備,主機我想要寫點信息?Setup: 用于開始 Control Transfer?Data Packet: 可選,表示傳輸?shù)臄?shù)據(jù),可以是主機發(fā)送到設備,也可以是設備發(fā)送到主機?Data0?Data1?Status Packet: 狀態(tài)包,用于響應傳輸,以及提供糾錯功能?Handshake Packets: ACK/NAK/STALL?Start of Frame Packets
Transfer
基于這些底層包,USB 協(xié)議定義了四種不同的傳輸類型,分別對應上節(jié)中的四種端點類型,分別是:
Control Transfers: 主要用來發(fā)送狀態(tài)和命令,比如用來請求設備、配置等描述以及選擇和設置指定的描述符。只有控制端點是雙向的。
Interrupt Transfers: 由于 USB 協(xié)議是主機主導的,設備端的中斷信息需要被及時響應,就要用到中斷傳輸,其提供了有保證的延遲以及錯誤檢測和重傳功能。中斷傳輸通常是非周期性的,并且傳輸過程保留部分帶寬,常用于時間敏感的數(shù)據(jù),比如鍵盤、鼠標等 HID 設備。
Isochronous Transfers: 等時傳輸,如其名字所言,該類傳輸是連續(xù)和周期性的,通常包含時間敏感的信息,比如音頻或視頻流。因此這類傳輸不保證到達,即沒有 ACK 響應。
Bulk Transfers: 用于傳輸大塊的突發(fā)數(shù)據(jù)(小塊也可以),不保留帶寬。提供了錯誤校驗(CRC16)和重傳機制來保證傳輸數(shù)據(jù)的完整性。塊傳輸只支持高速/全速模式。
這里以控制傳輸(Control Transfers)為例,來看看底層 Packet 如何組成一次完整的傳輸。控制傳輸實際上又可能最多包含三個階段,每個階段在應用層可以看成是一次 “USB 傳輸” (在Wireshark中占一行),分別是:
?Setup Stage: 主機發(fā)送到設備的請求,包含三次底層數(shù)據(jù)傳輸1.Setup Token Packet: 指定地址和端點號(應為0)2.Data0 Packet: 請求數(shù)據(jù),假設是 8 字節(jié)的?Device Descriptor Request3.ACK Handshake Packet: 設備的響應, 不允許用 STALL 或者 NAK 來響應 Setup Packet?Data Stage: 可選階段,包含一個或者多個 IN/OUT 傳輸,以 IN 為例,也包含三次傳輸1.IN Token Packet: 表示主機端要從設備端讀數(shù)據(jù)2.Datax Packet: 如果上面 Setup Stage 是?Device Descriptor Request, 這里返回?Device Descriptor Response?(的前8字節(jié),然后再根據(jù)實際長度再 IN 一次)。3.ACK/STALL/NAK Status Packet?Status Stage: 報告本次請求的狀態(tài),底層也是三次傳輸,但是和方向有關:?如果在 Data Stage 發(fā)送的是 IN Token,則該階段包括:1.OUT Token2.Data0 ZLP(zero length packet): 主機發(fā)送長度為0的數(shù)據(jù)3.ACK/NACK/STALL: 設備返回給主機?如果在 Data Stage 發(fā)送的是 OUT Token,則該階段包括:1.IN Token2.Data0 ZLP: 設備發(fā)送給主機,表示正常完成,否則發(fā)送 NACK/STALL3.ACK: 如果是 ZLP,主機響應設備,雙向確認
每個階段的數(shù)據(jù)都有自己的格式,例如 Setup Stage 的 Request,即 Data0 部分發(fā)送的 8 字節(jié)數(shù)據(jù)結構如下:
struct usb_ctrlrequest {__u8 bRequestType;// 對應 USB 協(xié)議中的 bmRequestType,包含請求的方向、類型和指定接受者__u8 bRequest;// 決定所要執(zhí)行的請求__le16 wValue;// 請求參數(shù)__le16 wIndex;// 同上__le16 wLength;// 如果請求包含 Data Stage,則指定數(shù)據(jù)的長度} __attribute__ ((packed));
下面是一些標準請求的示例:

ref: https://www.beyondlogic.org/usbnutshell/usb6.shtml
雖然 HCI 之下傳輸?shù)臄?shù)據(jù)包大部分情況下對應用開發(fā)者透明,但是了解底層協(xié)議發(fā)生了什么也有助于加深我們對 USB 的理解,后文中介紹 checkm8 漏洞時候就用到了相關知識。
主機端在主機端能做的事情相對有限,主要是分析和使用對應的 USB 設備。
抓包分析
使用 wireshark 可以分析 USB 流量,根據(jù)上面介紹的描述符字段以及 USB 傳輸過程進行對照,可以加深我們對 USB 協(xié)議的理解。如下是對某個安卓設備的?Device Descriptor Response?響應:
device.png也就是所謂安卓變磚恢復時經常用到的高通 9008 模式。說個題外話,最近對于高通芯片 BootROM 的研究發(fā)現(xiàn)了一些有趣的東西,后面可能會另外分享,Stay Tune!
應用開發(fā)
對于應用開發(fā)者而言,通常是使用封裝好的庫,早期只有 libusb,后來更新了 libusb1.0,早期的版本變成 libusb0.1,然后又有了?OpenUSB[3]?和其他的 USB 庫。但不管用哪個庫,調用的流程都是大同小異的。以 Python 的封裝 pyusb 為例,官方給的示例如下:
import usb.coreimport usb.util# find our devicedev = usb.core.find(idVendor=0xfffe, idProduct=0x0001)# was it found?if dev isNone:raiseValueError('Device not found')# set the active configuration. With no arguments, the first# configuration will be the active onedev.set_configuration()# get an endpoint instancecfg = dev.get_active_configuration()intf = cfg[(0,0)]ep = usb.util.find_descriptor(intf,# match the first OUT endpointcustom_match = \lambda e: \usb.util.endpoint_direction(e.bEndpointAddress)== \usb.util.ENDPOINT_OUT)assert ep isnotNone# write the dataep.write('test')
總的來說分為幾步,
1.根據(jù)設備描述符查找到指定的設備2.獲取該設備的配置描述符,選擇并激活其中一個3.在指定的配置中查找接口和端點描述符4.使用端點描述符進行數(shù)據(jù)傳輸
如果不清楚 USB 的工作原理,會覺得上面代碼的調用流程很奇怪,往 USB 上讀寫數(shù)據(jù)需要那么復雜嗎?但正是因為 USB 協(xié)議的高度拓展性,才得以支持這么多種類的外設,從而流行至今。
設備端對于想要開發(fā)設備端 USB 功能的開發(fā)者而言,使用最廣泛的要數(shù)樹莓派 Zero了,畢竟這是樹莓派系列中唯一支持 USB OTG 的型號。網上已經有很多資料教我們如何將樹莓派 Zero 配置成 USB 鍵盤、打印機、網卡等 USB 設備的教程。當然使用其他硬件也是可以的,配置自定義的 USB 設備端可以讓我們做很多有趣的事情,比如網卡中間人或者 Bad USB 這種近源滲透方式。后文中我們會使用 Zero 進行簡單測試。
一些相關的配置資料可以參考:
?https://github.com/RoganDawes/P4wnP1?Using RPi Zero as a Keyboard[4]
內核驅動
在介紹應用之間,我們先看看內核的實現(xiàn)。還是以 Linux 內核為例,具體來說,我們想了解如何通過添加內核模塊的方式實現(xiàn)一個新的自定義 USB 設備。俗話說得好,添加 Linux 驅動的最好方式是參看現(xiàn)有的驅動,畢竟當前內核中大部分都是驅動代碼。
因為 Linux 內核既能運行在主機端,也能運行在設備端,因此設備端的 USB 驅動有個不同的名字:?gadget?driver。對于不同設備,也提供不同的內核接口,即 Host-Side API 和 Gadget API。既然我們是想實現(xiàn)自己的設備,就需要從 gadget 驅動入手。
g_zero.ko?就是這么一個驅動,代碼在?drivers/usb/gadget/legacy/zero.c。該驅動實現(xiàn)了一個簡單的 USB 設備,包含 2 個配置描述,各包含 1 個功能,分別是 sink 和 loopback,前者接收數(shù)據(jù)并返回 0,后者接收數(shù)據(jù)并原樣返回:
?drivers/usb/gadget/function/f_sourcesink.c?drivers/usb/gadget/function/f_loopback.c
代碼量不多,感興趣的自行 RTFSC。另外值得一提的是,對于運行于 USB device 端的系統(tǒng)而言,內核中至少有三個層級處理 USB 協(xié)議,可能用戶層還有更多。gadget API 屬于三層的中間層。至底向上,三層分別是:
1.USB Controller Driver: 這是軟件的最底層,通過寄存器、FIFO、DMA、IRQ 等其他手段直接和硬件打交道,通常稱為?UDC?(USB Device Controller) Driver。2.Gadget Driver: 作為承上啟下的部分,通過調用抽象的 UDC 驅動接口,底層實現(xiàn)了硬件無關的 USB function。主要用于實現(xiàn)前面提到的 USB 功能,包括處理 setup packet (ep0)、返回各類描述符、處理各類修改配置情況、處理各類 USB 事件以及 IN/OUT 的傳輸?shù)鹊取?/span>3.Upper Level: 通過 Gadget Driver 抽象的接口,實現(xiàn)基于 USB 協(xié)議的上層應用,比如 USB 網卡、聲卡、文件存儲、HID 設備等。
關于 Linux USB 子系統(tǒng)的詳細設計結構,可以參考源碼中的文檔:?Linux USB API[5],以及其他一些資料,如下所示:
?https://bootlin.com/doc/legacy/linux-usb/linux-usb.pdf?https://static.lwn.net/images/pdf/LDD3/ch13.pdf?https://elinux.org/images/5/5e/Opasiak.pdf
GadgetFS/ConfigFS
參考現(xiàn)有的 Linux 驅動,依葫蘆畫瓢可以很容易實現(xiàn)一個自定義的 USB Gadget。但是這樣存在一些問題,如果我想實現(xiàn)一個八聲道的麥克風,還要重新寫一遍驅動、編譯、安裝,明明內核中麥克風的功能已經有了,復制粘貼就顯得很不優(yōu)雅。
那么,有沒有什么辦法可以方便組合和復用現(xiàn)有的 gadget function 呢?在 Linux 3.11 中,引入了 USB Gadget ConfigFS,提供了用戶態(tài)的 API 來方便創(chuàng)建新的 USB 設備,并可以組合復用現(xiàn)有內核中的驅動。
gfs.png前文提到的基于樹莓派 Zero 實現(xiàn)的各類 USB 設備,大部分都是基于 Gadget ConfigFS 接口實現(xiàn)的。基于 configfs 創(chuàng)建 USB gadget 的步驟一般如下:
CONFIGFS_HOME=/sys/kernel/config/usb_gadget# 1. 新建一個 gadget,并寫入實際的設備描述mkdir $CONFIGFS_HOME/mydev # 創(chuàng)建設備目錄后,該目錄下自動創(chuàng)建并初始化了一個設備模板cd $CONFIGFS_HOME/mydevecho 0x0100> bcdDevice # Version 1.0.0echo 0x0200> bcdUSB # USB 2.0echo 0x00> bDeviceClassecho 0x00> bDeviceProtocolecho 0x40> bMaxPacketSize0echo 0x0104> idProduct # Multifunction Composite Gadgetecho 0x1d6b> idVendor # Linux Foundation# 2. 新建一個配置,并寫入實際的配置描述mkdir configs/c.1# 創(chuàng)建一個配置實例: <config name>.<config number>cd configs/c.1echo 0x01>MaxPowerecho 0x80> bmAttributes# 3. 新建一個接口(function),或者將已有接口鏈接到當前配置下cd $CONFIGFS_HOME/mydevmkdir functions/hid.usb0 # 創(chuàng)建一個 function 實例: <function type>.<instance name>echo 1> functions/hid.usb0/protocolecho 8> functions/hid.usb0/report_length # 8-byte reportsecho 1> functions/hid.usb0/subclassln -s functions/hid.usb0 configs/c.1# 4. 將當前 USB 設備綁定到 UDC 驅動中echo ls /sys/class/udc > $CONFIGFS_HOME/mydev/UDC
這樣就實現(xiàn)了一個最簡單的 USB gadget,當然要完整實現(xiàn)的話還可以添加字符串描述,以及增加各個端點的功能。使用 configfs 實現(xiàn)一個 USB 鍵盤的示例可以參考網上其他文章,比如?Using RPi Zero as a Keyboard[6],或者 Github 上的開源項目,比如?P4wnP1[7]。
有些人覺得 ConfigFS 配置起來很繁瑣,所以開發(fā)了一些函數(shù)庫(如 libusbgx) 來通過調用創(chuàng)建 gadget;有人覺得通過函數(shù)操作也還是繁瑣,就創(chuàng)建了一些工具(如?gt[8]) 來通過處理一個類似于 libconfig 的配置文件直接創(chuàng)建 gadget,不過筆者用得不多。
FunctionFS
FunctionFS 最初是對 GadgetFS 的重寫,用于支持實現(xiàn)用戶態(tài)的 gadget function,并組合到現(xiàn)有設備中。這里說的 FunctionFS 實際上是新版基于 ConfigFS 的 GadgetFS 拓展。在上一節(jié)中說到創(chuàng)建設備 gadget 的第四步就是給對應的 configuration 添加 function,格式為?function_type.instance_name,type 對應一個已有的內核驅動,比如上節(jié)中是?hid。
如果要使用當前內核中沒有的 function 實現(xiàn)自定義的功能,那么內核還提供了一個驅動可以方便在用戶態(tài)創(chuàng)建接口,該驅動就是 ffs 即 FunctionFS。使用 ffs 的方式也很簡單,將上面第三步替換為:
cd $CONFIGFS_HOME/mydevmkdir functions/ffs.usb0ln -s functions/ffs.usb0 configs/c.1
創(chuàng)建一個類型為 ffs,名稱為 usb0 的function,然后掛載到任意目錄:
cd /mntmount usb0 ffs -t functionfs
掛載完后,/mnt/ffs?目錄下就已經有了一個 ep0 文件,如名字所言正是 USB 設備的零端點,用于收發(fā) Controller Transfer 數(shù)據(jù)以及各類事件。在該目錄中可以創(chuàng)建其他的端點,并使用類似文件讀寫的操作去實現(xiàn)端點的讀寫,內核源碼中提供了一個用戶態(tài)應用示例,代碼在?tools/usb/ffs-test.c。如果嫌 C 代碼寫起來復雜,還可以使用 Python 編寫 ffs 實現(xiàn),比如?python-functionfs[9]。
案例分析: checkm8 漏洞checkm8 漏洞就不用過多介紹了,曾經的神洞,影響了一系列蘋果設備,存在于 BootROM 中,不可通過軟件更新來修復,一度 Make iOS Jailbreak Great Again。當然現(xiàn)在可以通過 SEP 的檢查來對該漏洞進行緩解,這是后話。
關于 checkm8 的分析已經有很多了,我們就不再鸚鵡學舌,更多是通過 checkm8 的成因,來從漏洞角度加深對 USB device 開發(fā)的理解。
checkm8 漏洞發(fā)生在蘋果的救磚模式 DFU (Device Firmware Upgrade),即通過 USB 向蘋果設備刷機的協(xié)議。該協(xié)議是基于 USB 協(xié)議的一個拓展,具體來說:
?基于 USB Control Transfer?bmRequestType[6:5] 為 0x20,即?Type?為 Class?bmRequestType[4:0] 為 0x01,即?Recipient?為 Interface?bRequest 為 DFU 相關操作,比如 Detach、Download、Upload、GetStatus、Abort 等
DFU 接口初始化的代碼片段如下:
dfu.pngControl Transfer 主要是在 ep0 上傳輸,因此 ep0 的讀寫回調中就會根據(jù)收到的數(shù)據(jù)來派發(fā)到不同的 handler,對于 DFU 協(xié)議的分發(fā)偽代碼如下:
staticbyte*data_buf;staticsize_t data_rcvd;staticsize_t data_size;staticstruct 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 requestmemcpy(&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 函數(shù)中添加到的的全局函數(shù)數(shù)組。handle_reuqest 中傳入的是一個指針的指針,并在處理函數(shù)中復制為 io_buffer 的地址。而開頭的 data stage 階段,內部實現(xiàn)就是將收到的數(shù)據(jù)拷貝到 data_buf 即 io_buffer 中。
io_buffer 一直是有效的嗎?并不盡然,因為 io_buffer 在 DFU 退出階段會被 free 釋放掉,此后 data_buf 仍然持有著無效指針,就構成了一個典型的 UAF 場景,這正是 checkm8 的漏洞所在。至于如何觸發(fā)以及如何構造利用,可以需要額外的篇幅去進行介紹,感興趣的朋友可以參考文末的文章。
從 checkm8 漏洞中我們可以看到出現(xiàn)漏洞的根本成因:
?大量使用全局變量?在處理 USB 內部狀態(tài)機出現(xiàn)異常時,沒有充分清除全局變量的值,比如只將 io_buffer 置零而沒有將 data_buf 置零?在重新進入狀態(tài)機時,全局變量仍然有殘留,導致進入異常狀態(tài)或者處理異常數(shù)據(jù)
網上有人評論說這么簡單的漏洞為什么沒有通過自動化測試發(fā)現(xiàn)出來,個人感覺這其實涉及到模糊測試的兩大難題:
一是針對 stateful 的數(shù)據(jù)測試,每增加一種內部狀態(tài),測試的分支就成指數(shù)級別增長,從而增加了控制流覆蓋到目標代碼的難度;
二是硬件依賴,要測試這個 USB 狀態(tài)機,需要 mock 出底層的驅動接口,工作量和寫一個新的 USB 驅動差不多,更不用說 DFU 本身還會涉及存儲設備的讀寫,這部分接口是不是也要模擬?
因此這類漏洞的更多是通過代碼審計發(fā)現(xiàn)出來,不過廠商又執(zhí)著于?Security by Obsecurity,這就導致投入的更多是利益驅動的組織,對個人用戶安全而言并不算是件好事。如果 iBoot 開源,那么估計這個漏洞早就被提交給蘋果 SRC,成本也就幾千歡樂豆的事,也不至于鬧出這么大的輿情,甚至以 checkm8 為跳板,把 SEPOS 也擼了個遍。
后記本文是最近對 USB 相關的一些學習記錄,雖然文章是從前往后寫的,但實際研究卻是從后往前做的。即先看到了網上分析 checkm8 的文章,為了復現(xiàn)去寫一個 USB 設備,然后再去學習 USB 協(xié)議的細節(jié),可以算是個 Leaning By Hacking 的案例吧。個人感覺這種方式前期較為痛苦,但后期將點連成線之后還是挺醍醐灌頂?shù)模菜闶且环N值得推薦的研究方法。
參考資料?USB in a NutShell[10]?USB and the Real World[11]?pyusb/pyusb[12]?Linux USB API[13]?Kernel USB Gadget Configfs Interface[14]?Technical analysis of the checkm8 exploit[15]
引用鏈接
[1]?Universal Serial Bus:?https://en.wikipedia.org/wiki/USB[2]?Unicode:?http://www.unicode.org/[3]?OpenUSB:?http://sourceforge.net/p/openusb/wiki/Home/[4,6]?Using RPi Zero as a Keyboard:?https://www.rmedgar.com/blog/using-rpi-zero-as-keyboard-setup-and-device-definition[5]?Linux USB API:?https://www.kernel.org/doc/html/v4.18/driver-api/usb/index.html[7]?P4wnP1:?https://github.com/RoganDawes/P4wnP1[8]?gt:?https://github.com/kopasiak/gt[9]?python-functionfs:?https://github.com/vpelletier/python-functionfs[10]?USB in a NutShell:?https://www.beyondlogic.org/usbnutshell/usb1.shtml[11]?USB and the Real World:?https://elinux.org/images/a/ae/Ott--usb_and_the_real_world.pdf[12]?pyusb/pyusb:?https://github.com/pyusb/pyusb/blob/master/docs/tutorial.rst[13]?Linux USB API:?https://www.kernel.org/doc/html/v4.18/driver-api/usb/index.html[14]?Kernel USB Gadget Configfs Interface:?https://www.elinux.org/images/e/ef/USB_Gadget_Configfs_API_0.pdf[15]?Technical analysis of the checkm8 exploit:?https://habr.com/en/company/dsec/blog/472762/
