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

ref: https://www.beyondlogic.org/usbnutshell/usb6.shtml
雖然 HCI 之下傳輸?shù)臄?shù)據(jù)包大部分情況下對(duì)應(yīng)用開發(fā)者透明,但是了解底層協(xié)議發(fā)生了什么也有助于加深我們對(duì) USB 的理解,后文中介紹 checkm8 漏洞時(shí)候就用到了相關(guān)知識(shí)。
主機(jī)端在主機(jī)端能做的事情相對(duì)有限,主要是分析和使用對(duì)應(yīng)的 USB 設(shè)備。
抓包分析
使用 wireshark 可以分析 USB 流量,根據(jù)上面介紹的描述符字段以及 USB 傳輸過(guò)程進(jìn)行對(duì)照,可以加深我們對(duì) USB 協(xié)議的理解。如下是對(duì)某個(gè)安卓設(shè)備的?Device Descriptor Response?響應(yīng):
device.png也就是所謂安卓變磚恢復(fù)時(shí)經(jīng)常用到的高通 9008 模式。說(shuō)個(gè)題外話,最近對(duì)于高通芯片 BootROM 的研究發(fā)現(xiàn)了一些有趣的東西,后面可能會(huì)另外分享,Stay Tune!
應(yīng)用開發(fā)
對(duì)于應(yīng)用開發(fā)者而言,通常是使用封裝好的庫(kù),早期只有 libusb,后來(lái)更新了 libusb1.0,早期的版本變成 libusb0.1,然后又有了?OpenUSB[3]?和其他的 USB 庫(kù)。但不管用哪個(gè)庫(kù),調(diào)用的流程都是大同小異的。以 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')
總的來(lái)說(shuō)分為幾步,
1.根據(jù)設(shè)備描述符查找到指定的設(shè)備2.獲取該設(shè)備的配置描述符,選擇并激活其中一個(gè)3.在指定的配置中查找接口和端點(diǎn)描述符4.使用端點(diǎn)描述符進(jìn)行數(shù)據(jù)傳輸
如果不清楚 USB 的工作原理,會(huì)覺(jué)得上面代碼的調(diào)用流程很奇怪,往 USB 上讀寫數(shù)據(jù)需要那么復(fù)雜嗎?但正是因?yàn)?USB 協(xié)議的高度拓展性,才得以支持這么多種類的外設(shè),從而流行至今。
設(shè)備端對(duì)于想要開發(fā)設(shè)備端 USB 功能的開發(fā)者而言,使用最廣泛的要數(shù)樹莓派 Zero了,畢竟這是樹莓派系列中唯一支持 USB OTG 的型號(hào)。網(wǎng)上已經(jīng)有很多資料教我們?nèi)绾螌漭?Zero 配置成 USB 鍵盤、打印機(jī)、網(wǎng)卡等 USB 設(shè)備的教程。當(dāng)然使用其他硬件也是可以的,配置自定義的 USB 設(shè)備端可以讓我們做很多有趣的事情,比如網(wǎng)卡中間人或者 Bad USB 這種近源滲透方式。后文中我們會(huì)使用 Zero 進(jìn)行簡(jiǎn)單測(cè)試。
一些相關(guān)的配置資料可以參考:
?https://github.com/RoganDawes/P4wnP1?Using RPi Zero as a Keyboard[4]
內(nèi)核驅(qū)動(dòng)
在介紹應(yīng)用之間,我們先看看內(nèi)核的實(shí)現(xiàn)。還是以 Linux 內(nèi)核為例,具體來(lái)說(shuō),我們想了解如何通過(guò)添加內(nèi)核模塊的方式實(shí)現(xiàn)一個(gè)新的自定義 USB 設(shè)備。俗話說(shuō)得好,添加 Linux 驅(qū)動(dòng)的最好方式是參看現(xiàn)有的驅(qū)動(dòng),畢竟當(dāng)前內(nèi)核中大部分都是驅(qū)動(dòng)代碼。
因?yàn)?Linux 內(nèi)核既能運(yùn)行在主機(jī)端,也能運(yùn)行在設(shè)備端,因此設(shè)備端的 USB 驅(qū)動(dòng)有個(gè)不同的名字:?gadget?driver。對(duì)于不同設(shè)備,也提供不同的內(nèi)核接口,即 Host-Side API 和 Gadget API。既然我們是想實(shí)現(xiàn)自己的設(shè)備,就需要從 gadget 驅(qū)動(dòng)入手。
g_zero.ko?就是這么一個(gè)驅(qū)動(dòng),代碼在?drivers/usb/gadget/legacy/zero.c。該驅(qū)動(dòng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的 USB 設(shè)備,包含 2 個(gè)配置描述,各包含 1 個(gè)功能,分別是 sink 和 loopback,前者接收數(shù)據(jù)并返回 0,后者接收數(shù)據(jù)并原樣返回:
?drivers/usb/gadget/function/f_sourcesink.c?drivers/usb/gadget/function/f_loopback.c
代碼量不多,感興趣的自行 RTFSC。另外值得一提的是,對(duì)于運(yùn)行于 USB device 端的系統(tǒng)而言,內(nèi)核中至少有三個(gè)層級(jí)處理 USB 協(xié)議,可能用戶層還有更多。gadget API 屬于三層的中間層。至底向上,三層分別是:
1.USB Controller Driver: 這是軟件的最底層,通過(guò)寄存器、FIFO、DMA、IRQ 等其他手段直接和硬件打交道,通常稱為?UDC?(USB Device Controller) Driver。2.Gadget Driver: 作為承上啟下的部分,通過(guò)調(diào)用抽象的 UDC 驅(qū)動(dòng)接口,底層實(shí)現(xiàn)了硬件無(wú)關(guān)的 USB function。主要用于實(shí)現(xiàn)前面提到的 USB 功能,包括處理 setup packet (ep0)、返回各類描述符、處理各類修改配置情況、處理各類 USB 事件以及 IN/OUT 的傳輸?shù)鹊取?/span>3.Upper Level: 通過(guò) Gadget Driver 抽象的接口,實(shí)現(xiàn)基于 USB 協(xié)議的上層應(yīng)用,比如 USB 網(wǎng)卡、聲卡、文件存儲(chǔ)、HID 設(shè)備等。
關(guān)于 Linux USB 子系統(tǒng)的詳細(xì)設(shè)計(jì)結(jié)構(gòu),可以參考源碼中的文檔:?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 驅(qū)動(dòng),依葫蘆畫瓢可以很容易實(shí)現(xiàn)一個(gè)自定義的 USB Gadget。但是這樣存在一些問(wèn)題,如果我想實(shí)現(xiàn)一個(gè)八聲道的麥克風(fēng),還要重新寫一遍驅(qū)動(dòng)、編譯、安裝,明明內(nèi)核中麥克風(fēng)的功能已經(jīng)有了,復(fù)制粘貼就顯得很不優(yōu)雅。
那么,有沒(méi)有什么辦法可以方便組合和復(fù)用現(xiàn)有的 gadget function 呢?在 Linux 3.11 中,引入了 USB Gadget ConfigFS,提供了用戶態(tài)的 API 來(lái)方便創(chuàng)建新的 USB 設(shè)備,并可以組合復(fù)用現(xiàn)有內(nèi)核中的驅(qū)動(dòng)。
gfs.png前文提到的基于樹莓派 Zero 實(shí)現(xiàn)的各類 USB 設(shè)備,大部分都是基于 Gadget ConfigFS 接口實(shí)現(xiàn)的?;?configfs 創(chuàng)建 USB gadget 的步驟一般如下:
CONFIGFS_HOME=/sys/kernel/config/usb_gadget# 1. 新建一個(gè) gadget,并寫入實(shí)際的設(shè)備描述mkdir $CONFIGFS_HOME/mydev # 創(chuàng)建設(shè)備目錄后,該目錄下自動(dòng)創(chuàng)建并初始化了一個(gè)設(shè)備模板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. 新建一個(gè)配置,并寫入實(shí)際的配置描述mkdir configs/c.1# 創(chuàng)建一個(gè)配置實(shí)例: <config name>.<config number>cd configs/c.1echo 0x01>MaxPowerecho 0x80> bmAttributes# 3. 新建一個(gè)接口(function),或者將已有接口鏈接到當(dāng)前配置下cd $CONFIGFS_HOME/mydevmkdir functions/hid.usb0 # 創(chuàng)建一個(gè) function 實(shí)例: <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. 將當(dāng)前 USB 設(shè)備綁定到 UDC 驅(qū)動(dòng)中echo ls /sys/class/udc > $CONFIGFS_HOME/mydev/UDC
這樣就實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的 USB gadget,當(dāng)然要完整實(shí)現(xiàn)的話還可以添加字符串描述,以及增加各個(gè)端點(diǎn)的功能。使用 configfs 實(shí)現(xiàn)一個(gè) USB 鍵盤的示例可以參考網(wǎng)上其他文章,比如?Using RPi Zero as a Keyboard[6],或者 Github 上的開源項(xiàng)目,比如?P4wnP1[7]。
有些人覺(jué)得 ConfigFS 配置起來(lái)很繁瑣,所以開發(fā)了一些函數(shù)庫(kù)(如 libusbgx) 來(lái)通過(guò)調(diào)用創(chuàng)建 gadget;有人覺(jué)得通過(guò)函數(shù)操作也還是繁瑣,就創(chuàng)建了一些工具(如?gt[8]) 來(lái)通過(guò)處理一個(gè)類似于 libconfig 的配置文件直接創(chuàng)建 gadget,不過(guò)筆者用得不多。
FunctionFS
FunctionFS 最初是對(duì) GadgetFS 的重寫,用于支持實(shí)現(xiàn)用戶態(tài)的 gadget function,并組合到現(xiàn)有設(shè)備中。這里說(shuō)的 FunctionFS 實(shí)際上是新版基于 ConfigFS 的 GadgetFS 拓展。在上一節(jié)中說(shuō)到創(chuàng)建設(shè)備 gadget 的第四步就是給對(duì)應(yīng)的 configuration 添加 function,格式為?function_type.instance_name,type 對(duì)應(yīng)一個(gè)已有的內(nèi)核驅(qū)動(dòng),比如上節(jié)中是?hid。
如果要使用當(dāng)前內(nèi)核中沒(méi)有的 function 實(shí)現(xiàn)自定義的功能,那么內(nèi)核還提供了一個(gè)驅(qū)動(dòng)可以方便在用戶態(tài)創(chuàng)建接口,該驅(qū)動(dòng)就是 ffs 即 FunctionFS。使用 ffs 的方式也很簡(jiǎn)單,將上面第三步替換為:
cd $CONFIGFS_HOME/mydevmkdir functions/ffs.usb0ln -s functions/ffs.usb0 configs/c.1
創(chuàng)建一個(gè)類型為 ffs,名稱為 usb0 的function,然后掛載到任意目錄:
cd /mntmount usb0 ffs -t functionfs
掛載完后,/mnt/ffs?目錄下就已經(jīng)有了一個(gè) ep0 文件,如名字所言正是 USB 設(shè)備的零端點(diǎn),用于收發(fā) Controller Transfer 數(shù)據(jù)以及各類事件。在該目錄中可以創(chuàng)建其他的端點(diǎn),并使用類似文件讀寫的操作去實(shí)現(xiàn)端點(diǎn)的讀寫,內(nèi)核源碼中提供了一個(gè)用戶態(tài)應(yīng)用示例,代碼在?tools/usb/ffs-test.c。如果嫌 C 代碼寫起來(lái)復(fù)雜,還可以使用 Python 編寫 ffs 實(shí)現(xiàn),比如?python-functionfs[9]。
案例分析: checkm8 漏洞checkm8 漏洞就不用過(guò)多介紹了,曾經(jīng)的神洞,影響了一系列蘋果設(shè)備,存在于 BootROM 中,不可通過(guò)軟件更新來(lái)修復(fù),一度 Make iOS Jailbreak Great Again。當(dāng)然現(xiàn)在可以通過(guò) SEP 的檢查來(lái)對(duì)該漏洞進(jìn)行緩解,這是后話。
關(guān)于 checkm8 的分析已經(jīng)有很多了,我們就不再鸚鵡學(xué)舌,更多是通過(guò) checkm8 的成因,來(lái)從漏洞角度加深對(duì) USB device 開發(fā)的理解。
checkm8 漏洞發(fā)生在蘋果的救磚模式 DFU (Device Firmware Upgrade),即通過(guò) USB 向蘋果設(shè)備刷機(jī)的協(xié)議。該協(xié)議是基于 USB 協(xié)議的一個(gè)拓展,具體來(lái)說(shuō):
?基于 USB Control Transfer?bmRequestType[6:5] 為 0x20,即?Type?為 Class?bmRequestType[4:0] 為 0x01,即?Recipient?為 Interface?bRequest 為 DFU 相關(guān)操作,比如 Detach、Download、Upload、GetStatus、Abort 等
DFU 接口初始化的代碼片段如下:
dfu.pngControl Transfer 主要是在 ep0 上傳輸,因此 ep0 的讀寫回調(diào)中就會(huì)根據(jù)收到的數(shù)據(jù)來(lái)派發(fā)到不同的 handler,對(duì)于 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 中傳入的是一個(gè)指針的指針,并在處理函數(shù)中復(fù)制為 io_buffer 的地址。而開頭的 data stage 階段,內(nèi)部實(shí)現(xiàn)就是將收到的數(shù)據(jù)拷貝到 data_buf 即 io_buffer 中。
io_buffer 一直是有效的嗎?并不盡然,因?yàn)?io_buffer 在 DFU 退出階段會(huì)被 free 釋放掉,此后 data_buf 仍然持有著無(wú)效指針,就構(gòu)成了一個(gè)典型的 UAF 場(chǎng)景,這正是 checkm8 的漏洞所在。至于如何觸發(fā)以及如何構(gòu)造利用,可以需要額外的篇幅去進(jìn)行介紹,感興趣的朋友可以參考文末的文章。
從 checkm8 漏洞中我們可以看到出現(xiàn)漏洞的根本成因:
?大量使用全局變量?在處理 USB 內(nèi)部狀態(tài)機(jī)出現(xiàn)異常時(shí),沒(méi)有充分清除全局變量的值,比如只將 io_buffer 置零而沒(méi)有將 data_buf 置零?在重新進(jìn)入狀態(tài)機(jī)時(shí),全局變量仍然有殘留,導(dǎo)致進(jìn)入異常狀態(tài)或者處理異常數(shù)據(jù)
網(wǎng)上有人評(píng)論說(shuō)這么簡(jiǎn)單的漏洞為什么沒(méi)有通過(guò)自動(dòng)化測(cè)試發(fā)現(xiàn)出來(lái),個(gè)人感覺(jué)這其實(shí)涉及到模糊測(cè)試的兩大難題:
一是針對(duì) stateful 的數(shù)據(jù)測(cè)試,每增加一種內(nèi)部狀態(tài),測(cè)試的分支就成指數(shù)級(jí)別增長(zhǎng),從而增加了控制流覆蓋到目標(biāo)代碼的難度;
二是硬件依賴,要測(cè)試這個(gè) USB 狀態(tài)機(jī),需要 mock 出底層的驅(qū)動(dòng)接口,工作量和寫一個(gè)新的 USB 驅(qū)動(dòng)差不多,更不用說(shuō) DFU 本身還會(huì)涉及存儲(chǔ)設(shè)備的讀寫,這部分接口是不是也要模擬?
因此這類漏洞的更多是通過(guò)代碼審計(jì)發(fā)現(xiàn)出來(lái),不過(guò)廠商又執(zhí)著于?Security by Obsecurity,這就導(dǎo)致投入的更多是利益驅(qū)動(dòng)的組織,對(duì)個(gè)人用戶安全而言并不算是件好事。如果 iBoot 開源,那么估計(jì)這個(gè)漏洞早就被提交給蘋果 SRC,成本也就幾千歡樂(lè)豆的事,也不至于鬧出這么大的輿情,甚至以 checkm8 為跳板,把 SEPOS 也擼了個(gè)遍。
后記本文是最近對(duì) USB 相關(guān)的一些學(xué)習(xí)記錄,雖然文章是從前往后寫的,但實(shí)際研究卻是從后往前做的。即先看到了網(wǎng)上分析 checkm8 的文章,為了復(fù)現(xiàn)去寫一個(gè) USB 設(shè)備,然后再去學(xué)習(xí) USB 協(xié)議的細(xì)節(jié),可以算是個(gè) Leaning By Hacking 的案例吧。個(gè)人感覺(jué)這種方式前期較為痛苦,但后期將點(diǎn)連成線之后還是挺醍醐灌頂?shù)?,也算是一種值得推薦的研究方法。
參考資料?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/
