Service如何利用RegisterDeviceNotification监控Volume的装载和卸载

windows提供了以下api来向系统注册一个函数,当有volume增删(比如U盘插拔、新建分区)的时候,通知应用程序:

HDEVNOTIFY WINAPI RegisterDeviceNotification(
  __in  HANDLE hRecipient,// 可以是窗口句柄或者服务句柄
  __in  LPVOID NotificationFilter,
  __in  DWORD Flags // 制定hRecipient是窗口句柄,还是服务句柄
);

如果采用窗口句柄,根据msdn的文档,注册之后,系统会在volume增删的时候,向注册的窗口发送WM_DEVICECHANGE消息,只需要在窗口的消息循环中处理该消息就可以判断出U盘中的卷:
    case WM_DEVICECHANGE:
        if(wParam == DBT_DEVICEARRIVAL) //设备激活
        {
            PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam;
            if(lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME)
            {
                PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb;
                // 下面可以进一步找到装载的卷名,如E:/
            }
        }
        else if(wParam == DBT_DEVICEREMOVECOMPLETE)
        {
            PDEV_BROADCAST_HDR lpdb = (PDEV_BROADCAST_HDR)lParam;

            if(lpdb->dbch_devicetype == DBT_DEVTYP_VOLUME)
            {
                PDEV_BROADCAST_VOLUME lpdbv = (PDEV_BROADCAST_VOLUME)lpdb;
                // 下面可以进一步找到卸载卷名,如E:/
            }
        }
上面的代码经过验证是可行的。

但是窗口程序需要user登陆系统后才能执行,所以一般还是应该采用service来实现监控。但是如果采用在调用RegisterDeviceNotification的时候传入服务句柄,应该采取不同的处理方式,这点似乎与文档不一致。见下面代码的描述:
      case SERVICE_CONTROL_DEVICEEVENT:
          if(dwEventType == DBT_DEVICEARRIVAL)
           {
              PDEV_BROADCAST_HDR pHdr = (PDEV_BROADCAST_HDR)lpEventData;

              switch(pHdr -> dbch_devicetype)
              {
              case DBT_DEVTYP_VOLUME:
                  {
                      // 本来应该在这里提取卷名称,但是实际上代码永远不会运行到这里
                      break;
                  }
              case DBT_DEVTYP_DEVICEINTERFACE:
                  {
                      // 卷加载的时候,代码总是运行到这里
                          // pS->dbcc_name是形如这样的字符串://?/STORAGE#Volume#1&30a96598&0&Signature5C2864E7Offset4000Length67FC000#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}

                      PDEV_BROADCAST_DEVICEINTERFACE pS = (PDEV_BROADCAST_DEVICEINTERFACE)pHdr;
                      WriteLog(std::wstring(pS->dbcc_name));
                                          // 处理代码
                  }
              }
              return NO_ERROR;
              break;
          }
      }


研究了半天,最终还是在pS->dbcc_name上找到了突破口。
一个卷设备(比如E盘)在windows内核中是一个volume对象,对象名是/Device/HardDiskVolumeN,N为序号。但是用户态程序是不能直接通过/Device/HardDiskVolumeN对象名来打开volume对象的,必须通过三个SymbolLink来打开:
SymbolLink1形如//?/E:
SymbolLink2形如//?/Volume{GUID}
SymbolLink3形如//?/STORAGE#Volume#1&30a96598&0&Signature5C2864E7Offset4000Length67FC000#{GUID}
呵呵,发现了吧,pS->dbcc_name就是这里的SymbolLink3。
因此可以在UserMode的代码中用CreateFile打开pS->dbcc_name,获得一个Handle;
然后写一个driver,在driver的IOControl中,通过一个Handle查询对象的名字,也即是/Device/HardDiskVolumeN的字符串;
然后UserMode的代码通过DeviceIOControl调用Driver,调用的输入参数就是前面打开的Handle,这样就可以在UserMode中得到卷的名字了。
driver的通过Handle查询ObjectName的简化代码如下:
    ObReferenceObjectByHandle(handle, 0, NULL, KernelMode, &pObj1, NULL)
    unsigned char nameInfo1[ 512 ];
    OBJECT_NAME_INFORMATION * pNameInfo1 = (OBJECT_NAME_INFORMATION *)  (nameInfo1);
    ULONG length;
    NTSTATUS status = ObQueryNameString(pObj1, pNameInfo1, sizeof(nameInfo1) - sizeof(OBJECT_NAME_INFORMATION), &length);
    ObDereferenceObject(pObj1);

很绕吧,可惜目前只找到了这个九曲十八弯的方法。
PS:

UserMode下的APPI函数GetVolumePathNamesForVolumeNameW是用不上的,它只能把SymbolLink2的字符串转换成盘符;如果传入SymbolLink3的字符串返回失败。最终在纯用户态下把SymbolLink3转换成volume的名字以失败告终 @@


















你可能感兴趣的:(service,object,winapi,windows,文档,null)