作者:Hcamael****@知道创宇404实验室********
时间:2024年2月27日
在使用 Google 搜索相关学习资料的过程中,搜到一本书——《圈圈教你玩 USB》,在阅读中发现需要购买相关硬件设备。
硬件开发部分考虑在后期的内容中再进行学习研究,前期考虑拿手头上已有设备来进行学习研究。首先考虑的是使用手机作为USB设备连接电脑,但是研究后发现,安卓对USB设备开发的支持不是很友好,再加上在安卓层对USB开发进行封装,会导致我们不太好理解USB底层的细节。
随后,考虑到手头具备树莓派4b设备,决定尝试利用该设备进行USB设备开发。在进行Google搜索后,发现了一个名为key-mime-pi的项目,可作为我入门的起点。
1 USB设备开发相关工具
参考资料
经过一番研究,因为Windows上USB相关的工具可视化做的更好,所以最后选择Windows设备作为USB主机。
第一个工具是USB Tree View,该工具能很好的展示主机上USB设备树的主从情况,USB描述符信息等数据。如图1所示:
图1:USB Tree View界面
可以从上图的USB设备树中看出,一台主机中最底层的是USB主机控制器,该控制器直连CPU。接着上一层接入的是USB根集线器(HUB),可以类比为USB扩展坞,作用就是扩展出多个USB接口,但是总带宽还是由USB主机控制器决定的。
比如,在上图USB主机控制器版本是USB3.1,那么上一层的USB根集线器可以扩展出USB3.1及以下的USB口,比如USB2.0口。不过不管扩展出多少USB口,其总带宽都已经固定在10Gbps上(因为USB3.1的带宽为10Gbps)。并且,在集线器上,还可以再接多个集线器,比如上图中有一个通用USB集线器,其实就是示例主机上机箱扩展出的USB接口。
另外点击已经连接上的USB设备,还可以查看该USB设备更多详细信息,如图2所示:
图2:在USB Tree View上查看USB设备详细信息
如果理解上图中各部分信息,后文再进行说明。不过该工具也存在一些BUG,该工具无法正常解析HID信息,如图3所示:
图3:HID描述符
Windows上的wireshark在装上USBPcap后,能够抓取主机控制器上的USB流量,比如主机上有三个主机控制器,所以在Wireshark中就显示有三个USBPcap,如图4所示:
图4:Wireshark界面
如果不知道USB设置插入的USB口是属于哪一个主机控制器,可以使用USBTree View查看。不过由于Wireshark是抓取主机控制器上的流量,而一个USB主机控制器可以连接多个USB设备,所以当我要研究某一个USB设备时,需要通过Wireshark的过滤表达式对该主机控制器上的其他USB设备的流量进行过滤。
Bus Hound
可以抓取指定USB设备的流量,不过该工具的缺点是需要花钱,否则抓包size和数量都有限制,目前尚未找到该软件的破解版本。图5是该软件的界面:
图5:Bus Hound界面
2 使用树莓派4b作为PC的USB键盘
参考资料
接下来,通过阅读key-mime-pi
项目的源代码,发现使用树莓派4b设备模拟一个USB设备是一件非常容易的事情。根据该项目的代码,可以分解出以下两个步骤:
1.需要开启dwc2驱动:在树莓派的config.txt中添加dtoverlay=dwc2
,设备启动后,确认一下启动是否开启:
$ lsmod|grep dwc2dwc2 196608 0
2.运行如下所示的bash
脚本:
#!/usr/bin/env bash# Adapted from https://github.com/girst/hardpass-sendHID/blob/master/README.md# Exit on first error.set -e# Treat undefined environment variables as errors.set -umodprobe libcompositecd /sys/kernel/config/usb_gadget/mkdir -p g1cd g1echo 0x1d6b > idVendor # Linux Foundationecho 0x0104 > idProduct # Multifunction Composite Gadgetecho 0x0100 > bcdDevice # v1.0.0echo 0x0200 > bcdUSB # USB2STRINGS_DIR="strings/0x409"mkdir -p "$STRINGS_DIR"echo "6b65796d696d6570690" > "${STRINGS_DIR}/serialnumber"echo "keymimepi" > "${STRINGS_DIR}/manufacturer"echo "Generic USB Keyboard" > "${STRINGS_DIR}/product"FUNCTIONS_DIR="functions/hid.usb0"mkdir -p "$FUNCTIONS_DIR"echo 1 > "${FUNCTIONS_DIR}/protocol" # Keyboardecho 0 > "${FUNCTIONS_DIR}/subclass" # No subclassecho 8 > "${FUNCTIONS_DIR}/report_length"# Write the report descriptor# Source: https://www.kernel.org/doc/html/latest/usb/gadget_hid.htmlecho -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > "${FUNCTIONS_DIR}/report_desc"CONFIG_INDEX=1CONFIGS_DIR="configs/c.${CONFIG_INDEX}"mkdir -p "$CONFIGS_DIR"echo 250 > "${CONFIGS_DIR}/MaxPower"CONFIGS_STRINGS_DIR="${CONFIGS_DIR}/strings/0x409"mkdir -p "$CONFIGS_STRINGS_DIR"echo "Config ${CONFIG_INDEX}: ECM network" > "${CONFIGS_STRINGS_DIR}/configuration"ln -s "$FUNCTIONS_DIR" "${CONFIGS_DIR}/"ls /sys/class/udc > UDCchmod 777 /dev/hidg0
通过研究上面的bash
脚本,发现该项目利用的是Linux系统的USB gadget驱动,有需要的可以自行查看该部分的源码,位于Linux内核的:linux/drivers/usb/dwc2
和linux/drivers/usb/gadget
目录下。
该驱动细节在本文暂不进行研究。在运行上面的bash
脚本后,如无意外,可以在USB Tree View
中查看到树莓派设备模拟出的USB键盘。
这里提一个前置的知识点:USB是主从结构,一定会有一个为USB主机,一个为USB设备,并且通信永远都是由主机先发起的。
在本篇文章的环境下,首先打开Wireshark,开始捕获USBPcap1的流量,然后把树莓派的type-c口(电源供电口,树莓派4b设备的4个USB母口只能作为USB主机,只有type-c供电口可以作为USB设备)连接到主机的USB口上,这个时候主机并不会识别树莓派为USB设备,仅供电,所以这个时候树莓派开始启动。
然后开始把wireshark能捕获的流量地址都给过滤掉,这些都不属于树莓派的USB流量,如图6所示,是本文环境中的过滤语法:
图6:Wireshark过滤语法
然后在树莓派上以root权限运行上面提供的bash
脚本,再查看Wireshark上捕获到的流量,如图7所示:
图7:Wireshark根据过滤语法捕获到的流量
使用USBPcap捕获到的usb流量地址一般为:x.y.z,x为USB总线(Bus)号,一般为主机控制器的编号,示例中捕获到的USBPcap1流量,x的值都为1。
y为设备号,指向该Bus上的某个设备,但是USBPcap
和USBTreeView
对设备编号的方法是不同的。USBPcap
是按照顺序来定义设备号,而USBTreeView
则在开始时已经为所有USB接口进行了编号,无论是否连接了USB设备。
按照我的理解,USBPcap
可能更符合底层实际情况,因为它不是自己编的号,而是解析了抓到的USB流量。
z的值表示的是端点号(Endpoint),我觉得有点像一个程序的文件描述符(fd),USB主机和设备间就是通过端点号来进行通信的,当USB设备还未在主机上注册时,默认使用0
端点号来进行通信。
接下来对USB协议进行研究,经过研究发现,USB设备通过向主机发送各种描述符来告知USB主机自身的信息,这些描述符在Linux源码中通过结构体来定义,每个结构体字段的含义可以参考相关文章。下面对USB的几个重要的描述符进行分析。
第一个是设备描述符,该描述符的结构体定义位于:linux/include/uapi/linux/usb/ch9.h
,结构体如下:
/* USB_DT_DEVICE: Device descriptor */struct usb_device_descriptor { __u8 bLength; __u8 bDescriptorType; __le16 bcdUSB; __u8 bDeviceClass; __u8 bDeviceSubClass; __u8 bDeviceProtocol; __u8 bMaxPacketSize0; __le16 idVendor; __le16 idProduct; __le16 bcdDevice; __u8 iManufacturer; __u8 iProduct; __u8 iSerialNumber; __u8 bNumConfigurations;} __attribute__ ((packed));
首先查看USBPcap上捕获到的设备描述符,如图8所示:
图8:Wireshark上查看设备描述符
再对比一下USBTree View
上显示的设备描述符信息,如图9所示:
图9:USB Tree View上查看设备描述符
通过对比发现,控制设备描述符的位于bash
脚本的以下几行代码:
echo 0x1d6b > idVendor # Linux Foundationecho 0x0104 > idProduct # Multifunction Composite Gadgetecho 0x0100 > bcdDevice # v1.0.0echo 0x0200 > bcdUSB # USB2STRINGS_DIR="strings/0x409"mkdir -p "$STRINGS_DIR"echo "6b65796d696d6570690" > "${STRINGS_DIR}/serialnumber"echo "keymimepi" > "${STRINGS_DIR}/manufacturer"echo "Generic USB Keyboard" > "${STRINGS_DIR}/product"
首先定义了设备的协议为USB2.0,然后设置了供应商ID(0x1d6b)和产品的ID(0x0104),接着定义了bNumConfigurations
配置描述符数量为0x01
,剩下的都是一些字符串标识信息。
供应商信息大部分情况下作为标识信息作用,但是后续的文章中会发现有些驱动靠识别指定的供应商和产品ID来触发。目前只是模拟鼠标/键盘设备,它们不依靠这些ID触发,所以可以随意修改。
接下来主机将会向设备请求设备描述符中指定数量的配置描述符。
配置描述符的结构体定义为于:linux/include/uapi/linux/usb/ch9.h
,结构体如下:
struct usb_config_descriptor { __u8 bLength; __u8 bDescriptorType; __le16 wTotalLength; __u8 bNumInterfaces; __u8 bConfigurationValue; __u8 iConfiguration; __u8 bmAttributes; __u8 bMaxPower;} __attribute__ ((packed));
首先查看USBPcap上捕获到的配置描述符,如图10所示:
图10:主机发起的获取配置描述符请求
主机首先会请求固定长度为9的配置描述符,如图11所示:
图11:Wireshark上查看配置描述符
然后主机通过配置描述符的wTotalLength
字段,得知配置描述符的实际长度,接着主机会向USB设备请求完整的配置描述符,如图12,图13所示:
图12:主机发起的获取配置描述符请求
图13:USB设备响应完整的配置描述符数据包
从USBPcap捕获到的流量中可以发现,在配置描述符的响应包里,除了配置描述符的信息,还包含了接口描述符,端点描述符,并且因为USB键盘注册的是一个USB HID设备,所以在配置描述符中还包含着HID描述符,如图14所示:
图14:Wireshark中查看配置描述符
使用USB Tree View查看配置描述符,如图15所示:
图15:USB Tree View中查看配置描述符
接下来讲解一下在配置描述符中的几个字段:
bmAttributes
:该字段是控制电源的相关属性,是告诉主机设备是自供电还是需要USB主机供电,设备是否可以通过USB远程唤醒。
MaxPower
,该字段的目的是告诉主机,USB设备能接受的最高电流。
bNumInterfaces
:该字段再次让主机知道,USB设备有几个配置描述符。
bConfigurationValue
,该值为配置描述符的序号,用在Set Configuration Request
里,用于通知USB设置,哪一个USB配置描述符在主机的内核中注册成功了。
分析完配置描述符后,可以得知控制配置描述符在bash
脚本中的代码如下所示:
CONFIG_INDEX=1CONFIGS_DIR="configs/c.${CONFIG_INDEX}"mkdir -p "$CONFIGS_DIR"echo 250 > "${CONFIGS_DIR}/MaxPower"CONFIGS_STRINGS_DIR="${CONFIGS_DIR}/strings/0x409"mkdir -p "$CONFIGS_STRINGS_DIR"echo "Config ${CONFIG_INDEX}: ECM network" > "${CONFIGS_STRINGS_DIR}/configuration"
接口描述符的结构体定义为于:linux/include/uapi/linux/usb/ch9.h
,结构体如下:
/* USB_DT_INTERFACE: Interface descriptor */struct usb_interface_descriptor { __u8 bLength; __u8 bDescriptorType; __u8 bInterfaceNumber; __u8 bAlternateSetting; __u8 bNumEndpoints; __u8 bInterfaceClass; __u8 bInterfaceSubClass; __u8 bInterfaceProtocol; __u8 iInterface;} __attribute__ ((packed));
首先查看USBPcap上捕获到的接口描述符,如图16所示:
图16:Wireshark中查看接口描述符
再查看USBTree View
上的接口描述符信息,如图17所示:
图17:USB Tree View中查看接口描述符
在接口描述符中,字段的含义如下所示:
bNumEndpoints
定义了设备端点的数量,在USB协议中,端点的通信是单向的,在这里定义了两个端点描述符,一个表示的是输入,一个表示的是输出。
bInterfaceClass
定义了接口的类型,在上图中定义的是一个HID设备,所以主机会再去读取HID描述符。
bInterfaceProtocol
定义了接口协议为键盘,这样就会让键盘的HID驱动来处理后续通信。
bInterfaceClass
值对应的含义可以参考Linux内核源码(同样是在ch9.h中定义):
/* * Device and/or Interface Class codes * as found in bDeviceClass or bInterfaceClass * and defined by www.usb.org documents */#define USB_CLASS_PER_INTERFACE 0 /* for DeviceClass */#define USB_CLASS_AUDIO 1#define USB_CLASS_COMM 2#define USB_CLASS_HID 3#define USB_CLASS_PHYSICAL 5#define USB_CLASS_STILL_IMAGE 6#define USB_CLASS_PRINTER 7#define USB_CLASS_MASS_STORAGE 8#define USB_CLASS_HUB 9#define USB_CLASS_CDC_DATA 0x0a#define USB_CLASS_CSCID 0x0b /* chip+ smart card */#define USB_CLASS_CONTENT_SEC 0x0d /* content security */#define USB_CLASS_VIDEO 0x0e#define USB_CLASS_WIRELESS_CONTROLLER 0xe0#define USB_CLASS_PERSONAL_HEALTHCARE 0x0f#define USB_CLASS_AUDIO_VIDEO 0x10#define USB_CLASS_BILLBOARD 0x11#define USB_CLASS_USB_TYPE_C_BRIDGE 0x12#define USB_CLASS_MISC 0xef#define USB_CLASS_APP_SPEC 0xfe#define USB_CLASS_VENDOR_SPEC 0xff#define USB_SUBCLASS_VENDOR_SPEC 0xff
bInterfaceProtocol
在Linux源码中只定义了鼠标和键盘,如下图所示,其他设备的值设为0,或者由厂商开发的驱动定义,如图18所示:
图18:Linux源码中关于bInterfaceProtocol值的宏定义
端点描述符的结构体定义为于:linux/include/uapi/linux/usb/ch9.h
,结构体如下:
/* USB_DT_ENDPOINT: Endpoint descriptor */struct usb_endpoint_descriptor { __u8 bLength; __u8 bDescriptorType; __u8 bEndpointAddress; __u8 bmAttributes; __le16 wMaxPacketSize; __u8 bInterval; /* NOTE: these two are _only_ in audio endpoints. */ /* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */ __u8 bRefresh; __u8 bSynchAddress;} __attribute__ ((packed));
在USBPcap
上查看捕获到的端点描述符的信息,如图19所示:
图19:Wireshark中查看端点描述符
在端点描述符中定义了端口号,和该端口的方向,还有使用该端口进行通信的方式(上图中定义的通信方式为中断传输),通信数据包的最大尺寸(8)字节。
接着我列出了Linux Gadget目录的文件结构,如下所示:
$ tree.├── bcdDevice├── bcdUSB├── bDeviceClass├── bDeviceProtocol├── bDeviceSubClass├── bMaxPacketSize0├── configs│?? └── c.1│?? ├── bmAttributes│?? ├── hid.usb0 -> ../../../../usb_gadget/g1/functions/hid.usb0│?? ├── MaxPower│?? └── strings│?? └── 0x409│?? └── configuration├── functions│?? └── hid.usb0│?? ├── dev│?? ├── no_out_endpoint│?? ├── protocol│?? ├── report_desc│?? ├── report_length│?? └── subclass├── idProduct├── idVendor├── max_speed├── os_desc│?? ├── b_vendor_code│?? ├── qw_sign│?? └── use├── strings│?? └── 0x409│?? ├── manufacturer│?? ├── product│?? └── serialnumber└── UDC
经过研究发现,可以通过no_out_endpoint
文件,控制端点数,默认情况下有IN/OUT两个端点,如果no_out_endpoint
文件的值为1,端点描述符就只有一个IN端点。另一个report_length
文件,用来控制传输数据包的最大长度,也就是bMaxPacketSize
字段。
最后一个是字符串描述符,结构体的定义也是位于ch9.h
:
struct usb_string_descriptor { __u8 bLength; __u8 bDescriptorType; __le16 wData[1]; /* UTF-16LE encoded */} __attribute__ ((packed));
字符串描述符的作用主要是标识信息,比如在USB Tree View
上显示的USB设备信息,都是通过字符串描述符获取的。
可以在USBTree View
中查看所有字符串描述符,如图20所示:
图20:USB Tree View中查看字符串描述符
在接口描述符中,iInterface
字段的值就是字符串描述符的偏移。
当USB主机通过接口描述符得知USB设备是USB HID设备时,将会再获取HID报告描述符,在USBPcap中捕获到的HID报告描述符如图21所示:
图21:Wireshark查看HID报告描述符
定义HID报告描述符的代码在bash
脚本中如下所示:
# Write the report descriptor# Source: https://www.kernel.org/doc/html/latest/usb/gadget_hid.htmlecho -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > "${FUNCTIONS_DIR}/report_desc"
示例中的HID报告描述符来源于Linux内核示例,如图22所示:
图22:Linux USB HID gadget 驱动文档
所以下一步我们需要能顺利阅读HID报告描述符,可以参考官方文档,官方文档的优点是内容齐全,缺点内容是多,对于新手来说,不太适合入门,官方文档更适合入门后作为参考手册。
我们先查看key-mime-pi
项目的通信代码,树莓派如何告诉主机,哪些按键被控制了,相关代码如下所示:
def send(hid_path, control_keys, hid_keycode): with open(hid_path, 'wb+') as hid_handle: # hid_path = "/dev/hidg0" buf = [0] * 8 buf[0] = control_keys buf[2] = hid_keycode hid_handle.write(bytearray(buf)) hid_handle.write(bytearray([0] * 8))
每次操作按键需要向USB主机发送两个8字节的buf(在端点描述符里限制了最大的包大小为8字节)。第二个buf的8字节全都置为0,第一个buf的第一个字节为控制字符,经过研究得知,设置了8个控制字符,如下所示:
#define KEY_LEFTCTRL 0xe0 // Keyboard Left Control#define KEY_LEFTSHIFT 0xe1 // Keyboard Left Shift#define KEY_LEFTALT 0xe2 // Keyboard Left Alt#define KEY_LEFTMETA 0xe3 // Keyboard Left GUI#define KEY_RIGHTCTRL 0xe4 // Keyboard Right Control#define KEY_RIGHTSHIFT 0xe5 // Keyboard Right Shift#define KEY_RIGHTALT 0xe6 // Keyboard Right Alt#define KEY_RIGHTMETA 0xe7 // Keyboard Right GUI
GUI
是windows的win键,mac的command。
第一个buf的第二个字节不设置,默认为0,第三字节到第8字节,长度为6字节,为输入的按键。
在大致了解了如何向USB主机发送数据后,再来看看HID的报告描述符:
static struct hidg_func_descriptor my_hid_data = { .subclass = 0, /* No subclass */ .protocol = 1, /* Keyboard */ .report_length = 8, .report_desc_length = 63, .report_desc = { 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x06, /* USAGE (Keyboard) */ 0xa1, 0x01, /* COLLECTION (Application) */ 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ 0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */ 0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x95, 0x08, /* REPORT_COUNT (8) */ 0x81, 0x02, /* INPUT (Data,Var,Abs) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x81, 0x03, /* INPUT (Cnst,Var,Abs) */ 0x95, 0x05, /* REPORT_COUNT (5) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x05, 0x08, /* USAGE_PAGE (LEDs) */ 0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */ 0x29, 0x05, /* USAGE_MAXIMUM (Kana) */ 0x91, 0x02, /* OUTPUT (Data,Var,Abs) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x03, /* REPORT_SIZE (3) */ 0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */ 0x95, 0x06, /* REPORT_COUNT (6) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x65, /* LOGICAL_MAXIMUM (101) */ 0x05, 0x07, /* USAGE_PAGE (Keyboard) */ 0x19, 0x00, /* USAGE_MINIMUM (Reserved) */ 0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */ 0x81, 0x00, /* INPUT (Data,Ary,Abs) */ 0xc0 /* END_COLLECTION */ }};
首先是USAGE_PAGE
和USAGE
,这两个字段可以看参考文档,都是目前我们只能选取定义好的那些应用,会影响到一些驱动的功能识别。
主要看集合部分的内容,集合以COLLECTION
开始END_COLLECTION
结束。
集合的内容可以分为四部分,第一部分如下所示:
0x05, 0x07, /* USAGE_PAGE (Keyboard) */0x19, 0xe0, /* USAGE_MINIMUM (Keyboard LeftControl) */0x29, 0xe7, /* USAGE_MAXIMUM (Keyboard Right GUI) */0x15, 0x00, /* LOGICAL_MINIMUM (0) */0x25, 0x01, /* LOGICAL_MAXIMUM (1) */0x75, 0x01, /* REPORT_SIZE (1) */0x95, 0x08, /* REPORT_COUNT (8) */0x81, 0x02, /* INPUT (Data,Var,Abs) */
第一部分指定键盘的功能按键,最小值为Keyboard LeftControl
(0xe0),最大值为Keyboard Right GUI
(0xe7)。逻辑最小值为0,最大值为1,1表示按下,0表示释放。一个按键占1bit,有8个按键,一共占1字节。比如:0b00000001表示LeftControl
键被按下了。从这看,我们可以一次性把8个控制键都按下。
第二部分如下所示:
0x95, 0x01, /* REPORT_COUNT (1) */0x75, 0x08, /* REPORT_SIZE (8) */0x81, 0x03, /* INPUT (Cnst,Var,Abs) */
有8个1bit的值,总共1字节,并且是常量(Cnst全程是constant),上下文没看到有设置值,所以认为是默认值0,发送数据的时候就算不发送0也不影响,因为驱动不会主动识别该字节的数据。
第三部分如下所示:
0x95, 0x05, /* REPORT_COUNT (5) */0x75, 0x01, /* REPORT_SIZE (1) */0x05, 0x08, /* USAGE_PAGE (LEDs) */0x19, 0x01, /* USAGE_MINIMUM (Num Lock) */0x29, 0x05, /* USAGE_MAXIMUM (Kana) */0x91, 0x02, /* OUTPUT (Data,Var,Abs) */0x95, 0x01, /* REPORT_COUNT (1) */0x75, 0x03, /* REPORT_SIZE (3) */0x91, 0x03, /* OUTPUT (Cnst,Var,Abs) */
上述的HID描述符定义了一个LED功能,总共占1字节,高3bit为常量0,低5bit表示的键盘上的指示灯,常见的有:小键盘数字锁定灯,大写锁定灯,滚动锁定灯等。并且是由主机发送给设备的,键盘灯光的亮灭由USB主机来控制。
第四部分如下所示:
0x95, 0x06, /* REPORT_COUNT (6) */0x75, 0x08, /* REPORT_SIZE (8) */0x15, 0x00, /* LOGICAL_MINIMUM (0) */0x25, 0x65, /* LOGICAL_MAXIMUM (101) */0x05, 0x07, /* USAGE_PAGE (Keyboard) */0x19, 0x00, /* USAGE_MINIMUM (Reserved) */0x29, 0x65, /* USAGE_MAXIMUM (Keyboard Application) */0x81, 0x00, /* INPUT (Data,Ary,Abs) */
定义了键盘功能,按键的值从0-0x65,一共有102个键,逻辑值也是从0-0x65,一个按键占1字节,最多可以有6个按键,总共占6字节。
到这里键盘的HID报告描述符的定义就分析完了,我们发现该描述符定义的内容和我们的输入数据的格式是吻合的。
发送的buf第一字节就是表示8个控制按键,第二字节固定为0,后面6个字节为输入按键。
这个时候产生了两个问题:
1. 测试模拟的键盘是104键的键盘,为什么有102+8=110个值?经过研究发现如下所示:
#define KEY_NONE 0x00 // No key pressed#define KEY_ERR_OVF 0x01 // Keyboard Error Roll Over - used for all slots if too many keys are pressed ("Phantom key")// 0x02 // Keyboard POST Fail// 0x03 // Keyboard Error Undefined#define KEY_102ND 0x64 // Keyboard Non-US \ and |#define KEY_BACKSLASH 0x31 // Keyboard \ and |// 上面这两个冲突了,多了一个#define KEY_HASHTILDE 0x32 // Keyboard Non-US # and ~// 估计我键盘没有Non-US相关的键
根据上面的计算的得知,刚好是104键的键盘。
2. 为什么需要发送一个全为0的数据包,经过研究发现:USB设备发给USB主机的数据包是键盘在告知USB主机键盘当前的状态,一个完整的按键操作是按下按键,然后释放按键。发送的一个数据包是告知主机哪些按键被按下了,第二个全为0的数据包是告知主机所有按键已经被释放。
3 下一步研究方向
参考资
本篇文章的研究到此告一段落,接下来的文章内容考虑对以下几个方向进行研究:
微调HID报告描述符,看看对实际使用有什么影响。
通过修改接口描述符字段和HID报告描述符字段,来模拟一个鼠标。
研究一下手柄,讲道理手柄也是使用HID协议,但是Linux的代码里没看到相关定义。
研究非HID协议,比如U盘,网卡,打印机这些。
研究驱动系统细节,在Linux内核的drivers目录下,可以搜索module_usb_driver字符串,这是一个宏定义函数,usb主机端的驱动都是通过该函数注册到内核当中的。
4 参考链接
参考资料
[1] https://github.com/mtlynch/key-mime-pi
[2] https://zhuanlan.zhihu.com/p/558716468
[3] https://www.kernel.org/doc/html/latest/usb/gadget\_hid.html
[4] https://usb.org/sites/default/files/hut1\_4.pdf
作者名片
往 期 热 门
(点击图片跳转)
戳“阅读原文”更多精彩内容!