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

admin 2024年2月27日17:38:00评论9 views字数 14765阅读49分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所示:

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

图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所示:

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

图2:在USB Tree View上查看USB设备详细信息

如果理解上图中各部分信息,后文再进行说明。不过该工具也存在一些BUG,该工具无法正常解析HID信息,如图3所示:

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

图3:HID描述符

1.2 Wireshark

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

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

图4:Wireshark界面

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

1.3 Bus Hound

Bus Hound可以抓取指定USB设备的流量,不过该工具的缺点是需要花钱,否则抓包size和数量都有限制,目前尚未找到该软件的破解版本。图5是该软件的界面:

原创 Paper | USB设备开发:从入门到实践指南(一)图5:Bus Hound界面

2 使用树莓派4b作为PC的USB键盘

参考资料

接下来,通过阅读key-mime-pi项目的源代码,发现使用树莓派4b设备模拟一个USB设备是一件非常容易的事情。根据该项目的代码,可以分解出以下两个步骤:

1.需要开启dwc2驱动:在树莓派的config.txt中添加dtoverlay=dwc2,设备启动后,确认一下启动是否开启:

$ lsmod|grep dwc2
dwc2                  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 -u

modprobe libcomposite

cd /sys/kernel/config/usb_gadget/
mkdir -p g1
cd g1

echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2

STRINGS_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" # Keyboard
echo 0 > "${FUNCTIONS_DIR}/subclass" # No subclass
echo 8 > "${FUNCTIONS_DIR}/report_length"
# Write the report descriptor
# Source: https://www.kernel.org/doc/html/latest/usb/gadget_hid.html
echo -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=1
CONFIGS_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 > UDC

chmod 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所示,是本文环境中的过滤语法:

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

图6:Wireshark过滤语法
然后在树莓派上以root权限运行上面提供的bash脚本,再查看Wireshark上捕获到的流量,如图7所示:

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

图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所示:

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

图8:Wireshark上查看设备描述符

再对比一下USBTree View上显示的设备描述符信息,如图9所示:

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

图9:USB Tree View上查看设备描述符

通过对比发现,控制设备描述符的位于bash脚本的以下几行代码:

echo 0x1d6b > idVendor  # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB    # USB2

STRINGS_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所示:

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

图10:主机发起的获取配置描述符请求

主机首先会请求固定长度为9的配置描述符,如图11所示:

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

图11:Wireshark上查看配置描述符

然后主机通过配置描述符的wTotalLength字段,得知配置描述符的实际长度,接着主机会向USB设备请求完整的配置描述符,如图12,图13所示:

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

图12:主机发起的获取配置描述符请求

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

图13:USB设备响应完整的配置描述符数据包

从USBPcap捕获到的流量中可以发现,在配置描述符的响应包里,除了配置描述符的信息,还包含了接口描述符,端点描述符,并且因为USB键盘注册的是一个USB HID设备,所以在配置描述符中还包含着HID描述符,如图14所示:

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

图14:Wireshark中查看配置描述符

使用USB Tree View查看配置描述符,如图15所示:

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

图15:USB Tree View中查看配置描述符

接下来讲解一下在配置描述符中的几个字段:

  • bmAttributes:该字段是控制电源的相关属性,是告诉主机设备是自供电还是需要USB主机供电,设备是否可以通过USB远程唤醒。
  • MaxPower,该字段的目的是告诉主机,USB设备能接受的最高电流。
  • bNumInterfaces:该字段再次让主机知道,USB设备有几个配置描述符。
  • bConfigurationValue,该值为配置描述符的序号,用在Set Configuration Request里,用于通知USB设置,哪一个USB配置描述符在主机的内核中注册成功了。

分析完配置描述符后,可以得知控制配置描述符在bash脚本中的代码如下所示:

CONFIG_INDEX=1
CONFIGS_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所示:

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

图16:Wireshark中查看接口描述符

再查看USBTree View上的接口描述符信息,如图17所示:

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

图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所示:

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

图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所示:

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

图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所示:

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

图20:USB Tree View中查看字符串描述符

在接口描述符中,iInterface字段的值就是字符串描述符的偏移。

2.1.6 HID报告描述符

当USB主机通过接口描述符得知USB设备是USB HID设备时,将会再获取HID报告描述符,在USBPcap中捕获到的HID报告描述符如图21所示:

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

图21:Wireshark查看HID报告描述符

定义HID报告描述符的代码在bash脚本中如下所示:

# Write the report descriptor
# Source: https://www.kernel.org/doc/html/latest/usb/gadget_hid.html
echo -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所示:

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

图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

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

作者名片

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

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

原文始发于微信公众号(知道创宇404实验室):原创 Paper | USB设备开发:从入门到实践指南(一)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月27日17:38:00
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   原创 Paper | USB设备开发:从入门到实践指南(一)http://cn-sec.com/archives/2530508.html

发表评论

匿名网友 填写信息