文件系統的過濾與監控
11.3 設備的綁定前期工作
11.3.1 動態地選擇綁定函數
sfilter 有一個有趣的地方是,使用了動態加載的方法來使用內核函數。只有高版本的Windows系統上纔有IoAttachDeviceToDeviceStackSafe 這個函數。如果我們直接使用這個函數,那麼在低版本的Windows系統上就會直接加載失敗。因此,使用動態加載此類函數的好處就是,即使在低版本的Windows上也能成功加載。
在動態加載該函數時,如果失敗則會使指針爲NULL,而此時即使爲NULL,我們也可以檢測替換爲函數IoAttachDeviceToDeviceStack,這樣驅動就可以兼容不同版本的Windows了。
使用MmGetSystemRoutineAddress 可以動態地尋找一個內核函數的指針。該API函數原型如下:
NTKERNELAPI
PVOID MmGetSystemRoutineAddress(IN PUNICODE_STRING SystemRoutineName);
**SystemRoutineName **是一個字符串,是要動態尋找地址的內核函數的名字。
下面代碼中的“#if WINVER>=0X0501”,類似的代碼在本書後面的代碼中以讓常常看到,這裏是編譯時的判斷指令。當編譯結果期望適用的Windows版本高於0x0501時,使用動態加載函數的方式;當編譯的結果期望適用於的Windows版本比這個低的似乎後,沒有必要嘗試動態加載高級版本的函數。
NTSTATUS SfAttachDeviceToDeviceAttack(
IN PDEVICE_OBJECT SourceDevice,
IN PDEVICE_OBJECT TargetDevice
)
{
PAGED_CODE();
//並不是當Windows版本高於該版本時候就運行一下代碼
//應該理解爲;當編譯的期望目標操作系統版本高於0x0501時
//編譯以下代碼,反之不編譯
#if WINVER >= 0x0501
if (IS_WINDOWSXP_OR_LATER())
{
ASSERT( NULL != gSfDynamicFunctions.AttachDeviceToDeviceStackSafe );
return (gSfDynamicFunctions.AttachDeviceToDeviceStackSafe)
(
SourceDevice,
TargetDevice,
AttachedToDeviceObject );
} else {
// ASSERT( NULL == gSfDynamicFunctions.AttachDeviceToDeviceStackSafe );
#endif
*AttachedToDeviceObject = TargetDevice;
*AttachedToDeviceObject = IoAttachDeviceToDeviceStack( SourceDevice,
TargetDevice );
if (*AttachedToDeviceObject == NULL) {
return STATUS_NO_SUCH_DEVICE;
}
return STATUS_SUCCESS;
#if WINVER >= 0x0501
}
#endif
}
不知道從現在的操作系統驅動編程中,還需不需要對這個綁定函數進行動態加載了。不過可能有其他的函數需要動態加載吧。
11.3.2 註冊文件系統變動回調
根據前面的知識,我們已經知道過濾對文件系統卷的請求方法,就是將他綁定。雖然這些設備卷都沒有設備名,但是要找到這些對象也很簡單。
如果不想處理動態的文件系統卷,完全可以這樣簡單的綁定。但是sfilter的要求更高,他要求當一個U盤插入USB扣,一個“J:” 之類的捲動態生成時,sfilter依然可以捕獲這個時間,並生成一個過濾設備來綁定它。
一個新的存儲媒介被系統發現並在文件系統內生成一個卷的過程稱爲掛載(Mount),與之相反的過程稱爲解掛載(Diskmount)。該過程開始時,文件系統的控制設備將得到一個IRP,其主功能好爲IRP_MJ_FILE_SYSTEM_CONTROL ,次功能號爲 IRP_MN_MOUNT。換一句話說,如果過濾驅動中已經生成了一個設備綁定文件系統的CDO,那麼程序中就可以得到這樣的一個IRP:在其中若知道一個新的卷正在解掛載,這時候程序就可以執行上面所說的操作了。
那麼現在的問題就是如何知道系統中有哪些文件系統,還有就是應該在什麼時候綁定它們的控制設備。
IoRegisterFsRegistrationChange 是一個非常有用的系統調用。該調用註冊一個回調函數,當系統中有任何文件系統被激活或者註銷時,註冊過的回調函數就會被調用。這種回調函數稱爲文件系統的變動回調。
文件系統的激活和掛載是兩回事。所謂文件系統的激活,是指當系統中沒有任何卷採用了NTFS文件系統時,Window並沒有加載NTFS文件系統驅動,此時可以稱爲NTFS未激活;當一個新的使用了NTFS的的卷被加載到系統時候,則NTFS就被加載了,此時就可以說NTFS被激活。第二次加載新的NTFS的卷時,就和文件系統的激活沒什麼關係了,因爲對應的文件系統已經被激活在系統中了。與文件系統的激活相反的過程則稱爲文件系統的卸載。
IoRegisterFsRegistrationChange 註冊的回調函數,只有文件系統被激活或者註銷纔會回調,和新加捲或者拔出卷沒有直接關係。
下面看sfilter的DriverEqntry中對這個函數的調用,代碼如下:
// SfFsNotification 是一個回調函數,這個函數有固定的原型
status = IoRegisterFsRegistrationChange( DriverObject, SfFsNotification );
if (!NT_SUCCESS( status )) {
//如果失敗了,前面分匹配的FastIo函數就沒用了,釋放掉
KdPrint(( "SFilter!DriverEntry: Error registering FS change notification, status=%08x\n", status ));
DriverObject->FastIoDispatch = NULL;
ExFreePool( fastIoDispatch );
//前面生成的控制設備也一併刪除
IoDeleteDevice( gSFilterControlDeviceObject );
return status;
}
11.3.3 文件系統變動回調的一個實現
有必要爲此寫一個回調函數 SfFsNotification 。請注意這個回調函數的原型必須符合下面的原型。
第一個參數是一個設備對象指針,這個設備對象就是文件系統的控制設備(請注意文件系統的控制設備可能已經被別的文件過濾驅動綁定,此時,這個設備的對象指針總是指向當前設備棧頂的那個設備)。第二個參數是一個BOOLEAN值,如果爲TRUE則表示文件系統的激活,反之則表示文件系統的卸載。
VOID sfFsNotification(
IN PDEVICE_OBJECT DeviceObject,
IN BOOLEAN FsActive
)
{
UNICODE_STRING name;
WCHAR nameBuffer[MAX_DEVNAME_LENGTH];
PAGED_CODE();
//下面的獲取設備和SF_LOG_PRINT沒有什麼實際意義主要是爲了獲取一些log,讓我們知道綁定了哪些設備
RtlInitEmptyUnicodeString( &name, nameBuffer, sizeof(nameBuffer) );
SfGetObjectName( DeviceObject, &name );
SF_LOG_PRINT( SFDEBUG_DISPLAY_ATTACHMENT_NAMES,
("SFilter!SfFsNotification: %s %p \"%wZ\" (%s)\n",
(FsActive) ? "Activating file system " : "Deactivating file system",
DeviceObject,
&name,
GET_DEVICE_TYPE_NAME(DeviceObject->DeviceType)) );
//如果文件系統被激活,那麼綁定文件系統的控制設備
if (FsActive) {
SfAttachToFileSystemDevice( DeviceObject, &name );
} else {
//反之則解除綁定 綁定函數和接觸綁定函數都是後面給出的。
SfDetachFromFileSystemDevice( DeviceObject );
}
}
這裏涉及一些關於驅動加載方式的問題。驅動的動態加載是按照第一章介紹的加載驅動的辦法,我們用工具或者輸入命令行命令手動讓驅動啓動。
驅動的靜態加載則一般是用inf文件安裝驅動,並設置啓動模式爲Windows啓動時自啓動。
IoRegisterFsRegistrationChange可以註冊對激活文件系統的回調,但是對調用這個函數時候早已經激活的文件系統,回調是否被調用呢?早期不會,而在Windows 2000 以後則會。這裏爲了避免麻煩,不再學習早期的東西。
回顧一下我們在DriverEntry中實現的東西
第一步: 生成一個控制設備,當然此前必須給控制設備指定名稱。
第二步:設置普通分發函數。
第三步:設置快速IO分發函數。
第四步:編寫一個文件系統變通回調函數,在其中綁定剛激活的文件系統的控制設備。
第五步: 使用 IoRegisterFsRegistrationChange 進行註冊。
那麼應該如何綁定一個文件系統的控制設備呢,也就是如何實現函數SfAttachToFileSystemDevice呢?在後面進行描述。
11.3.4 文件系統識別器
上一節最好的一個問題是如何綁定一個剛剛被激活的文件系統控制設備。前面實現過SfAttachDeviceToStack 用來綁定這個設備,但是並不是每次文件系統變動回調發現有新的文件系統激活就直接綁定它。
首先需要判斷這個文件系統是否我是我們關心的。過濾驅動可能只對文件系統的CDO的設備類型中的某些感興趣。假設我們只關心磁盤文件系統、光盤文件系統和網絡文件系統,那麼就只需要注意這三種類型即可。下面的代碼定義了一個宏來判斷一個設備的類型是否是本驅動所關心的。
#define IS_DESIRED_DEVICE_TYPE(_type) (((_type==)FILE_DEVICE_DISK_FILE_SYSTEM)||(_type==FILE_DEVICE_CD_ROM_FILE_SYSTEM)||(_type==FILE_DEVICE_NETWORK_FILE_SYSTEM))
下一個問題是必須跳過文件系統識別器。所謂文件系統識別器,是文件系統驅動的一個很小的替身。爲了避免沒有使用到的文件系統驅動佔據內核內存,Windwos系統不加載這些大驅動,而以該文件系統驅動對應的文件系統識別器來代替。當新的物理存儲媒介進入系統後,IO管理器會依次嘗試各種文件系統對它進行識別,若識別成功,則立即加載真正的文件系統驅動,對應的文件系統識別器則被卸載掉。文件系統識別器的控制設備看起來就像一個文件系統的控制設備,綁定它的話可能會出錯,所以跳過它是最好的選擇。雖然有的時候我們又必須選擇綁定它。因此,我們需要學會判斷它。
通過驅動的名字可以分辨出Windows 的標準文件系統識別器,Windows的標準文件系統識別器似乎都是由驅動“FileSystem\Fs_Rec” 生成的,所以直接判斷驅動的名字就可以解決問題的一部分。下面的代碼完成這個判斷,如果識別爲文件系統識別器則放棄綁定。
RtlInitUnicodeString(&fsrecName,L"FileSystem\Fs_Rec");
SfGetObjectNmae(DeviceObject->DriverObject,&fsName);
if(RtlCompareUnicodeString(&fsName,fsrecName,TRUE)==0)
{
return STATUS_SUCCESS;
}
上面這段代碼可以放在文件系統變動回調函數中,也可以放在綁定文件系統控制設備的函數中。但是如果有一些非微軟規定的文件系統識別器,沒有生成在FileSystem\Fs_Rec 下,則該方法不可取。
明日計劃
文件系統控制設備的綁定