?
文件過濾系統驅動開發
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 ?
的判斷是為了辨別對攔截到的進程間的兩種通信方式:命名管道與郵槽的處理。