Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / system

Simple URB (USB Request Block) Monitor

4.89/5 (27 votes)
9 Nov 2012CPOL8 min read 219.7K   12.1K  
Simple URB (USB Request Block) Monitor

Introduction

This is about "what I know thing", and I can be wrong about what I'll show you specially in this article. If you find out that I'm wrong please let me know so we can discuss it further.

This is my second article, and it about how to monitor URB (USB Request Block) which is used by USB device driver. Honestly, I do not know exactly how USB protocol work, well, in fact I got this idea when I try to understand how to write USB driver. If you want to know exactly how this protocol work, please take a look at official USB "specification maintainer". The basic idea is the same as my previous article but this time with little bit variation. Okay lets get to detail.

I. USB driver object stack.

Basically, most of USB drivers have to maintain two kinds of object, functional device object (FDO) and physical device object (PDO), as ordinary PnP driver did. FDO is created by USB driver but PDO is created by it's parent driver, usually USB Hub Bus driver. So the stack for the driver objects may look like this:

335364/FDO-PDO.JPG

If you feel uncomfortable with the above picture, please forgive me (I am not an artist). If we can put a filter object in "attach point" in the picture, we can collect data for monitoring purpose. How come? Well, please read the next section.

II. How USB driver communicate with it's hardware.

As you may already knew that USB driver never "talk to" it's device directly. It "talks to" it's hardware via it's bus driver. Basically it:

  1. Build a URB (USB Request Block).
  2. Build an IRP control request (IRP_MJ_INTERNAL_DEVICE_CONTROL request type) with control code IOCTL_INTERNAL_USB_SUBMIT_URB.
  3. Assign URB to this IRP's stack.
  4. Send this kind of IRP to the next lower driver or bus driver synchronously or asynchronously. (IoCallDriver()).
If you want to see the detail, please take a look at the MSDN about how to send a URB to the bus driver. If we have succeeded to attach our filter device object at "attach point" to the PDO, we will receive this kind of IRP and we can start to collect data.

III. Attach to and detach from target device.

The problem is: PDO doesn't exists (or not active) until our USB target device plug into it's hub. Basically, when a USB device plug into it's hub, PnP Manager will send an IRP with the type of IRP_MN_QUERY_DEVICE_RELATIONS to corresponding hub bus driver, this hub driver will response to this request by creating PDO for it's child and report the number of it's child(s) which it holds to PnP Manger (If you want to see the detail, please read WDK documentation in the section about "Adding a PnP Device to running system"). Thus we need to monitor USB Hub driver too, by creating a filter device object and then attach it to USB Hub device object, and the code is look like this (DkCreateAndAttachHubFilt() function in file DkFltMgr.c)

C++
...
    // 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;
...

We do this by issuing IOCTL_DKSYSPORT_START_MON to start monitor. When IRP_MN_QUERY_DEVICE_RELATIONS (with BusRelations query type) is sent to our driver which contain Hub filter object, we just forward it and after that, we create target filter device object and then attach it to device object created by USB Hub bus driver, just like this codes (DkHubFltPnPHandleQryDevRels() function in file DkPnp.c):

C++
...
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;

...

When our driver receive IRP_MJ_REMOVE which addressed to our target filter device object, we detach this after we forward, like this codes (DkTgtPnp() function in file DkPnp.c):

C++
...
    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;

...

Some of you may ask, why don't you do this when you stop monitor (by issuing IOCTL_DKSYSPORT_STOP_MON) ? Good question, well, this because we will "destroy" or "break" the "stack building" of an active device object and/or PnP Manager is still thinking that our filter object still exists if we detach our filter object which reside in between FDP and PDO. So detaching filter target device object while device is still active is bad idea and our driver will prevent this.
Is it possible to detach from active device in this case? Well, I think it possible, because I think kernel objects are arranged in a form of linked list, they have pointer to each other, all you have to do is "keep the track" of the stack and rebuild this stack, but this is just my opinion (untested) and I can be wrong.

IV. Collecting data

As stated in the previous section, USB driver send an IRP to it's hub bus driver (to "talk to" it's device) with the request type of IRP_MN_INTERNAL_DEVICE_CONTROL and with control request type of IOCTL_INTERNAL_USB_SUBMIT_URB. When our driver which contain filter object for target device receive this kind of request, we can collect data before (PRE) and after (POST) we forward it to the next device object. DkTgtInDevCtl() routine handle this request. And the codes is just like this (in file DkDevCtl.c):

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);
...

And the completion routine for collecting data after it forward, is just like this (DkTgtInDevCtlCompletion() function in file DkDevCtl.c):

C++
...
    // 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. Limitations (Weaknesses).

These are the limitations of our monitor that I can think of:

  1. This monitor assumes that the target driver will do IoCallDriver() to the next lower driver when it "talk to" it's hardware. It is possible that target driver not to do that, it may choose to "talk to" it's hardware via it's own library and/or USB controller (I do not know how to do it, but I think it is possible). When it did, our monitor can't capture data.
  2. Lower filter of target driver which reside above our filter object in the stack may filter and/or modify them, so what we'll see in the monitor may differ from what the target driver send or receive.
  3. USB hardware may receive different data from data being sent by target driver, because bus driver may modify the data. So, if you use this monitor to monitor a USB hardware, you should take into account this situation in your data analysis.
  4. This monitor doesn't track "the pairness" of the request which mean, for example, the direction of data no. 11 is PRE (data before forward) and the data direction no. 12 is POST (data after forward), and this doesn't mean that data no. 11 is paired with data no.12.
  5. This monitor can only collect 512 bytes data or otherwise data will be discarded.

VI. Installation and usage

VI.A. Driver Installation

This is the same as my previous article before, but you have to choose DkSysPort.inf when required rather than DkPortMon2.inf and the driver name is DkSysPort rather than DkPortMon. If you want to uninstall this driver, make sure the monitor is not active, then go to Device Manager, and then uninstall it (It should be no restart required).

On Windows 7 (32 bit without service pack) follow these steps;

  1. Download and extract the package above.
  2. Open Control Panel.
  3. Click Hardware And Sound.
  4. Click Device Manager and then click your computer name that appear in it.
  5. Select menu Action - Add legacy hardware, then a hardware wizard will appear and then click Next.
  6. Choose Install the hardware that I manually select from the list. (Advance). and then click Next.
  7. Select System Drivers that appear in the list box and then click Next.
  8. Click Have Disk... (button click) and then locate DkSysPort.inf in the sub directory of Win7 in the extracted package above.
  9. This will show DkSysPort System Driver in the list box. And then click Next.
  10. Click Next again, this will start driver installation process. When security dialouge box appear, ignore it by clicking Install this driver software anyway and then click Finish.
  11. Check to see if this driver is installed properly by expanding System Devices in the Device Manager tree.

VI.B. Usage

  1. Run the program called "DkURBMonGui.exe" in the Bin directory.
  2. Plug your USB target device in one of USB port on your PC.
  3. Click Tool - Detect Device, this will tell you which USB device plug into which USB root hub (e.g., \Device\USBPDO-4). This Hub name is used to be monitored.
  4. Unplug your target USB device.
  5. Select USB root hub name by clicking Tool - Select USB Hub..., and then select the USB hub name that detect device menu found (in step. 3) to be monitored.
  6. Start the monitor, click Tool - Start Monitor.
  7. Plug your USB target device.
  8. If you have finished monitoring, unplug your target device, then click Tool - Stop Monitor to stop it.

The picture DkURBMon in detect mode should look like this: 335364/URBMonDet.JPG

The picture DkURBMon in monitor mode should look like this: 335364/URBMon.JPG

For Windows 7, you need to run as Administrator to run DkURBMonGuid.exe.

If you want to see some debugging messages that is comming from the driver, use DbgView from Sysinternal. On Windows 7, you need to Run as Administrator this utility and then check menu of Enable Verbose Kernel Output.

VII. Compilation and Linking the source

This is the same as my previous article.

Bug fixed

  1. April, 26th 2012 *pTmp++ = HexChar[pDat->Data[ul] && 0x0F]; change to *pTmp++ = HexChar[pDat->Data[ul] & 0x0F]; in DkApp.cpp, function DkPortOnDataReceice(PDKPORT_DAT pDat)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)