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