Windows基础知识
+ -

Windows应用MFC程序检测硬件设备插入和或移除

2023-02-20 62 0
翻译原文:https://www.codeproject.com/Articles/14500/Detecting-Hardware-Insertion-and-or-Removal

使用 WM_DEVICECHANGE 和 RegisterDeviceNotification() 检测硬件添加/删除的用户模式应用程序。
Windows应用MFC程序检测硬件设备插入和或移除

说明

热插拔设备现在是 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 无线网卡时遇到多条消息。

限制

  • 显然,程序只能在应用程序运行时检测到设备的变化,例如在系统上电之前插入的设备,或者在应用程序启动之前将不会被检测到。但这可以通过在远程计算机上保存当前状态然后在应用程序启动期间检查差异来解决。
  • 我们可以禁用该设备,但这就是我们所能做的。我们无法针对登录用户访问控制设备,也无法提供只读访问权限。恕我直言,我认为只有将整个程序重新实现为内核模式过滤器驱动程序才能解决这个问题。

0 篇笔记 写笔记

Windows Inf文件设备GUID分类(设备安装程序类)
Class=SystemClassGuid={4d36e97d-e325-11ce-bfc1-08002be10318}Class=USBClassGuid={36FC9E60-C465-11CF-8056-444553540000}Class=AudioClassGuid={4d36e96c-......
WDDM 停止、复位、移除设备及驱动卸载
说完了驱动的创建与启动,这里顺使说一下设备的停止、复位、卸载设备设备的停止/BddDdiStopDevice设备停止的回调函数为BddDdiStopDevice InitialData.DxgkDdiStopDevice = BddDdiStopDevi......
使用CLSIDFromString将字符串GUID转化成十六进制GUID/UUID
Windows的一般会给相同类型的设备接口注册相同的GUID,但GUID有时别人给的是一个字符串,但是在实际使用过程中我们又使用的是GUID类型,其结构定义如下:typedef struct _GUID { unsigned long Data1; unsigned short ......
Windows驱动预定义设备GUID
全球唯一标识符 (GUID) 是一个字母数字标识符,用于指示产品的唯一性安装。在许多流行软件应用程序(例如 Web 浏览器和媒体播放器)中,都使用 GUIDGUID 的格式为“xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”,其中每个 x 是 0-9 或 a-f 范围内......
SetupApi关于INF文件处理的函数
INF文件处理功能提供安装功能,包括以下内容:打开和关闭INF文件。检索有关INF文件的信息。检索有关复制操作的源文件和目标目录的信息。执行INF文件节中指定的安装操作。FunctionDescriptionInstallHinfSectionExecutes a spec......
SetupApi磁盘提示和错误处理函数大全
可以使用setup函数提示用户插入新介质,或处理复制、重命名或删除文件时出现的错误。下表列出了为请求安装介质和报告错误提供对话框的功能。FunctionDescriptionSetupCopyErrorGenerates a dialog box that informs th......
SetupApi文件队列函数大全
使用setup函数,可以为各种操作对文件进行排队。可以为复制、重命名和删除文件建立文件队列。通常,应用程序将整个安装所需的所有文件操作排队,然后“提交”队列,以便在单个批中执行操作。FunctionDescriptionSetupCloseFileQueueDestroys a......
SetupApi默认队列回调例程函数
如果将回调例程与文件队列相关联,则每次系统执行一个排队的文件操作时都会调用回调例程。通常,您可以使用默认队列回调例程SetupDefaultQueueCallback来处理这些通知。FunctionDescriptionSetupDefaultQueueCallbackHand......
SetupApi CAB文件函数
CAB文件是单个文件,通常扩展名为.CAB,包含多个压缩文件作为文件库。CAB文件用于组织将复制到用户系统的安装文件。压缩文件可以分布在多个CAB文件上。FunctionDescription SetupIterateCabinetSends a notification to......
SetupApi 磁盘空间列表函数
磁盘空间列表函数用于创建和修改磁盘空间列表。这些列表可用于计算处理将在安装过程中复制或删除的文件所需的总磁盘空间。FunctionDescriptionSetupAddInstallSectionToDiskSpaceListSearches for CopyFile and ......
SetupApi MRB文件列表
Most recently used (MRU) source lists 最近使用的(MRU)源列表驻留在用户的计算机上,包含有关以前安装中使用的源路径的信息。提示用户输入源路径时,可以使用此信息。安装应用程序可以访问特定于用户的源列表,如果应用程序具有管理员权限,还可以访问系统范围的源列表。......
SetupApi 文件日志函数
可以使用日志文件记录安装期间复制到系统的文件的相关信息。日志文件可以是系统日志,也可以是您自己的安装日志文件。FunctionDescriptionSetupInitializeFileLogInitializes a log file for use.SetupLogE......
SetupApi用户接口函数
可以在类安装程序和联合安装程序中使用以下设置函数来确定当前进程是否可以与用户交互。FunctionDescription SetupGetNonInteractiveModeReturns the value of a SetupAPI non-interactive flag......
SetupApi 日志函数
从Windows Vista开始,即插即用(PnP)设备安装应用程序、类安装程序和协同安装程序可以使用以下函数将日志条目写入SetupAPI文本日志。FunctionDescriptionSetupGetThreadLogTokenRetrieves the log token......
Windows设备树及设备信息管理接口
Windows下的所有设备都会挂接在其设备树上,设备树由PNP管理器来维护。如一个设备树如下图所示:设备树上每个节点代表一个设备,凡是有子节点的节点是一个总线设备,其负责枚举下子节点设备,这样进行层层枚举,形成一棵设备树。其中根节点由PNP管理器创建,其用于挂载ACPI子节点。ACPI是一个抽象的......
作者信息
我爱内核
Windows驱动开发,网站开发
好好学习,天天向上。
取消
感谢您的支持,我会继续努力的!
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

您的支持,是我们前进的动力!