Windows应用MFC程序检测硬件设备插入和或移除
使用 WM_DEVICECHANGE 和 RegisterDeviceNotification() 检测硬件添加/删除的用户模式应用程序。
- 下载源文件 - 26.5 Kb https://www.codeproject.com/KB/system/HwDetect/HwDetect_src.zip
- 下载演示程序 - 20 Kb 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;
}
特别注意第 8 行,NotificationFilter.dbcc_classguid. 查看Doron Holan 的博客
PnP 设备通常与两个不同的 GUID、设备接口 GUID 和设备类 GUID 相关联。
设备类 GUID 定义了广泛的设备类别。如果你打开设备管理器,默认视图是“按类型”。每种类型都是一个设备类,其中每个类都是设备类 GUID 的唯一 ID。设备类 GUID 定义类的图标、默认安全设置、安装属性(例如用户不能手动安装此类的实例,它必须由 PNP 枚举)和其他设置。设备类 GUID 不定义 I/O 接口(请参阅词汇表),而是将其视为一组设备。我认为一个很好的澄清示例是 Ports 类。COM 和 LPT 设备都是端口类的一部分,但每个设备都有自己独特的 I/O 接口,彼此不兼容。一个设备只能属于一个设备类。
设备接口 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 的列表:
设备接口名称 | GUID |
---|---|
USB原始设备 | {a5dcbf10-6530-11d2-901f-00c04fb951ed} |
磁盘设备 | {53f56307-b6bf-11d0-94f2-00a0c91efb8b} |
网卡 | {ad498944-762f-11d0-8dcb-00c04fc3358c} |
人机接口设备 (HID) | {4d1e55b2-f16f-11cf-88cb-001111000030} |
Palm | {784126bf-4190-11d4-b5c2-00c04f687a67} |
解码 DEV_BROADCAST_DEVICEINTERFACE
让我们改变我们的处理程序代码OnMyDeviceChange()如下:
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;
....
....
}
从 MSDN,我们知道
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_name,我们可以知道系统中插入了什么设备。可悲的是,答案是否定的,dbcc_name供操作系统内部使用并且是一种身份,它不是人类可读的。的示例dbcc_name如下:
\?\USB#Vid_04e8&Pid_503b#0002F9A9828E0F06#{a5dcbf10-6530-11d2-901f-00c04fb951ed}
- \?\USB:USB表示这是一个USB设备类
- Vid_04e8&Pid_053b: Vid/Pid 是VendorID和ProductID (但这是特定于设备类的,USB 使用Vid/ Pid,不同的设备类使用不同的命名约定)
- 002F9A9828E0F06:似乎是一个唯一的 ID(不确定它是如何生成的)
- {a5dcbf10-6530-11d2-901f-00c04fb951ed}:设备接口类 GUID
现在,通过使用此解码信息,我们可以通过两种方法获取设备描述或设备友好名称:
- 直接读取注册表:对于我们的示例dbcc_name,它将是\HKLM\SYSTEM\CurrentControlSet\Enum\USB\Vid_04e8&Pid_503b\0002F9A9828E0F06
- 使用SetupDiXxx
API:SetupDiXxx()
Windows 有一组 API 允许应用程序以编程方式检索硬件设备信息。例如,我们可以通过 . 获取设备描述或设备友好名称dbcc_name。程序的流程大致如下:
- 用于SetupDiGetClassDevs()获取设备信息集的句柄HDEVINFO,你可以把句柄看成是一个目录句柄。
- 用于SetupDiEnumDeviceInfo()枚举信息集中的所有设备,你可以将此操作视为目录列表。在每次迭代中,我们将得到一个SP_DEVINFO_DATA,您可以将此句柄视为文件句柄。
- 枚举时,使用SetupDiGetDeviceInstanceId()读取每个设备的实例ID,可以认为这个操作是读取文件属性。实例 ID 的形式为“ USB\Vid_04e8&Pid_503b\0002F9A9828E0F06”,与dbcc_name.
如果实例 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); }
禁用设备
假设您有正确的HDEVINFOand SP_DEVINFO_DATA(实际上,我们将 the 保存dbcc_name为树节点额外数据并在我们右键单击设备图标然后调用SetupDiGetClassDevsand时检索该数据SetupDiEnumDevicInfo),禁用设备的流程如下:
- SP_PROPCHANGE_PARAMS正确设置结构
- 调用SetupDiSetClassInstallParams()和传入结构SP_PROPCHANGE_PARAMS体
- 打电话SetupDiCallClassInstaller()给DIF_PROPERTYCHANGE
SetupDiSetClassInstallParams()其实DIF码有点复杂,不同的DIF码需要调用不同的结构体。有关详细信息,请参阅 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
}
小问题
我DBT_DEVICEARRIVAL/DBT_DEVICEREMOVECOMPLETE在同一次插入/拔出 USB 无线网卡时遇到多条消息。
限制
- 显然,程序只能在应用程序运行时检测到设备的变化,例如在系统上电之前插入的设备,或者在应用程序启动之前将不会被检测到。但这可以通过在远程计算机上保存当前状态然后在应用程序启动期间检查差异来解决。
- 我们可以禁用该设备,但这就是我们所能做的。我们无法针对登录用户访问控制设备,也无法提供只读访问权限。恕我直言,我认为只有将整个程序重新实现为内核模式过滤器驱动程序才能解决这个问题。