深入探索,尝试对 USB 和游戏手柄进行模拟。这不仅能加深我们对 USB 设备工作原理的理解,还能为开发各类有趣的应用提供技术支持。
一、USB 模拟
修改协议:在脚本中,将表示设备类型的协议值修改为 2,以表示设备。具体操作为:echo 2 > "${FUNCTIONS_DIR}/protocol" # mouse。
修改 HID 描述符:HID 描述符需要定义按键、坐标和滚轮等功能。现代功能丰富,除了基本的左右键,还有中键、滚轮以及一些额外按键。我们编写的 HID 描述符如下:
需要注意的是,这样的操作只实现了按下左键,并没有释放。要实现完整的点击过程,需要分别发送按下和释放的指令,代码如下:
对于鼠标的其他功能,如控制其他按键、横向和纵向移动以及滚轮滚动,只需对 mouse_buf 数组中的相应位进行修改即可。
二、USB 游戏手柄模拟
(一)普通游戏手柄模拟
在成功模拟 USB 鼠标和键盘后,模拟一个普通的 USB 游戏手柄也变得可行。首先,我们需要修改模拟脚本中的一些参数,以确保设备被正确识别为游戏手柄:
当 protocol 设置为 0 时,设备不会被鼠标和键盘的驱动识别,而是会被游戏手柄的驱动根据 idVendor 和 idProduct 进行匹配。在 Linux 系统中,游戏手柄的驱动代码通常位于 drivers/input/joystick/xpad.c,我们可以查看该驱动中能识别的 XBox 游戏手柄列表,确认我们模拟的手柄型号(这里是 Microsoft X-Box 360 pad)是否在其中。
接下来,我们需要编写游戏手柄的 HID 描述符,以定义其功能和按键布局:
这个 HID 描述符定义了 14 个按键,用于实现各种游戏操作。同时,还定义了一个 Hat Switch,用于控制方向,其值用 4bit 表示,代表上下左右四个方向,移动距离以厘米为单位,最大值为 315cm。此外,还定义了四个摇杆控制轴(XYZRz),每个轴的值占 8bit,用于精确控制游戏中的角色移动和视角。
完成上述设置后,运行脚本,我们可以在 Linux 主机上看到相关的设备信息。通过查看 /dev/input 目录,我们可以发现新增了与游戏手柄相关的文件,如 js1 和 event6。我们可以通过读取这些文件来获取手柄的输入信息,例如使用以下命令查看 js1 文件的内容:cat /dev/input/js1|hexdump -C。为了更直观地观察手柄的输入,我们还可以编写一个 Python 脚本来解析 js1 文件的数据结构:
这个脚本能够实时输出手柄的操作信息,包括操作时间、操作值、操作类型和操作的按键或轴编号。通过分析这些信息,我们可以更好地理解手柄的工作原理和用户的操作行为。
(二)深入研究游戏手柄
在最初研究游戏手柄时,我们考虑通过抓包分析现成手柄的数据来进行模拟。我们选择了 Switch Pro 手柄和一款国产手柄作为研究对象,并在 Linux 源码中发现了与 Switch 手柄相关的驱动:drivers/hid/hid - nintendo.c。然而,在实际研究过程中,我们遇到了一些问题。
正版的 Switch Pro 手柄在接入 Windows 或 Ubuntu 系统时,虽然能够抓到 USB 数据包和 HID 描述符,但主机无法正常使用手柄。在 Windows 系统上,由于缺乏逆向驱动的经验,我们暂时无法深入研究。而在 Ubuntu 系统上,通过查看日志发现,虽然系统成功适配到了 hid_nintendo 驱动,但出现了大量错误,导致手柄驱动注册失败,无法在 /dev/input 目录下生成相应的文件。这些错误主要与读取 SPI flash 失败以及校准数据读取失败有关。
对于国产手柄,其在 Windows 系统上会伪装成 Switch Pro 手柄,当检测到无法正常使用时,会再次伪装成 XBox 手柄。然而,该手柄在代码实现上可能存在严重问题,导致在使用 USBTree View、Wireshark 的 USBPcap 以及 Windows 部分 USB 驱动时出现崩溃现象,甚至影响到 Linux 虚拟机中的 USB 驱动识别。这可能是由于手柄存在防破解机制,导致在进行抓包和分析时出现异常。
由于以上问题,我们决定对 Linux 内核中的 nintendo 驱动进行研究和调试。调试驱动的方法有多种,包括使用 eBPF、开启内核编译的 DEBUG 参数以及编辑驱动源码添加调试输出。经过分析,我们选择了编辑驱动源码的方法,因为这种方法相对耗时较少。
三、动态修改 Ubuntu 驱动
(一)安装内核源码
为了修改驱动,我们首先需要安装与当前内核版本相同的内核源码。通过以下命令可以获取当前内核版本,并安装相应的内核源码:
(二)编写驱动编译加载脚本
为了方便地重新编译和加载驱动,我们编写了一个脚本,该脚本能够在修改驱动后快速完成编译和加载操作:
这个脚本接受一个参数,即驱动名称。它首先卸载当前的驱动模块,然后使用 make 命令重新编译驱动,并使用脚本对编译后的驱动进行签名,最后将新的驱动模块加载到内核中,并实时查看内核日志以观察驱动的加载情况。
(三)添加调试输出
为了更好地调试 nintendo 驱动,我们可以通过两种方式添加调试输出:
定义新的调试函数:参考 hid_dbg,我们定义了一个新的调试函数 mhid_dbg,用于输出调试信息。通过将 hid - nintendo.c 中的 hid_dbg 替换为 mhid_dbg,我们可以在驱动运行时输出更多详细的信息。
使用 print_hex_dump 函数:在 nintendo_hid_event