用Delphi编写U盘整盘数据清除程序
由于U盘小巧易携、使用方便、容量适度,使用频度较其他移动存储介质要高,信息安全隐患也要大得多。黑客、病毒和木马都将目标瞄准了移动存储介质特有的“信息安全软肋”,目前移动存储介质的摆渡、木马、远程控制程序多达百种,很多还采用了隐藏技术,防护的难度越来越大。一般的删除不能使原文档彻底清除,有时甚至是“文件粉碎”这样的工具也不能保证万无一失,因为像很多Word这样的应用软件在打开文档时,会生成一个临时文件,所有操作都在临时文件上进行,只在关闭时才将临时文件拷回到原文件的名录上。这样,在一个存储介质上会存在多个“有实无名”的隐身文档副本,没有名录信息对操作系统而言是不可见的,一般方法访问不到,但是通过特殊的技术可以将“实体”的大部分内容恢复。因此,针对U盘流动性大、数据易泄漏的特点,在保护隐私、跨网使用和清除摆渡泄密的需求下,开发出对U盘数据安全可靠清除的程序很有必要。目前市场上的数据清除设备动辄上万元,基本上是使用嵌入式系统开发的专用产品,本文介绍的方法和程序在原理和实际效果上与其没有差别,在某些方面甚至有更好的拓展性、适应性和灵活性。当然,技术到产品的跃升还需做很多艰苦的工作,希望此文能抛砖引玉、与大家共享技术带来的便利。
2 文件系统的原理
无论是FAT还是NTFS文件系统,尽管安全机制和存取的效率差别很大,但是原理基本相同,都是由索引部分和文件数据实体部分组成,索引提供名录的有效存取、检索,数据实体存放文件的真正内容。为了提高文件系统对文件处理的效率,在删除文件时并不是将上述的两部分全部消除,只是在索引部分标记为“已删除”,而文件的实体数据依然原封不动地存在介质上,从表面上已经查不到这些删除文件的存在,但是通过一定的方法,还是可以一定程度上进行恢复,这是多种文件恢复工具的工作原理。
即使采用高级格式化,也只是将文件系统的索引部分清除,而数据实体部分基本上没有涉及,仍可进行数据恢复和提取。
3 U盘的特点
U盘与磁介质移动盘不同,U盘是内部的FLASH(电可擦除芯片组)的电荷来维持电位高低,从而进行二进制数据存储的,一旦电荷发生变化,则二进制数据也会发生根本性的改变,或者说物理上不太可能恢复和还原。但是磁介质不同,由于每次的磁头位置不可能完全重合,可能会有微弱的磁化残留,也就是上一次写的数据虽然被覆盖,在磁力显微镜下仍可看出其残留的痕迹,至于是否能将被覆盖的上一次信息完整地恢复出来,学术界目前争论不休,鲜见实测检验的权威报告。光介质存储也存在类似的问题,只是使用频度远低于前两者,还未引起重视。
U盘因为电荷清除后信息就消失了,不会有类似弱磁残留的问题,可以保证清除效果。
4 关键技术
4.1 U盘容量的获取
获取U盘容量有两个API函数,第一个是:
GetDiskFreeSpaceEx(Directory:PChar;var FreeAvailable,TotalSpace:TLargeInteger;TotalFree: PLargeInteger): Bool;//此函数在SysUtils单元
这个函数返回的容量(int64类型变量TotalSpace所返回的值)就是U盘标写的容量,但是这个函数必须是在U盘已经格式化后才能正常工作,否则返回的容量是错误值。
另一个函数是:
DeviceIoControl(hDevice: THandle; dwIoControlCode: DWORD; lpInBuffer: Pointer;nInBufferSize: DWORD;lpOutBuffer: Pointer; nOutBufferSize: DWORD;var lpBytesReturned: DWORD; lpOverlapped: POverlapped): BOOL;//这个函数在Windows单元。
这个函数是向设备级发送请求,返回的是底层设备的原始值。在使用这个函数时,不要求U盘设备已经格式化,在“属性”里的“文件系统”一栏可以显示为“RAW”,即没有任何格式的移动盘。笔者在实验中发现,这个函数是在设备层进行请求,有时在资源管理器中不显示该盘符时,使用物理硬设备符(如用文件方式打开设备生成句柄hDevice时,用'//./PHYSICALDRIVE1'而不是'//./u:'),仍然可以得到U盘的容量,这对U盘数据恢复很有意义。但是这个函数与前一个函数相比,其返回的容量总是小于前函数的结果,并且没有规律。
4.2 真实容量分析
要对U盘进行完全彻底的数据清除,首先必须要知道它的容量。由于U盘本身的特性,一般在商标上标写的容量并不一定是真正的容量。原因有三:1、正规品牌的U盘标写的是正常使用的容量,在其内部还有一部分是作为坏区补偿的备用容量,当检测有坏片时,U盘内部的电路会将坏区与补偿部分进行互相置换,所以其真正容量会大于标写的容量;2、某些不良商家会将小容量的U盘Mask成大容量的盘,这时所注的容量也不正确;3、目前很多黑客、木马会将大容量的U盘改写成小容量,使一部分存储区成为一般手段无法访问的隐蔽区,作为恶意信息的真正藏身地。表面上似乎如何检测U盘真正的容量是关键,实质上能否完全清除U盘上的所有存储信息才是安全保障的根本所在。
4.3 覆盖U盘所有存储空间
由上可知,无论哪个函数,包括系统显示的的容量,都不是U盘真正的容量,而且多出来的容量不同厂家的产品各不相同。笔者通过测试,发现多出来的容量和额定的容量在连续的地址空间,即多出来的部分紧接在额定的后面。
因此,有效的解决方法是:当清除到额定容量时,并不停止,继续清除,直到发生“写错误”时才退出清除过程。(笔者曾对一枚128M的U盘清除到135M时才显示清除完毕)
看到这里读者可能会问:如果用这种解决方法,获取U盘容量有什么意义?实际上获取容量是为了大概估算清除时间,在程序设计时进度条控件需要总量来计算进度的百分比,前述介绍的U盘容量获取的结果与真实容量差别不大,可以作为参考值。
5 主要代码实现
5.1 程序界面与控件布局
5.2 搜索当前在线的U盘
因为U盘的数据清除与移动硬盘不同,U盘可以从逻辑的0扇区开始一直清除下去,
也就是从活动分区DBR开始清除下去,U盘一般没有硬盘引导扇区MBR,即使有也可以进行清除,而移动硬盘不能从从MBR开始清除,因为MBR扇区里存有划分的多个分区的信息和整个硬盘的容量信息。从设备检测来说,U盘属于移动设备(DRIVE_REMOVABLE),而移动硬盘属性仍然是固定设备(DRIVE_FIXED),关于移动硬盘的数据清除将另文介绍。
搜索当前在线的U盘列表的程序代码如下:
procedure TForm1.Button1Click(Sender: TObject);
var
buf:array [0..MAX_PATH-1] of char;
m_Result:Integer;
i:Integer;
str_temp:string;
begin
m_Result:=GetLogicalDriveStrings(MAX_PATH,buf);
//获取当前逻辑驱动器的排列和数量
ListBox1.Items.Clear;
for i:=0 to (m_Result div 4) do
begin
str_temp:=string(buf[i*4]+buf[i*4+1]+buf[i*4+2]);
if GetDriveType(pchar(str_temp)) = DRIVE_REMOVABLE then
//这里确定是U盘驱动器符
begin
//ShowMessage(str_temp+'盘为U盘');
ListBox1.Items.Add(str_temp);
end;
end;
end;
5.3 容量估算与显示
对ListBox1的OnClick事件进行处理,一方面保证所处理的盘肯定是U盘,另一方面可以确认清除的目标盘无误,因为数据是永久性的清除,无法恢复。
事件处理程序如下:
procedure TForm1.ListBox1Click(Sender: TObject);
var
dr:string;
i,j,k:Integer;
c: Char;
totalsize:int64;
userFreeBytes,totalBytes,freeBytes:Int64;
nOutput:ULONG;
actget:DWORD;
getbuff:DISK_GEOMETRY;
fbuf:pchar;
begin
i:=ListBox1.ItemIndex;//当前U盘列表 www.2cto.com
dr:=copy(ListBox1.Items[i],1,1);//查找逻辑盘符第一个字母
driver:=pchar('//./'+dr+':'); //这里也可以用硬件设备名
//如'//./PHYSICALDRIVE1'
hDeviceHandle := CreateFile(driver, GENERIC_ALL, FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING,0, 0);
if (hDeviceHandle<> INVALID_HANDLE_VALUE) then
begin
DeviceIoControl(hDeviceHandle,
IOCTL_DISK_GET_DRIVE_GEOMETRY,
nil,
0,
@getbuff,sizeof(DISK_GEOMETRY),
actget,
nil);
totalSize:=int64(getbuff.Cylinders.QuadPart) * int64(getbuff.TracksPerCylinder) *int64(getbuff.SectorsPerTrack)*int64(getbuff.BytesPerSector);
Label2.Caption:=Formatfloat('###,##0',totalSize);//从底层硬设备得到的容量
SectorWithBytes:=int64(getbuff.BytesPerSector);// SectorWithBytes是个全局变量,记录每个扇区有多少字节
closehandle(hDeviceHandle);
end;
if GetDiskFreeSpaceEx(PChar(dr+':/'),userFreeBytes,totalBytes,@freeBytes) then //如果已经格式化,可以得到容量。
begin
Label8.Caption:=Formatfloat('###,##0',totalBytes);
end else
begin
Label11.Caption:='未格式化的盘(RAW) ';
//没有格式化过的盘,不能得到容量信息
end;
end;
使用DeviceIoControl(…)时,输入参数里需要DISK_GEOMETRY类型的变量,在Delphi中没有现成的引用单元,必须自建一个“unit IOctl”,源代码如下:
unit IOctl;
interface
uses
Windows;
type
MEDIA_TYPE=(
Unknown,
F5_1Pt2_512,
F3_1Pt44_512,
F3_2Pt88_512,
F3_20Pt8_512,
F3_720_512,
F5_360_512,
F5_320_512,
F5_320_1024,
F5_180_512,
F5_160_512,
RemovableMedia,
FixedMedia,
F3_120M_512,
F3_640_512,
F5_640_512,
F5_720_512,
F3_1Pt2_512,
F3_1Pt23_1024,
F5_1Pt23_1024,
F3_128Mb_512,
F3_230Mb_512,
F8_256_128
);
DISK_GEOMETRY = record
Cylinders:LARGE_INTEGER;
MediaType:MEDIA_TYPE;
TracksPerCylinder:Ulong;
SectorsPerTrack:Ulong;
BytesPerSector:Ulong;
end;
const
FILE_DEVICE_DISK = 00000007;
IOCTL_DISK_GET_DRIVE_GEOMETRY =(FILE_DEVICE_DISK shl 16) + (0 shl 14) + (0 shl 2)+ (0);
implementation
end.
5.4 数据清除
我们使用最为简单但是可靠有效的清除方式,就是从U盘的第一个扇区(逻辑序号为0的扇区),覆盖无效的数据一直到U盘的最后尾部。确定尾部的方法前面已经介绍过,一种是根据容量来确定覆盖的长度,另一种是根据覆盖的“写错误”发生时为止,这是清除U盘最干净的方式。
无论是按检测的U盘容量进行清除,还是进行超容量的试探性清除,最好是分段进行,以方便控制和进度显示。下面是一个通用数据清除的子程序,源代码如下:
Function TForm1.CleanUSBDisk(dri:char;FromWhere,CleanSize:int64):int64;
//参数分别是:驱动器盘符,起始清除的位置,总共清除的数量
Var
p:pchar;
i,j,EachClean:integer;
writesec:string;
driver:pchar;
fillbyte:Byte;
ActuralCleanSize:int64;
begin
result:=0;
driver:=pchar('//./'+dri+':');
p:=allocmem(CleanSize);
setlength(writesec, CleanSize);
writesec:='';
fillbyte:=0;//这是填充数据,可以根据需要填充任何数据
for i:=0 to CleanSize -1 do
writesec := writesec + chr(fillbyte);
p:=pchar(writesec);
hDeviceHandle := CreateFile(driver, GENERIC_ALL,
FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING,0, 0);
if (hDeviceHandle <> INVALID_HANDLE_VALUE) then
begin
FileSeek(hDevicehandle,FromWhere,0);//指向数据覆盖的开始位置
ActuralCleanSize := FileWrite(hDevicehandle,p[0], CleanSize);
result:= ActuralCleanSize;
if ActuralCleanSize <> CleanSize then
raise exception.create('Write错误%d');//发生写异常,可能是超过了盘的真实容量,这是判断真实容量和全覆盖的技术实现方法
closehandle(hDeviceHandle);
end;
end;
说明:1、输入参数FromWhere和CleanSize必须是“每扇区字节数”(一般是512B)的整数倍;
2、填充的数据可以根据用户自己的喜好定义,上述程序因篇幅只是简单地填充00;
3、返回值是正确填充的容量,与输入参数CleanSize相比可判断该子程序执行的情况。
5.5可靠性和稳定性考虑
前述基本上解决了功能问题,在实际发布过程中,还要考虑很多可靠性和稳定性方面的问题,笔者例出几条供参考:
1、清除的过程最好使用线程。这样可以避免界面不响应的问题,线程中的完成情况,通过Delphi的Sychrinize过程传递到主界面上,不会发生死锁冲突;
2、如果不使用线程,在调用CleanUSBDisk函数的循环间隙调用Application.ProcessMessages,可以让主界面有机会响应一些正常的消息;
3、要考虑在程序非正常退出时还原工作环境。如打开的驱动器句柄要关闭、申请的内存要释放等。
6 结语
本文从U盘的结构和内部工作机制上对其进行了详细的剖析,从硬设备层和逻辑分区层对U盘进行完整的数据清除,原理上也基本适用于移动硬盘。对于固定硬盘,不包含系统工作文档的逻辑分区也适用。其实际效果与专用清除设备没有差别,可扩展性上具有更大的灵活性,如对冗余容量的清除、用户自定义填充数据、清除效果扫描回显等。此外,有时U盘插入后,在资源管理器中并不显示盘符,但是在“控制面板”--“计算机管理”里可以检测到U盘,这时用硬设备名,如“//./PHYSICALDRIVE1”(表示第二个存储设备),进行U盘数据清除,同时还可以修复U盘的异常故障。
本文的程序只完成了最基本的实用功能,如图5-1所示,在Windows XP+Delphi 7下调试通过,清除效果通过专业测定。
摘自 fjwolf的专栏