文件过滤驱动

  • Post author:
  • Post category:其他



由于项目需要,这十天做了个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 你的驱动名字”。