教程长篇分享

USB HID 流量分析

USB HID(Human Interface Device),中文译为人机接口设备,是一种允许人与计算机交互的接口的设备,主要用于连接各种人机界面设备,如键盘、鼠标、游戏手柄、数字仪表、触摸屏等。USB HID 设备与计算机通信使用的是 USB HID 协议,这个协议规定了 USB HID 设备与主机之间的通信协议和数据格式。

本文将对 USB HID 协议进行简单的介绍,演示如何用 Wireshark 捕获、过滤 USB 流量,重点对鼠标、键盘流量进行分析,利用 Python 脚本解析流量,从中还原出鼠标轨迹和键盘输入信息。文章比较长,可以根据自己需要查看对应部分的内容。

工具

  1. USB 流量嗅探工具

    • USBPcap(Windows)

    • usbmon(Linux)

  2. Wireshark

  3. Python3

查看 USB 信息

Windows

Windows 下查看 USB 信息没有 Linux 下那么直观。

设备管理器

最简单的方法是使用“设备管理器”:按 Win 打开“开始菜单”并键入“设备管理器”;或通过“运行”对话框打开“设备管理器”,方法是按 Win+R 并执行devmgmt.msc命令。

Get-PnpDevice 命令

可以用 Powershell 的 Get-PnpDevice 命令来列出所有连接的 USB 设备:

PS C:\> Get-PnpDevice -PresentOnly | Where-Object { $_.InstanceId -match '^USB' }
- sample output -
Status       Class           FriendlyName                            InstanceId
------       -----           ------------                            ----------
OK           USB             USB Composite Device                    USB\VID_0408...
OK           USB             USB Root Hub (USB 3.0)                  USB\ROOT_HUB...
OK           HIDClass        USB Input Device                        USB\VID_046D...
OK           USB             Generic USB Hub                         USB\VID_05E3...
OK           USB             USB Composite Device                    USB\VID_046D...
OK           Camera          HP HD Camera                            USB\VID_0408...
OK           Bluetooth       Intel(R) Wireless Bluetooth(R)          USB\VID_8087...
OK           USB             Generic SuperSpeed USB Hub              USB\VID_05E3...
OK           HIDClass        USB Input Device                        USB\VID_046D...

要列出 USB 设备完整的信息(包括厂商 ID 和设备 ID)可以使用 Format-List 过滤器:

PS C:\> Get-PnpDevice -PresentOnly | Where-Object { $_.InstanceId -match '^USB' } | Format-List
- sample output -
...
Caption                     : USB Input Device
Description                 : USB Input Device
InstallDate                 :
Name                        : USB Input Device
Status                      : OK
Availability                :
ConfigManagerErrorCode      : CM_PROB_NONE
ConfigManagerUserConfig     : False
CreationClassName           : Win32_PnPEntity
DeviceID                    : USB\VID_046D&PID_C535&MI_00\7&11A3A82D&1&0000
ErrorCleared                :
ErrorDescription            :
LastErrorCode               :
PNPDeviceID                 : USB\VID_046D&PID_C535&MI_00\7&11A3A82D&1&0000
PowerManagementCapabilities :
PowerManagementSupported    :
StatusInfo                  :
SystemCreationClassName     : Win32_ComputerSystem
SystemName                  : DESKTOP-0041
ClassGuid                   : {745a17d0-73d3-11f0-b7fe-01a0c90f47da}
CompatibleID                : {USB\Class_03&SubClass_01&Prot_01, USB\Class_03&SubClass_01}
HardwareID                  : {USB\VID_046D&PID_C535, USB\VID_046D&PID_C535}
Manufacturer                : (Standard system devices)
PNPClass                    : HIDClass
Present                     : True
Service                     : HidUsb
PSComputerName              :
Class                       : HIDClass
FriendlyName                : USB Input Device
InstanceId                  : USB\VID_046D&PID_C535&MI_00\6&11B3A84D&1&0000
Problem                     : CM_PROB_NONE
ProblemDescription          :

Linux

在 Linux 中,可以使用 lsusb 命令显示系统中以及连接到系统的 USB 总线信息,如图所示:

lsusb

每一行代表一个 USB 设备,其中包含以下信息:

  • Bus: USB 设备所连接的 USB 总线的编号,如Bus 001表示连接到 USB 总线 1 上。
  • Device: 设备的编号,如Device 003表示连接到总线上的第三个设备。
  • ID: 设备的厂商 ID 和产品 ID,格式为Vendor ID:Product ID
  • Description: 设备的描述信息,如设备的名称或制造商等。

用 Wireshark 捕获 USB 流量

Windows

Windows 上可以用 USBPcap 来捕获 USB 流量。

windows捕获usb流量

在你的 Wireshark 中,可能会看到有 USBPcap1、USBPcap2、USBPcap3 多个接口。这些 USBPcap 接口每个都代表了一个或多个 USB 设备与计算机之间的通信通道,因此,如果计算机上连接了多个 USB 设备,就可能会看到多个 USBPcap 接口。可以通过选择这些接口来捕获特定的 USB 设备或 USB hub 上的所有设备的通信数据包或者直接选择所有可用的接口。

如果想知道每个接口下具体包含哪些设备,可通过 USBPcapCMD.exe(在 USBPcap 目录和 Wireshark\extcap 目录下)来查看。

USBPcapCMD

Linux

从 Wireshark 1.2.0、libpcap 1.0.0 和 Linux 2.6.11 开始,可以使用 Linux usbmon 接口在 Linux 上捕获 USB 流量。具体步骤如下:

检查是否属于 wireshark 组中:

groups $USER

如果不在,可通过以下命令加入:

sudo adduser $USER wireshark

确保非超级用户也可以捕获流量,输入以下命令之后选择 ``:

sudo dpkg-reconfigure wireshark-common

以下命令每次重启之后都要重新输入:

载入 usbmon:

sudo modprobe usbmon

授予普通用户对 usbmon 设备的读权限:

sudo setfacl -m u:$USER:r /dev/usbmon*

打开 Wireshark,可以看到有好几个 usbmonX,其中 X 代表的是对应的 USB 总线编号(是从 1 开始的编号),上文查看USB信息部分有提到。usbmon0 比较特殊,它表示所有总线。选择之前你要捕获的设备所在的总线编号对应的 usbmon 或直接选择 usbmon0 即可。

如有其他问题请参考官方文档:https://wiki.wireshark.org/CaptureSetup/USB

用 Wireshark 过滤 USB HID 流量

从这部分开始我们深入具体的流量包,来介绍一些 USB HID 协议的内容。

常见的两种数据传输方式

usb hid流量

在正式介绍 USB HID 协议之前,先解释一下上图,这是我们最初捕获到的几个流量包,其中包含了最常见的两种 USB 数据传输方式:

  • GET DESCRIPTOR
  • URB_INTERRUPT in

GET DESCRIPTOR 和 URB_INTERRUPT in 都属于 USB 数据传输方式,但它们的用途不同,GET DESCRIPTOR 用于获取设备信息,而 URB_INTERRUPT in 用于实时地获取设备数据。

GET DESCRIPTOR

GET DESCRIPTOR 是 USB 设备和主机之间进行控制传输的一种方式,用于获取设备信息(即各种描述符信息)。GET DESCRIPTOR 一般使用控制传输的 SETUP、DATA 和 STATUS 三个阶段来完成。

USB 描述符

USB 描述符是用于描述 USB 设备、接口、端点等属性的数据结构,主要包括以下几种类型:

  1. 设备描述符(Device Descriptor):用于描述 USB 设备的基本属性,如设备厂商 ID、设备产品 ID、设备类别等。
  2. 配置描述符(Configuration Descriptor):用于描述 USB 设备的配置信息,包括设备接口、端点数量、传输类型等信息。
  3. 接口描述符(Interface Descriptor):用于描述USB设备的接口信息,包括接口类别、子类别、协议等信息。
  4. 端点描述符(Endpoint Descriptor):用于描述USB设备的端点信息,包括端点地址、端点类型、端点方向、最大包大小等信息。
  5. 字符串描述符(String Descriptor):用于描述USB设备的字符串信息,如设备名称、制造商名称、产品名称等。
  6. HID 描述符(HID Descriptor):仅用于 HID(Human Interface Device)设备,描述 HID 设备的属性和报告信息。

USB 描述符可以通过USB协议中的 GET_DESCRIPTOR 请求来获取。主机可以向 USB 设备发送 GET_DESCRIPTOR 请求,并指定需要获取的描述符类型和描述符索引号,设备将会返回相应的描述符数据。

设备描述符(DEVICE DESCRIPTOR)

设备描述符

  • bDeviceClass/bDeviceSubClass:HID 设备在其设备描述符中的类/子类值均为 0。
  • idVendor:厂商 ID。
  • idProduct:产品 ID。

接口描述符(INTERFACE DESCRIPTOR)

接口描述符

  • bInterfaceClass:3,标识 HID 设备。
  • bInterfaceSubClass:1 表示设备支持引导协议, 0 表示设备只支持报告协议。
  • bInterfaceProtocol:1表示HID设备是键盘,2表示HID设备是鼠标。

请记住,接口描述符不能手动请求,必须与配置和端点描述符一起获取。

URB_INTERRUPT in

URB_INTERRUPT in

URB_INTERRUPT in 是一种 USB 中断传输方式,用于实时地从 USB 设备中读取数据。它允许 USB 设备向主机发送中断数据包,主机通过轮询的方式来获取数据。URB_INTERRUPT in 的数据传输是非常实时的,因为设备每隔一段时间就会发送一个中断数据包,主机只需要等待这个数据包到达即可获取数据。

HID 报告数据就通过这种方式来传输,即图中的 HID Data 字段。

报告

报告是 USB HID 协议的一种数据结构。报告可以由设备发送给主机,也可以由主机发送给设备。当设备向主机发送报告时,通常包含状态变化信息,例如按键、鼠标移动等;当主机向设备发送报告时,通常包含配置设备的命令,例如设置键盘的 LED。这个协议依赖于标准的USB框架

用来解析还原鼠标移动轨迹、键盘输入信息的那部分数据就是报告。

USB HID 报告所在的字段

根据网上的文章,捕获到的鼠标键盘流量的报告数据放在 Leftover Capture Data 部分,但是我捕获之后发现我的流量包里并没有 Leftover Capture Data 部分。一番搜索得知,是最新版本的 Wireshark 改进了 HID 解码能力,HID 数据会出现在 HID Data 中。但是我发现,导出特定分组时,导出的 pcapng 文件原本的 HID Data 域又变成 Leftover Capture Data 了,这应该是个 bug。

Leftover Capture Data 是 Wireshark 中的一个警告信息,意味着在捕获过程中有一些数据包未被完全捕获。这可能是因为数据包长度超出了捕获缓冲区的大小,或者捕获过程中出现了一些错误导致数据包未能完全捕获。老版本的 Wireshark 并不能完全解析鼠标键盘的数据包,所以将该数据放在了 Leftover Capture Data 部分中;而现在新版的 Wireshark 已经支持了 USB HID 的解析,所以原本的数据被解析成了 HID Data。

HID Data

Leftover Capture Data

USB HID 协议

USB HID 设备主要基于两种协议:

  • 报告协议
  • 引导协议

报告协议的结构通过报告描述符定义;引导协议则采用固定的标准结构,能够保证在启动阶段(操作系统启动前)就与主机通信。

USB HID 设备使用中断传输进行通信,因为它们并不总是传输数据,但当传输时需要非常快速的响应,而且传输的数据通常很小。

报告协议

报告协议通常使用 HID 报告描述符来描述报告的格式和含义。报告描述符描述了报告的长度、报告类型、报告 ID 等信息,以及报告中各个字段的含义。主机通过向设备发送 HID Class-Specific Requests 来获取设备的报告描述符和报告数据。

报告描述符

报告描述符用于定义报告的格式,保证报告能够被主机正确地解析,后面具体讲鼠标、键盘流量解析的时候会给出具体例子。

引导协议

在引导协议中,设备将其状态和操作转换为一组预定义的报告格式,而不是使用报告描述符来描述报告的格式。这些预定义的报告格式通常比较简单,以确保在设备启动阶段能够正确地与主机通信。主机通过向设备发送 HID Class-Specific Requests 来获取设备支持的引导协议和报告数据。

利用过滤器过滤流量

在分析 USB HID 流量时,常用的过滤字段有:

usb.device_address  # Device ID
usb.capdata         # Leftover Capture Data
usbhid.data         # HID Data

一般我们分析的时候会先筛选出特定的设备,通过分析得出对应的设备 ID 后,可用通过usb.device_address语句筛选出特定设备的流量,比如要查看的设备 ID 为 5 时:

usb.device_address == 5

要还原击键信息或鼠标轨迹时,我们一般只关注流量包的数据段信息,对于 HID 类设备来说,这个数据在 Leftover Capture Data 或 HID Data 中,我们可以用以下语句筛选出包含这两个字段的数据包:

usb.capdata || usbhid.data

也可以只过滤出你捕获中有的那一部分,只需要单独使用以下语句:

usb.capdata
usbhid.data

结合起来,过滤含有 Leftover Capture Data 字段的设备 ID 为 5 的流量包:

usb.capdata && usb.device_address == 5

其他同理,更多过滤字段请参考官方文档。

USB 过滤字段参考:https://www.wireshark.org/docs/dfref/u/usb.html

USB HID 过滤字段参考:https://www.wireshark.org/docs/dfref/u/usbhid.html

导出捕获数据

你可以通过文件 -> 保存直接保存所有捕获的数据包。

保存

但是为了更好地控制将导出的内容(例如,仅当前过滤/显示的数据包),通过文件 -> 导出特定分组来导出指定的数据包通常是首选,我们通过这种方式只导出我们上面过滤出来的数据包以便下一步的分析和后续脚本的解析。

导出特定分组

用 tshark 提取报告部分

在命令行下可以使用tshark -help得到选项的简单介绍,具体的需要查阅官方文档https://www.wireshark.org/docs/man-pages/tshark.html。

以下是即将用到的参数的介绍:

输入文件:
  -r: -r <infile> 设置读取本地文件
处理选项:
  -R: -R <read filter>,包的读取过滤器,可以在wireshark的filter语法上查看;在wireshark的视图->过滤器视图,在这一栏点击表达式,就会列出来对所有协议的支持
  -Y: -Y <display filter>,使用读取过滤器的语法,在单次分析中可以代替-R选项
输出选项:
  -T: -T pdml|ps|text|fields|psml,设置解码结果输出的格式,包括text,ps,psml和pdml,默认为text
  -e: 如果-T fields选项指定,-e用来指定输出哪些字段

对前面导出得到的 pcapng 文件,我们可以用 tshark 来提取出报告的字段(这里对应的是 usbhid.data),并保存到文件hiddata.txt中:

tshark -r example.pcapng -T fields -e usbhid.data > hiddata.txt

打开文件,可以看到我们得到了只含报告数据部分的文件,之后只要解析这个文件就可以还原出鼠标轨迹和击键信息了:

usbhid.data

USB HID 流量解析

鼠标流量解析(引导协议)

报告格式

引导协议鼠标数据报告的长度为 4 个字节(如果不是 4 个字节就是报告协议的,比如我手上在用的狗屁王的报告就是 13 字节的,这种具体怎么分析后面会谈到)。

下面是引导协议的鼠标的报告描述符:

Usage Page (Generic Desktop), 
Usage (Mouse), 
Collection (Application), 
 Usage (Pointer), 
 Collection (Physical), 
 Report Count (3), 
 Report Size (1), 
 Usage Page (Buttons), 
 Usage Minimum (1), 
 Usage Maximum (3), 
 Logical Minimum (0), 
 Logical Maximum (1), 
 Input (Data, Variable, Absolute), 
 Report Count (1), 
 Report Size (5), 
 Input (Constant), 
 Report Size (8), 
 Report Count (2), 
 Usage Page (Generic Desktop), 
 Usage (X), 
 Usage (Y), 
 Logical Minimum (-127), 
 Logical Maximum (127), 
 Input (Data, Variable, Relative), 
 End Collection, 
End Collection

下表定义了对应的报告格式:

Offset Size Description
0 Byte Button status.
1 Byte X movement.
2 Byte Y movement.

Button status: 该字节为位域,其中最低三位为标准格式。 剩余的 5 位可用于特定设备的目的。

Bit Bit Length Description
0 1 当设置为 1 时,表示正在单击鼠标左键。
1 1 当设置为 1 时,表示正在单击鼠标右键。
2 1 当设置为 1 时,表示正在单击鼠标中键。
3 5 这些位保留用于特定于设备的功能。

上表中 Bit 0 表示最低位。

X movement: 8 位符号整数,表示 X 方向上的运动。 当值为负时,表示向左;当值为正时,表示向右。

Y movement: 8 位符号整数,表示 Y 方向上的运动。 当值为负时,表示向上;当值为正时,表示向下。

脚本

根据以上信息以及之前得到的报告数据文件,我们很容易推出每一个流量包种鼠标移动的信息。

但一个个推导没有必要,我们可以用脚本简化这一过程。

原本打算用网上流行的 wangyihang 的脚本:https://github.com/WangYihang/UsbMiceDataHacker

但是因为我的 Wireshark 自带的 tshark 提取出来的数据不带冒号,并且我的鼠标数据包不是 4 个字节,以及一些细节上的问题,所以我自己重新写了一个,改进的地方有:

  • 同时兼容新旧版 Wireshark 捕获的 USB HID 流量(主要区别在于报告在 usbhid.data 还是 usb.capdata 中)
  • 同时兼容解析 tshark 处理后带冒号和不带冒号的报告数据
  • 可选绘制的鼠标轨迹按键状态更灵活,不同状态的鼠标轨迹用不同颜色和图例标示
  • 支持将鼠标轨迹导出为图片文件
  • 同时支持 Windows 和 Linux 下的使用

同样地,该脚本也依赖于 tshark 命令来提取报告数据,因此,你需要保证在脚本中可以使用 tshark 命令。同时,脚本使用 matplotlib 库进行绘图,所以你需要安装这一依赖。

具体脚本如下(下面的脚本可能存在一些瑕疵,后续可能会改进,要获取最新的脚本请移步 Github:https://github.com/p0ise/pcap2track):

#!/usr/bin/env python
# coding:utf-8
import argparse
import os
from tempfile import NamedTemporaryFile
import struct
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D

def unpack_mouse_data(data: bytearray):
    # 解包鼠标数据包
    button_state = data[0]
    if len(data) == 4:
        (x, y) = struct.unpack_from("<bb", data, offset=1)
    elif len(data) == 8:
        (x, y) = struct.unpack_from("<bb", data, offset=1)
    elif len(data) == 13:
        (x, y) = struct.unpack_from("<hh", data, offset=2)

    # 返回解包的数据
    return button_state, x, y

def state2text(button_state: int) -> str:
    button_map = {
        0x0: 'No Button',
        0x1: 'Left Click',
        0x2: 'Right Click',
        0x4: 'Middle Click',
    }
    states = []
    if button_state == 0:
        states.append(button_map[0])
    else:
        for key in button_map:
            if button_state & key:
                states.append(button_map[key])

    return " And ".join(states)

def main():
    # 解析命令行参数
    parser = argparse.ArgumentParser(
        description='Read mouse data from pcapng file and plot mouse trajectory.')
    parser.add_argument('pcapng_file', help='path to the pcapng file')
    parser.add_argument('button_mask', metavar='button_mask', default=15, type=int, choices=range(16), nargs='?',
                        help='a mask of mouse button states to be included in the trace to display, default is 15')
    parser.add_argument('-o', '--output', default='output.png',
                        help='output file path, default is "output.png"')
    args = parser.parse_args()

    # 通过tshark解析pcapng文件,获取鼠标数据包
    tmpfile = NamedTemporaryFile(delete=False)
    tmpfile.close()

    command = "tshark -r %s -T fields -e usbhid.data -e usb.capdata > %s" % (
        args.pcapng_file, tmpfile.name)
    os.system(command)

    with open(tmpfile.name, 'r') as f:
        lines = f.readlines()

    os.unlink(tmpfile.name)

    x_position = y_position = 0
    last_button_state = -1

    # 绘制鼠标轨迹图
    fig, ax = plt.subplots()
    colormap = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f',
                '#bcbd22', '#17becf', '#aec7e8', '#ffbb78', '#98df8a', '#ff9896', '#c5b0d5', '#c49c94']

    x_values = [x_position]
    y_values = [y_position]

    states = []

    # 解析鼠标数据包,获取鼠标轨迹坐标
    for line in lines:
        capdata = line.strip().replace(':', '')
        if capdata:
            data = bytearray.fromhex(capdata)
            button_state, x_offset, y_offset = unpack_mouse_data(data)
            x_position += x_offset
            y_position -= y_offset

            if button_state != last_button_state:
                if len(x_values) > 1:
                    color = colormap[last_button_state]
                    ax.plot(x_values, y_values, color=color)
                    x_values = [x_values[-1]]
                    y_values = [y_values[-1]]
                last_button_state = button_state

            # 筛选符合条件的按钮状态
            if button_state & args.button_mask or (args.button_mask & 0b1000 and not button_state):
                if button_state not in states:
                    states.append(button_state)
            else:
                x_values = []
                y_values = []

            x_values.append(x_position)
            y_values.append(y_position)
        else:
            pass

    if len(x_values) > 1:
        color = colormap[last_button_state]
        ax.plot(x_values, y_values, color=color)

    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_title('Mouse Trajectory')

    handles = [Line2D([], [], color=colormap[i], label=state2text(i))
               for i in states]
    plt.legend(handles=handles)
    plt.savefig(args.output)
    plt.show()

if __name__ == "__main__":
    main()

用法:

pcap2track.py [-h] [-o OUTPUT] pcapng_file [button_mask]

-o: 用于指定输出图片的名称,默认为output.png

button_mask: 4 位位域,其中每个位对应一个按键状态,默认值为 15(即 0b1111)。当某个位设置为 1 时,表示显示对应按键状态的鼠标轨迹。具体位结构为:

Bit Bit Length Description
0 1 当设置为 1 时,表示显示鼠标左键的轨迹。
1 1 当设置为 1 时,表示显示鼠标右键的轨迹。
2 1 当设置为 1 时,表示显示鼠标中键的轨迹。
3 1 当设置为 1 时,表示显示无按键时的轨迹。

键盘流量解析(引导协议)

报告格式

引导协议的键盘报告的大小是 8 个字节,不是所有字节都会被使用,并且只用前 3 个或 4 个字节来实现也是可以的。

以下是引导协议的键盘的报告描述符:

Usage Page (Generic Desktop), 
Usage (Keyboard), 
Collection (Application), 
 Report Size (1), 
 Report Count (8), 
 Usage Page (Key Codes), 
 Usage Minimum (224), 
 Usage Maximum (231), 
 Logical Minimum (0), 
 Logical Maximum (1), 
 Input (Data, Variable, Absolute), ;Modifier byte 
 Report Count (1), 
 Report Size (8), 
 Input (Constant), ;Reserved byte 
 Report Count (5), 
 Report Size (1), 
 Usage Page (LEDs), 
 Usage Minimum (1), 
 Usage Maximum (5), 
 Output (Data, Variable, Absolute), ;LED report 
 Report Count (1), 
 Report Size (3), 
 Output (Constant), ;LED report padding 
 Report Count (6), 
 Report Size (8), 
 Logical Minimum (0), 
 Logical Maximum(255), 
 Usage Page (Key Codes), 
 Usage Minimum (0), 
 Usage Maximum (255), 
 Input (Data, Array), 
End Collection

对应的报告结构如下:

Offset Size Description
0 Byte Modifier keys status.
1 Byte Reserved field.
2 Byte Keypress #1.
3 Byte Keypress #2.
4 Byte Keypress #3.
5 Byte Keypress #4.
6 Byte Keypress #5.
7 Byte Keypress #6.

Modifier keys status: 这个字节是一个位域,其中每个位对应一个特定的修改键。 当某个位设置为 1 时,表示对应的修改键被按下。 该字节的位结构为:

Bit Bit Length Description
0 1 Left Ctrl.
1 1 Left Shift.
2 1 Left Alt.
3 1 Left GUI (Windows/Super key.)
4 1 Right Ctrl.
5 1 Right Shift.
6 1 Right Alt.
7 1 Right GUI (Windows/Super key.)

Reserved field: 该字节是 USB HID 规范保留的,因此软件应该忽略它。

Keypress fields: 一个键盘报告最多可以指示 6 个按键。这些值都是无符号的 8 位值,表示正在按下的键。USB 扫描码到 ASCII 字符的转换表可以参考https://www.win.tue.nl/~aeb/linux/kbd/scancodes-14.html。

当其中一个 Shift 的修改键被设置为 1 时,应该使用用于 Shift 的扫描码表。

脚本

同样地,我们可以用脚本还原出键盘输入的文本信息,这里也是自己写了一版,对比网上现有的脚本:

  • 补充更全的按键映射表
  • 对于修改键同时支持左右 shift
  • 支持同时多个按键按下时的解析
  • 同时兼容新旧版 Wireshark 捕获的 USB HID 流量(主要区别在于报告在 usbhid.data 还是 usb.capdata 中)
  • 同时兼容解析 tshark 处理后带冒号和不带冒号的报告数据
  • 同时支持 Windows 和 Linux 下的使用

具体脚本如下(下面的脚本可能存在一些瑕疵,后续可能会改进,要获取最新的脚本请移步 Github:https://github.com/p0ise/pcap2text):

#!/usr/bin/env python
# coding:utf-8
import argparse
import os
from tempfile import NamedTemporaryFile

BOOT_KEYBOARD_MAP = {
    0x00: (None, None),                         # Reserved (no event indicated)
    0x01: ('', ''),                             # ErrorRollOver
    0x02: ('', ''),                             # POSTFail
    0x03: ('', ''),                             # ErrorUndefined
    0x04: ('a', 'A'),                           # a
    0x05: ('b', 'B'),                           # b
    0x06: ('c', 'C'),                           # c
    0x07: ('d', 'D'),                           # d
    0x08: ('e', 'E'),                           # e
    0x09: ('f', 'F'),                           # f
    0x0a: ('g', 'G'),                           # g
    0x0b: ('h', 'H'),                           # h
    0x0c: ('i', 'I'),                           # i
    0x0d: ('j', 'J'),                           # j
    0x0e: ('k', 'K'),                           # k
    0x0f: ('l', 'L'),                           # l
    0x10: ('m', 'M'),                           # m
    0x11: ('n', 'N'),                           # n
    0x12: ('o', 'O'),                           # o
    0x13: ('p', 'P'),                           # p
    0x14: ('q', 'Q'),                           # q
    0x15: ('r', 'R'),                           # r
    0x16: ('s', 'S'),                           # s
    0x17: ('t', 'T'),                           # t
    0x18: ('u', 'U'),                           # u
    0x19: ('v', 'V'),                           # v
    0x1a: ('w', 'W'),                           # w
    0x1b: ('x', 'X'),                           # x
    0x1c: ('y', 'Y'),                           # y
    0x1d: ('z', 'Z'),                           # z
    0x1e: ('1', '!'),                           # 1
    0x1f: ('2', '@'),                           # 2
    0x20: ('3', '#'),                           # 3
    0x21: ('4', '$'),                           # 4
    0x22: ('5', '%'),                           # 5
    0x23: ('6', '^'),                           # 6
    0x24: ('7', '&'),                           # 7
    0x25: ('8', '*'),                           # 8
    0x26: ('9', '('),                           # 9
    0x27: ('0', ')'),                           # 0
    0x28: ('\n', '\n'),                         # Return (ENTER)
    0x29: ('[ESC]', '[ESC]'),                   # Escape
    0x2a: ('\b', '\b'),                         # Backspace
    0x2b: ('\t', '\t'),                         # Tab
    0x2c: (' ', ' '),                           # Spacebar
    0x2d: ('-', '_'),                           # -
    0x2e: ('=', '+'),                           # =
    0x2f: ('[', '{'),                           # [
    0x30: (']', '}'),                           # ]
    0x31: ('\\', '|'),                          # \
    0x32: ('', ''),                             # Non-US # and ~
    0x33: (';', ':'),                           # ;
    0x34: ('\'', '"'),                          # '
    0x35: ('`', '~'),                           # `
    0x36: (',', '<'),                           # ,
    0x37: ('.', '>'),                           # .
    0x38: ('/', '?'),                           # /
    0x39: ('[CAPSLOCK]', '[CAPSLOCK]'),         # Caps Lock
    0x3a: ('[F1]', '[F1]'),                     # F1
    0x3b: ('[F2]', '[F2]'),                     # F2
    0x3c: ('[F3]', '[F3]'),                     # F3
    0x3d: ('[F4]', '[F4]'),                     # F4
    0x3e: ('[F5]', '[F5]'),                     # F5
    0x3f: ('[F6]', '[F6]'),                     # F6
    0x40: ('[F7]', '[F7]'),                     # F7
    0x41: ('[F8]', '[F8]'),                     # F8
    0x42: ('[F9]', '[F9]'),                     # F9
    0x43: ('[F10]', '[F10]'),                   # F10
    0x44: ('[F11]', '[F11]'),                   # F11
    0x45: ('[F12]', '[F12]'),                   # F12
    0x46: ('[PRINTSCREEN]', '[PRINTSCREEN]'),   # Print Screen
    0x47: ('[SCROLLLOCK]', '[SCROLLLOCK]'),     # Scroll Lock
    0x48: ('[PAUSE]', '[PAUSE]'),               # Pause
    0x49: ('[INSERT]', '[INSERT]'),             # Insert
    0x4a: ('[HOME]', '[HOME]'),                 # Home
    0x4b: ('[PAGEUP]', '[PAGEUP]'),             # Page Up
    0x4c: ('[DELETE]', '[DELETE]'),             # Delete Forward
    0x4d: ('[END]', '[END]'),                   # End
    0x4e: ('[PAGEDOWN]', '[PAGEDOWN]'),         # Page Down
    0x4f: ('[RIGHTARROW]', '[RIGHTARROW]'),     # Right Arrow
    0x50: ('[LEFTARROW]', '[LEFTARROW]'),       # Left Arrow
    0x51: ('[DOWNARROW]', '[DOWNARROW]'),       # Down Arrow
    0x52: ('[UPARROW]', '[UPARROW]'),           # Up Arrow
    0x53: ('[NUMLOCK]', '[NUMLOCK]'),           # Num Lock
    0x54: ('[KEYPADSLASH]', '/'),               # Keypad /
    0x55: ('[KEYPADASTERISK]', '*'),            # Keypad *
    0x56: ('[KEYPADMINUS]', '-'),               # Keypad -
    0x57: ('[KEYPADPLUS]', '+'),                # Keypad +
    0x58: ('[KEYPADENTER]', '[KEYPADENTER]'),   # Keypad ENTER
    0x59: ('[KEYPAD1]', '1'),                   # Keypad 1 and End
    0x5a: ('[KEYPAD2]', '2'),                   # Keypad 2 and Down Arrow
    0x5b: ('[KEYPAD3]', '3'),                   # Keypad 3 and PageDn
    0x5c: ('[KEYPAD4]', '4'),                   # Keypad 4 and Left Arrow
    0x5d: ('[KEYPAD5]', '5'),                   # Keypad 5
    0x5e: ('[KEYPAD6]', '6'),                   # Keypad 6 and Right Arrow
    0x5f: ('[KEYPAD7]', '7'),                   # Keypad 7 and Home
    0x60: ('[KEYPAD8]', '8'),                   # Keypad 8 and Up Arrow
    0x61: ('[KEYPAD9]', '9'),                   # Keypad 9 and Page Up
    0x62: ('[KEYPAD0]', '0'),                   # Keypad 0 and Insert
    0x63: ('[KEYPADPERIOD]', '.'),              # Keypad . and Delete
    0x64: ('', ''),                             # Non-US \ and |
    0x65: ('', ''),                             # Application
    0x66: ('', ''),                             # Power
    0x67: ('[KEYPADEQUALS]', '='),              # Keypad =
    0x68: ('[F13]', '[F13]'),                   # F13
    0x69: ('[F14]', '[F14]'),                   # F14
    0x6a: ('[F15]', '[F15]'),                   # F15
    0x6b: ('[F16]', '[F16]'),                   # F16
    0x6c: ('[F17]', '[F17]'),                   # F17
    0x6d: ('[F18]', '[F18]'),                   # F18
    0x6e: ('[F19]', '[F19]'),                   # F19
    0x6f: ('[F20]', '[F20]'),                   # F20
    0x70: ('[F21]', '[F21]'),                   # F21
    0x71: ('[F22]', '[F22]'),                   # F22
    0x72: ('[F23]', '[F23]'),                   # F23
    0x73: ('[F24]', '[F24]'),                   # F24
}

def parse_boot_keyboard_report(data: bytearray):
    # 数据解析
    modifiers = data[0]  # 修改键字节
    keys = data[2:8]      # 键码字节

    # 将修改键字节中的位解码为按键修饰符
    ctrl = (modifiers & 0x11) != 0
    shift = (modifiers & 0x22) != 0
    alt = (modifiers & 0x44) != 0
    gui = (modifiers & 0x88) != 0

    # 解析键码字节并将其映射为字符
    characters = []
    for key in keys:
        if key != 0:
            # 键码不为0则查询映射表
            if key in BOOT_KEYBOARD_MAP:
                characters.append(BOOT_KEYBOARD_MAP[key][shift])
            else:
                characters.append(None)
    return (ctrl, shift, alt, gui, characters)

def help_formatter(prog):
    return argparse.HelpFormatter(prog, max_help_position=40)

def main():
    # 解析命令行参数
    parser = argparse.ArgumentParser(
        description='Parse keyboard report data and output as text', formatter_class=help_formatter)
    parser.add_argument('pcapng_file', help='path to the pcapng file')
    args = parser.parse_args()

    # 通过tshark解析pcapng文件,获取键盘数据包
    tmpfile = NamedTemporaryFile(delete=False)
    tmpfile.close()

    command = "tshark -r %s -T fields -e usbhid.data -e usb.capdata > %s" % (
        args.pcapng_file, tmpfile.name)
    os.system(command)

    with open(tmpfile.name, 'r') as f:
        lines = f.readlines()

    os.unlink(tmpfile.name)

    # 解析键盘数据包,获取输入字符
    text = ""
    for line in lines:
        capdata = line.strip().replace(':', '')
        if capdata:
            data = bytearray.fromhex(capdata)
            characters = parse_boot_keyboard_report(data)[-1]
            for character in characters:
                if character:
                    text += character
        else:
            pass

    raw_text = repr(text)
    print(f'Raw output:\n{raw_text}')
    print(f'Text output:\n{text}')

if __name__ == "__main__":
    main()

用法:

python pcap2text.py pcapng_file

连续相同按键的处理问题

实际敲文本测试的时候发现解析出来的文本会比实际敲的多出几个重复的字符,看了下流量数据发现是连续的相同按键的问题。按下一个按键的时间比较短的时候,输入的就会是一个字符;按住的时间长了,它就会变成重复的一串字符。

要解决这个问题,可能要在提取流量包数据的时候加上时间戳数据,然后进行处理,后续视情况可能会更新脚本解决这个问题。

报告协议的流量包分析

对于使用报告协议的设备,可能每个使用的报告都不同,我们可以通过读取报告描述符或者猜测来得到报告的格式。

为什么不所有设备都直接读取报告描述符来分析呢?是因为我试了之后发现有些设备的报告描述符会出现读不出来的情况,具体原因还未探明,这时候就先猜着看吧......

直接读取报告描述符

不知道因为何种原因,HID 报告描述符似乎不像其他描述符那么好获得,这里需要借助一些工具来读取报告描述符。

Linux

Linux 下可以用 usbhid-dump 工具读取报告描述符,并通过 hidrd-convert 以人类可读的格式转储报告描述符。

这里简单介绍一下 usbhid-dump 的用法:

# usbhid-dump
002:002:001:DESCRIPTOR         1679049673.272914    # 总线号2:设备地址2:接口号1:块类型描述符
 05 01 09 02 A1 01 09 01 A1 00 05 09 19 01 29 10
 15 00 25 01 75 01 95 10 81 02 05 01 09 30 09 31
 16 01 80 26 FF 7F 75 10 95 02 81 06 09 38 15 81
 25 7F 75 08 95 01 81 06 05 0C 0A 38 02 81 06 C0
 C0

002:002:000:DESCRIPTOR         1679049673.274171    # 总线号2:设备地址2:接口号1:块类型描述符
 05 01 09 02 A1 01 09 01 A1 00 05 09 19 01 29 10
 15 00 25 01 75 01 95 10 81 02 05 01 09 30 09 31
 15 00 27 FF 7F 00 00 75 10 95 02 81 02 09 38 15
 81 25 7F 75 08 95 01 81 06 05 0C 0A 38 02 81 06
 C0 C0

下面是一些可用的选项:

  -s, -a, --address=bus[:dev]      指定总线号和设备地址
  -d, -m, --model=vid[:pid]        指定厂商ID和产品ID
  -i, --interface=NUMBER           指定接口号

比如指定总线号为2、设备地址为2的设备:

usbhid-dump -a2:2

可通过以下命令组合,获取总线号为2、设备地址为3、接口为0的设备的报告描述符,同时用 grep 命令去掉带有 : 的行,并通过 xxd -r -p 命令转为二进制形式,最后通过 hidrd-convert -o spec 命令输出人类可读的报告:

sudo usbhid-dump -a2:3 -i0 | grep -v : | xxd -r -p | hidrd-convert -o spec

当然,你也可以把通过 usbhid-dump 得到的报告描述符的十六进制值复制到在线工具 USB Descriptor and Request Parser 上以转为人类可读的格式:

USB Descriptor and Request Parser

Windows

Windows 下可以使用 win-hid-dump 工具,可以把它当作 Windows 版的 usbhid-dump 工具,在命令行运行就会列出所有设备的报告描述符:

winhiddump

我实际运行之后发现会对有些设备报错,无法重建报告描述符(比如我的罗技鼠标的报告),具体原因不明。

无法重建报告描述符

而且在这个工具的官方仓库还看到一条限制,说是重建的报告可能和原报告并不一致,具体自己斟酌吧。

猜测报告结构

上文提到了一些无法获取到报告描述符的情况,这种时候就只能自己猜测了,这里提供一些简单的思路,有更好的办法欢迎各位补充。

通过之前的分析,我们知道引导协议的鼠标和键盘的报告中需要的数据,其实报告协议里也是需要这些基本的数据的,所以我们只要比较流量中发生变化的数据,看它是哪些字段在变化,然后把那些数据套进去就试好了,比如鼠标里面主要的就一个按键状态和X、Y移动的数据,其他设备也是一样的。

有设备

有设备就很简单了,抓包打开,然后根据设备引导协议报告的特点操作设备,然后分析报告即可。

比如,我测试我的罗技鼠标,先测试按键状态,按下左键前后:

0000   01 00 00 00 00 00 00 00 01 93 40 00 00
0000   00 00 00 00 00 00 00 00 01 93 40 00 00

不难猜测第一个字节就是代表按键状态的数据。

同理,我们再分别测试X、Y方向的移动和鼠标滚轮等等,就可以猜测出大致的报告结构了,这里测试出来我的鼠标移动数据都是两个字节长度的有符号的整型。

没有设备

没有设备的话要难猜一点,但思路是一样的,对比上下流量包的数据,然后进行猜测。

参考

  1. Windows: lsusb Equivalent – PowerShell, https://www.shellhacks.com/windows-lsusb-equivalent-powershell/
  2. USB Human Interface Devices, https://wiki.osdev.org/USB_Human_Interface_Devices
  3. Device Class Definition for HID 1.11, https://www.usb.org/document-library/device-class-definition-hid-111
  4. Capturing USB traffic, https://github.com/liquidctl/liquidctl/blob/main/docs/developer/capturing-usb-traffic.md
  5. Get HID Report Descriptors with “win-hid-dump” & “mac-hid-dump”, https://todbot.com/blog/2021/01/29/get-hid-report-descriptors-with-win-hid-dump-mac-hid-dump/

本文链接: https://linuxstory.org/usb-hid-traffic-analysis/

LinuxStory 原创教程,转载请注明出处,否则必究相关责任。

对这篇文章感觉如何?

太棒了
3
不错
0
爱死了
0
不太好
0
感觉很糟
0

You may also like

Leave a reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

More in:教程

教程

在 Ubuntu Linux 上安装 Clang

无论您使用的是 Ubuntu 22.04、20.04 或其他任何版本,并且想要安装 Clang(一个开源的 C、C++ 和 Objective-C 编译器),本文将对您有所帮助。Clang 是 GNU […]