Windows文件过滤驱动开发

来源:互联网 发布:python源代码分享 编辑:程序博客网 时间:2024/04/29 07:23

原文地址:http://blog.sina.com.cn/s/blog_7785041f0101jekb.html


Windows文件过滤驱动开发

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

附录:
理解常见的IRP消息意义! 这里
如果写键盘驱动,可以参考这里
0 0