长亭百川云 - 文章详情

原创 Paper | USB设备开发:从入门到实践指南(一)

知道创宇404实验室

88

2024-07-13

作者:Hcamael****@知道创宇404实验室********

时间:2024年2月27日

在使用 Google 搜索相关学习资料的过程中,搜到一本书——《圈圈教你玩 USB》,在阅读中发现需要购买相关硬件设备。

硬件开发部分考虑在后期的内容中再进行学习研究,前期考虑拿手头上已有设备来进行学习研究。首先考虑的是使用手机作为USB设备连接电脑,但是研究后发现,安卓对USB设备开发的支持不是很友好,再加上在安卓层对USB开发进行封装,会导致我们不太好理解USB底层的细节。

随后,考虑到手头具备树莓派4b设备,决定尝试利用该设备进行USB设备开发。在进行Google搜索后,发现了一个名为key-mime-pi的项目,可作为我入门的起点。

1 USB设备开发相关工具

参考资料

经过一番研究,因为Windows上USB相关的工具可视化做的更好,所以最后选择Windows设备作为USB主机。

1.1 USB Tree View

第一个工具是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描述符

1.2 Wireshark

Windows上的wireshark在装上USBPcap后,能够抓取主机控制器上的USB流量,比如主机上有三个主机控制器,所以在Wireshark中就显示有三个USBPcap,如图4所示:

图4:Wireshark界面

如果不知道USB设置插入的USB口是属于哪一个主机控制器,可以使用USBTree View查看。不过由于Wireshark是抓取主机控制器上的流量,而一个USB主机控制器可以连接多个USB设备,所以当我要研究某一个USB设备时,需要通过Wireshark的过滤表达式对该主机控制器上的其他USB设备的流量进行过滤。

1.3 Bus Hound

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/dwc2linux/drivers/usb/gadget目录下。

该驱动细节在本文暂不进行研究。在运行上面的bash脚本后,如无意外,可以在USB Tree View中查看到树莓派设备模拟出的USB键盘。

2.1 通过流量再学习理解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上的某个设备,但是USBPcapUSBTreeView对设备编号的方法是不同的。USBPcap是按照顺序来定义设备号,而USBTreeView则在开始时已经为所有USB接口进行了编号,无论是否连接了USB设备。

按照我的理解,USBPcap可能更符合底层实际情况,因为它不是自己编的号,而是解析了抓到的USB流量。

z的值表示的是端点号(Endpoint),我觉得有点像一个程序的文件描述符(fd),USB主机和设备间就是通过端点号来进行通信的,当USB设备还未在主机上注册时,默认使用0端点号来进行通信。

2.1.1 设备描述符

接下来对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触发,所以可以随意修改。

接下来主机将会向设备请求设备描述符中指定数量的配置描述符。

2.1.2 配置描述符

配置描述符的结构体定义为于: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"

2.1.3 接口描述符

接口描述符的结构体定义为于: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值的宏定义

2.1.4 端点描述符

端点描述符的结构体定义为于: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字段。

2.1.5 字符串描述符

最后一个是字符串描述符,结构体的定义也是位于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字段的值就是字符串描述符的偏移。

2.1.6 HID报告描述符

当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报告描述符,可以参考官方文档,官方文档的优点是内容齐全,缺点内容是多,对于新手来说,不太适合入门,官方文档更适合入门后作为参考手册。

2.1.6.1 解析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_PAGEUSAGE,这两个字段可以看参考文档,都是目前我们只能选取定义好的那些应用,会影响到一些驱动的功能识别。

主要看集合部分的内容,集合以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

作者名片

往 期 热 门

(点击图片跳转)

戳“阅读原文”更多精彩内容!

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备2024055124号-2