程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> VC文件過濾系統驅動開發Filemon學習筆記

VC文件過濾系統驅動開發Filemon學習筆記

編輯:關於VC++

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 的判斷 是為了辨別對攔截到的進程間的兩種通信方式:命名管道與郵槽的處理。

周末來 了,祝大家周末愉快。余下的整理後再帖,希望和大家多交流。

連城碧 QQ 276265852 MSN:[email protected]

下載filemon源代碼:http://www.vckbase.com/code/downcode.asp?id=3014

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved