USB 数据流侦听

作者:Greg Kroah-Hartman

第一天:我打开盒子,看到一个小的 USB 设备——不比四分之一美元硬币大——一张 CD 和编辑的一张便条,“让它在 Linux 上工作!” “好的”,我想,“这应该很容易。”

Snooping the USB Data Stream

图 1. MARX Software Security 的 CrypToken

我将设备插入我的笔记本电脑,并运行一个名为 usbview 的小程序,以了解 Linux 内核认为这个设备是什么(图 2)。这个设备一定是自称为 USB CrypToken,因为这是设备中包含的字符串。不幸的是,设备名称是红色的,这意味着没有内核驱动程序绑定到该设备。我要么必须编写一个驱动程序,要么找到一种方法使用 libusb 从用户空间与设备通信(有关 libusb 的更多信息,请参阅我的文章“在用户空间编写真实的驱动程序”,《LJ》,2004 年 6 月)。

Snooping the USB Data Stream

图 2. usbview 通过供应商和产品 ID 识别设备。

我不满足于依赖漂亮的 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(KERN_INFO "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 联系到他。

加载 Disqus 评论