由于项目需要,这十天做了个Windows文件过滤驱动, 感觉windows下的驱动也不是那么的神秘, Mirosoft可以说是把API做到了极致(尽管有时真的是没必要), 他们喜欢把API包装了又包装, 不让你看清楚底层的东西. 不过另一方面, 调用这些API也是挺方便的(当然了,API多了,有些API会有些Bug也是在所难免的,比如ObQueryNameString). 由于是零基础开始做这个驱动的, 所以这里稍微把过程记录的详细些!
1. 搭建编译环境及WDM介绍.
这里给几个link:
ref1
WDM开发之路
编译环境搭建好了后, 在WDK的编译环境中, 输入build -cZg(或者直接build), 就可以编译生成驱动文件的.sys文件了. Debug版本的编译会在objchk_wxp_x86下, Release版本的编译会在objfre_wxp_x86下面.
2. 具体写程序的时候, 网上参考多的是 “楚狂人” 的”Windows文件系统过滤驱动开发教程(第二版)”, 有很多地方都可以下载, 大家可以自己找找. 这个教程确实不错, 不过, 我对初学者的建议是:
(1) 对照代码看这个教程好些, 仅仅直接看教程,有些地方可能不太了解.
(2) 里面说了很多, 如果有些地方不理解, 自己写代码尝试,可能是最好的理解方式.
另外, 写程序时, 不能不提到DDK的帮助文档, 和安装路径下,src文件夹中的参考代码, 他们太好了!
读代码, 写代码,再调试, 是最好的编程提高方式!
噢,说到调试, 内核级的调试,必须小心, 一不小心就会造成蓝屏, 另外, 可以找DbgView.exe 和KmdManager.exe来帮助调试, 更高级的就要Windbg了, 不过, 我个人感觉, 如果是自己写的程序, 又不是特别的大,只要自己想清楚了, 靠KdPrint(())就可以解决问题, 用不赵Windbg这样的高级专业工具, 当然, 如果会使用Windbg那是更好不过了.
3 .开发总结感受:
当你对FileSpy,sfileter, FileMon,RegMon 等的源码到了一定程序 , 并做了足够的一些程序练习和调试后, 这里探讨些,开发的细节问题. 首先介绍个不错的
windows文件过滤驱动经验总结
. 这里对获取文件路径这点上, 我补充下是: 根据我的经验, 只能在IRP_MJ_CREATE中获取到文件的全路径, 其它地方, 比如说, 要对文件增,删,改的时候,即使自己构造IRP包, 仍然是不能得到全路径,仅仅能得到”去掉盘符”后的文件地址(即相对某个分区盘的相对地址).
如下的地方, 我会搞一些代码的snippet,来分析:
(1)在驱动中取得盘符和路径是分开的.取得盘符是用RtlVolumeDeviceToDosName, 而路径则是,首先用IoGetCurrentIrpStackLocation(Irp)获得当前的IO_STACK_LOCATION , 然后获取里面的FileObject对象指针, 再获得其FileName字段
(2)禁止访问目录:
实现禁止访问目录是比较简单的,有很多的方法。我是在IRP_MJ_DIRECTORY_CONTROL
中判断是不是要禁止的目录,然后拦截。拦截的操作我就不多说了,基本一样:
Irp->IoStatus.Status = STATUS_ACCESS_DENIED;
Irp->IoStatus.Information = 0;
status = Irp->IoStatus.Status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
(3)设置目录为只读
由于禁止目录访问的方法很容易,所以很容易让人觉得禁止写是不是就是在IRP_MJ_WRITE中
判断目录路径然后直接拦截了。我测试后发现不能这样实现,论坛上的人告我将目录设置为只读
实际上是把目录下所有文件设置为只读,即有一目录C:\\Project你想设置为只读,实际的操作是:
对于该目录下任一文件xx.xx,当该文件想进行写的时候,驱动会得到路径\\Project\\xx.xx,此时
判断文件的父目录是不是\\Project并拦截之,即可实现\\Project\\xx.xx的只读控制。
到目前我实现就是使用这样的方法,是不是可以直接对目录进行拦截,我不知道。
(4)禁止删除(重命名)
也还算简单,
在IRP_MJ_SET_INFORMATION中,
FileDispositionInformation == currentIrpStack->Parameters.SetFile.FileInformationClass
|| FileRenameInformation == currentIrpStack->Parameters.SetFile.FileInformationClass
(5)禁止创建文件
这里主要是区别一下新建文件和打开文件.
我的理解是:当我们调用CreateFile并且希望创建一个文件的时候,系统会首先发送一个标志为FILE_OPEN的请求,并且判断底层文件系统的返回值,如果返回成功,则表明文件存在并且已经成功打开,否则如果返回结果是NO SUCH FILE,则紧接着创建一个FILE_OPEN_IF请求,得以将文件创建,所以如果我们在Create的Options当中发现了 FILE_CREATE,FILE_OPEN_IF和FILE_OVERWRITE_IF三个标志,则表明一定是在创建而不是打开.
代码如下:
CreateDisposition = (irpSp->Parameters.Create.Options>> 24) & 0x000000ff;
if(CreateDisposition==FILE_CREATE||CreateDisposition==FILE_OPEN_IF
||CreateDisposition==FILE_OVERWRITE_IF)
{
DbgPrint(\”It is a CREATE FILE operation\\n\”);
Irp->IoStatus.Status = STATUS_ACCESS_DENIED;
Irp->IoStatus.Information = 0;
status = Irp->IoStatus.Status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
(6)怎么自己构建一个IRP请求(从你的当前driver层, 往下传, 并使用回调,这样不会出现重入的问题)
代码如下:
BOOLEAN
MyGetFullPathNameByIRP(
PDEVICE_OBJECT DeviceObject,
PFILE_OBJECT FileObject,
FILE_INFORMATION_CLASS FileInformationClass,
PVOID FileQueryBuffer,
ULONG FileQueryBufferLength
)
{
PIRP irp;
KEVENT event;
IO_STATUS_BLOCK IoStatusBlock;
PIO_STACK_LOCATION ioStackLocation;
KdPrint((“Getting file name for %x\n”, FileObject));
//
// Initialize the event
//
KeInitializeEvent(&event, SynchronizationEvent, FALSE);
//
// Allocate an irp for this request. This could also come from a
// private pool, for instance.
//
irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);
if(!irp) {
//
// Failure!
//
return FALSE;
}
//
// Build the IRP’s main body
//
irp->AssociatedIrp.SystemBuffer = FileQueryBuffer;
irp->UserEvent = &event;
irp->UserIosb = &IoStatusBlock;
irp->Tail.Overlay.Thread = PsGetCurrentThread();
irp->Tail.Overlay.OriginalFileObject = FileObject;
irp->RequestorMode = KernelMode;
irp->Flags = 0;
//
// Set up the I/O stack location.
//
ioStackLocation = IoGetNextIrpStackLocation(irp);
ioStackLocation->MajorFunction = IRP_MJ_QUERY_INFORMATION;
ioStackLocation->DeviceObject = DeviceObject;
ioStackLocation->FileObject = FileObject;
ioStackLocation->Parameters.QueryFile.Length = FileQueryBufferLength;
ioStackLocation->Parameters.QueryFile.FileInformationClass = FileInformationClass;
//
// Set the completion routine.
//
IoSetCompletionRoutine(irp, MyQueryFileComplete, 0, TRUE, TRUE, TRUE);
//
// Send it to the FSD
//
(void) IoCallDriver(DeviceObject, irp);
//
// Wait for the I/O
//
KeWaitForSingleObject(&event, Executive, KernelMode, TRUE, 0);
//
// Done! Note that since our completion routine frees the IRP we cannot
// touch the IRP now.
//
return NT_SUCCESS( IoStatusBlock.Status );
}
这里的回调函数MyQueryFileComplete实现如下:
MyQueryFileComplete(
PDEVICE_OBJECT DeviceObject,
PIRP Irp,
PVOID Context
)
{
//
// Copy the status information back into the “user” IOSB.
//
*Irp->UserIosb = Irp->IoStatus;
if( !NT_SUCCESS(Irp->IoStatus.Status) ) {
KdPrint((” ERROR ON IRP: %x\n”, Irp->IoStatus.Status ));
}
//
// Set the user event – wakes up the mainline code doing this.
//
KeSetEvent(Irp->UserEvent, 0, FALSE);
//
// Free the IRP now that we are done with it.
//
IoFreeIrp(Irp);
//
// We return STATUS_MORE_PROCESSING_REQUIRED because this “magic” return value
// tells the I/O Manager that additional processing will be done by this driver
// to the IRP – in fact, it might (as it is in this case) already BE done – and
// the IRP cannot be completed.
//
return STATUS_MORE_PROCESSING_REQUIRED;
}
(7)怎么构造自己的一个Map
在文件名的保存上,我们可能需要构造一个Map, Map构造和数据结构中的一样, 其基本思想是:
首先定义个Map桶长, 然后定义一个Hash的算法, 对Map中的元素对,提供一个全局数组进行管理(也可以是个链表,数组更方便), 当entry冲突的时候, 就在改entry上再挂一个链表, 链表的总长不超过前面定义的Map桶长.
这里有一个实现:更多Hash算法的实现访问
这里
//
// Hash table for keeping names around. This is necessary because
// at any time the name information in the fileobjects that we
// see can be deallocated and reused. If we want to print accurate
// names, we need to keep them around ourselves.
//
PHASH_ENTRY HashTable[NUMHASH];
//
// Reader/Writer lock to protect hash table.
//
ERESOURCE HashResource;
//
// Hash function. Basically chops the low few bits of the file object
//
#if defined(_IA64_)
#define HASHOBJECT(_fileobject) (((ULONG_PTR)_fileobject)>>5)%NUMHASH
#else
#define HASHOBJECT(_fileobject) (((ULONG)_fileobject)>>5)%NUMHASH
#endif
PHASH_ENTRY hashEntry, newEntry;
//
// Lookup the object in the hash table to see if a name
// has already been generated for it
//
KeEnterCriticalRegion();
ExAcquireResourceSharedLite( &HashResource, TRUE );
hashEntry = HashTable[ HASHOBJECT( fileObject ) ];
while( hashEntry && hashEntry->FileObject != fileObject ) {
hashEntry = hashEntry->Next;
}
//
// Did we find an entry?
//
if( hashEntry ) {
//
// Yes, so get the name from the entry.
//
strcpy( fullPathName, hashEntry->FullPathName );
ExReleaseResourceLite( &HashResource );
KeLeaveCriticalRegion();
return;
}
ExReleaseResourceLite( &HashResource );
KeLeaveCriticalRegion();
注,这里使用的是CriticalRegion,这个是微软进行线程同步的一种手段, 一般的, 我们还可以使用mutex来实现:
#define MUTEX_INIT(v) KeInitializeMutex( &v, 0 )
#define MUTEX_WAIT(v) KeWaitForMutexObject( &v, Executive, KernelMode, FALSE, NULL )
#define MUTEX_RELEASE(v) KeReleaseMutex( &v, FALSE )
调用的时候, 就是,整个程序初时化的时候,进行init;
程序中使用的时候:
MUTEX_WAIT( HashMutex );
……
MUTEX_RELEASE( HashMutex );
4. 关于INF文件的书写
INF是用来安装我们的驱动的, 最simple的方法是从DDK的src中拷贝一个出来, 然后更改成我们的驱动所需要的INF文件!
INF文件中, 有个StartType值, 它的意义是何时启动驱动程序。可为:
SERVICE_BOOT_START (0x0)
由操作系统 loader 启动。使用此值仅用于操作系统基本服务。
SERVICE_SYSTEM_START (0x1)
操作系统初始化式启动。
SERVICE_AUTO_START (0x2)
SCM 在系统启动期间启动
SERVICE_DEMAND_START (0x3)
SCM 根据需要启动
SERVICE_DISABLED (0x4)
此服务不可被启动
有了INF之后, 有这几个命令比较有用:
sc delete //在command下删除一个driver服务
net start //在command下启动一个driver服务
net stop //command下关闭一个driver服务
另外如果是minifile, 如果要手动启动一个driver服务, 需要在命令提示行中使用 “fltmc load 你的驱动名字” 才能真正载入minifile驱动。其他两个值的话,系统启动时会自动加载,
当然也可以使用 fltmc load 来加载了。相应的卸载命令是 “fltmc unload 你的驱动名字”。