USB 数据流侦听
第一天:我打开盒子,看到一个小的 USB 设备——不比四分之一美元硬币大——一张 CD 和编辑的一张便条,“让它在 Linux 上工作!” “好的”,我想,“这应该很容易。”
我将设备插入我的笔记本电脑,并运行一个名为 usbview 的小程序,以了解 Linux 内核认为这个设备是什么(图 2)。这个设备一定是自称为 USB CrypToken,因为这是设备中包含的字符串。不幸的是,设备名称是红色的,这意味着没有内核驱动程序绑定到该设备。我要么必须编写一个驱动程序,要么找到一种方法使用 libusb 从用户空间与设备通信(有关 libusb 的更多信息,请参阅我的文章“在用户空间编写真实的驱动程序”,《LJ》,2004 年 6 月)。
我不满足于依赖漂亮的 GUI 程序,我在 /proc/bus/usb/devices 文件中查找以获取原始设备信息。描述此设备的部分如下所示
T: Bus=01 Lev=02 Prnt=03 Port=02 Cnt=01 Dev#= 4 Spd=1.5 MxCh= 0 D: Ver= 1.00 Cls=ff(vend.) Sub=00 Prot=ff MxPS= 8 #Cfgs= 1 P: Vendor=0d7a ProdID=0001 Rev= 1.07 S: Manufacturer=Marx S: Product=USB crypToken C:* #Ifs= 1 Cfg#= 1 Atr=80 MxPwr= 16mA I: If#= 0 Alt= 0 #EPs= 0 Cls=ff(vend.) Sub=00 Prot=ff Driver=(none)
我想知道是否有其他 Linux 用户尝试过这个设备,我查阅了 Linux USB 工作设备列表(请参阅在线资源部分)。在快速搜索字段中插入供应商 ID 0d7a,结果未找到记录。也许这个项目需要比我想象的更多的工作。
第二天:CD,我把它扔到哪里了?我找到了它,把它放入驱动器,看,上面有一个名为 linux.txt 的文件。哇,一个承认 Linux 可能是可行的操作系统来支持的供应商——这些年来事情肯定发生了变化。在进一步研究并阅读 CD 上的文档后,我意识到该设备是一个小型加密令牌,可用于执行各种有趣的事情,例如从设备读取唯一的序列号(每个设备都不同),通过设备使用仅存储在设备中的 128 位密钥加密数据,以及将数据保存在设备上的安全存储区域中。
CD 上有一个共享库,可用于与 USB 设备通信,以允许程序访问此设备提供的功能。还有一个小型测试程序,演示了不同的库函数如何工作。该库使用 libusb 直接从用户空间与设备通信,这意味着此设备不需要内核驱动程序。但是,该库的许可证不允许在根据 GPL 许可的程序中使用它,这对许多潜在用途来说是不幸的。我需要找到某种方法来允许 GPL 程序与 USB 设备通信。
第三天:在翻找我旧的 USB 补丁集合时,我找到了一位开发人员的参考资料,他修改了内核 usbfs 核心代码以记录流经它的所有数据。这个补丁将允许任何人读取任何使用 usbfs 与 USB 设备通信的程序的原始 USB 数据。由于 libusb 使用 usbfs 与 USB 设备通信,这可能提供了一种反向工程此设备的方法。不幸的是,该补丁没有与参考资料一起出现,并且在互联网上挖掘任何真实代码也没有结果。
第四天:由于没有可用的补丁来完成我想做的事情,我不妨自己做。所以,开始抓取最新的 2.6 内核源代码树并深入研究。
内核源代码树的 drivers/usb/core/ 目录中的文件 inode.c、devices.c 和 devio.c 实现了 usbfs 文件系统。主要文件系统代码在 inode.c 文件中。它包含创建虚拟文件系统和其中的虚拟文件的所有各种 VFS 代码。devices.c 文件处理 /proc/bus/usb/devices 文件的创建和读取。此文件显示系统中当前所有 USB 设备和这些设备的信息。
devio.c 文件控制通过 usbfs 文件系统对 USB 设备的原始访问。为了让用户空间程序通过 usbfs 与 USB 设备通信,它需要在代表 USB 设备的文件上使用 ioctl() 命令。可以通过 usbfs 发送到 USB 设备的所有不同 ioctl 消息都在 include/linux/usbdevfs.h 文件中详细说明。
因此,为了记录通过 usbfs 对所有设备的所有访问,应该修改 devio.c 文件。深入研究该文件,usbdev_ioctl 函数看起来是进行此日志记录的合适位置。它为每个对 usbfs 文件的 ioctl 调用而调用。在该函数中,有一个大的 switch 语句,它根据不同的 ioctl 命令调用适当的函数。这是记录发送到设备的命令类型的完美位置。因此,我在每个 case 语句中添加了一个简单的 printk() 调用,使它们看起来像这样
... case USBDEVFS_CLAIMINTERFACE: printk("CLAIMINTERFACE\n); ret = proc_claiminterface(ps, (void __user *)arg); break; case USBDEVFS_RELEASEINTERFACE: printk("RELEASEINTERFACE\n"); ret = proc_releaseinterface(ps, (void __user *)arg); break; ...
一个简单的编译、安装和模块加载稍后证实,每个 usbfs 访问现在都记录到内核日志中,可以通过运行 dmesg 程序看到。我确定运行 lsusb 程序为lsusb -v产生了大量的 usbfs 访问,因为该程序从所有设备检索所有原始 USB 配置数据。
第五天:既然可以轻松注意到不同类型的 usbfs 访问,现在是时候记录这些访问生成的数据了。在查看 /proc/bus/usb/devices 文件中对设备的描述时,似乎我只关心对控制端点的访问,因为没有为该设备分配端点。
进一步深入研究 devio.c 文件,我确定 proc_control() 函数处理所有控制消息。在那里,代码使用以下代码确定请求是读取还是写入控制消息
if (ctrl.bRequestType & 0x80) {
USB bRequestType 变量是一个位字段,最高位确定请求的方向。因此,在这个 if 语句的读取部分,我添加了以下行
printk("control read: " "bRequest=%02x bRrequestType=%02x " "wValue=%04x wIndex=%04x\n", ctrl.bRequest, ctrl.bRequestType, ctrl.wValue, ctrl.wIndex);
来记录控制请求信息。在读取完成后,我添加以下行来记录从设备读取的实际数据
printk("control read: data "); for (j = 0; j < ctrl.wLength; ++j) printk("%02x ", ctrl.data[j]); printk("\n");
在对 if 语句的写入部分进行大致相同的修改后,我构建、重新加载 usbcore 模块并验证我现在可以记录与设备的来回控制消息。返回的消息是
CONTROL control read: bRequest=06 bRrequestType=80 wValue=0300 wIndex=0000 control read: data 00 00 61 63
第六天:查看我对内核代码所做的修改,我认为这项工作可能是其他用户也可能喜欢的。因此,现在是时候清理代码,使其达到 USB 维护者可能接受为主内核源代码树的状态。
首先,我认识到对 printk() 的调用是不正确的。所有 printk() 调用都必须伴随适当的日志记录级别。这些日志记录级别通过在消息前预先添加适当的 KERN_ 值添加到 printk 调用中。include/linux/kernel.h 文件包含必须使用的以下有效值
#define KERN_EMERG "<0>" /* system is unusable */ #define KERN_ALERT "<1>" /* action must be taken immediately */ #define KERN_CRIT "<2>" /* critical conditions */ #define KERN_ERR "<3>" /* error conditions */ #define KERN_WARNING "<4>" /* warning conditions */ #define KERN_NOTICE "<5>" /* normal but significant condition */ #define KERN_INFO "<6>" /* informational */ #define KERN_DEBUG "<7>" /* debug-level messages */
因此,我将 usbfs_ioctl() 函数中的 printk 调用从
printk("CLAIMINTERFACE\n);
现在内核管理员不应该抱怨不正确的 printk() 用法。
但是,在进一步查看日志消息时,很难确定消息正在为哪个确切的设备记录。需要向 printk() 调用添加更多信息。幸运的是,include/linux/device.h 文件中已经存在一些宏可以帮助我们。它们是 dev_printk() 宏及其助手宏 dev_dbg()、dev_warn()、dev_info() 和 dev_err()。这些宏都需要一个指向 struct device 变量的额外指针,这允许它们为消息打印出唯一的设备 ID。因此,我再次更改 printk() 调用,使其看起来像这样
dev_info(&dev->dev, "CLAIMINTERFACE\n");
然后,控制消息 printk() 调用更改为
dev_info(&dev->dev, "control read: " "bRequest=%02x bRrequestType=%02x " "wValue=%04x wIndex=%04x\n", ctrl.bRequest, ctrl.bRequestType, ctrl.wValue, ctrl.wIndex); dev_info(&dev->dev, "control read: data "); for (j = 0; j < ctrl.wLength; ++j) printk("%02x ", ctrl.data[j]); printk("\n");
转储数据的 printk 调用不需要更改,因为它们仍然与 dev_info() 的调用在同一行上打印。
现在日志消息信息量更大了,看起来像这样
usb 1-1: CONTROL usb 1-1: control read: bRequest=06 bRrequestType=80 wValue=0300 wIndex=0000 usb 1-1: control read: data 00 00 61 63
我可以准确地确定正在与哪个 USB 设备通信,这有助于我筛选掉我不关心的设备的消息。
第七天:糟糕,我现在意识到,如果我期望这个内核更改被社区接受,我最好不要总是生成这些消息。否则,每个人的系统日志都会被他们不关心的消息溢出。如何在仅在被要求时记录消息?
我首先考虑创建一个新的内核构建配置选项。简单地修改 drivers/usb/core/Kconfig 文件添加一个新选项很简单,但是在检查所需的代码更改时,我很快意识到将所有新的日志记录语句包装在 #ifdef CONFIG_USBFS_LOGGING 语句中将导致 USB 维护者拒绝我的内核补丁。#ifdef 通常在内核代码中是不允许的,因为它会降低可读性,并使随着时间的推移维护代码几乎不可能。
相反,我考虑创建一个可以在运行时更改的选项。我在 devio.c 文件中添加了以下代码行
static int usbfs_snoop = 0; module_param (usbfs_snoop, bool, S_IRUGO | S_IWUSR); MODULE_PARM_DESC (usbfs_snoop, "true to log all usbfs traffic");
这为主 usbcore 模块添加了一个名为 usbfs_snoop 的新模块参数。在构建代码后,可以通过运行 modinfo 程序看到这一点
$ modinfo usbcore license: GPL parm: blinkenlights:true to cycle leds on hubs parm: usbfs_snoop:true to log all usbfs traffic
通过使用以下行加载模块
modprobe usbcore usbfs_snoop=1
用户可以启用该选项。
我使用了定义 module_param() 而不是旧式的 MODULE_PARM(),因为这是在 2.6 内核中描述模块参数的正确方法。主要区别在于此定义具有第三个参数。如果将此第三个参数设置为 0 以外的值,则会导致该参数显示在 sysfs 中,并允许用户在模块加载时查询和修改该选项。包含此代码后,sysfs 中 usbcore 模块的目录看起来像
$ ls -l /sys/module/usbcore/ -r--r--r-- 1 root root 4096 May 13 15:33 blinkenlights -r--r--r-- 1 root root 4096 May 13 15:33 refcnt -rw-r--r-- 1 root root 4096 May 13 15:33 usbfs_snoop
现在可以像往常一样加载模块
modprobe usbcore
当我决定打开日志记录时,我只需执行
echo 1 > /sys/module/usbcore/usbfs_snoop
并且 devio.c 文件中的内核变量 usbfs_snoop 会动态更改。
既然我可以确定用户是否希望打印出侦听消息,我需要再次修改 dev_info() 调用。我创建了以下宏来执行此操作
#define snoop(dev, format, arg...) \ do { \ if (usbfs_snoop) \ dev_info( dev , format , ## arg); \ } while (0)
此宏测试 usbfs_snoop 变量的值,如果为真,则调用 dev_info() 行。该宏包装在do { } while (0)语句中,以允许它在任何类型的代码中使用,而无需担心任何副作用。所有包含多行代码的内核宏都以这种方式编写,原因就在于此。有关此的更多详细信息,请阅读内核新手 FAQ(请参阅资源)。
接下来,我将所有先前添加的 dev_info() 调用更改为对 snoop() 的调用,使代码看起来像这样
snoop(&dev->dev, "control read: " "bRequest=%02x bRrequestType=%02x " "wValue=%04x wIndex=%04x\n", ctrl.bRequest, ctrl.bRequestType, ctrl.wValue, ctrl.wIndex);
但是在打印数据的地方,snoop() 宏无法正常工作。我需要直接检查 usbfs_snoop 变量的值,将代码包装在 if 语句中
if (usbfs_snoop) { dev_info(&dev->dev, "control read: data "); for (j = 0; j < ctrl.wLength; ++j) printk("%02x ", ctrl.data[j]); printk("\n"); }
我很高兴,并且希望 USB 维护者也会对这些更改感到满意。我阅读了如何通过查阅文件 Documentation/SubmittingPatches 生成正确的内核补丁,生成一个 diff 文件并通过电子邮件发送出去。
我们现在有一种方法来侦听所有 usbfs 流量,这可以帮助我们反向工程任何使用 libusb 与 USB 设备通信的设备。它还允许我们侦听在 VMware 会话中运行的来宾操作系统中的任何 USB 访问,从而可以更轻松地反向工程 Microsoft Windows USB 驱动程序。但是所有这些都必须等到下一期专栏。
本文的资源: /article/7605。
Greg Kroah-Hartman 目前是各种不同驱动程序子系统的 Linux 内核维护者。他在 IBM 工作,从事与 Linux 内核相关的工作,可以通过 greg@kroah.com 联系到他。