| 
				 一、引言 
随着计算机技术的发展,USB存储设备如U盘、移动硬盘等设备越来越普及,随之而来的一个非常显著的问题就是:如何保证这些设备的安全,比如有些涉密计算机不允许使用某些移动存储设备,但是却允许另外一些USB存储设备访问。 
下面将对USB存储设备的监控所涉及到的技术及原理进行探讨,最后给出一个USB存储设备的监控程序的例子。 
二、原理 
一个USB设备插入到计算机USB端口上时,操作系统硬件管理程序将会发现设备,然后查找该设备的驱动程序是否存在,如果存在,系统加载驱动程序,然后给USB设备分配盘符等。 
从上面的分析中可以知道,如果要阻止USB设备在计算机上使用,至少有两个方法可以使用:一是修改设备驱动程序,在设备驱动程序里面加入对设备进行判断的代码,从而阻止非授权USB设备在系统上的识别;第二种方法是不修改驱动程序,而在USB设备枚举完成后,立即把设备卸载,从而在系统中无法使用该设备。 
上面两种方法中,第一种需要熟悉驱动程序开发技术,难度比较大;第二种原理比较简单,实现起来也相对容易。本文将采用第二种方法。 
第二种方法的原理是:当插入USB存储设备时,应该立即获取该USB设备的信息,然后判断这些信息是否是经过授权的,如果非法,立即调用卸载函数卸载该USB设备。 
三、开发过程 
从上面的分析中可以知道,系统可以分为三部分:USB存储设备的检测、USB设备信息的读取判断、设备的卸载。 
1. USB检测 
Windows系统中,当PC机上添加或者删除一个即插即用设备时,将触发系统的WM_DEVICECHANGE消息。对于USB设备的检测也一样,在程序中捕获这个消息,然后在消息处理函数获取设备参数。 
声明过程用于检测设备的变化: 
procedure WMDeviceChange(var AMessage:TMessage);message WM_DEVICECHANGE; 
程序捕捉到这个消息以后,需要进行判断,消息的AMessage.wParam表明了设备信息及当前状态: 
DBT_DEVNODES_CHANGED://设备节点发生了变化(关键点A) 
DBT_DEVICEARRIVAL://插入设备了(关键点B) 
但是,WM_DEVICECHANGE消息不但响应硬件设备的改变,而且PC上装入光盘等存储设备时,该消息也会响应。那么,如何判断系统插入的是USB存储设备还是放入光盘呢? 
通过跟踪调试可以知道,PC机插入设备时,AMessage.wParam值为DBT_DEVICEARRIVAL,可以通过AMessage.LParam的值判断插入的是否是存储设备。 
PDEV_BROADCAST_HDR(Message.LParam).dbch_devicetype的值为DBT_DEVTYP_VOLUME表示插入了存储设备,但这个值的还是无法区分是USB存储设备还是光盘等设备。 
插入USB设备的时候,AMessage.wParam的值首先变为DBT_DEVNODES_CHANGED(上面关键点A),然后才变成DBT_DEVICEARRIVAL(关键点B);而插入光盘等没有引起系统硬件状态改变的介质时,只会响应关键点B,而不会响应关键点A。因此,通过联合这两个值的状态,就可以确切知道系统插入的是否为USB存储设备。示例代码如下: 
procedure TForm1.WMDeviceChange(var Message: TMessage);  
var 
  pid:DWORD; 
begin 
  if DisMountCmdOk then    //如果已经发出卸载命令,则不再响应该消息 
    exit; 
  //监测USB存储设备的插入 
  if SelfDisMount then //是否是本程序自己卸载USB设备 
    begin 
      SelfDisMount:=false;  //如果是本程序自己卸载了USB设备,则不再响应该消息 
      exit; 
    end; 
  case Message.wParam of 
    DBT_DEVICEARRIVAL:   //关键点B:插入设备了 
      begin 
      case PDEV_BROADCAST_HDR(Message.LParam).dbch_devicetype of 
        DBT_DEVTYP_OEM:     ListBox1.Items.Add('DBT_DEVTYP_OEM'); 
        DBT_DEVTYP_DEVNODE: ListBox1.Items.Add('DBT_DEVTYP_DEVNODE'); 
        DBT_DEVTYP_VOLUME:  //这个值对U盘和光盘都起作用 
          begin 
            if IsHardWareChanged then   //通过IsHardWareChanged区分USB存储设备和光盘等 
              begin 
                IsHardWareChanged:=false; 
                AllowUSB:=false; 
                //在此处获取刚插入U盘的盘符 
                diskvol:=FirstDriveFromMask(PDEV_BROADCAST_HDR(Message.LParam).dbcv_unitmask); 
                GetUSBInfo(diskvol, Pid);//获取标志信息 
                ListBox1.Items.Add('DBT_DEVTYP_VOLUME:插入USB 存储设备;盘符:'+diskvol+':;序列号:'+inttostr(pid)); 
//判断是否为授权U盘,用变量AllowUSB标志。代码略 
//理论上在此处可以放置卸载设备代码,但是经过测试发现:如果把卸载代码(在//Timer1Timer过程中) 
//放置在此处,卸载将会非常缓慢。所以采用了延时的方法解决这个问题 
                isusb:=true; 
                DisMountCmdOk:=true;//卸载命令已经发出 
                //使用定时器延时后弹出设备 
                timer1.Enabled:=true; 
              end 
            else 
            ListBox1.Items.Add('DBT_DEVTYP_VOLUME:插入CD等其他存储介质'); 
          end; 
      end; 
      end; 
    DBT_DEVNODES_CHANGED: //关键点A:插入USB设备后首先响应这里 
      begin 
         IsHardWareChanged:=true; 
 //设备发生变化。这个值将被用来区分是USB存储设备还是别的存储介质(如光盘) 
      end; 
  end; 
  inherited; 
end; 
弹出、卸载USB存储设备的代码在定时器的消息响应中。当系统检测到USB存储设备后,需要弹出USB存储设备时,使定时器有效;延时时间到后就可弹出USB存储设备。 
2.USB读取 
上文中使用过程GetVolSerial获取USB存储设备标志信息。该过程完成对PC机上刚插入的USB存储设备标志信息的获得,从而作为我们判断设备是否合法的依据。 
API函数GetVolumeInformation用于获取指定根路径的卷和文件系统的相关信息。此处只需获得卷的序列号作为标志信息,所以只关心参数lpVolumeSerialNumber的值。 
BOOL GetVolumeInformation( 
LPCTSTR lpRootPathName, // 根路径指针  
LPTSTR lpVolumeNameBuffer,  // 卷名称指针 
DWORD nVolumeNameSize,  // 卷名称字符串长度 
LPDWORD lpVolumeSerialNumber,   //序列号指针 
LPDWORD lpMaximumComponentLength,   //文件名称最大长度指针 
LPDWORD lpFileSystemFlags,  //文件系统指针 
LPTSTR lpFileSystemNameBuffer,  //文件系统名称指针  
DWORD nFileSystemNameSize   //文件系统名称字符串长度指针  
); 
把API函数GetVolumeInformation进行封装成GetVolSerial。实现代码如下: 
function GetUSBInfo(diskVol:string;var lpVolumeSerialNumber: DWORD):boolean; 
var 
  lpRootPathName: PChar; 
  lpVolumeNameBuffer:PChar; 
  nVolumeNameSize: DWORD; 
  lpMaximumComponentLength, lpFileSystemFlags: DWORD; 
  lpFileSystemNameBuffer: PChar; 
  nFileSystemNameSize: DWORD; 
  ifVolOK:boolean; 
begin 
  lpVolumeNameBuffer:=AllocMem(256); 
  lpFileSystemNameBuffer:=AllocMem(256); 
  lpRootPathName:= PChar(diskVol+':\'); 
  nVolumeNameSize:=256; 
  lpVolumeSerialNumber:=0; 
  lpMaximumComponentLength:=256; 
  lpFileSystemFlags:=0; 
  nFileSystemNameSize:=256; 
  ifVolOK:=GetVolumeInformation(lpRootPathName,lpVolumeNameBuffer,256, 
@lpVolumeSerialNumber,lpMaximumComponentLength,lpFileSystemFlags,lpFileSystemNameBuffer,nFileSystemNameSize); 
  Freemem(lpVolumeNameBuffer); 
  Freemem(lpFileSystemNameBuffer); 
  result:=ifVolOK; 
end; 
该函数将返回盘符为diskVol的USB存储设备的序列号到参数lpVolumeSerialNumber中。程序可以检测该参数,从而判定USB存储设备是否合法。 
这里提供的函数比较简单,网上有很多类似代码探讨如何获得U盘的序列号,但是都不是很理想。感兴趣的读者可以进一步探讨这个问题。 
3.USB卸载 
上文中提到,USB存储设备的卸载是通过定时器的消息响应来完成的。实验表明,如果把卸载代码放在判定设备是否合法后面,系统卸载USB存储设备将会非常缓慢。 
procedure TForm1.Timer1Timer(Sender: TObject); 
begin 
   //AllowUSB=true; //此处应该恢复可以使用USB存储设备(测试程序中注释掉) 
   isusb:=false; 
   Timer1.Enabled:=false; 
   SelfDisMount:=true;  //表示此次卸载是本程序自己完成的 
   IniDevice; //获得设备列表,以及USB存储设备的在列表中的ID 
   RejectUSB; //卸载设备 
   DisMountCmdOk:=false; 
end; 
卸载设备的时候,首先调用SetupDiGetClassDevsA函数建立系统当前设备列表,然后调用函数SetupDiEnumDeviceInfo遍历这个列表,查找设备名称为“USB Mass Storage Device”的设备(Windows设备管理程序中,所有USB存储设备都使用这个名字),获得其在设备列表中的ID,然后调用函数CM_Request_Device_Eject请求系统卸载该设备。这些代码分别在过程IniDevice和RejectUSB中实现。 
(1)获得设备信息 
1)获取系统中所有设备信息到hDevInfo指针所指空间 
function GetDevInfo(var hDevInfo: hDevInfo): boolean; 
begin  
  hDevInfo := SetupDiGetClassDevsA(nil,nil,0,DIGCF_PRESENT or DIGCF_ALLCLASSES); 
  Result := hDevInfo <> Pointer(INVALID_HANDLE_VALUE); 
end; 
API函数SetupDiGetClassDevsA获取系统当前设备列表到指针hDevInfo的数据空间。 
2)遍历DevInfo,获得U盘在当前系统设备列表中的ID 
function EnumAddDevices(ShowHidden: Boolean;DevInfo: hDevInfo): Boolean; 
var 
  i, Status, Problem: DWord; 
  pszText: PChar; 
  DeviceInfoData:TSPDevInfoData; 
begin 
  DeviceInfoData.cbSize := SizeOf(TSPDevInfoData); 
  i := 0; 
  //遍历设备列表,查找USB存储设备信息 
  while SetupDiEnumDeviceInfo(DevInfo, i, DeviceInfoData) do 
  begin 
    inc(i); 
    //获取设备节点状态信息 
    if (CM_Get_DevNode_Status(@Status, @Problem, DeviceInfoData.DevInst, 0) <> CR_SUCCESS) then 
    begin 
      break; 
    end; 
    try 
      GetMem(pszText, 256); 
      ConstructDeviceName(DevInfo, DeviceInfoData, pszText, DWord(nil));  
//创建设备可见名称列表 
      if pos(MyDevice,StrPas(pszText))<>0 then      
//比较字符串,找到USB存储设备 
        MyDevice_ID:=i-1;  //得到USB存储设备在当前设备列表中的ID 
    finally 
      FreeMem(pszText); 
    end; 
  end; 
  Result := true; 
end; 
API函数SetupDiEnumDeviceInfo获取当前设备列表(DevInfo)中当前设备节点(第i个节点)的信息到参数DeviceInfoData。 
API函数CM_Get_DevNode_Status查询当前设备节点的状态信息,如果查询表示设备存在并且工作正常。 
函数ConstructDeviceName是程序中自己实现的非系统函数,其功能是获得到当前设备节点的可见设备名称,该名称就是设备管理器显示的设备名称。限于篇幅,此处不再详细介绍该函数的实现,读者可以参考源代码。 
3)获得设备ID 
procedure IniDevice;  
begin 
  MyDevice_ID:=0; 
  DevInfo := nil; 
  if not GetDevInfo(DevInfo) then 
    begin 
      ShowMessage('枚举设备失败!'); 
      exit; 
    end; 
  EnumAddDevices(TRUE,DevInfo); 
end; 
该过程调用上面两个函数,过程执行完毕后,将把USB存储设备在系统当前设备列表中的ID存储到参数MyDevice_ID中,卸载过程将使用该ID完成设备的卸载。 
(2)卸载USB存储设备 
procedure RejectUSB; 
var 
  DeviceInfoData:TSPDevInfoData; 
  Status, Problem: DWord; 
  VetoType: TPNPVetoType; 
  VetoName: array[0..256] of Char; 
  result_index:Cardinal; 
begin 
DeviceInfoData.cbSize := SizeOf(TSPDevInfoData); 
//判断设备ID 
    if (not SetupDiEnumDeviceInfo(DevInfo, MyDevice_ID, DeviceInfoData)) then 
      exit; 
    //查询设备状态 
if (CM_Get_DevNode_Status(@Status, @Problem, DeviceInfoData.DevInst, 0) <> CR_SUCCESS) then 
      exit; 
    VetoName[0] := #0; 
    //请求系统卸载设备 
result_index:=CM_Request_Device_Eject(DeviceInfoData.DevInst, VetoType, @VetoName, SizeOf(VetoName), 0); 
    case result_index of 
      CR_SUCCESS: 
               SelfDisMount:=true; 
  end; 
end; 
过程中比较重要的代码是: 
if (not SetupDiEnumDeviceInfo(DevInfo, MyDevice_ID, DeviceInfoData)) then 
      exit; 
这段代码判断当前设备是否是上面得到的ID所标志的设备。接着查询设备状态,然后调用API函数CM_Request_Device_Eject请求系统卸载设备。 
具体代码比较复杂,详见本文附带源代码。源代码中作了非常详细的注释,相信读者完全可以掌握。 
四、结语 
具体实现时,在窗体上放置一个Listbox控件,用于显示当前插入设备的简单信息。使用的时候,首先启动程序,然后插入USB设备即可察看程序运行结果。 
本文所附源代码没有完成USB存储设备是否合法的判定。程序直接将刚插入的USB存储设备卸载。文章中已经给出了获取USB存储设备标志信息的方法(调用GetUSBInfo函数即可),因此,读者只需做一个简单判断即可知道当前插入的USB存储设备是否是授权可以使用的设备。USB操作的所有的方法放置在USBinfo.pas文件中。 
另外,系统中插入的USB设备可能会自动运行,因此还应该在程序中加入禁止设备自动运行的代码。鉴于篇幅和时间,程序中没有实现该功能。程序在XP、Delphi7下调试通过。 			
				 |