USB Serial Numbers in Windows
A project required us to map the serial numbers of USB flash drives, to the DOS drive letters associated with them, with a very high degree of certainty. This is not the serial number of the volume (or partition) but of the device itself.
This means that if we insert a PNY Attaché 512 MB memory stick, and Windows assigns it the drive letter F:\
, then we
need to know that F:\
is a USB flash drive and that the serial number of that device is 075916911BF9
.
The only place that seems to do this in Windows is the Safely Remove Hardware
box if you select Display Device Components
.
The available options are WMI and named pipes.
WMI can be disabled, and named pipes do not appear to work for USB drives. Instead, we turned to the Windows XP registry. That was not as simple as it should have been, but we later found a post on stackoverflow.com which essentially confirmed our findings. That answer was written during the timeframe of this project.
Registry hacking to obtain this information is not pretty, and the interface changes somewhere between XP and Vista. It changes again in a minor way on Windows 7. Windows 2000 was the same as XP. This project did not require support for Windows 98, ME, or Server 2003.
Here is an overview of what is happening, and at the bottom of this post is our initial C code.
For Windows 2000/XP
1. Iterate through HKEY_LOCAL_MACHINE\SYSTEM\MountedDevices.
In this case, we are checking for the values with names that started with \DosDevices\
. Once found, the last two
letters of the value name give you the drive letter. The data was in this format:
\\??\STORAGE#RemovableMedia#8&1965b174&0&RM#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
Everything between \\??\
and the CLSID (what is contained between the curly brackets) is a registry key. In order
to “properly” use it, you would replace the hash symbols with \
and the path exists under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum
.
With the information from MountedDevices, we would add \\STORAGE\RemovableMedia\8&1965b174&0&RM
to the path to
find out what driver is being used and stuff like that.
This is useful to know, but it doesn’t tell us the serial number of the USB device. In order to do that, we need to proceed to step 2.
2. Steal the Parent Id Prefix from the previous registry key.
Through a little registry searching, we found that the string between the 2nd
and 3rd hash marks (minus &RM
) is called the “Parent Id Prefix.” This means that from the data:
\\??\STORAGE#RemovableMedia#8&1965b174&0&RM#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
We will want:
8&1965b174&0
3. Find out which device has the same Parent Id Prefix. Unfortunately for this part, it is easiest to start off with the correct serial number. Since we don’t have that information, any automation will require two levels of enumerating registry keys.
My understanding is that every USB device ever plugged into the computer is stored under
HKEY_LOCAL_MACHINES\SYSTEM\CurrentControlSet\Enum\USBSTOR
. The keys under it contain some information about the devices,
and the keys under those are serial numbers. This is what we want, but we have to figure out which one is right.
The only way that we have found to do this so far is to check the value of the ParentIdPrefix
under the serial key. In
my case, this was how it appeared:
HKEY_LOCAL_MACHINES\SYSTEM\CurrentControlSet\Enum\USBSTOR\Disk&Ven_&Prod_USB_DISK_28X&Rev_PMAP\075916911BF9&0\ParentIdPrefix => "8&1965b174&0"
That matches what we had from above, which means that this is the correct device! Now we know that the serial key we are currently examining is the right one.
One more problem presents itself here. When we disconnect a USB device, Windows XP does not remove the registry keys we started our search with. In order to make sure we only have devices that are plugged in, we need to go to step 4.
4. Verify the drive letter against what is returned by GetLogicalDriveStrings().
For Vista/7:
1. Get the serial number When we iterate through HKEY_LOCAL_MACHINE\SYSTEM\MountedDevices on Vista, we find that the data now points us to USBSTOR\ already. All we have to do is grab the data between the 2nd and 3rd hash marks.
Windows 7 complicates this a little. With 2000/XP, the string began with \\??\
. In Vista it is _??_
, and in 7 it
changes to #??#
. The # symbol is what is being used as a delimiter for the registry key, so we can either pick what is
between the 4th and 5th hash mark, or we can skip over the first four letters of the string. That is the method we took in
the code below.
2. Enjoy the simplicity.
Well, to be perfectly honest, we have not verified that
all the other steps can be cut out. We still check against GetLogicalDriveStrings()
. Here, take a look for yourself.
Initial POC (tested on 2000, XP, Vista, 7)
int NextDevice(DWORD *index, TCHAR *drive, TCHAR *serial, DWORD szs, TCHAR *parentidprefix, DWORD szpip)
{
// Declare some variables we'll need
TCHAR *val, *data, *ptr, *ptr2;
DWORD sz, len, szval, szdata, type;
int ret = 0;
// Make the HKEY instance persistent
static HKEY hkey = NULL;
// Obtain share name and timeout from HKEY_CURRENT_USER first
if (hkey == NULL)
{
if (RegOpenKey(HKEY_LOCAL_MACHINE, _T("SYSTEM\\\MountedDevices"), &hkey) != ERROR_SUCCESS)
hkey = NULL;
}
if (hkey != NULL)
{
// Size of largest data blocks we anticipate needing
szval = 256;
szdata = 1024;
// Create buffers big enough for the anticipated data
val = (TCHAR *)malloc(sizeof(TCHAR) * szval);
data = (TCHAR *)malloc(sizeof(TCHAR) * szdata);
while ((sz = RegEnumValue(hkey, (*index)++, val, &szval, NULL, &type, (LPBYTE)data, &szdata)) == ERROR_SUCCESS)
{
/**
* Windows XP/2000 mehod:
* Only use the entries that have a DOS drive letter associated with them.
*/
if ((wcsncmp(val, _T("\\\\DosDevices\\\\"), 12) == 0) && (wcsncmp(data, _T("\\\\??\\\\STORAGE#RemovableMedia#"), 27) == 0))
{
// Get the drive letter
wcsncpy_s(drive, 3, val + 12, 2);
// Supply our own terminating NULL character
*(drive + 2) = 0;
/**
* This should pull the data between the 2nd and 3rd hash marks since
* we want to use it instead of the registry key that it represents.
*/
ptr = wcschr(data + 26, '#') + 1;
ptr2 = wcschr(ptr, '#');
// Copy out the ParentIdPrefix to return
len = (DWORD)(((DWORD)(ptr2 - ptr) < szpip) ? (ptr2 - ptr) : (szpip - 1));
wcsncpy_s(parentidprefix, szpip, ptr, len);
// Let's add a NULL to this string also
*(parentidprefix + len) = 0;
// Kill the Removable Device tag on the string
if (ptr = wcsstr(parentidprefix, _T("&RM")))
{
*ptr++ = 0;
*ptr++ = 0;
*ptr++ = 0;
}
ret = 1;
break;
}
/**
* Vista/7 mehod:
* Only use the entries that have a DOS drive letter associated with them. Vista
* uses "_??_", Windows 7 uses "#??#".
*/
else if (((wcsncmp(data + 1, _T("??"), 2) == 0) && (wcsncmp(data + 4, _T("USBSTOR#Disk"), 12) == 0)) &&
(wcsncmp(val, _T("\\\\DosDevices\\\\"), 12) == 0))
{
// Get the drive letter
wcsncpy_s(drive, 3, val + 12, 2);
// Supply our own terminating NULL character
*(drive + 2) = 0;
/* This should pull the data between the 2nd and 3rd hash marks since
* we want to use it instead of the registry key that it represents.
*/
ptr = wcschr(data + 4, '#') + 1;
ptr = wcschr(ptr, '#') + 1;
ptr2 = wcschr(ptr, '#');
// Copy out the serial number to return
len = (DWORD)(((DWORD)(ptr2 - ptr) < szs) ? (ptr2 - ptr) : (szs - 1));
wcsncpy_s(serial, szs, ptr, len);
// Let's add a NULL to this string also
*(serial + len) = 0;
// Kill the &# at the end of the string...
if (ptr = wcsstr(serial, _T("&")))
{
while (*ptr)
*ptr++ = 0;
}
ret = 1;
break;
}
szval = 256;
szdata = 1024;
}
// Free up the memory we were using...
free(val);
free(data);
if (sz != ERROR_SUCCESS)
RegCloseKey(hkey);
}
return ret;
}
int GetXPSerial(TCHAR *parentidprefix, TCHAR *serial, DWORD szs, TCHAR *friendlyname, DWORD szfn)
{
// Declare some variables we'll need
TCHAR *path, *val, *pip, *ptr;
DWORD index, index2, szval, len;
HKEY hkey, hkey2, hkey3;
path = (TCHAR *)malloc(sizeof(wchar_t) * 255);
wcscpy_s(path, 255, _T("SYSTEM\\\\CurrentControlSet\\\\Enum\\\\USBSTOR"));
// Obtain share name and timeout from HKEY_CURRENT_USER first
if (RegOpenKey(HKEY_LOCAL_MACHINE, path, &hkey) == ERROR_SUCCESS)
{
// Size of largest data blocks we anticipate needing
szval = 256;
// Create buffers big enough for the anticipated data
val = (TCHAR *)malloc(sizeof(TCHAR) * szval);
pip = (TCHAR *)malloc(sizeof(TCHAR) * szval);
/* Iterate threw the child keys. This will probably be a list of all USB
* storage devices ever connected to the computer.
*/
index = 0;
while (RegEnumKeyEx(hkey, index++, val, &szval, NULL, NULL, NULL, NULL) == ERROR_SUCCESS)
{
// Prepare the path for another iteration
*(val + szval) = 0;
wcscpy_s(path, 255, _T("SYSTEM\\\\CurrentControlSet\\\\Enum\\\\USBSTOR\\\\"));
wcscat_s(path, 255, val);
len = (DWORD)wcslen(path);
/* Descend into each key. This will be the serial number of the device, if
* it has one, with "&0" added to the end.
*/
if (RegOpenKey(HKEY_LOCAL_MACHINE, path, &hkey2) == ERROR_SUCCESS)
{
/* We should only have on serial number but we still have to call this
* function.
*/
index2 = 0;
szval = 256;
while (RegEnumKeyEx(hkey2, index2++, val, &szval, NULL, NULL, NULL, NULL) == ERROR_SUCCESS)
{
// Prepare the path for another peek...
*(val + szval) = 0;
// Use a little trickery to truncate path, just in case there are multiple serials
wcscpy_s(path + len, 255 - len, _T("\\\\"));
wcscat_s(path, 255, val);
if (RegOpenKey(HKEY_LOCAL_MACHINE, path, &hkey3) == ERROR_SUCCESS)
{
szval = 256;
if (RegQueryValueEx(hkey3, _T("ParentIdPrefix"), NULL, NULL, (LPBYTE)pip, &szval) == ERROR_SUCCESS)
{
*(pip + szval) = 0;
if (wcscmp(pip, parentidprefix) == 0)
{
// Copy the Serial number back
wcscpy_s(serial, szs, val);
// Kill "&0" from the end
if (ptr = wcsstr(serial, _T("&0")))
{
*ptr++ = 0;
*ptr++ = 0;
}
/**
* It is easier to just return from here instead of trying
* to break from two loops.
*/
free(val);
free(pip);
RegCloseKey(hkey2);
RegCloseKey(hkey);
return 1;
}
}
}
szval = 256;
}
RegCloseKey(hkey2);
}
szval = 256;
}
// Free up the memory we were using...
free(val);
free(pip);
RegCloseKey(hkey);
}
return 0;
}
/**
* Pick out USB removable drives.
*/
void LoadDevices()
{
// Create some variables
TCHAR *validdrives, *drive, *parentidprefix, *serial, *friendlyname, *ptr;
DWORD sz = 64, szvd, index = 0;
// And allocate them...
parentidprefix = (TCHAR *)malloc(sizeof(wchar_t) * sz);
friendlyname = (TCHAR *)malloc(sizeof(wchar_t) * sz);
serial = (TCHAR *)malloc(sizeof(wchar_t) * sz);
drive = (TCHAR *)malloc(sizeof(wchar_t) * 3);
*serial = 0;
// Get a list of currently valid drive letters
szvd = GetLogicalDriveStrings(0, NULL);
validdrives = (TCHAR *)malloc(sizeof(wchar_t) * szvd + 2);
GetLogicalDriveStrings(szvd, validdrives);
while (NextDevice(&index, drive, serial, sz, parentidprefix, sz))
{
ptr = validdrives;
while (*ptr)
{
if (wcsncmp(ptr++, drive, 2) == 0)
{
if (*serial == 0)
GetXPSerial(parentidprefix, serial, sz, friendlyname, sz);
MessageBox(NULL, serial, drive, MB_OK);
break;
}
ptr += wcslen(ptr) + 1;
}
*serial = 0;
}
free(parentidprefix);
free(friendlyname);
free(validdrives);
free(serial);
free(drive);
return;
}
Hopefully this is of some use to you. One word of warning: It is possible for USB devices to not have a serial number. When that happens Windows assigns one based on the bus that the device is attached to. We do not have a thumb drive like that to test it with, but others have said that when the second character of the “serial” is an ampersand that it means the device does not have a serial number. This code does not test for it.