告别抓瞎!手把手教你用Python脚本模拟USB主机获取设备描述符

张开发
2026/4/20 13:27:49 15 分钟阅读

分享文章

告别抓瞎!手把手教你用Python脚本模拟USB主机获取设备描述符
用Python脚本解码USB设备从底层协议到实战解析USB设备早已渗透进我们数字生活的每个角落——从键盘鼠标到移动硬盘这些看似简单的硬件背后都遵循着一套复杂的通信协议。但你是否好奇过当插入U盘的瞬间电脑究竟是如何识别它的本文将带你用Python直接与USB设备对话通过发送标准请求获取原始描述符数据揭开硬件通信的神秘面纱。1. 环境准备与工具链搭建要直接与USB设备通信我们需要绕过操作系统的高级抽象层直接访问底层协议。Python的pyusb库提供了这样的能力但它依赖于libusb这个跨平台的USB访问库。安装基础依赖Ubuntu/Debian示例sudo apt-get install libusb-1.0-0-dev pip install pyusb在Windows系统上除了安装libusb的驱动程序外还需要特别注意设备权限问题。USB设备的访问通常需要管理员权限但我们可以通过设置设备权限规则来避免每次都需要sudoimport usb.core dev usb.core.find(idVendor0x1234, idProduct0x5678) dev.set_configuration()提示如果遇到权限错误可以创建/etc/udev/rules.d/50-myusb.rules文件添加以下内容后重新插拔设备SUBSYSTEMusb, ATTR{idVendor}1234, ATTR{idProduct}5678, MODE06662. 发现与识别USB设备连接上USB设备后第一步是定位它在USB总线上的住址。每个USB设备在连接时都会被分配一个唯一的设备地址1-127我们需要先扫描总线找到目标设备。典型设备发现代码import usb.core # 列出所有USB设备 for device in usb.core.find(find_allTrue): print(f发现设备: {hex(device.idVendor)}:{hex(device.idProduct)}) print(f制造商: {usb.util.get_string(device, device.iManufacturer)}) print(f产品名: {usb.util.get_string(device, device.iProduct)})设备描述符中的几个关键字段字段名描述示例值idVendor厂商ID0x0781 (SanDisk)idProduct产品ID0x5567 (Ultra Fit)bDeviceClass设备类0x00 (由接口定义)bNumConfigurations配置数13. 构造Get Descriptor请求USB协议定义了标准请求格式Get Descriptor是最基础且重要的请求之一。我们需要手动构造这个请求包包含以下关键字段bmRequestType请求方向(0x80表示设备到主机)、请求类型(标准请求)和接收方(设备)bRequest请求代码Get Descriptor对应0x06wValue高字节为描述符类型低字节为描述符索引wIndex语言ID(仅字符串描述符)或0wLength期望返回的数据长度Python实现请求构造def get_descriptor(dev, desc_type, desc_index0, langid0, length255): return dev.ctrl_transfer( 0x80, # bmRequestType (设备到主机) 0x06, # bRequest (GET_DESCRIPTOR) (desc_type 8) | desc_index, # wValue langid, # wIndex length # wLength )常见描述符类型代码描述符类型值说明DEVICE1设备描述符CONFIGURATION2配置描述符STRING3字符串描述符INTERFACE4接口描述符ENDPOINT5端点描述符4. 解析原始描述符数据获取到的描述符数据是原始的二进制格式需要按照USB规范进行解析。以设备描述符为例它的结构固定为18字节各字段含义如下设备描述符解析示例data get_descriptor(dev, 1) # 获取设备描述符 descriptor { bLength: data[0], bDescriptorType: data[1], bcdUSB: (data[3] 8) | data[2], bDeviceClass: data[4], bDeviceSubClass: data[5], bDeviceProtocol: data[6], bMaxPacketSize: data[7], idVendor: (data[9] 8) | data[8], idProduct: (data[11] 8) | data[10], bcdDevice: (data[13] 8) | data[12], iManufacturer: data[14], iProduct: data[15], iSerialNumber: data[16], bNumConfigurations: data[17] } print(fUSB版本: {hex(descriptor[bcdUSB])}) print(f厂商ID: {hex(descriptor[idVendor])}) print(f产品ID: {hex(descriptor[idProduct])})字符串描述符的获取稍显特殊需要先获取支持的语言ID列表然后再用特定语言ID请求字符串内容# 首先获取支持的语言ID langids get_descriptor(dev, 3, 0, 0, 255) langid_list [langids[i] | (langids[i1] 8) for i in range(2, langids[0], 2)] # 然后获取特定字符串 string get_descriptor(dev, 3, descriptor[iProduct], langid_list[0], 255) product_name string[2:2string[0]-2].decode(utf-16le)5. 深入配置描述符与接口设备描述符只是冰山一角更丰富的信息藏在配置描述符中。一个配置描述符实际上是一个描述符集合包含配置描述符本身接口描述符端点描述符可能的类特定描述符完整配置描述符解析流程# 获取配置描述符总长度 config_desc get_descriptor(dev, 2, 0, 0, 4) total_length (config_desc[3] 8) | config_desc[2] # 获取完整配置描述符集 full_config get_descriptor(dev, 2, 0, 0, total_length) # 解析过程需要遍历描述符集合 position 0 while position len(full_config): length full_config[position] desc_type full_config[position1] if desc_type 2: # 配置描述符 print(f配置值: {full_config[position5]}) print(f最大功耗: {full_config[position8]}mA) elif desc_type 4: # 接口描述符 print(f接口号: {full_config[position2]}) print(f端点数: {full_config[position4]}) elif desc_type 5: # 端点描述符 print(f端点地址: {hex(full_config[position2])}) print(f最大包大小: {full_config[position4]}) position length6. 实战USB键盘协议解析让我们以一个实际的USB键盘为例看看如何解析其HID报告描述符。HID人机接口设备类设备有特殊的描述符结构# 首先找到HID接口 for cfg in dev: for intf in cfg: if intf.bInterfaceClass 3: # HID类 print(找到HID接口!) hid_desc get_descriptor(dev, 0x21, intf.bInterfaceNumber) # HID描述符 report_length (hid_desc[7] 8) | hid_desc[6] report_desc dev.ctrl_transfer( 0x81, 0x06, 0x2200, intf.bInterfaceNumber, report_length ) print(HID报告描述符:, report_desc)HID报告描述符定义了设备如何打包和解释数据。虽然解析它需要理解HID规范但我们可以观察到一些关键信息输入报告的长度按键数据使用的用法页键盘、鼠标等修饰键和普通键的位域布局7. 高级技巧与错误处理在实际操作中我们会遇到各种边界情况和错误。以下是一些实用技巧超时处理try: data dev.ctrl_transfer(..., timeout1000) # 1秒超时 except usb.core.USBError as e: if e.errno 110: # 操作超时 print(设备响应超时请检查连接)多配置设备处理if dev.bNumConfigurations 1: print(设备支持多种配置:) for i in range(dev.bNumConfigurations): cfg dev[i] print(f配置{i}: {cfg.bConfigurationValue}) selected int(input(选择配置(0-{}): .format(dev.bNumConfigurations-1))) dev.set_configuration(dev[selected])批量传输端点通信# 找到批量传输端点 endpoint_in None endpoint_out None for cfg in dev: for intf in cfg: for ep in intf: if usb.util.endpoint_direction(ep.bEndpointAddress) usb.util.ENDPOINT_IN: endpoint_in ep else: endpoint_out ep # 进行批量传输 if endpoint_in: data dev.read(endpoint_in.bEndpointAddress, endpoint_in.wMaxPacketSize, timeout1000) print(收到数据:, data)通过这套方法我们不仅能读取标准USB描述符还能与设备进行更复杂的交互。比如对U盘发送SCSI命令或者与自定义USB设备通信。这种底层访问能力为设备调试、逆向工程和自动化测试打开了新的大门。

更多文章