简介 现在对于IT的安全来说,热插拨设备是个很大的威胁。在这篇文章中,我将试着开发一个用户应用程序来检测本机系统上的设备改变。例如:插入一个USB设备、Ipod、USB无线网卡等等。这个程序同样也可以停用任何支持插拔的设备。在文章的后面,我会简述一下程序的工作原理和它的局限性。 怎么来检测硬件设备的改变? 事实上,Windows操作系统会对上层程序发送WM_DEVICECHANGE消息来通知设备的改变。我们所要作的仅仅是添加一个句柄来处理这个事件。 Collapse 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有些限制: 1 只有顶层窗体的程序才能收到这个消息 2 仅仅串口、磁盘发生改变,才对每个程序广播这个消息 的确不错,至少你可以知道移动U盘、移动硬盘、光盘被安装或弹出了,通过DEV_BROADCAST_VOLUME.dbcv_unitmask你也可以获得其对应的盘符。但实际上,你不知道底层处理的是哪个物理设备实际上被安装到了系统中。 API:RegisterDeviceNotification() 所以,你不得不调用RegisterDeviceNotification()API来注册其他类型的设备改变,或是你的程序仅仅是一个服务程序、没有顶层窗体的程序。例如:如下的例子是用来注册一个设备类型的接口的: Collapse 1. DEV_BROADCAST_DEVICEINTERFACE NotificationFilter; 2. ZeroMemory( &NotificationFilter, sizeof(NotificationFilter) ); 3. NotificationFilter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE); 4. NotificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; 5. // assume we want to be notified with USBSTOR 6. // to get notified with all interface on XP or above 7. // ORed 3rd param with DEVICE_NOTIFY_ALL_INTERFACE_CLASSES and dbcc_classguid will be ignored 8. NotificationFilter.dbcc_classguid = GUID_DEVINTERFACE_USBSTOR; 9. HDEVNOTIFY hDevNotify = RegisterDeviceNotification(this->GetSafeHwnd(), amp;NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE); 10. if( !hDevNotify ) { 11. // error handling... 12. return FALSE; 13. } 请注意第8行,NotificationFilter.dbcc_classguid关注的就是你关心的一类设备。 参考这个blog: Doron Holan's blog 一个支持即插即用的设备,有2个不同的GUID相关,一个设备接口GUID, 一个是设备类GUID 设备类GUID:定义了广泛意义上一类设备的GUID,如果你打开设备管理器[我的电脑右键—>设备管理器],默认的是按照“类型”排列的,每一个“类型”就是一个设备类,同时每一个设备类有一个唯一的ID就是设备类GUID。设备GUID定义了此类设备的图标、默认的安全设置、安装属性(例如用户不能手动安装这类设备,而必须通过PNP来遍历),以及其他的设置信息。设备类GUID没有定义对应的I/O接口(请参考术语表),而更像是设备的分组。我认为一个比较好的例子是端口类。串口COM和并口LPT 都是端口类的一部分,但其各有各的I/O接口,而且彼此互不兼容.一个设备仅仅属于一个设备类。我们可以通过设备驱动的INF文件的开头来查看该设备的设备类GUID。 设备接口GUID:定义了相互关联I/O接口的GUID,每一个接口GUID的具体实例都支持基本的I/O设置。设备接口GUID也是对应的驱动程序基于PNP状态来注册、启用、禁用设备。如果需要,一个设备甚至可以注册多个同样GUID的实例(假使每个都有相同的名字)[注:在实际的程序中,多次插拔USB口,确实会驱出相同的串口,例如port12,port12,port12…],尽管在现实世界中完全不需要这样。一个简单的I/O关联接口是键盘设备,每个键盘设备的接口GUID必须相同。 可以通过如下的注册表路径来查看当前设备类GUID, 设备接口GUID: \\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class \\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceClasses 常用设备的接口GUID如下: 设备接口名称 GUID USB Raw Device/USB设备 {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的解码 如下是修改处理捕获对应事件的函数: Collapse 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中,我们知道 Collapse 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知道那个设备安装到了当前系统。J,答案是不对,dbcc_name仅仅是操作系统内部使用来做为ID的,其实不易读的,例如下面的这个dbcc_name: \\?\USB#Vid_04e8&Pid_503b#0002F9A9828E0F06#{a5dcbf10-6530-11d2-901f-00c04fb951ed} \\?\USB: USB 意思是这是一个USB设备类 Vid_04e8&Pid_053b: Vid/Pid 是一个厂商ID和产品ID(但这是由设备类指定的,USB设备类使用VID/PID,不同的设备类使用不同的命名约定) 002F9A9828E0F06: 不清楚是怎么生成的,是唯一设备ID {a5dcbf10-6530-11d2-901f-00c04fb951ed}:设备接口类GUID 现在,我们来解出设备描述信息或是设备别名,有2种办法: 1 直接读注册表, \\HKLM\SYSTEM\CurrentControlSet\Enum\USB\Vid_04e8&Pid_503b\0002F9A9828E0F06 2 使用 SetupDiXxx 系列API API:SetupDiXxx() Windows定义了一组API,让用户通过编程的办法来获取对应的硬件设备信息。例如,我们可以通过dbcc_name来获得设备描述信息或是设备别名。下面是这个办法都具体步骤: 1 首先通过SetupDiGetClassDevs()来获得设备信息集 HDEVINFO,这个操作等同于是一个获取目录句柄的过程。 2 接着使用SetupDiEnumDeviceInfo()来遍历出这个设备信息集内的所有设备,这个操作等同于把目录列表的过程。对于每个遍历出的,我们可以获得SP_DEVINFO_DATA,这个等同于是文件句柄。 3 在上面的枚举过程中,使用SetupDiGetDeviceInstanceId()来读取每个设备的实例ID,这个操作等同于是读文件的属性,一个设备的实例ID类似这个:”USB\Vid_04e8&Pid_503b\0002F9A9828E0F06”,和dbcc_name非常像。 4 如果设备的实例ID等同于dbcc_name,则通过SetupDiGetDeviceRegistryProperty()来获取设备描述信息或是设备别名信息。 程序如下: Collapse 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座位树节点的tag,当右键单击某一个节点的时候,可以通过调用SetupDiGetClassDevs和SetupDiEnumDeviceInfo来获得所需东西),按照如下的步骤即可禁用一个设备: 1 给SP_PROPCHANGE_PARAMS结构体赋上正确的值 2 把上面赋完值的SP_PROPCHANGE_PARAMS作为参数传入到SetupDiSetClassInstallParams() 3 调用SetupDiCallClassInstaller(),传递参数DIF_PROPEFRTYCHANGE 实际上,DIF也是按位做与运算后兼容的,你也可以去传递不同的DIF参数来调用SetupDiSetClassInstallParams()。 更多信息,请参考MSDN”Handling DIF Codes” Collapse 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 } 附录: 我使用这个程序,已经多次测试了USB的无线网卡的,插入、拔出测试。 局限性: 1 明显的,必须先运行该程序,才能检测硬件设备。例如:设备在操作系统启动前就已经连接,或者在这个程序运行前的连接都不会被检测。但这个问题,可以通过保存当前系统配置到远程计算机上,等启动完这个程序后再坚持不同的配置来解决 2 我们可以禁用设备,换而言之这也是我们所有能做到。我们不能访问设备底层控制。 我认为可以通过重新用基于内核的过滤驱动来实现,则可以解决这个问题。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END