Detecting Hardware Insertion and/or Removal

简介:

      热插拔设置现在已经逐渐成为IT 安全的一大隐患,在这篇文章里面,我们尝试去开发一个应用程序,去检测系统设备的改变,例如,插入一个U盘、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消息:
     1.  这个应用程序必须有一个最上层的窗口
     2. 变化的接口或volume必须是上面的
     这样也不错,起码的你能知道桌面上的改变,并且你可以通过DEV_BROADCAST_VOLUME.dbcv_unitmask得到有影响的驱动文章。但不好的地方是你不能确切地知道哪个物理设备被插入到了系统里面来。

API函数: RegisterDeviceNotification()

     为了响应别的设备的改变,或者为了在非顶层窗口响应设备更改,你必须得调用RegisterDeviceNotification()函数,你可像下面的代码一样做:
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行处,你可以在下面的链接中看到更多关于这个的信息:  Doron Holan's blog
     一个Pnp(plug and play即插即用)的设备一般有两个不同的GUID(全球唯一标识符),一个是设备接口标识符,而另一个则是设备类标识符。
     一个设备类标识符定义设备的一个完全类型,如果你打开设备管理器,默认视图是用类型来区分的,每一个类型都是一个设备类,并且每一个类都是一个唯一的ID,一个设备类全球唯一标识符定义这个类的图标、缺省的完全设置、安装特性(就像一个用户不能修改这个类的一个实例安装,因为它必须先要被PnP枚举出来),
     a. \\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class
     b. \\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceClasses

    下面的表格列出基本的设备接口GUIDS:
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:

     修改我们的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设备
     Vid_04e8&Pid_503b: Vid/Pid是生产商ID和产品ID
     002F9A9828E0F06:一个唯一的标识
     {a5dcbf10-6530-11d2-901f-00c04fb951ed}: 设备接口ID
     现在利用这些得到的解码信息,我们可以用两种方法得到设备的描述和设备友好的名字:
     1.直接读注册表值: 在我们的例子里面是:\\HKLM\SYSTEM\CurrentControlSet\Enum\USB\Vid_04e8&Pid_503b\0002F9A9828E0F06
     2.用SetupDiXxx函数

API函数: SetupDiXxx()

     Windows有一系列的API提供给应用程序用代码读取硬件设备信息,例如,欠们可以用dbcc_name得到设备的描述和设备的友好名称。下面的步骤大致也是一样的:
     1. 用SetupDiGetClassDevs()函灵敏去得到一个设备的句柄,你可以把这个句柄当成一个直接句柄。
     2.用SetupDiEnumDeviceInfo()去列举所有的设备信息,你可以把这个操作当成一个文件夹列表,每次迭代后,我们可以得到一个SP_DEVINFO_DATA,你可以把这个句柄当成一个文件句柄
     3.当枚举的时候,用SetupDiGetDeviceInstanceId()去读每个设备的实例ID,你可以把这个操作想像成遍历文件夹时,去读文件夹里面每一个文件的特性,这个实例ID的格式有可能是这样的:  \USB\Vid_04e8&Pid_503b\0002F9A9828E0F06,跟dbcc_name很像。
    4.如果实例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),使一个设备无效的步骤如下:
     1. 设置SP_PROPCHANGE_PARAMS结构体的属性
     2. 用SP_PROPCHANGE_PARAMS做参数,调用SetupDiSetClassInstallParams()
     3. 用DIF_PROPERTYCHANGE调用SetupDiCallClassInstaller()

     事实上,DIF代码有点复杂,你要针对不同的DIF代码,以不同的结构体来调用SetupDiSetClassInstallParams(),可以在MSDN上面查看更多的信息,使设备无效的代码如下:

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
}

本文的缺点和不足:

     1. 明显地,这个程序只要监测到程序运行时的设备改变,如果设备是在操作系统开机之前插入,或者是在此程序打开前插入,那么就不会被检测到。但是这里有个解决的办法,可以用一个远程计算机来存储当前的状态,然后当程序启动后比较这两者有什么不同。
      2. 我们吸使设备无效,但我们所有能做的也只是使设备无效,相对于登录用户,我们不能再访问和控制这个设备了,或者我们只可以提供只读访问。
    原文链接:   http://www.codeproject.com/Articles/14500/Detecting-Hardware-Insertion-and-or-Removal

你可能感兴趣的