USB设备开发学习(二)

接着上一篇遗留的问题,继续研究。

1. 模拟鼠标

在上一篇的Paper中,我们尝试对USB键盘进行模拟,下一步再尝试模拟一个鼠标。

在能成功模拟键盘的基础上,要实现鼠标的模拟是很容易的,只需要修改两部分。

首先,需要对协议进行修改,1表示的是键盘,2表示的是鼠标:

1
echo 2 > "${FUNCTIONS_DIR}/protocol" # mouse

其次,需要对HID描述符进行修改,对于鼠标来说,至少需要两个按键(左右键)和XY二维坐标系(控制鼠标的移动)。但是现在的鼠标功能越来越多了,比如我们可以加一个滚轮,这就需要增加一个一维坐标系,和一个按键(中键)。另外大拇指的地方还可以设置两个按键用来翻页。

根据上述描述的功能,我们来便携一个鼠标的HID描述符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
0x05, 0x01,          # USAGE_PAGE (Generic Desktop)
0x09, 0x02, # USAGE (Mouse)
0xa1, 0x01, # COLLECTION (Application)
0x85, 0x66, # Report ID 0x66
0x09, 0x01, # USAGE (Pointer)
0xa1, 0x00, # COLLECTION (Physical)
0x05, 0x09, # USAGE_PAGE (Button)
0x19, 0x01, # USAGE_MINIMUM (0x1)
0x29, 0x05, # USAGE_MAXIMUM (0x5)
0x15, 0x00, # LOGICAL_MINIMUM (0)
0x25, 0x01, # LOGICAL_MAXIMUM (1)
0x75, 0x01, # REPORT_SIZE (1)
0x95, 0x05, # REPORT_COUNT (5)
0x81, 0x02, # INPUT (Data,Var,Abs)

0x95, 0x01, # REPORT_COUNT (3)
0x75, 0x03, # REPORT_SIZE (1)
0x81, 0x01, # INPUT (Cnst,Array,Abs)

0x05, 0x01, # USAGE_PAGE (Generic Desktop Controls)
0x09, 0x30, # USAGE (X)
0x09, 0x31, # USAGE (Y)
0x16, 0x01, 0x80, # LOGICAL_MINIMUM (-0x7FFF)
0x26, 0xFF, 0x7F, # LOGICAL_MAXIMUM (0x7FFF)
0x95, 0x02, # REPORT_COUNT (2)
0x75, 0x10, # REPORT_SIZE (16)
0x81, 0x06, # INPUT (Data,Var,Rel)

0x09, 0x38, # USAGE (Wheel)
0x15, 0x81, # LOGICAL_MINIMUM (-0x7F)
0x25, 0x7F, # LOGICAL_MAXIMUM (0x7F)
0x95, 0x01, # REPORT_COUNT (1)
0x75, 0x08, # REPORT_SIZE (8)
0x81, 0x06, # INPUT (Data,Var,Rel)
0xc0, # END_COLLECTION
0xc0 # END_COLLECTION

在讲解上面的HID描述符的时候,我们首先要知道一件事,鼠标的厂商如果想要生产一个USB免驱的鼠标,那么并不是他们想增加什么按键就能增加的,而是需要按照HID规范文档[1],因为操作系统开发内置的驱动的时候也是按照该文档来进行开发的。

接下来讲解一下上面的HID描述符,首先我们定义了5个Button按键,一个按键占1bit,1位按下,0位释放,一共占了5bit,由于需要8bit对其,所以还有3bit的padding。

这5个按键分别代表什么呢?可以自己尝试一下,或者搜索Linux鼠标驱动的源码,如下所示

1
2
3
4
5
6
// drivers/hid/usbhid/usbmouse.c
input_report_key(dev, BTN_LEFT, data[0] & 0x01); // 左键
input_report_key(dev, BTN_RIGHT, data[0] & 0x02); // 右键
input_report_key(dev, BTN_MIDDLE, data[0] & 0x04); // 滚轮中键
input_report_key(dev, BTN_SIDE, data[0] & 0x08); // 侧边按键(翻页)
input_report_key(dev, BTN_EXTRA, data[0] & 0x10); // 侧边按键(翻页)

接着定义了一个XY直角坐标系,代表了鼠标是如何移动的,其值从-0x7FFF到0x7FFF,一个轴占2字节,两个共占4字节。

最后定义了滚轮,最小值为-0x7F,最大值为0x7F,共占1字节。

上述的三部分总共占据了6字节,因为定义了一个USB中断传输为8字节,所以还可以加上2字节的padding,不加也无所谓,因为也不会报错。

到此为止,一个鼠标我们就模拟成功了,按照上一篇的步骤,运行我们的脚本,我们就能通过写/dev/hidg0来控制主机上鼠标的点击与移动了。

比如我们需要点击左键:

1
2
3
4
5
6
7
def sendBuf(buf):
with open("/dev/hidg0", "wb") as f:
f.write(bytes(buf))

mouse_buf = [0] * 8
mouse_buf[0] = 0b00000001
sendBuf(mouse_buf)

但是需要注意,按照上述代码操作以后,表示的是按下左键,但是并不会释放,我们正常使用鼠标左键点击的实际过程其实是包含两部分的。首先,按下左键,然后释放左键。这才是一个完整的点击过程。如果我们要实现一个完整的点击过程,需要这么修改:

1
2
3
4
5
6
7
8
9
10
def sendBuf(buf):
with open("/dev/hidg0", "wb") as f:
f.write(bytes(buf))

def clickLeft():
mouse_buf = [0] * 8
mouse_buf[0] |= 0b00000001
sendBuf(mouse_buf)
mouse_buf[0] &= 0b11111110
sendBuf(mouse_buf)

如果我们需要控制鼠标的其他功能,可以对上述代码进行简单的修改,比如定义的五个按键,只需要控制mouse_buf[0]的低5bit的值。控制鼠标横向移动,则是控制mouse_buf[1:3]的值,纵向移动则是mouse_buf[3:5],控制滚轮则是mouse_buf[5]。后续具体细节大家可以私下自行测试研究。

模拟USB游戏手柄

接下来就是本篇的核心内容:模拟一个USB游戏手柄。

该部分内容说简单也简单,在能成功模拟USB鼠标键盘之后,也可以很容易的模拟出一个USB游戏手柄。但是如果深入研究当今常用的游戏手柄,也会产生很多问题。

首先,我们从简单的学起,先实现一个普通的游戏手柄,首先看我们要如何修改模拟鼠标键盘的脚本:

1
2
3
4
5
echo 0 > "${FUNCTIONS_DIR}/protocol" # None
echo 0 > "${FUNCTIONS_DIR}/subclass" # No subclass

echo 0x045e > idVendor
echo 0x028e > idProduct

当protocol等于0的时候,不会被鼠标键盘的驱动识别到。这个时候会根据idVendor/idProduct匹配出具体产品的驱动,在Linux上,手柄的驱动一般为drivers/input/joystick/xpad.c,可以查看该驱动中能识别的游戏手柄:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xpad_device[] = {
......
{ 0x045e, 0x0202, "Microsoft X-Box pad v1 (US)", 0, XTYPE_XBOX },
{ 0x045e, 0x0285, "Microsoft X-Box pad (Japan)", 0, XTYPE_XBOX },
{ 0x045e, 0x0287, "Microsoft Xbox Controller S", 0, XTYPE_XBOX },
{ 0x045e, 0x0288, "Microsoft Xbox Controller S v2", 0, XTYPE_XBOX },
{ 0x045e, 0x0289, "Microsoft X-Box pad v2 (US)", 0, XTYPE_XBOX },
{ 0x045e, 0x028e, "Microsoft X-Box 360 pad", 0, XTYPE_XBOX360 },
{ 0x045e, 0x028f, "Microsoft X-Box 360 pad v2", 0, XTYPE_XBOX360 },
{ 0x045e, 0x0291, "Xbox 360 Wireless Receiver (XBOX)", MAP_DPAD_TO_BUTTONS, XTYPE_XBOX360W },
{ 0x045e, 0x02d1, "Microsoft X-Box One pad", 0, XTYPE_XBOXONE },
{ 0x045e, 0x02dd, "Microsoft X-Box One pad (Firmware 2015)", 0, XTYPE_XBOXONE },
{ 0x045e, 0x02e3, "Microsoft X-Box One Elite pad", MAP_PADDLES, XTYPE_XBOXONE },
{ 0x045e, 0x0b00, "Microsoft X-Box One Elite 2 pad", MAP_PADDLES, XTYPE_XBOXONE },
{ 0x045e, 0x02ea, "Microsoft X-Box One S pad", 0, XTYPE_XBOXONE },
{ 0x045e, 0x0719, "Xbox 360 Wireless Receiver", MAP_DPAD_TO_BUTTONS, XTYPE_XBOX360W },
{ 0x045e, 0x0b0a, "Microsoft X-Box Adaptive Controller", MAP_PROFILE_BUTTON, XTYPE_XBOXONE },
{ 0x045e, 0x0b12, "Microsoft Xbox Series S|X Controller", MAP_SELECT_BUTTON, XTYPE_XBOXONE },
......

从上面我们可以看出,我们模拟的手柄为:Microsoft X-Box 360 pad

接下来就是编写HID描述符了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
0x09, 0x05, // Usage (Game Pad)
0xA1, 0x01, // Collection (Application)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x35, 0x00, // Physical Minimum (0)
0x45, 0x01, // Physical Maximum (1)
0x75, 0x01, // Report Size (1)
0x95, 0x0E, // Report Count (14)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x0E, // Usage Maximum (0x0E)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

0x95, 0x02, // Report Count (2)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)

0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x25, 0x07, // Logical Maximum (7)
0x46, 0x3B, 0x01, // Physical Maximum (315)
0x75, 0x04, // Report Size (4)
0x95, 0x01, // Report Count (1)
0x65, 0x14, // Unit (System: English Rotation, Length: Centimeter)
0x09, 0x39, // Usage (Hat switch)
0x81, 0x42, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,Null State)

0x65, 0x00, // Unit (None)
0x95, 0x01, // Report Count (1)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)

0x26, 0xFF, 0x00, // Logical Maximum (255)
0x46, 0xFF, 0x00, // Physical Maximum (255)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x32, // Usage (Z)
0x09, 0x35, // Usage (Rz)
0x75, 0x08, // Report Size (8)
0x95, 0x04, // Report Count (4)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)

0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)

0xC0, // End Collection

接下来分析一下该HID描述符:

  1. 定义了14个按键,占14bit,8bit对其,有2bit的padding填充。
  2. 定义了Hat Switch(这个我认为是很早游戏手柄的按键的叫法,现在我认为属于手柄的方向按键),占4bit,分别代表了上下左右,按住按键移动的距离单位用cm(厘米),移动的最大值是315cm。
  3. 接下来就是摇杆控制的XY还有体感控制的Z和Rz,每个方向的值占8bit,也就是1字节,4个就占了4字节,单位没有单独定义,所以也是cm,最大值为255。
  4. 最后还有1字节的padding填充,所有按键定义总共加起来有8字节。

所有环节准备就绪了,接下来就可以运行我们的脚本,然后可以在Linux主机上看到下列信息:

1
2
3
4
5
6
7
8
9
10
$ sudo dmesg
[91788.951749] usb 3-2: new high-speed USB device number 6 using xhci_hcd
[91789.279411] usb 3-2: New USB device found, idVendor=045e, idProduct=028e, bcdDevice= 2.00
[91789.279438] usb 3-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[91789.279442] usb 3-2: Product: My Test Pro Controller
[91789.279447] usb 3-2: Manufacturer: test Nintendo Switch Pro
[91789.279452] usb 3-2: SerialNumber: 0202020001314
[91789.285488] input: test Nintendo Switch Pro My Test Pro Controller as /devices/pci0000:00/0000:00:16.0/0000:0b:00.0/usb3/3-2/3-2:1.0/0003:045E:028E.0003/input/input8
[91789.286937] hid-generic 0003:045E:028E.0003: input,hidraw1: USB HID v1.01 Gamepad [test Nintendo Switch Pro My Test Pro Controller] on usb-0000:0b:00.0-2/input0
# 注意事项:别看日志中显示的是Switch手柄,因为这些字符串都可以自行随意设置,而内核驱动是根据idVendor/idProduct来设备手柄是什么设备的,所以对Linux内核来说这就是一个XBox手柄。

接着我们可以发现在/dev/input目录下多了两个文件:

1
2
3
4
5
6
$ ls -alF /dev/input/by-id
total 0
drwxr-xr-x 2 root root 120 Jan 25 20:44 ./
drwxr-xr-x 4 root root 360 Jan 25 20:44 ../
lrwxrwxrwx 1 root root 9 Jan 25 20:44 usb-test_Nintendo_Switch_Pro_My_Test_Pro_Controller_0202020001314-event-joystick -> ../event6
lrwxrwxrwx 1 root root 6 Jan 25 20:44 usb-test_Nintendo_Switch_Pro_My_Test_Pro_Controller_0202020001314-joystick -> ../js1

接着我们可以通过直接读/dev/input/js1文件:cat /dev/input/js1|hexdump -C来查看手柄的输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 树莓派USB设备端
def send(buf):
with open("/dev/hidg0", "wb") as f:
f.write(bytes(buf))
buf = [0] * 8
buf[0] = 0b00000001
send(buf)

# Linux主机端
cat /dev/input/js1|hexdump -C
00000000 70 7f 79 05 00 00 81 00 70 7f 79 05 00 00 81 01 |p.y.....p.y.....|
00000010 70 7f 79 05 00 00 81 02 70 7f 79 05 00 00 81 03 |p.y.....p.y.....|
00000020 70 7f 79 05 00 00 81 04 70 7f 79 05 00 00 81 05 |p.y.....p.y.....|
00000030 70 7f 79 05 00 00 81 06 70 7f 79 05 00 00 81 07 |p.y.....p.y.....|
00000040 70 7f 79 05 00 00 81 08 70 7f 79 05 00 00 81 09 |p.y.....p.y.....|
00000050 70 7f 79 05 00 00 81 0a 70 7f 79 05 00 00 81 0b |p.y.....p.y.....|
00000060 70 7f 79 05 00 00 81 0c 70 7f 79 05 00 00 81 0d |p.y.....p.y.....|
00000070 70 7f 79 05 01 80 82 00 70 7f 79 05 01 80 82 01 |p.y.....p.y.....|
00000080 70 7f 79 05 01 80 82 02 70 7f 79 05 01 80 82 03 |p.y.....p.y.....|
00000090 70 7f 79 05 00 00 82 04 70 7f 79 05 01 80 82 05 |p.y.....p.y.....|


000000a0 90 12 7a 05 01 00 01 00 08 46 7a 05 01 00 01 01 |..z......Fz.....|
js1文件的数据结构为:
struct js_event {
__u32 time; /* event timestamp in milliseconds */
__s16 value; /* value */
__u8 type; /* event type */
__u8 number; /* axis/button number */
};

我们可以写一个python脚本让js1的数据更有可视性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#!/usr/bin/env python3
# -*- coding=utf-8 -*-

import os
import sys
import struct
import select

fmt_js0 = '<IhBB'
js0_size = struct.calcsize(fmt_js0)

# Open the joystick device.
fn_js0 = sys.argv[1]
js0dev = os.open(fn_js0, os.O_RDONLY)

# Create an epoll object
epoll = select.epoll()
epoll.register(js0dev, select.EPOLLIN)

old_js0 = None

while True:
events = epoll.poll()
for fd, event in events:
if fd == js0dev:
event_js0 = os.read(js0dev, js0_size)
if event_js0 != old_js0:
old_js0 = event_js0
time, value, type, number = struct.unpack(fmt_js0, event_js0)
print(f'JS0 - binary: {event_js0}')
print('JS0 - time: {}, value: {}, type: {}, number: {}'.format(time, value, type, number))

下面展示一些数据的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
data = [0] * 8
buf[0] = 0b00000001
# 主机数据
JS0 - binary: b'\x00H\x81\x05\x01\x00\x01\x00'
JS0 - time: 92358656, value: 1, type: 1, number: 0

buf[0] = 0b00000000
JS0 - binary: b'LP\x82\x05\x00\x00\x01\x00'
JS0 - time: 92426316, value: 0, type: 1, number: 0

buf[0] = 0b10000000
JS0 - binary: b'(\xd2\x82\x05\x01\x00\x01\x07'
JS0 - time: 92459560, value: 1, type: 1, number: 7

从这我们可以看出,type=1应该表示为按键,除了按键还有滚轮方向键这些(XYZRz和Hat Switch),number=x表示第几个按键,按照我们编写的HID描述符来说,number的值应该在0-13之间,而value就是具体的值,对于按键来说,只能是0或者1。

或者我们能安装joystick包:sudo apt install joystick,使用jstest命令来进行更加可视化的观察:

1
2
3
4
5
6
$ jstest /dev/input/js1
Driver version is 2.1.0.
Joystick (test Nintendo Switch Pro My Test Pro Controller) has 6 axes (X, Y, Z, Rz, Hat0X, Hat0Y)
and 14 buttons (BtnA, BtnB, BtnC, BtnX, BtnY, BtnZ, BtnTL, BtnTR, BtnTL2, BtnTR2, BtnSelect, BtnStart, BtnMode, BtnThumbL).
Testing ... (interrupt to exit)
Axes: 0:-32767 1:-32767 2:-32767 3:-32767 4: 0 5:-32767 Buttons: 0:off 1:off 2:off 3:off 4:off 5:off 6:off 7:off 8:off 9:off 10:off 11:off 12:off 13:off

手柄的模拟第一阶段就到此为止,我们已经可以成功的模拟一个功能简单的游戏手柄。

接下来就是对游戏手柄的深入研究,或者也不算是深入研究,只是我在研究游戏手柄最初踩到的一个坑,从而让我多研究了一部分内容。

在研究游戏手柄最初,我考虑的方案是用我手术现成的手柄连接到电脑上,然后抓包,分析实际的手柄数据包。

我手头上现成的手柄有两个,一个上Switch Pro,一个是国产的可以伪装成Switch Pro的手柄。

并且我在Linux的源码中发现swithc手柄的相关驱动:drivers/hid/hid-nintendo.c

综合上述因素,我选择了Switch手柄作为研究的切入口,但是最终我发现我被坑了,浪费了我几天的时间。

正版的Switch Pro手柄,不管是接入Windows系统还是Ubuntu系统,都能抓到USB相关的数据包还有HID描述符,但是电脑无法正常使用手柄。在Windows上暂时没法研究,不过经过我一番搜索,发现Switch Pro手柄是不支持USB接入的,只能通过蓝牙接入。

那在Ubuntu上的nintendo驱动又是怎么回事呢?通过查看日志发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ sudo dmesg
Jan 25 21:17:09 hehe kernel: [93718.957323] usb 3-2: new full-speed USB device number 7 using xhci_hcd
Jan 25 21:17:09 hehe kernel: [93719.300499] usb 3-2: New USB device found, idVendor=057e, idProduct=2009, bcdDevice= 2.00
Jan 25 21:17:09 hehe kernel: [93719.300508] usb 3-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
Jan 25 21:17:09 hehe kernel: [93719.300513] usb 3-2: Product: Pro Controller
Jan 25 21:17:09 hehe kernel: [93719.300542] usb 3-2: Manufacturer: Nintendo Co., Ltd.
Jan 25 21:17:09 hehe kernel: [93719.300555] usb 3-2: SerialNumber: 020002002001
Jan 25 21:17:09 hehe kernel: [93719.315256] nintendo 0003:057E:2009.0004: hidraw1: USB HID v81.11 Joystick [Nintendo Co., Ltd. Pro Controller] on usb-0000:0b:00.0-2/input0
Jan 25 21:17:11 hehe kernel: [93721.649661] nintendo 0003:057E:2009.0004: failed reading SPI flash; ret=-110
Jan 25 21:17:11 hehe kernel: [93721.649674] nintendo 0003:057E:2009.0004: using factory cal for left stick
Jan 25 21:17:13 hehe kernel: [93723.697308] nintendo 0003:057E:2009.0004: failed reading SPI flash; ret=-110
Jan 25 21:17:13 hehe kernel: [93723.697321] nintendo 0003:057E:2009.0004: using factory cal for right stick
Jan 25 21:17:15 hehe kernel: [93725.745498] nintendo 0003:057E:2009.0004: failed reading SPI flash; ret=-110
Jan 25 21:17:15 hehe kernel: [93725.745509] nintendo 0003:057E:2009.0004: Failed to read left stick cal, using defaults; e=-110
Jan 25 21:17:17 hehe kernel: [93727.793565] nintendo 0003:057E:2009.0004: failed reading SPI flash; ret=-110
Jan 25 21:17:17 hehe kernel: [93727.793577] nintendo 0003:057E:2009.0004: Failed to read right stick cal, using defaults; e=-110
Jan 25 21:17:19 hehe kernel: [93729.841197] nintendo 0003:057E:2009.0004: failed reading SPI flash; ret=-110
Jan 25 21:17:19 hehe kernel: [93729.841208] nintendo 0003:057E:2009.0004: using factory cal for IMU
Jan 25 21:17:21 hehe kernel: [93731.889086] nintendo 0003:057E:2009.0004: failed reading SPI flash; ret=-110
Jan 25 21:17:21 hehe kernel: [93731.889099] nintendo 0003:057E:2009.0004: Failed to read IMU cal, using defaults; ret=-110
Jan 25 21:17:21 hehe kernel: [93731.889105] nintendo 0003:057E:2009.0004: Unable to read IMU calibration data
Jan 25 21:17:23 hehe kernel: [93733.936993] nintendo 0003:057E:2009.0004: Failed to set report mode; ret=-110
Jan 25 21:17:23 hehe kernel: [93733.937724] nintendo 0003:057E:2009.0004: probe - fail = -110
Jan 25 21:17:23 hehe kernel: [93733.937740] nintendo: probe of 0003:057E:2009.0004 failed with error -110
$ lsmod|grep hid
hid_nintendo 45056 0
ff_memless 24576 1 hid_nintendo
mac_hid 12288 0
hid_generic 12288 0
usbhid 77824 0
hid 180224 3 hid_nintendo,usbhid,hid_generic

发现ubuntu成功适配到了hid_nintendo驱动,但是却报了一堆的错误,我估计是这一堆错误导致手柄驱动注册失败的,在Linux下能被正常识别的手柄应该像我们上面的案例一样,能在/dev/input/目录下生成jsXeventX文件,因为经过我研究,在Linux上使用手柄的软件都是通过/dev/input/jsX文件来和手柄进行交互的。

但是目前的情况下,在/dev/input目录下并没有生成Switch手柄相关的文件。这个时候就是考虑对相关驱动进行研究调试,或者换设备了。

首先说一说简单的方案,换国产手柄进行尝试,经过测试发现这个国产手柄更是个大坑。首先在Windows上,首先它会伪装成一个Switch Pro游戏手柄,但是前面说了,Switch Pro无法在Windows上正常使用,该手柄可以检测到无法使用时,会再次伪装成一个XBox手柄,这个时候就能被Windows正常识别和使用了。从这里看会感觉这个手柄还不错,优先伪装成Swith,如果失败了再伪装成XBox。

但是,该手柄在代码实现上估计有大BUG,会导致USBTree View, Wireshark的USBPcap, 还有Windows的部分USB驱动崩溃(这里我怀疑时USBPcap导致的)。这个时候只能考虑重启电脑来修复问题,但是发现电脑无法正常关机,只能通过长按电源键强制关机。另外因为我的Linux是装在Windows上的虚拟机,不知道是不是同样问题导致的,在Linux无法识别到相关驱动(就算是nintendo驱动也没识别到),并且不知道是何原因,Windows主机强制重启多次后,会导致Linux虚拟机的图形界面网卡管理出现问题,需要重新安装network-manager。另外一个猜测,也许是该手柄防破解机制?不过一个100多的便宜手柄需要防破解吗?

综上原因,所以我把该手柄丢进小黑屋,只用来正常使用,不拿来研究了。

因此接下来只能考虑对相关驱动进行研究了,如果只通过静态分析,难度很大,毕竟我也没做过这类的驱动开发,就算有源码的情况下,纯静态分析也得花费很长一段时间。所以动态调试是一条必须要考虑的事情,但是考虑现实因素,驱动位于内核中,调试驱动可以等于调试Linux内核,这不像调试一个普通的ELF程序,直接使用gdb就行。需要有一个比较长的环境搭建的过程,虽然以前也调试过内核,不过我不是专门做这块的,上一次调试内核也是两三年前了,我本地并没有现成的环境。

由于动态调试的道路比较繁琐,再考虑一下其他调试方案,我能想到的有三种:

  1. 使用eBPF。
  2. 驱动本身有调试输出,不过需要内核编译开启DEBUG参数。
  3. 因为需要研究的是Linux驱动,而Linux的驱动大部分都是属于插件类型,可以在系统启动之后进行加载,卸载等动作,所以可以考虑编辑驱动源码,添加调试输出,然后把原本的驱动卸载,加载我修改后的驱动。

对于上述三个方案,首先,我随意研究过eBPF,但是研究的不深,开发相关程序也没写过多少,所以还是比较花时间的。其次第二个方案,需要重新编译Linux内核,这个通过需要浪费大量时间。因此我考虑了第三个方案。

动态修改Ubuntu驱动

首先安装内核源码:

1
2
3
4
5
6
7
8
9
10
11
12
# 需要包装安装的源码和当前内核同一版本
$ uname -r
6.5.0-14-generic
$ apt list linux-source-6.5.0 -a
Listing... Done
linux-source-6.5.0/jammy-updates,jammy-updates 6.5.0-15.15~22.04.1 all
linux-source-6.5.0/jammy-updates,jammy-updates 6.5.0-14.14~22.04.1 all
$ sudo apt install linux-source-6.5.0=6.5.0-14.14~22.04.1
# 移动到自己方便查看修改的目录
$ mkdir ~/kernelTest
$ mv /usr/src/linux-source-6.5.0.tar.bz2 ~/kernelTest
$ cd ~/kernelTest && tar xjf linux-source-6.5.0.tar.bz2

接着我编写了一个简单的脚本,当我们修改了相关驱动的时候,快速重新编译驱动然后加载进内核:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

DRIVER_NAME=$1
DRIVER_PATH="drivers/hid/hid-${DRIVER_NAME}.ko"

sudo rmmod $DRIVER_PATH

make CONFIG_HID_ROCCAT=n CONFIG_I2C_HID_CORE=n CONFIG_AMD_SFH_HID=n CONFIG_INTEL_ISH_HID=n -C /lib/modules/6.5.0-14-generic/build M=/home/hehe/kernelTest/linux-source-6.5.0/drivers/hid/ modules -j8
./scripts/sign-file sha512 ./certs/signing_key.pem certs/signing_key.x509 $DRIVER_PATH

sudo insmod $DRIVER_PATH

tail -f /var/log/kern.log # 相当于dmesg

输出调试信息方案有两种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1. 参考了hid_dbg,重新定一个了
#define mhid_dbg(hid, fmt, ...) dev_printk(KERN_DEBUG, &(hid)->dev, fmt, ##__VA_ARGS__)
比如我可以把hid-nintendo.c中的hid_dbg都替换成mhid_dbg
2. 使用print_hex_dump函数,使用方法如下:
static int nintendo_hid_event(struct hid_device *hdev,
struct hid_report *report, u8 *raw_data, int size)
{
struct joycon_ctlr *ctlr = hid_get_drvdata(hdev);

if (size < 1)
return -EINVAL;

print_hex_dump(KERN_INFO, "JOYCON HID RECV: ", DUMP_PREFIX_OFFSET, 16, 1, raw_data, size, false);

return joycon_ctlr_handle_event(ctlr, raw_data, size);
}

这样我们就能对nintendo驱动进行调试,下面记录一下我的研究结果:

每个hid驱动首先关注hid_driver结构体:

1
2
3
4
5
6
7
8
9
10
11
static struct hid_driver nintendo_hid_driver = {
.name = "nintendo",
.id_table = nintendo_hid_devices,
.probe = nintendo_hid_probe,
.remove = nintendo_hid_remove,
.raw_event = nintendo_hid_event,

#ifdef CONFIG_PM
.resume = nintendo_hid_resume,
#endif
};

.id_table的结构数据表明了什么情况下会匹配到当前驱动,匹配成功后执行.probe函数,当接收到设备发来的数据时,触发.raw_event函数,设备移除时触发.remove函数,设备有可能会休眠,休眠结束后会触发.resume函数。

下一步我们需要研究nintendo_hid_probe函数,经过研究发现switch手柄也有专门的协议,比如有定义握手包,有读取手柄芯片上数据的功能。让我发现一个很奇怪的事情,因为很大一块读取手柄数据的代码逻辑是为了判断该手柄是左手柄还是右手柄,玩过switch的同学就知道这代表啥意思。

但问题是,switch的分左右的手柄没有usb接口,这驱动为啥要适配USB_DEVICE_ID_NINTENDO_JOYCONLUSB_DEVICE_ID_NINTENDO_JOYCONR

并且通过抓包,我的Switch Pro手柄并不会响应驱动中定义的协议,我以前看到过研究switch手柄的paper,记得里面提过switch手柄相关的协议流量,不过抓的是JOYCONLJOYCONR手柄的流量,但是讲道理Switch Pro也应该支持,难度我花了大价钱买了个假货?(有可能,怪不得没用多久摇杆就漂移了。)

通过逆向驱动中的nintendo协议,我编写了以下脚本,当我模拟Switch Pro设备成功后,监听/dev/hidg0,回应主机发来的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/env python3
# -*- coding=utf-8 -*-

import os
import sys
import select
import struct

DEV="/dev/hidg0"

'''
struct joycon_input_report {
u8 id;
u8 timer;
u8 bat_con; /* battery and connection info */
u8 button_status[3];
u8 left_stick[3];
u8 right_stick[3];
u8 vibrator_report;

union {
struct joycon_subcmd_reply subcmd_reply;
/* IMU input reports contain 3 samples */
u8 imu_raw_bytes[sizeof(struct joycon_imu_data) * 3];
};
} __packed;
struct joycon_subcmd_reply {
u8 ack; /* MSB 1 for ACK, 0 for NACK */
u8 id; /* id of requested subcmd */
u8 data[]; /* will be at most 35 bytes */
} __packed;
'''
class JOYCON_INPUT_REPORT:
MIN_SIZE = 0xD
JC_INPUT_SUBCMD_REPLY = 0x21
# def __init__(self, data):
# self.id, self.timer, self.bat_con, self.button_status, self.left_stick, self.right_stick, self.vibrator_report = struct.unpack('BBB3s3s3sB', data[:self.MIN_SIZE])
# self.subcmd_ack, self.subcmd_id = struct.unpack('BB', data[self.MIN_SIZE: self.MIN_SIZE+2])
# self.subcmd_data = data[self.MIN_SIZE+2:]
def __init__(self):
self.id = 0
self.timer = 0
self.batcon = 0
self.button_status = b"\x00\x00\x00"
self.left_stick = b"\x00\x00\x00"
self.right_stick = b"\x00\x00\x00"
self.vibrator_report = 0
self.subcmd_ack = 1
self.subcmd_id = 0
self.subcmd_data = []

def genPayload(self):
payload = bytearray([self.id, self.timer, self.batcon])
payload += self.button_status
payload += self.left_stick
payload += self.right_stick
payload += bytearray([self.vibrator_report, self.subcmd_ack, self.subcmd_id])
payload += bytearray(self.subcmd_data)
return payload

'''
struct joycon_subcmd_request {
u8 output_id; /* must be 0x01 for subcommand, 0x10 for rumble only */
u8 packet_num; /* incremented every send */
u8 rumble_data[8];
u8 subcmd_id;
u8 data[]; /* length depends on the subcommand */
} __packed;
'''
class JOYCON_SUBCMD_REQUEST:
MIN_SIZE = 0xB
JC_SUBCMD_REQ_DEV_INFO = 0x02
JC_SUBCMD_SET_REPORT_MODE = 0x03
JC_SUBCMD_SPI_FLASH_READ = 0x10
JC_SUBCMD_SET_PLAYER_LIGHTS = 0x30
JC_SUBCMD_ENABLE_IMU = 0x40
JC_SUBCMD_ENABLE_VIBRATION = 0x48

JC_CAL_USR_MAGIC_0 = 0xB2
JC_CAL_USR_MAGIC_1 = 0xA1

def __init__(self, data):
self.output_id, self.packet_num, self.rumble_data, self.subcmd_id = struct.unpack('BB8sB', data[:self.MIN_SIZE])
self.data = data[self.MIN_SIZE:]

def parse(self):
if self.subcmd_id == self.JC_SUBCMD_SPI_FLASH_READ:
if len(self.data) == 5:
report = JOYCON_INPUT_REPORT()
report.subcmd_id = self.subcmd_id
report.id = JOYCON_INPUT_REPORT.JC_INPUT_SUBCMD_REPLY
report.subcmd_data = [0] * 5 + [self.JC_CAL_USR_MAGIC_0, self.JC_CAL_USR_MAGIC_1] + [0] * 27
return report.genPayload()
elif self.subcmd_id in [self.JC_SUBCMD_SET_REPORT_MODE, self.JC_SUBCMD_ENABLE_VIBRATION, self.JC_SUBCMD_ENABLE_IMU, self.JC_SUBCMD_SET_PLAYER_LIGHTS]:
report = JOYCON_INPUT_REPORT()
report.subcmd_id = self.subcmd_id
report.id = JOYCON_INPUT_REPORT.JC_INPUT_SUBCMD_REPLY
report.subcmd_data = [0] * 34
return report.genPayload()
elif self.subcmd_id == self.JC_SUBCMD_REQ_DEV_INFO:
report = JOYCON_INPUT_REPORT()
report.subcmd_id = self.subcmd_id
report.id = JOYCON_INPUT_REPORT.JC_INPUT_SUBCMD_REPLY
report.subcmd_data = [0] * 4 + [0x41, 0x42, 0x43, 0x44, 0x45, 0x46] + [0] * 24
return report.genPayload()
return None

class HIDParse(object):
JC_OUTPUT_USB_CMD = 0x80
JC_INPUT_USB_RESPONSE = 0x81

def __call__(self, data):
if len(data) == 2:
if data[0] == self.JC_OUTPUT_USB_CMD:
return bytearray([self.JC_INPUT_USB_RESPONSE, data[1]])
elif len(data) >= JOYCON_SUBCMD_REQUEST.MIN_SIZE:
subcmd_req = JOYCON_SUBCMD_REQUEST(data)
return subcmd_req.parse()

return None

class HIDSock:
def __init__(self, dev):
self.dev = dev
self.parse = HIDParse()
self.listenRead()

def callback(self, data):
print(f"[DEBUG]: do callback, data = {data}")
info = self.parse(data)
print(f"[DEBUG]: parse data: {info}")
if info:
with open(self.dev, "wb") as f:
f.write(info)

def listenRead(self):
fd_read = open(self.dev, "rb")
poll = select.poll()
poll.register(fd_read, select.POLLIN)
try:
while True:
events = poll.poll(1000) # Timeout of 1000 ms
for fd, event in events:
if event & select.POLLIN:
data = os.read(fd, 64)
self.callback(data)
finally:
fd_read.close()

def sendBuf(buf):
with open(DEV, "wb") as f:
f.write(buf)

def main():
hid = HIDSock(DEV)
bufList = [
0x21, 0x0D, 0x8E, 0x84, 0x00, 0x12, 0x01, 0x18, 0x80, 0x01, 0x18, 0x80, 0x80, 0x90, 0x10, 0x10,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
]
sendBuf(bytearray(bufList))

if __name__ == "__main__":
main()

接着在看现在是否能成功加载nintendo驱动了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ sudo dmesg
Jan 25 22:35:37 hehe kernel: [98280.604727] usb 3-2: new high-speed USB device number 10 using xhci_hcd
Jan 25 22:35:37 hehe kernel: [98280.931988] usb 3-2: New USB device found, idVendor=057e, idProduct=2009, bcdDevice= 2.00
Jan 25 22:35:37 hehe kernel: [98280.932016] usb 3-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
Jan 25 22:35:37 hehe kernel: [98280.932022] usb 3-2: Product: My Test Pro Controller
Jan 25 22:35:37 hehe kernel: [98280.932028] usb 3-2: Manufacturer: test Nintendo Switch Pro
Jan 25 22:35:37 hehe kernel: [98280.932034] usb 3-2: SerialNumber: 0202020001314
Jan 25 22:35:37 hehe kernel: [98280.939409] nintendo 0003:057E:2009.0007: hidraw1: USB HID v81.01 Joystick [test Nintendo Switch Pro My Test Pro Controller] on usb-0000:0b:00.0-2/input0
Jan 25 22:35:37 hehe kernel: [98281.006883] nintendo 0003:057E:2009.0007: using user cal for left stick
Jan 25 22:35:37 hehe kernel: [98281.008835] nintendo 0003:057E:2009.0007: using user cal for right stick
Jan 25 22:35:37 hehe kernel: [98281.010804] nintendo 0003:057E:2009.0007: Failed to read left stick cal, using defaults; e=-22
Jan 25 22:35:37 hehe kernel: [98281.012867] nintendo 0003:057E:2009.0007: Failed to read right stick cal, using defaults; e=-22
Jan 25 22:35:37 hehe kernel: [98281.014782] nintendo 0003:057E:2009.0007: using user cal for IMU
Jan 25 22:35:37 hehe kernel: [98281.024835] nintendo 0003:057E:2009.0007: controller MAC = 41:42:43:44:45:46
Jan 25 22:35:37 hehe kernel: [98281.028080] input: Nintendo Switch Pro Controller as /devices/pci0000:00/0000:00:16.0/0000:0b:00.0/usb3/3-2/3-2:1.0/0003:057E:2009.0007/input/input9
Jan 25 22:35:37 hehe kernel: [98281.028758] input: Nintendo Switch Pro Controller IMU as /devices/pci0000:00/0000:00:16.0/0000:0b:00.0/usb3/3-2/3-2:1.0/0003:057E:2009.0007/input/input10
$ ls -alF /dev/input/by-id
total 0
drwxr-xr-x 2 root root 100 Jan 25 22:35 ./
drwxr-xr-x 4 root root 360 Jan 25 22:35 ../
lrwxrwxrwx 1 root root 9 Jan 25 22:35 usb-test_Nintendo_Switch_Pro_My_Test_Pro_Controller_0202020001314-event-if00 -> ../event6

发现成功加载了eventX,但是却没加载出jsX,我目前认为是该nintendo驱动的问题,经过我研究发现hid-nintendo驱动是最近几年才加入Linux内核的,也许该驱动还不完善,或者本身就没有考虑适配Switch Pro,目前没能想明白该驱动的实际用途。

不过也不能说该驱动一点用没有,如果我们想使用switch手柄,仍然能通过读取eventX来获取手柄的输入,不过eventX的结构体和jsX的不同,eventX的结构体为input_event,如下所示:

1
2
3
4
5
6
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};

目前我还未对该结构体和js_event结构的关系进行研究,猜测jsX的数据是根据eventX的内容进行一些处理后生成的,该问题待后续进一步研究。

本篇总结

通过本篇文章,我们了解了如何模拟一个USB鼠标,USB游戏手柄设备,并且可以学习如何对Linux内核中的HID驱动进行修改然后输出相关调试信息。后续文章中,将会对/dev/input/eventX事件进行深入研究,还有会对非HID的USB进行研究学习。

参考链接

  1. https://usb.org/sites/default/files/hut1_4.pdf

USB设备开发学习(二)

https://nobb.site/2024/01/25/0x85/

Author

Hcamael

Posted on

2024-01-25

Updated on

2024-01-25

Licensed under