一、引言
随着计算机技术的发展,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下调试通过。
|