一个用户模式的应用程序,用于检测硬件的添加/删除与WM_DEVICECHANGE 和 RegisterDeviceNotification()
源代码:https://www.codeproject.com/KB/system/HwDetect/HwDetect_src.zip
演示DEMO:https://www.codeproject.com/KB/system/HwDetect/HwDetect_exe.zip
介绍
热插拔设备现在是IT安全的一大威胁。在本文中,我们将尝试开发一个用户模式应用程序来检测系统上的设备更改,即插入USB驱动器、iPod、USB无线网卡等。该程序还可以禁用任何新插入的设备。我们将对它的工作原理有一个基本的了解,并在本文的结尾讨论它的局限性。
如何检测硬件更改?
事实上,Windows操作系统会在设备更改后发布WM_DEVICECHANGE。我们需要做的就是添加一个处理程序来处理这个事件。
BEGIN_MESSAGE_MAP(CHWDetectDlg, CDialog)
// ... other handlers
ON_MESSAGE(WM_DEVICECHANGE, OnMyDeviceChange)
END_MESSAGE_MAP()
LRESULT CHWDetectDlg::OnMyDeviceChange(WPARAM wParam, LPARAM lParam)
{
// for more information, see MSDN help of WM_DEVICECHANGE
// this part should not be very difficult to understand
if ( DBT_DEVICEARRIVAL == wParam || DBT_DEVICEREMOVECOMPLETE == wParam ) {
PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam;
switch( pHdr->dbch_devicetype ) {
case DBT_DEVTYP_DEVICEINTERFACE:
PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)pHdr;
// do something...
break;
case DBT_DEVTYP_HANDLE:
PDEV_BROADCAST_HANDLE pDevHnd = (PDEV_BROADCAST_HANDLE)pHdr;
// do something...
break;
case DBT_DEVTYP_OEM:
PDEV_BROADCAST_OEM pDevOem = (PDEV_BROADCAST_OEM)pHdr;
// do something...
break;
case DBT_DEVTYP_PORT:
PDEV_BROADCAST_PORT pDevPort = (PDEV_BROADCAST_PORT)pHdr;
// do something...
break;
case DBT_DEVTYP_VOLUME:
PDEV_BROADCAST_VOLUME pDevVolume = (PDEV_BROADCAST_VOLUME)pHdr;
// do something...
break;
}
}
return 0;
}
但是,默认情况下,Windows操作系统只会将WM_DEVICECHANGE发布到具有顶级窗口的所有应用程序,以及仅在端口和体积发生变化时。
好吧,这还不错,因为至少你知道什么时候挂载/卸载了一个额外的“磁盘”,你可以通过DEV_BROADCAST_VOLUME.dbcv_unitmask. 缺点是您不知道系统中实际插入了什么物理设备。
API:RegisterDeviceNotification()
要获得其他类型设备更改的通知,或者在作为服务运行且没有顶级窗口时获得通知,必须调用RegisterDeviceNotification()API。例如,要在接口更改时获得通知,可以执行以下操作。
DEV_BROADCAST_DEVICEINTERFACE NotificationFilter;
ZeroMemory( &NotificationFilter, sizeof(NotificationFilter) );
NotificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
NotificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
// assume we want to be notified with USBSTOR
// to get notified with all interface on XP or above
// ORed 3rd param with DEVICE_NOTIFY_ALL_INTERFACE_CLASSES and dbcc_classguid will be ignored
NotificationFilter.dbcc_classguid = GUID_DEVINTERFACE_USBSTOR;
HDEVNOTIFY hDevNotify = RegisterDeviceNotification(this->GetSafeHwnd(),
amp;NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
if( !hDevNotify ) {
// error handling...
return FALSE;
}
PnP设备通常与两个不同的GUID相关联,一个设备接口GUID和一个设备类GUID。
设备类GUID定义了广泛的设备类别。如果打开设备管理器,默认视图是“按类型”。每种类型都是一个设备类,其中每个类都是由设备类GUID唯一标识的。设备类GUID定义类的图标、默认安全设置、安装属性(如用户无法手动安装此类的实例,必须由PNP枚举)和其他设置。设备类GUID没有定义I/O接口(请参阅术语表),而是将其视为一组设备。我认为一个很好的例子是Ports类。COM和LPT设备都是Ports类的一部分,但是它们都有各自不同的I/O接口,这些接口彼此不兼容。一个设备只能属于一个设备类。设备类GUID是您在INF文件顶部看到的GUID。
设备接口GUID定义特定的I/O接口协定。预期接口GUID的每个实例都将支持相同的基本I/O集。设备接口GUID是驱动程序将根据PnP状态注册和启用/禁用的。一个设备可以为自己注册多个设备接口,而不限于一个接口GUID。如果需要的话,设备甚至可以注册同一个GUID的多个实例(假设每个实例都有自己的ReferenceString),尽管我从未见过现实世界需要这样做。一个简单的I/O接口契约是键盘设备与原始输入线程的接口。以下是键盘设备接口GUID的每个实例必须支持的键盘设备契约。
您可以在以下注册表中看到设备类和设备接口类的当前列表:
\\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class
\\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceClasses
通用设备接口类GUID的列表如下所示:
Device Interface Name GUID
USB Raw Device {a5dcbf10-6530-11d2-901f-00c04fb951ed}
Disk Device {53f56307-b6bf-11d0-94f2-00a0c91efb8b}
Network Card {ad498944-762f-11d0-8dcb-00c04fc3358c}
Human Interface Device (HID) {4d1e55b2-f16f-11cf-88cb-001111000030}
Palm {784126bf-4190-11d4-b5c2-00c04f687a67}
解码 DEV_BROADCAST_DEVICEINTERFACE
LRESULT CHWDetectDlg::OnMyDeviceChange(WPARAM wParam, LPARAM lParam)
{
....
....
if ( DBT_DEVICEARRIVAL == wParam || DBT_DEVICEREMOVECOMPLETE == wParam )
{
PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lParam;
switch( pHdr->dbch_devicetype )
{
case DBT_DEVTYP_DEVICEINTERFACE:
PDEV_BROADCAST_DEVICEINTERFACE pDevInf = (PDEV_BROADCAST_DEVICEINTERFACE)pHdr;
UpdateDevice(pDevInf, wParam);
break;
....
....
}
DEV_BROADCAST_DEVICEINTERFACE 结构体定义如下:
typedef struct _DEV_BROADCAST_DEVICEINTERFACE {
DWORD dbcc_size;
DWORD dbcc_devicetype;
DWORD dbcc_reserved;
GUID dbcc_classguid;
TCHAR dbcc_name[1];
} DEV_BROADCAST_DEVICEINTERFACE *PDEV_BROADCAST_DEVICEINTERFACE;
通过使用dbcc_uu名称,我们可以知道哪些设备已插入系统。遗憾的是,答案是否定的,dbcc_uu名称是用于操作系统内部使用的,是一个标识,它是不可读的。dbcc_U名称示例如下:
\\?\USB#Vid_04e8&Pid_503b#0002F9A9828E0F06#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
- \?\USB:USB表示这是一个USB设备类
- Vid_04e8&Pid_503b:Vid/Pid是VendorID和ProductID(但这是设备类特定的,USB使用Vid/Pid,不同的设备类使用不同的命名约定)
- 002F9A9828E0F06:似乎是唯一的ID(不确定如何生成)
- {a5dcbf10-6530-11d2-901f-00c04fb951ed}:设备接口类GUID
现在,通过使用这些解码信息,我们可以通过两种方法获得设备描述或设备友好名称:
- 直接读取注册表:对于我们的示例dbcc_ame,它将是,\HKLM\SYSTEM\CurrentControlSet\Enum\USB\Vid\u 04e8&Pid\u 503b\0002F9A9828E0F06
- 使用SetupDiXxx
API:SetupDiXxx()
Windows有一组API,允许应用程序以编程方式检索硬件设备信息。例如,我们可以使用dbcc\u名称获取设备描述或设备友好名称。程序流程大致如下:
- 使用SetupDiGetClassDevs()获取设备信息集HDEVINFO的句柄,可以将该句柄视为目录句柄。
- 使用SetupDiEnumDeviceInfo()枚举信息集中的所有设备,可以将此操作视为目录列表。在每次迭代中,我们都会得到一个SP\u DEVINFO\u数据,您可以将这个句柄看作文件句柄。
- 在枚举过程中,使用SetupDiGetDeviceInstanceId()读取每个设备的实例ID,您可以将此操作视为读取文件属性。实例ID的格式为“USB\Vid\u04e8&Pid\u503b\0002F9A9828E0F06”,与dbcc\u名称非常相似。
- 如果实例ID与dbcc_name匹配,则调用SetupDiGetDeviceRegistryProperty()来检索描述或友好名称
void CHWDetectDlg::UpdateDevice(PDEV_BROADCAST_DEVICEINTERFACE pDevInf, WPARAM wParam)
{
// dbcc_name:
// \\?\USB#Vid_04e8&Pid_503b#0002F9A9828E0F06#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
// convert to
// USB\Vid_04e8&Pid_503b\0002F9A9828E0F06
ASSERT(lstrlen(pDevInf->dbcc_name) > 4);
CString szDevId = pDevInf->dbcc_name+4;
int idx = szDevId.ReverseFind(_T('#'));
ASSERT( -1 != idx );
szDevId.Truncate(idx);
szDevId.Replace(_T('#'), _T('\\'));
szDevId.MakeUpper();
CString szClass;
idx = szDevId.Find(_T('\\'));
ASSERT(-1 != idx );
szClass = szDevId.Left(idx);
// if we are adding device, we only need present devices
// otherwise, we need all devices
DWORD dwFlag = DBT_DEVICEARRIVAL != wParam
? DIGCF_ALLCLASSES : (DIGCF_ALLCLASSES | DIGCF_PRESENT);
HDEVINFO hDevInfo = SetupDiGetClassDevs(NULL, szClass, NULL, dwFlag);
if( INVALID_HANDLE_VALUE == hDevInfo )
{
AfxMessageBox(CString("SetupDiGetClassDevs(): ")
+ _com_error(GetLastError()).ErrorMessage(), MB_ICONEXCLAMATION);
return;
}
SP_DEVINFO_DATA* pspDevInfoData =
(SP_DEVINFO_DATA*)HeapAlloc(GetProcessHeap(), 0, sizeof(SP_DEVINFO_DATA));
pspDevInfoData->cbSize = sizeof(SP_DEVINFO_DATA);
for(int i=0; SetupDiEnumDeviceInfo(hDevInfo,i,pspDevInfoData); i++)
{
DWORD DataT ;
DWORD nSize=0 ;
TCHAR buf[MAX_PATH];
if ( !SetupDiGetDeviceInstanceId(hDevInfo, pspDevInfoData, buf, sizeof(buf), &nSize) )
{
AfxMessageBox(CString("SetupDiGetDeviceInstanceId(): ")
+ _com_error(GetLastError()).ErrorMessage(), MB_ICONEXCLAMATION);
break;
}
if ( szDevId == buf )
{
// device found
if ( SetupDiGetDeviceRegistryProperty(hDevInfo, pspDevInfoData,
SPDRP_FRIENDLYNAME, &DataT, (PBYTE)buf, sizeof(buf), &nSize) ) {
// do nothing
} else if ( SetupDiGetDeviceRegistryProperty(hDevInfo, pspDevInfoData,
SPDRP_DEVICEDESC, &DataT, (PBYTE)buf, sizeof(buf), &nSize) ) {
// do nothing
} else {
lstrcpy(buf, _T("Unknown"));
}
// update UI
// .....
// .....
break;
}
}
if ( pspDevInfoData ) HeapFree(GetProcessHeap(), 0, pspDevInfoData);
SetupDiDestroyDeviceInfoList(hDevInfo);
}
禁用设备
假设您有正确的HDEVINFO和SP_DEVINFO_DATA(实际上,我们将dbcc_name保存为树节点额外数据,并在右键单击设备图标然后调用SetupDiGetClassDevs和SetupDiEnumDevicInfo时检索该数据),禁用设备的流程如下所示:
- 正确设置SP_prop更改参数结构
- 调用SetupDiSetClassInstallParams()并传入SP_PROPCHANGE_PARAMS结构
- 使用DIF_PROPERTYCHANGE调用SetupDiCallClassInstaller()
事实上,DIF代码有点复杂,对于不同的DIF代码,必须使用不同的结构调用SetupDiSetClassInstallParams()。有关更多信息,请参阅MSDN“处理DIF代码”。
SP_PROPCHANGE_PARAMS spPropChangeParams ;
spPropChangeParams.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
spPropChangeParams.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE ;
spPropChangeParams.Scope = DICS_FLAG_GLOBAL ;
spPropChangeParams.HwProfile = 0; // current hardware profile
spPropChangeParams.StateChange = DICS_DISABLE
if( !SetupDiSetClassInstallParams(hDevInfo, &spDevInfoData,
// note we pass spPropChangeParams as SP_CLASSINSTALL_HEADER
// but set the size as sizeof(SP_PROPCHANGE_PARAMS)
(SP_CLASSINSTALL_HEADER*)&spPropChangeParams, sizeof(SP_PROPCHANGE_PARAMS)) )
{
// handle error
}
else if(!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, hDevInfo, &spDevInfoData))
{
// handle error
}
else
{
// ok, show disable success dialog
// note, after that, the OS will post DBT_DEVICEREMOVECOMPLETE for the disabled device
}
参考:https://www.codeproject.com/Articles/14500/Detecting-Hardware-Insertion-and-or-Removal