VC文件过滤系统驱动开发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"DeviceFilemon";
  UNICODE_STRING     deviceNameUnicodeString;
  WCHAR          deviceLinkBuffer[] = L"DosDevicesFilemon";
  UNICODE_STRING     deviceLinkUnicodeString;
  ULONG          i;
  DbgPrint (("Filemon.SYS: entering DriverEntry
"));
  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
"));
      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!
"));
    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
"));
      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")得到此偏移量。此函数是拦截文件操作的中心,在其中获得了被操作的文件名字,并且根据操作类型,在

  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 的判断是为了辨别对拦截到的进程间的两种通信方式:命名管道与邮槽的处理。

  分发函数剖析:

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

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 )

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值