USB设备开发学习(二)
接着上一篇遗留的问题,继续研究。
1. 模拟鼠标
在上一篇的Paper中,我们尝试对USB键盘进行模拟,下一步再尝试模拟一个鼠标。
在能成功模拟键盘的基础上,要实现鼠标的模拟是很容易的,只需要修改两部分。
首先,需要对协议进行修改,1
表示的是键盘,2
表示的是鼠标:
1 | echo 2 > "${FUNCTIONS_DIR}/protocol" # mouse |
其次,需要对HID描述符进行修改,对于鼠标来说,至少需要两个按键(左右键)和XY二维坐标系(控制鼠标的移动)。但是现在的鼠标功能越来越多了,比如我们可以加一个滚轮,这就需要增加一个一维坐标系,和一个按键(中键)。另外大拇指的地方还可以设置两个按键用来翻页。
根据上述描述的功能,我们来便携一个鼠标的HID描述符:
1 | 0x05, 0x01, # USAGE_PAGE (Generic Desktop) |
在讲解上面的HID描述符的时候,我们首先要知道一件事,鼠标的厂商如果想要生产一个USB免驱的鼠标,那么并不是他们想增加什么按键就能增加的,而是需要按照HID规范文档[1],因为操作系统开发内置的驱动的时候也是按照该文档来进行开发的。
接下来讲解一下上面的HID描述符,首先我们定义了5个Button按键,一个按键占1bit,1位按下,0位释放,一共占了5bit,由于需要8bit对其,所以还有3bit的padding。
这5个按键分别代表什么呢?可以自己尝试一下,或者搜索Linux鼠标驱动的源码,如下所示
1 | // drivers/hid/usbhid/usbmouse.c |
接着定义了一个XY直角坐标系,代表了鼠标是如何移动的,其值从-0x7FFF到0x7FFF,一个轴占2字节,两个共占4字节。
最后定义了滚轮,最小值为-0x7F,最大值为0x7F,共占1字节。
上述的三部分总共占据了6字节,因为定义了一个USB中断传输为8字节,所以还可以加上2字节的padding,不加也无所谓,因为也不会报错。
到此为止,一个鼠标我们就模拟成功了,按照上一篇的步骤,运行我们的脚本,我们就能通过写/dev/hidg0
来控制主机上鼠标的点击与移动了。
比如我们需要点击左键:
1 | def sendBuf(buf): |
但是需要注意,按照上述代码操作以后,表示的是按下左键,但是并不会释放,我们正常使用鼠标左键点击的实际过程其实是包含两部分的。首先,按下左键,然后释放左键。这才是一个完整的点击过程。如果我们要实现一个完整的点击过程,需要这么修改:
1 | def sendBuf(buf): |
如果我们需要控制鼠标的其他功能,可以对上述代码进行简单的修改,比如定义的五个按键,只需要控制mouse_buf[0]
的低5bit的值。控制鼠标横向移动,则是控制mouse_buf[1:3]
的值,纵向移动则是mouse_buf[3:5]
,控制滚轮则是mouse_buf[5]
。后续具体细节大家可以私下自行测试研究。
模拟USB游戏手柄
接下来就是本篇的核心内容:模拟一个USB游戏手柄。
该部分内容说简单也简单,在能成功模拟USB鼠标键盘之后,也可以很容易的模拟出一个USB游戏手柄。但是如果深入研究当今常用的游戏手柄,也会产生很多问题。
首先,我们从简单的学起,先实现一个普通的游戏手柄,首先看我们要如何修改模拟鼠标键盘的脚本:
1 | echo 0 > "${FUNCTIONS_DIR}/protocol" # None |
当protocol等于0的时候,不会被鼠标键盘的驱动识别到。这个时候会根据idVendor/idProduct匹配出具体产品的驱动,在Linux上,手柄的驱动一般为drivers/input/joystick/xpad.c
,可以查看该驱动中能识别的游戏手柄:
1 | xpad_device[] = { |
从上面我们可以看出,我们模拟的手柄为:Microsoft X-Box 360 pad
。
接下来就是编写HID描述符了:
1 | 0x05, 0x01, // Usage Page (Generic Desktop Ctrls) |
接下来分析一下该HID描述符:
- 定义了14个按键,占14bit,8bit对其,有2bit的padding填充。
- 定义了Hat Switch(这个我认为是很早游戏手柄的按键的叫法,现在我认为属于手柄的方向按键),占4bit,分别代表了上下左右,按住按键移动的距离单位用cm(厘米),移动的最大值是315cm。
- 接下来就是摇杆控制的XY还有体感控制的Z和Rz,每个方向的值占8bit,也就是1字节,4个就占了4字节,单位没有单独定义,所以也是cm,最大值为255。
- 最后还有1字节的padding填充,所有按键定义总共加起来有8字节。
所有环节准备就绪了,接下来就可以运行我们的脚本,然后可以在Linux主机上看到下列信息:
1 | $ sudo dmesg |
接着我们可以发现在/dev/input
目录下多了两个文件:
1 | ls -alF /dev/input/by-id |
接着我们可以通过直接读/dev/input/js1
文件:cat /dev/input/js1|hexdump -C
来查看手柄的输入:
1 | # 树莓派USB设备端 |
我们可以写一个python脚本让js1的数据更有可视性:
1 | #!/usr/bin/env python3 |
下面展示一些数据的样例:
1 | data = [0] * 8 |
从这我们可以看出,type=1
应该表示为按键,除了按键还有滚轮方向键这些(XYZRz和Hat Switch),number=x
表示第几个按键,按照我们编写的HID描述符来说,number
的值应该在0-13
之间,而value
就是具体的值,对于按键来说,只能是0或者1。
或者我们能安装joystick
包:sudo apt install joystick
,使用jstest
命令来进行更加可视化的观察:
1 | jstest /dev/input/js1 |
手柄的模拟第一阶段就到此为止,我们已经可以成功的模拟一个功能简单的游戏手柄。
接下来就是对游戏手柄的深入研究,或者也不算是深入研究,只是我在研究游戏手柄最初踩到的一个坑,从而让我多研究了一部分内容。
在研究游戏手柄最初,我考虑的方案是用我手术现成的手柄连接到电脑上,然后抓包,分析实际的手柄数据包。
我手头上现成的手柄有两个,一个上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 | $ sudo dmesg |
发现ubuntu成功适配到了hid_nintendo
驱动,但是却报了一堆的错误,我估计是这一堆错误导致手柄驱动注册失败的,在Linux下能被正常识别的手柄应该像我们上面的案例一样,能在/dev/input/
目录下生成jsX
和eventX
文件,因为经过我研究,在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就行。需要有一个比较长的环境搭建的过程,虽然以前也调试过内核,不过我不是专门做这块的,上一次调试内核也是两三年前了,我本地并没有现成的环境。
由于动态调试的道路比较繁琐,再考虑一下其他调试方案,我能想到的有三种:
- 使用eBPF。
- 驱动本身有调试输出,不过需要内核编译开启DEBUG参数。
- 因为需要研究的是Linux驱动,而Linux的驱动大部分都是属于插件类型,可以在系统启动之后进行加载,卸载等动作,所以可以考虑编辑驱动源码,添加调试输出,然后把原本的驱动卸载,加载我修改后的驱动。
对于上述三个方案,首先,我随意研究过eBPF,但是研究的不深,开发相关程序也没写过多少,所以还是比较花时间的。其次第二个方案,需要重新编译Linux内核,这个通过需要浪费大量时间。因此我考虑了第三个方案。
动态修改Ubuntu驱动
首先安装内核源码:
1 | # 需要包装安装的源码和当前内核同一版本 |
接着我编写了一个简单的脚本,当我们修改了相关驱动的时候,快速重新编译驱动然后加载进内核:
1 | !/bin/bash |
输出调试信息方案有两种:
1 | 1. 参考了hid_dbg,重新定一个了 |
这样我们就能对nintendo驱动进行调试,下面记录一下我的研究结果:
每个hid驱动首先关注hid_driver
结构体:
1 | static struct hid_driver nintendo_hid_driver = { |
.id_table
的结构数据表明了什么情况下会匹配到当前驱动,匹配成功后执行.probe
函数,当接收到设备发来的数据时,触发.raw_event
函数,设备移除时触发.remove
函数,设备有可能会休眠,休眠结束后会触发.resume
函数。
下一步我们需要研究nintendo_hid_probe
函数,经过研究发现switch
手柄也有专门的协议,比如有定义握手包,有读取手柄芯片上数据的功能。让我发现一个很奇怪的事情,因为很大一块读取手柄数据的代码逻辑是为了判断该手柄是左手柄还是右手柄,玩过switch的同学就知道这代表啥意思。
但问题是,switch的分左右的手柄没有usb接口,这驱动为啥要适配USB_DEVICE_ID_NINTENDO_JOYCONL
和USB_DEVICE_ID_NINTENDO_JOYCONR
?
并且通过抓包,我的Switch Pro
手柄并不会响应驱动中定义的协议,我以前看到过研究switch手柄的paper,记得里面提过switch手柄相关的协议流量,不过抓的是JOYCONL
和JOYCONR
手柄的流量,但是讲道理Switch Pro
也应该支持,难度我花了大价钱买了个假货?(有可能,怪不得没用多久摇杆就漂移了。)
通过逆向驱动中的nintendo协议,我编写了以下脚本,当我模拟Switch Pro
设备成功后,监听/dev/hidg0
,回应主机发来的请求:
1 | #!/usr/bin/env python3 |
接着在看现在是否能成功加载nintendo驱动了:
1 | $ sudo dmesg |
发现成功加载了eventX
,但是却没加载出jsX
,我目前认为是该nintendo
驱动的问题,经过我研究发现hid-nintendo
驱动是最近几年才加入Linux内核的,也许该驱动还不完善,或者本身就没有考虑适配Switch Pro
,目前没能想明白该驱动的实际用途。
不过也不能说该驱动一点用没有,如果我们想使用switch手柄,仍然能通过读取eventX
来获取手柄的输入,不过eventX
的结构体和jsX
的不同,eventX
的结构体为input_event
,如下所示:
1 | struct input_event { |
目前我还未对该结构体和js_event
结构的关系进行研究,猜测jsX
的数据是根据eventX
的内容进行一些处理后生成的,该问题待后续进一步研究。
本篇总结
通过本篇文章,我们了解了如何模拟一个USB鼠标,USB游戏手柄设备,并且可以学习如何对Linux内核中的HID驱动进行修改然后输出相关调试信息。后续文章中,将会对/dev/input/eventX
事件进行深入研究,还有会对非HID的USB进行研究学习。
参考链接
USB设备开发学习(二)