教程長篇分享

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 […]