简单的 URB (USB 请求块) 监视器






4.89/5 (26投票s)
简单的 URB (USB 请求块) 监视器
引言
这篇文章是关于“我所知道的”,我所展示的内容可能不完全正确。如果您发现我错了,请告知我,以便我们进一步讨论。
这是我的第二篇文章,内容是如何监视 USB 设备驱动程序使用的 URB (USB 请求块)。老实说,我并不完全了解 USB 协议的工作原理,实际上,当我试图理解如何编写 USB 驱动程序时,我获得了这个想法。如果您想确切了解此协议的工作原理,请参考 官方 USB “规范维护者”。基本思想与我上一篇文章相同,但这次略有不同。好的,我们开始详细介绍。
I. USB 驱动程序对象堆栈。
基本上,大多数 USB 驱动程序都需要维护两种对象:功能设备对象 (FDO) 和物理设备对象 (PDO),就像普通的 PnP 驱动程序一样。FDO 由 USB 驱动程序创建,而 PDO 由其父驱动程序创建,通常是 USB 集线器总线驱动程序。因此,驱动程序对象的堆栈可能如下所示
如果您对上图感到不适,请原谅我(我不是艺术家)。如果我们可以在图片中的“附加点”放置一个过滤器对象,我们就可以收集数据用于监视。怎么做?请阅读下一节。
II. USB 驱动程序如何与硬件通信。
正如您可能已经知道的那样,USB 驱动程序从不直接“与”设备“对话”。它通过其总线驱动程序“与”硬件“对话”。基本上,它会:
- 构建一个 URB (USB 请求块)。
- 构建一个 IRP 控制请求(
IRP_MJ_INTERNAL_DEVICE_CONTROL
请求类型),控制码为IOCTL_INTERNAL_USB_SUBMIT_URB
。 - 将 URB 分配给此 IRP 的堆栈。
- 同步或异步地将此类型的 IRP 发送到下一个较低的驱动程序或总线驱动程序(
IoCallDriver()
)。
III. 附加到目标设备并从中分离。
问题是:PDO 在我们的 USB 目标设备插入其集线器之前不存在(或未激活)。基本上,当 USB 设备插入其集线器时,PnP 管理器会向相应的集线器总线驱动程序发送一个类型为 IRP_MN_QUERY_DEVICE_RELATIONS
的 IRP,该集线器驱动程序将通过为此子设备创建 PDO 来响应此请求,并向 PnP 管理器报告它所持有的子设备的数量(如果您想查看详细信息,请阅读 WDK 文档中关于“在运行的系统中添加 PnP 设备”的部分)。因此,我们还需要监视 USB 集线器驱动程序,方法是创建一个过滤设备对象,然后将其附加到 USB 集线器设备对象,代码如下(文件 DkFltMgr.c 中的 DkCreateAndAttachHubFilt()
函数):
...
// 1. Get device object pointer of USB Root Hub driver to attach to
RtlInitUnicodeString(&usTgtName, (PWSTR) pIrp->AssociatedIrp.SystemBuffer);
ntStat = IoGetDeviceObjectPointer(&usTgtName, GENERIC_ALL, &pFlObj, &pTgtDevObj);
if (!NT_SUCCESS(ntStat)){
DkDbgVal("Error get USB Hub device object!", ntStat);
return ntStat;
}
// 2. Create filter object
ntStat = IoCreateDevice(pDevExt->pDrvObj, 0, NULL,
pTgtDevObj->DeviceType, 0, FALSE, &pDevExt->pHubPDO);
if (!NT_SUCCESS(ntStat)){
DkDbgVal("Error create USB Hub filter!", ntStat);
ObDereferenceObject(pFlObj);
goto EndFunc;
}
ObDereferenceObject(pFlObj);
// 3. Attach to bus driver
pDevExt->pNextHubPDO = NULL;
pDevExt->pNextHubPDO = IoAttachDeviceToDeviceStack(pDevExt->pHubPDO, pTgtDevObj);
if (pDevExt->pNextHubPDO == NULL){
ntStat = STATUS_NO_SUCH_DEVICE;
DkDbgStr("Error attach device!");
goto EndFunc;
}
pDevExt->pHubPDO->Flags |=
(pDevExt->pNextHubPDO->Flags &
(DO_BUFFERED_IO | DO_DIRECT_IO | DO_POWER_PAGABLE));
pDevExt->pHubPDO->Flags &= ~DO_DEVICE_INITIALIZING;
...
我们通过发出 IOCTL_DKSYSPORT_START_MON
来启动监视。当发送到包含集线器过滤器对象的驱动程序的 IRP_MN_QUERY_DEVICE_RELATIONS
(类型为 BusRelations
)时,我们只需转发它,然后创建目标过滤器设备对象,并将其附加到 USB 集线器总线驱动程序创建的设备对象,代码如下(文件 DkPnp.c 中的 DkHubFltPnPHandleQryDevRels()
函数):
...
switch (pStack->Parameters.QueryDeviceRelations.Type){
case BusRelations:
DkDbgStr("PnP, IRP_MN_QUERY_DEVICE_RELATIONS: BusRelations");
ntStat = DkForwardAndWait(pDevExt->pNextHubFlt, pIrp);
// After we forward the request, the bus driver have created or deleted
// a child device object. When bus driver created one (or more),
// this is the PDO of our target device, we create and attach
// filter object to it.
// Note that we only attach the last detected USB device
// (numb. of PDOs - 1) on it's Hub.
if (NT_SUCCESS(ntStat)){
pDevRel = (PDEVICE_RELATIONS) pIrp->IoStatus.Information;
if (pDevRel){
DkDbgVal("Child(s) number", pDevRel->Count);
if ((pDevRel->Count > 0) && (pDevRel->Count > pDevExt->ulTgtIndex)){
if (pDevExt->pTgtDevObj == NULL){
DkDbgStr("Create and attach target device");
pDevExt->ulTgtIndex = pDevRel->Count - 1;
DkCreateAndAttachTgt(pDevExt,
pDevRel->Objects[pDevRel->Count - 1]);
}
} else {
// Do nothing
}
}
}
IoReleaseRemoveLock(&pDevExt->ioRemLock, (PVOID) pIrp);
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return ntStat;
...
当我们的驱动程序收到针对目标过滤器设备对象的 IRP_MJ_REMOVE
时,我们在转发之后就分离它,代码如下(文件 DkPnp.c 中的 DkTgtPnp()
函数):
...
case IRP_MN_REMOVE_DEVICE:
DkDbgStr("IRP_MN_REMOVE_DEVICE");
IoSkipCurrentIrpStackLocation(pIrp);
ntStat = IoCallDriver(pDevExt->pNextTgtDevObj, pIrp);
IoReleaseRemoveLockAndWait(&pDevExt->ioRemLockTgt, (PVOID) pIrp);
DkDetachAndDeleteTgt(pDevExt);
return ntStat;
...
有些人可能会问,为什么在停止监视(通过发出 IOCTL_DKSYSPORT_STOP_MON
)时不这样做?好问题,这是因为当我们“销毁”或“破坏”活动设备对象的“堆栈构建”时,或者 PnP 管理器仍然认为我们的过滤器对象仍然存在,如果我们分离位于 FDP 和 PDO 之间的过滤器对象,就会发生这种情况。因此,在设备活动时分离过滤器目标设备对象是一个糟糕的主意,我们的驱动程序将阻止这种情况发生。
在这种情况下,是否可以从活动设备分离?嗯,我认为这是可能的,因为我认为内核对象以链表的形式排列,它们之间有指针,您所要做的就是“跟踪”堆栈并重建堆栈,但这只是我的看法(未经测试),我可能是错的。
IV. 数据收集
如前一节所述,USB 驱动程序会向其集线器总线驱动程序发送一个 IRP(“与”设备“对话”),请求类型为 IRP_MN_INTERNAL_DEVICE_CONTROL
,控制请求类型为 IOCTL_INTERNAL_USB_SUBMIT_URB
。当包含目标设备过滤器对象的驱动程序收到这种请求时,我们可以在转发之前(PRE)和转发之后(POST)收集数据。DkTgtInDevCtl()
例程处理此请求。代码如下(文件 DkDevCtl.c):
...
// Our interest is IOCTL_INTERNAL_USB_SUBMIT_URB, where USB device send URB to
// it's USB bus driver
if (pStack->Parameters.DeviceIoControl.IoControlCode == IOCTL_INTERNAL_USB_SUBMIT_URB){
// URB is collected BEFORE (PRE) forward to bus driver or next lower object
pUrb = (PURB) pStack->Parameters.Others.Argument1;
if (pUrb != NULL){
RtlInitUnicodeString(&usUSBFuncName,
DkDbgGetUSBFuncW(pUrb->UrbHeader.Function));
DkTgtCompletePendedIrp(usUSBFuncName.Buffer,
usUSBFuncName.Length, (PUCHAR) pUrb, pUrb->UrbHeader.Length, 1);
}
// Forward this request to bus driver or next lower object
// with completion routine
IoCopyCurrentIrpStackLocationToNext(pIrp);
IoSetCompletionRoutine(pIrp,
(PIO_COMPLETION_ROUTINE) DkTgtInDevCtlCompletion,
NULL, TRUE, TRUE, TRUE);
ntStat = IoCallDriver(pDevExt->pNextTgtDevObj, pIrp);
...
转发后的数据收集完成例程如下(文件 DkDevCtl.c 中的 DkTgtInDevCtlCompletion()
函数):
...
// URB is collected AFTER (POST) forward to bus driver or next lower object
if (NT_SUCCESS(pIrp->IoStatus.Status)){
pStack = IoGetCurrentIrpStackLocation(pIrp);
pUrb = (PURB) pStack->Parameters.Others.Argument1;
if (pUrb != NULL){
RtlInitUnicodeString(&usUSBFuncName,
DkDbgGetUSBFuncW(pUrb->UrbHeader.Function));
DkTgtCompletePendedIrp(usUSBFuncName.Buffer,
usUSBFuncName.Length, (PUCHAR) pUrb, pUrb->UrbHeader.Length, 0);
} else {
DkDbgStr("Bus driver returned success but the result is NULL!");
}
} else {
DkDbgVal("Bus driver returned an error!", pIrp->IoStatus.Status);
}
...
V. 限制(缺点)。
以下是我能想到的关于我们的监视器的限制:
- 此监视器假定目标驱动程序在“与”硬件“对话”时会调用
IoCallDriver()
到下一个较低的驱动程序。目标驱动程序可能不会这样做,它可能选择通过自己的库和/或 USB 控制器来“与”硬件“对话”(我不知道如何做到这一点,但我认为这是可能的)。当它这样做时,我们的监视器就无法捕获数据。 - 堆栈中位于我们过滤器对象之上的目标驱动程序的较低过滤器可能会过滤和/或修改它们,因此我们在监视器中看到的内容可能与目标驱动程序发送或接收的内容不同。
- USB 硬件接收到的数据可能与目标驱动程序发送的数据不同,因为总线驱动程序可能会修改数据。因此,如果您使用此监视器来监视 USB 硬件,在数据分析中应考虑这种情况。
- 此监视器不跟踪请求的“配对性”,例如,数据编号 11 的方向是 PRE(转发前数据),数据编号 12 的方向是 POST(转发后数据),但这并不意味着数据编号 11 与数据编号 12 配对。
- 此监视器只能收集 512 字节的数据,否则数据将被丢弃。
VI. 安装和使用
VI.A. 驱动程序安装
这与我之前的文章相同,但您必须在需要时选择 DkSysPort.inf 而不是 DkPortMon2.inf,驱动程序名称为 DkSysPort 而不是 DkPortMon。如果您要卸载此驱动程序,请确保监视器未处于活动状态,然后转到设备管理器,然后卸载它(无需重新启动)。
在 Windows 7(32 位无服务包)上,请按照以下步骤操作:
- 下载并解压上述软件包。
- 打开控制面板。
- 单击硬件和声音。
- 单击设备管理器,然后单击其中显示的计算机名称。
- 选择菜单操作 - 添加过时硬件,然后会出现硬件向导,单击下一步。
- 选择从列表中手动选择要安装的硬件。(高级)。,然后单击下一步。
- 在列表框中选择系统设备,然后单击下一步。
- 单击从磁盘安装...(按钮),然后找到解压包中 Win7 子目录下的 DkSysPort.inf。
- 这将显示列表框中的DkSysPort 系统驱动程序。然后单击下一步。
- 再次单击下一步,这将开始驱动程序安装过程。当出现安全对话框时,通过单击仍要安装此驱动程序软件来忽略它,然后单击完成。
- 通过展开设备管理器树中的系统设备来检查驱动程序是否已正确安装。
VI.B. 使用
- 运行 Bin 目录下的名为“DkURBMonGui.exe”的程序。
- 将您的 USB 目标设备插入 PC 的一个 USB 端口。
- 单击工具 - 检测设备,这将告诉您哪个 USB 设备插入了哪个 USB 根集线器(例如 \Device\USBPDO-4)。这个集线器名称用于监视。
- 拔下您的目标 USB 设备。
- 通过单击工具 - 选择 USB 集线器...来选择 USB 根集线器名称,然后选择在步骤 3 中检测设备菜单找到的要监视的 USB 集线器名称。
- 启动监视器,单击工具 - 启动监视器。
- 插入您的 USB 目标设备。
- 如果您已完成监视,请拔下您的目标设备,然后单击工具 - 停止监视器。
DkURBMon 在检测模式下的图片应如下所示:
DkURBMon 在监视模式下的图片应如下所示:
对于 Windows 7,您需要以管理员身份运行才能运行 DkURBMonGuid.exe。
如果您想查看驱动程序中的一些调试消息,请使用 Sysinternal 的 DbgView。在 Windows 7 上,您需要以管理员身份运行此实用程序,然后勾选启用详细内核输出菜单。
VII. 编译和链接源代码
这与我之前的文章相同。
Bug 修复
- 2012 年 4 月 26 日,将 DkApp.cpp 中
DkPortOnDataReceice(PDKPORT_DAT pDat)
函数的*pTmp++ = HexChar[pDat->Data[ul] && 0x0F];
更改为*pTmp++ = HexChar[pDat->Data[ul] & 0x0F];