Get USB Drive Serial Number on Windows in C++

Getting the serial number of a USB device in Windows is a lot harder than it should be. ( But it’s a lot easier than getting the USB serial number on Os X!)

It is relatively simple to get USB information if you have the device handle of the USB device itself. And you can get information about a mounted volume pretty easily. But to match up a mounted volume with a USB device is tricky and annoying.

There are many code examples on the net about how to do it in C#, visual basic, and similar broken garbage languages. If you are forced to program in C# or visual basic – get help. There are alternatives to suicide. There are some example of how to do it through WMI, which is a Windows operating system service – which is slow, buggy and unreliable. If you have to do it in WMI, you are beyond help.

First here is the code:

#include <WinIOCtl.h>
#include <api/usbioctl.h>
#include <Setupapi.h>

DEFINE_GUID( GUID_DEVINTERFACE_USB_DISK,   
             0x53f56307L, 0xb6bf, 0x11d0, 0x94, 0xf2, 
             0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b );

void getDeviceInfo( int vol )
{
   UsbDeviceInfo info;

   // get the device handle
   char devicePath[7] = "\\\\.\\@:";
   devicePath[4] = (char)( vol + 'A' );

   HANDLE deviceHandle = CreateFile( devicePath, 0, 
                                     FILE_SHARE_READ | 
                                     FILE_SHARE_WRITE, NULL, 
                                     OPEN_EXISTING, 0, NULL );
   if ( deviceHandle == INVALID_HANDLE_VALUE )
      return;

   // to get the device number
   DWORD volumeDeviceNumber = getDeviceNumber( deviceHandle );
   CloseHandle( deviceHandle );

   // Get device interface info set handle
   // for all devices attached to the system
   HDEVINFO hDevInfo = SetupDiGetClassDevs( 
      &GUID_DEVINTERFACE_USB_DISK, NULL, NULL,
      DIGCF_PRESENT | DIGCF_DEVICEINTERFACE );

   if ( hDevInfo == INVALID_HANDLE_VALUE )  
        return;

   // Get a context structure for the device interface
   // of a device information set.
   BYTE Buf[1024];
   PSP_DEVICE_INTERFACE_DETAIL_DATA pspdidd = 
      (PSP_DEVICE_INTERFACE_DETAIL_DATA)Buf;
   SP_DEVICE_INTERFACE_DATA         spdid;
   SP_DEVINFO_DATA                  spdd;

   spdid.cbSize = sizeof( spdid );

   DWORD dwIndex = 0;
   while ( true )  
   {
      if ( ! SetupDiEnumDeviceInterfaces( hDevInfo, NULL, 
                                          &GUID_DEVINTERFACE_USB_DISK, 
                                          dwIndex, &spdid ))
         break;

      DWORD dwSize = 0;
      SetupDiGetDeviceInterfaceDetail( hDevInfo, &spdid, NULL, 
                                       0, &dwSize, NULL );

      if (( dwSize != 0 ) && ( dwSize <= sizeof( Buf )))
      {
         pspdidd->cbSize = sizeof( *pspdidd ); // 5 Bytes!

         ZeroMemory((PVOID)&spdd, sizeof(spdd));
         spdd.cbSize = sizeof(spdd);

         long res = SetupDiGetDeviceInterfaceDetail( 
            hDevInfo, &spdid, pspdidd,
            dwSize, &dwSize, &spdd );
         if ( res ) 
         {
            HANDLE hDrive = CreateFile( pspdidd->DevicePath,0,
                                        FILE_SHARE_READ | FILE_SHARE_WRITE,
                                        NULL, OPEN_EXISTING, 0, NULL );
            if ( hDrive != INVALID_HANDLE_VALUE ) 
            {
               DWORD usbDeviceNumber = getDeviceNumber( hDrive );

               if ( usbDeviceNumber == volumeDeviceNumber ) 
               {
                  fprintf( "%s", pspdidd->DevicePath );
               }
            }
            CloseHandle( hDrive );
         }
      }
      dwIndex++;
   } 

   SetupDiDestroyDeviceInfoList(hDevInfo);
   return;  
}

You pass in the volume number. This is just the drive letter, represented as an integer. Drive “A:” is zero. So the first thing we do is create a drive path. For example, if the mounted volume you want to get a serial number for is “F:”, you’d pass in “5”, and construct a device path \\.\F:.

Next you get a device handle for that volume using CreateFile(). Originally this function was meant to create regular file system files. But today it can be used to open handles to devices of all kinds. Each device type is represented by different device paths.

Next, you get the device number. When a volume is mounted, it will be associated with a device, and this function returns its number. Why the OS doesn’t just give you a device path here is ridiculous. The device numbers will be low. Typically a number under 10. Don’t be surprised. I was.

You get the device number by calling DeviceIOControl() with the handle to your device:

DWORD getDeviceNumber( HANDLE deviceHandle )
{
   STORAGE_DEVICE_NUMBER sdn;
   sdn.DeviceNumber = -1;
   DWORD dwBytesReturned = 0;
   if ( !DeviceIoControl( deviceHandle,
                          IOCTL_STORAGE_GET_DEVICE_NUMBER,
                          NULL, 0, &sdn, sizeof( sdn ),
                          &dwBytesReturned, NULL ) )
   {
      // handle error - like a bad handle.
      return U32_MAX;
   }
   return sdn.DeviceNumber;
}

There is an old windows API, in setupaip.h that was designed to help write installers. It has now been obsoleted by newer installation APIs, but you can still use it to enumerate devices. Basically you pass in the GUID of the type of device interface you want. In this case we just want to enumerate USB flash disks. That guid is defined at the top of the file.

You setup for enumeration with SetupDiGetClassDevs(), and iterate over the devices with SetupDiEnumDeviceInterfaceDetail(). When you are done you close the iterator with SetupDiDestroyDeviceInfoList().

Then for each device you get the device name and number using SetupDiGetDeviceInterfaceDetail, and match up the device number of each device with the one you got for the volume. When you find a match then you have the device path for your actual USB flash drive. At that point you could start querying the device itself using functions like DeviceIoControl(), but in this case the information we want is coded right into the device path

Here is a typical device path for a USB flash disk:

\\?\usbstor#disk&ven_cbm&prod_flash_disk&rev_5.00#31120000dc0ce201&0#
   {53f56307-b6bf-11d0-94f2-00a0c91efb8b}

The device path for a flash disk must start with “usbstor”. This first part of the path is name of the driver, which on windows is called “usbstor”. The vendor id, product id, product revision and serial number are highlighted in red.

The following regular expressions will extract this information from the device path:

ven_([^&#]+)       // vendor id
prod_([^&#]+)      // product id
rev_([^&#]+)       // revision id 
&[^#]*#([^&#]+)    // serial number

Next here is a method to recognize if a volume is removable media (e.g. like a usb or firewire disk):

bool isRemovableMedia( s32 vol )
{
   char rootPath[5] = "@:\\";
   rootPath[0] = (char)( vol + 'A' );

   char szDosDeviceName[MAX_PATH];
   char dosDevicePath[3] = "@:";

   // get the drive type
   UINT DriveType = GetDriveType( rootPath );

   if ( DriveType != DRIVE_REMOVABLE )
      return false;

   dosDevicePath[0] = (char)( vol + 'A' );
   QueryDosDevice( dosDevicePath, szDosDeviceName, MAX_PATH );

   if ( strstr( szDosDeviceName,"\\Floppy") != NULL )
   {
      // its a floppy
      return false;
   }

   return true;
}

Resources:

CreateFile function MSDN Reference for CreateFile
SetupDiEnumDeviceInterfaces function MSDN Reference for SetupDiEnumDeviceInterfaces
GetDriveType function MSDN Reference for GetDriveType
DeviceIoControl function MSDN Refence for DeviceIoControl
DeviceIoControl function MSDN Refence for DeviceIoControl
How to get the Serial Number from a USB disk Serial Number code examples in WMI and Visual Basic
Windows Driver Kit (WDK) The windows device driver toolkit. Has headers you may need
Flash Memory Toolkit A program to view the serial number, for debugging
DiskId32 Includes a link to a code example that gets serial numbers from hard disks
How to Prepare a USB Drive for Safe Removal Shows a similar method for mapping volumes to USB Devices