S.l.e!ep.¢%

像打了激速一样,以四倍的速度运转,开心的工作
简单、开放、平等的公司文化;尊重个性、自由与个人价值;
posts - 1098, comments - 335, trackbacks - 0, articles - 1
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

  文件过滤系统驱动开发 Filemon 学习笔记

                            

 

WINDOWS 文件过滤系统驱动开发,可用于硬盘还原,防病毒,文件安全防护,文件加密等诸多领域。而掌握核心层的理论及实践,对于成为一名优秀的开发人员不可或缺。

 

WINDOWS 文件过滤系统驱动开发的两个经典例子, Filemon SFilter ,初学者在经过一定的理论积累后,对此两个例子代码的研究分析,会是步入驱动开发殿堂的重要一步,相信一定的理论积累以及贯穿剖析理解此两个例程后,就有能力开始进行文件过滤系统驱动开发的实际工作了。

对于 SFilter 例子的讲解,楚狂人的教程已经比较流行,而 Filemon 例子也许因框架结构相对明晰,易于剖析理解,无人贴出教程,本人在剖析 Filemon 的过程中积累的一些笔记资料,陆续贴出希望对初学者有所帮助,并通过和大家的交流而互相提高。

 

                     Filemon 学习笔记 第一篇:

 

 

Filemon 的大致架构为,在此驱动程序中,创建了两类设备对象。

一类设备对象用于和 Filemon 对应的 exe 程序通信,以接收用户输入信息,比如挂接或监控哪个分区,是否要挂接,是否要监控,监控何种操作等。此设备对象只创建了一个,在驱动程序的入口函数 DriverEntry 中。此类设备对象一般称为控制设备对象,并有名字,以方便应用层与其通信操作。

第二类设备对象用于挂接到所须监控的分区,比如 c :, d :或 e :, f :,以便拦截到引应用层对该分区所执行的读,写等操作。此类设备对象为安全起见,一般不予命名,可根据须监控多少分区而创建一个或多个。

 

驱动入口函数大致如下

 

NTSTATUS

DriverEntry(

    IN PDRIVER_OBJECT DriverObject,

    IN PUNICODE_STRING RegistryPath

    )

{

    NTSTATUS                ntStatus;

    PDEVICE_OBJECT          guiDevice;

    WCHAR                   deviceNameBuffer[]  = L"\\Device\\Filemon";

    UNICODE_STRING          deviceNameUnicodeString;

    WCHAR                   deviceLinkBuffer[]  = L"\\DosDevices\\Filemon";

    UNICODE_STRING          deviceLinkUnicodeString;

    ULONG                   i;

 

    DbgPrint (("Filemon.SYS: entering DriverEntry\n"));

    FilemonDriver = DriverObject;

 

    //   

    // Setup the device name

    //   

    RtlInitUnicodeString (&deviceNameUnicodeString,

                          deviceNameBuffer );

 

    //

    // Create the device used for GUI communications

    // 此设备对象用来和用户交互信息

    ntStatus = IoCreateDevice ( DriverObject,

                                sizeof(HOOK_EXTENSION),

                                &deviceNameUnicodeString,

                                FILE_DEVICE_FILEMON,

                                0,

                                TRUE,

                                &guiDevice );

 

    //

    // If successful, make a symbolic link that allows for the device

    // object's access from Win32 programs

    //

    if(NT_SUCCESS(ntStatus)) {

 

        //

        // Mark this as our GUI device

        //

        ((PHOOK_EXTENSION) guiDevice->DeviceExtension)->Type = GUIINTERFACE;

 

        //

        // Create a symbolic link that the GUI can specify to gain access

        // to this driver/device

        //

        RtlInitUnicodeString (&deviceLinkUnicodeString,

                              deviceLinkBuffer );

        ntStatus = IoCreateSymbolicLink (&deviceLinkUnicodeString,

                                         &deviceNameUnicodeString );

        if(!NT_SUCCESS(ntStatus)) {

 

            DbgPrint (("Filemon.SYS: IoCreateSymbolicLink failed\n"));

            IoDeleteDevice( guiDevice );

            return ntStatus;           

        }

 

        //

        // Create dispatch points for all routines that must be handled.

        // All entry points are registered since we might filter a

        // file system that processes all of them.

        //

        for( i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++ ) {

 

            DriverObject->MajorFunction[i] = FilemonDispatch;

        }

#if DBG       

        //

        // Driver unload is only set if we are debugging Filemon. This is

        // because unloading a filter is not really safe - threads could

        // be in our fastio routines (or about to enter them), for example,

        // and there is no way to tell. When debugging, we can risk the

        // occasional unload crash as a trade-off for not having to

        // reboot as often.

        //

        // DriverObject->DriverUnload = FilemonUnload;

#endif // DBG

 

        //

        // Set up the Fast I/O dispatch table

        //

        DriverObject->FastIoDispatch = &FastIOHook;

 

    } else {

 

        //

        // If something went wrong, cleanup the device object and don't load

        //

        DbgPrint(("Filemon: Failed to create our device!\n"));

        return ntStatus;

    }

 

    //

    // Initialize the name hash table

    //

    for(i = 0; i < NUMHASH; i++ ) HashTable[i] = NULL;

 

    //

    // Find the process name offset

    //

    ProcessNameOffset = FilemonGetProcessNameOffset();// 为了得到当前进程名字

 

    //

    // Initialize the synchronization objects

    //

#if DBG

    KeInitializeSpinLock( &CountMutex );

#endif

    ExInitializeFastMutex( &LogMutex );

    ExInitializeResourceLite( &FilterResource );

    ExInitializeResourceLite( &HashResource );

 

    //

    // Initialize a lookaside for file names

    //

    ExInitializeNPagedLookasideList( &FullPathLookaside, NULL, NULL,

                                 0, MAXPATHLEN, 'mliF', 256 );

 

    //

    // Allocate the first output buffer

    //

    CurrentLog = ExAllocatePool( NonPagedPool, sizeof(*CurrentLog) );

    if( !CurrentLog ) {

 

        //

        // Oops - we can't do anything without at least one buffer

        //

        IoDeleteSymbolicLink( &deviceLinkUnicodeString );

        IoDeleteDevice( guiDevice );

        return STATUS_INSUFFICIENT_RESOURCES;

    }

 

    //

    // Set the buffer pointer to the start of the buffer just allocated

    //

    CurrentLog->Len  = 0;

    CurrentLog->Next = NULL;

    NumLog = 1;

 

    return STATUS_SUCCESS;

}

 

在此驱动入口点函数中,主要做了生成新的设备对象,此设备对象用来和应用层信息交互,比如应用层向驱动传递需要挂接或者监控的分区盘符,或者是否挂接盘符,是否监控操作等。

上面创建设备对象的代码为:

ntStatus = IoCreateDevice ( DriverObject,

                                sizeof(HOOK_EXTENSION),

                                &deviceNameUnicodeString,

                                FILE_DEVICE_FILEMON,

                                0,

                                TRUE,

                                &guiDevice );

 

    //

    // If successful, make a symbolic link that allows for the device

    // object's access from Win32 programs

    //

    if(NT_SUCCESS(ntStatus)) {

 

        //

        // Mark this as our GUI device

        //

        ((PHOOK_EXTENSION) guiDevice->DeviceExtension)->Type = GUIINTERFACE;

 

        //

        // Create a symbolic link that the GUI can specify to gain access

        // to this driver/device

        //

        RtlInitUnicodeString (&deviceLinkUnicodeString,

                              deviceLinkBuffer );

        ntStatus = IoCreateSymbolicLink (&deviceLinkUnicodeString,

                                         &deviceNameUnicodeString );

        if(!NT_SUCCESS(ntStatus)) {

 

            DbgPrint (("Filemon.SYS: IoCreateSymbolicLink failed\n"));

            IoDeleteDevice( guiDevice );

            return ntStatus;            

        }

上面代码完成的功能为创建了用于与应用层交互的控制设备对象,名字在参数 &deviceNameUnicodeString, 中。设备对象创建成功后又调用 IoCreateSymbolicLink 创建了一个符号连接,以便于应用层交互。

 

在入口点函数 DriverEntry 代码中,还有一处代码:

ProcessNameOffset = FilemonGetProcessNameOffset();// 为了得到当前进程名字。

此函数体如下:

ULONG

FilemonGetProcessNameOffset(

    VOID

    )

{

    PEPROCESS       curproc;

    int             i;

 

    curproc = PsGetCurrentProcess();// 调用 PsGetCurrentProcess 取得 KPEB 基址

 

    // 然后搜索 KPEB ,得到 ProcessName 相对 KPEB 的偏移量

    // Scan for 12KB, hoping the KPEB never grows that big!

    //

    for( i = 0; i < 3*PAGE_SIZE; i++ ) {

    

        if( !strncmp( SYSNAME, (PCHAR) curproc + i, strlen(SYSNAME) )) {

 

            return i;

        }

    }

 

    //

    // Name not found - oh, well

    //

    return 0;

 

这个函数通过查找 KPEB (Kernel Process Environment Block) ,取得进程名, GetProcessNameOffset 主要是调用 PsGetCurrentProcess 取得 KPEB 基址,然后搜索 KPEB ,得到 ProcessName 相对 KPEB 的偏移量,存放在全局变量 ProcessNameOffset 中,得到此偏移量的作用是:无论当前进程为哪个,其名字在 KPEB 中的偏移量不变,所以都可以通过此偏移量得到。而在入口点函数 DriverEntry 执行时,当前进程必为系统进程,所以在此函数中方便地根据系统进程名 SYSNAME #define SYSNAME    "System" )得到此偏移量。

 

分发函数剖析:

在入口点函数中,通过代码:

for( i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++ ) {

 

            DriverObject->MajorFunction[i] = FilemonDispatch;

        }

简单地把各个分发例程设置成了 FilemonDispatch; 然后我们追踪其函数体:

 

NTSTATUS

FilemonDispatch(

    IN PDEVICE_OBJECT DeviceObject,

    IN PIRP Irp

    )

{

    //

    // Determine if its a request from the GUI to us, or one that is

    // directed at a file system driver that we've hooked

    //

    if( ((PHOOK_EXTENSION) DeviceObject->DeviceExtension)->Type == GUIINTERFACE ) {

 

        return FilemonDeviceRoutine( DeviceObject, Irp );

 

    } else {

 

        return FilemonHookRoutine( DeviceObject, Irp );

    }

}

 

函数体先判断需要处理 IRP 包的设备对象的类型,看是属于控制设备对象,还是属于用于挂接并监控文件读写操作的过滤设备对象。如果是属于后者 则进入: FilemonHookRoutine( DeviceObject, Irp )

此函数是拦截文件操作的中心,在其中获得了被操作的文件名字,并且根据操作类型,在

switch( currentIrpStack->MajorFunction ) {

}

中针对不同的 MajorFunction ,打印出相关操作信息。

因此函数体太长 不再全部列出。

其函数体总体框架为:得到被操作的文件名字,打印相关操作信息,然后下发 IRP 到底层驱动。

在下发 IRP 到底层驱动处理前,本层驱动必须负责设置下层 IO 堆栈的内容。这样下一层驱动调用 IoGetCurrentIrpStackLocation() 时能得到相应的数据。

设置下层 IO 堆栈的内容,一般用两个函数来实现:

IoCopyCurrentIrpStackLocationToNext( Irp )

此函数一般用在本驱动设置了完成例程时调用,把本层 IO _STACK_LOCATION 中的参数 copy 到下层,但与完成例程相关的参数信息例外。因为本驱动设置的完成例程只对本层驱动有效。

IoSkipCurrentIrpStackLocationToNext(Irp)

此函数的作用是:直接把本层驱动 IO 堆栈的内容设置为下层驱动 IO 堆栈指针的指向。因两层驱动 IO 堆栈的内容完全一致,省却 copy 过程。

 

而在 Filemon 的处理中,它用了一个特别的办法,没有调用此两个函数, FilemonHookRoutine 函数体里面有三句代码:

 

    PIO_STACK_LOCATION  currentIrpStack = IoGetCurrentIrpStackLocation(Irp);

PIO_STACK_LOCATION  nextIrpStack    = IoGetNextIrpStackLocation(Irp);

 

*nextIrpStack = *currentIrpStack;// 此步设置了下层驱动的 IO_STACK_LOCATION

直接设置了下层驱动 IO 堆栈的值。

 

FilemonHookRoutine 函数里,用一个宏实现了复杂的获得拦截到的被操作文件的名字:

 

if( FilterOn && hookExt->Hooked ) {

 

        GETPATHNAME( createPath );

}

 

GETPATHNAME( createPath ) 宏展开为:

 

#define GETPATHNAME(_IsCreate)                                                  \

        fullPathName = ExAllocateFromNPagedLookasideList( &FullPathLookaside ); \

        if( fullPathName ) {                                                    \

            FilemonGetFullPath( _IsCreate, FileObject, hookExt, fullPathName ); \

        } else {                                                                \

            fullPathName = InsufficientResources;                               \

        }     

 

在函数: FilemonGetFullPath( _IsCreate, FileObject, hookExt, fullPathName ) 中实现了获得被操作的文件名字,此函数代码较多,判断条件复杂,理解起来比较麻烦,下面重点讲解。

对函数 FilemonGetFullPath 的理解关键在于理顺结构,

此函数的功能就是获得文件名字,获得文件名字一般在三种状态下:

一:在打开文件请求中,但在打开文件前。

二:在打开文件请求中,但在打开文件后,通过在本层驱动中设置完成例程。在完成例程中获得。

三:在过滤到读写等操作时。

而在此函数中,它包含了第一种和第三种方法,通过一些烦琐的条件判断,先判断出目前是处于什么状态中,然后根据不同状态采取不同方法。

先分析当在第一种条件下,此函数的处理方法,可以精炼为如下:

 

 

VOID

FilemonGetFullPath(

    BOOLEAN createPath,

    PFILE_OBJECT fileObject,

    PHOOK_EXTENSION hookExt,

    PCHAR fullPathName

    )

{

       ULONG               pathLen, prefixLen, slashes;

    PCHAR               pathOffset, ptr;

    BOOLEAN             gotPath;

    PFILE_OBJECT        relatedFileObject;

   

    ANSI_STRING         fileName;

    ANSI_STRING         relatedName;

   

    UNICODE_STRING      fullUniName;

 

 

       prefixLen = 2; // "C:"

 

       if( !fileObject ) {

 

        sprintf( fullPathName, "%C:", hookExt->LogicalDrive );

        return;

    }

 

      

    //

    // Initialize variables

    //

    fileName.Buffer = NULL;

    relatedName.Buffer = NULL;

    gotPath = FALSE;

 

       if( !fileObject->FileName.Buffer)

       {

              sprintf( fullPathName, "%C:", hookExt->LogicalDrive);

              return;

       }else

              DbgPrint("fileOjec->FileName:%s",fileObject->FileName);

 

        if( !NT_SUCCESS( RtlUnicodeStringToAnsiString( &fileName, &fileObject->FileName, TRUE ))) {

 

            sprintf( fullPathName, "%C: <Out of Memory>", hookExt->LogicalDrive );

            return;

        }

 

        

        pathLen = fileName.Length + prefixLen;

        relatedFileObject = fileObject->RelatedFileObject;

 

                //

        // Only look at related file object if this is a relative name

        //

        if( fileObject->FileName.Buffer[0] != L'\\' &&

            relatedFileObject && relatedFileObject->FileName.Length ) {

                     DbgPrint("relatedFileObject filename : %s",relatedFileObject->FileName);

              

                     if( !NT_SUCCESS( RtlUnicodeStringToAnsiString( &relatedName, &relatedFileObject->FileName, TRUE ))) {

 

              

                sprintf( fullPathName, "%C: <Out of Memory>", hookExt->LogicalDrive );

                RtlFreeAnsiString( &fileName );

                return;

            }

            pathLen += relatedName.Length+1;

        }

 

              if( fileObject->DeviceObject->DeviceType != FILE_DEVICE_NETWORK_FILE_SYSTEM ) {

 

            sprintf( fullPathName, "%C:", hookExt->LogicalDrive );

        }

 

               if( pathLen >= MAXPATHLEN ) {

           

            strcat( fullPathName, " <Name Too Long>" );

 

        } else {

   

            //

            // Now we can build the path name

            //

            fullPathName[ pathLen ] = 0;

           

            pathOffset = fullPathName + pathLen - fileName.Length;

            memcpy( pathOffset, fileName.Buffer, fileName.Length + 1 );

   

            if( fileObject->FileName.Buffer[0] != L'\\' &&

                relatedFileObject && relatedFileObject->FileName.Length ) {

 

                //

                // Copy the component, adding a slash separator

                //

                *(pathOffset - 1) = '\\';

                pathOffset -= relatedName.Length + 1;

                   

                memcpy( pathOffset, relatedName.Buffer, relatedName.Length );

 

                //

                // If we've got to slashes at the front zap one

                //

                if( pathLen > 3 && fullPathName[2] == '\\' && fullPathName[3] == '\\' )  {

                   

                    strcpy( fullPathName + 2, fullPathName + 3 );

                }

            }

        } 

      

 

}

上面的精简后的函数代码为只考虑目前处于第一种情况,即打开文件请求中,但文件尚未打开时。

在此时,文件的名字信息存储在文件对象 fileObject->FileName, fileObject->RelatedFileObject->FileName, FileObject->FileName RelatedObject 的相对路径, 通过对两者的解析组合出文件名字。

 

 

而在 FilemonGetFullPath 函数体中的另一段代码:

 

 

FilemonGetFullPath

{

…………………..

…………………..

…………………..

if( !gotPath && !createPath ) {

 

       

        fileNameInfo = (PFILE_NAME_INFORMATION) ExAllocatePool( NonPagedPool,

                                                                MAXPATHLEN*sizeof(WCHAR) );

 

        if( fileNameInfo &&

            FilemonQueryFile(hookExt->FileSystem, fileObject, FileNameInformation,

                             fileNameInfo, (MAXPATHLEN - prefixLen - 1)*sizeof(WCHAR) )) {

 

            fullUniName.Length = (SHORT) fileNameInfo->FileNameLength;

            fullUniName.Buffer = fileNameInfo->FileName;

            if( NT_SUCCESS( RtlUnicodeStringToAnsiString( &fileName, &fullUniName, TRUE ))) {

 

                fullPathName[ fileName.Length + prefixLen ] = 0;

 

                if( hookExt->Type == NPFS ) {

                   

                    strcpy( fullPathName, NAMED_PIPE_PREFIX );

 

                } else if( hookExt->Type == MSFS ) {

 

                    strcpy( fullPathName, MAIL_SLOT_PREFIX );

 

                } else if( fileObject->DeviceObject->DeviceType != FILE_DEVICE_NETWORK_FILE_SYSTEM ) {

 

                    sprintf( fullPathName, "%C:", hookExt->LogicalDrive );

 

                } else {

               

                    //

                    // No prefix for network devices

                    //

                }

 

                memcpy( &fullPathName[prefixLen], fileName.Buffer, fileName.Length );

                gotPath = TRUE;

                RtlFreeAnsiString( &fileName );

                fileName.Buffer = NULL;

            }

        }

        if( fileNameInfo ) ExFreePool( fileNameInfo );

}

………………….

…………………..

………………………

}

 

上面这段代码是处理另外一种情况,即是在其他读写请求中,自己根据拦截到的 fileObject 构建 IRP ,下发到底层,以此来查询文件名信息。整个过程还是易于理解的。

 

在理清这两种脉络后,再剖析此整个函数,就很容易理解整个函数代码了。

代码中对 MajorFunction == IRP_MJ_CREATE_NAMED_PIPE

         MajorFunction == IRP_MJ_CREATE_MAILSLOT   的判断是为了辨别对拦截到的进程间的两种通信方式:命名管道与邮槽的处理。


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理