One of my current projects involves mapping the serial numbers of USB flash drives to the DOS drive letters associated with them. This is not the serial number of the volume (or partition) but of the device itself.
This means that when I insert my PNY Attaché 512 MB memory stick, Windows will assign it the drive letter “F:\”. I 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 (select “Display Device Components”).
Unfortunately I was unable to find anybody on the internet who knew how to do it. Well, I say nobody. It appears to be possible with WMI or a similar thing can be accomplished with named pipes (which doesn’t appear to work for USB drives?).
There had to be a way to do it, and I don’t trust WMI. Yes, that distrust is probably irrational but the service can be disabled. Instead I turned to the Windows XP registry. That wasn’t as simple as it should have been, but I later found a post on stackoverflow.com that essentially confirmed my findings.
None of it is pretty, and the interface changes somewhere between XP and Vista (I didn’t check on Server 2003). It changes again in a minor way on Windows 7. Windows 2000 was the same as XP and I’m not trying to make this work on Windows 98 or ME.
Here is an overview of what is happening, and at the bottom is my initial C code. Again, it isn’t pretty.
For Windows 2000/XP:
1. Iterate through HKEY_LOCAL_MACHINE\SYSTEM\MountedDevices.In my case, I was 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, I 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 I 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 numberWhen 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 I took in the code below.2. Enjoy the simplicity.Well, to be perfectly honest, I haven’t verified that all of the other steps can be cut out. I still check against GetLogicalDriveStrings(). Here, take a look for yourself.The Code (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's 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;
}
I hope 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. I 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.
Recent Comments