Windows 文件過濾驅動經驗總結

by ai3000
看了 ChuKuangRen 的第二版《文件過濾驅動開發教程》後,頗有感觸。我想,交流都是
建立在平等的基礎上,在抱怨氛圍和環境不好的同時應該先想一想自己究竟付出了多少?
只知索取不願付出的人也就不用抱怨了,要怪也只能怪自己。發自己心得的人無非是兩種
目的,一是引發一些討論,好糾正自己錯誤的認識,以便從中獲取更多的知識使自己進步
的更快。二是做一份備忘,當自己遺忘的時候能夠馬上找到相關資料。我這裏也總結了近
幾年做文件過濾驅動時所積累下來的一些小小經驗,這分筆記也是看了 ChuKuangRen 的
教程後,臨時想到的一小部分而已,是想到哪寫到哪,不是很全,如果以後再回想起什麼
也會不斷補充。因其工作原因,近段時間在 SOLARIS 驅動與 Linux 內核方面投入的精
力比較多,Windows 下的文件過濾驅動一直也沒有怎麼去碰,所以最後還是那句老話
FIXME。



1、獲得文件全路徑以及判斷時機

除在所有 IRP_MJ_XXX 之前自己從頭創建 IRP 發送到下層設備查詢全路徑外,
不要嘗試在 IRP_MJ_CREATE 以外的地方獲得全路徑,因爲只有在 IRP_MJ_CREATE
中才會使用 ObCreateObject() 來建立一個有效的 FILE_OBJECT。而在 IRP_READ
IRP_WRITE 中它們是直接操作 FCB (File Control Block)的。


2、從頭建立 IRP 發送關注點

無論你建立什麼樣的 IRP,是 IRP_MJ_CREATE 也好還是 IRP_MJ_DIRECTORY_CONTROL
也罷,最要提醒的就是一些標誌。不同的標誌會代來不同的結果,有些結果是直接
返回失敗。這裏指的標誌不光是 IRP->Flags,還要考慮 IO_STACK_LOCATION->Flags
還有其它等等。尤其是你要達到一些特殊目的,這時候更需要注意,如 IRP_MN_QUERY_DIRECTORY,
不同的標誌結果有很大的不同。


3、從頭建立 IRP 獲取全路徑注意點

自己從頭建立一個 IRP_MJ_QUERY_INFORMATION 的 IRP 獲取全路徑時需要注意,
不僅在 IRP_MJ_CREATE 要做區別處理,在 IRP_MJ_CLOSE 也要做同樣的處理,
否則如果目標是 NTFS 文件系統的話可能產生 deadlock。如果是 NTFS 那麼在
IRP_MJ_CLEANUP 的時候也需要對 FO_STREAM_FILE 類型的文件做同樣處理。



4、獲得本地/遠程訪問用戶名(域名/SID)

方法只有在 IRP_MJ_CREATE 中才可用,那是因爲 IO_SECURITY_CONTEXT 只有
在 IO_STACK_LOCATION->Parameters.Create.SecurityContext 纔會有效。
這樣你纔有可能從 IO_SECURITY_CONTEXT->SecurityContext->AccessState->
SubjectSecurityContext.XXXToken 中獲得訪問 TOKEN,從而進一步得到用戶名
或 SID。記得 IFS 中有一個庫,它的 LIB 導出一個函數可以讓你在獲得以上
信息後得到用戶名與域名。但如果你想兼容 NT4 的話,只能自己分析來得出本
地和遠程的 SID。



5、文件與目錄的判斷

正確的方法在楚狂人的文檔裏已經說過了,再補充一句。如果你的文件過濾驅動
要兼容所有文件系統,那麼不要十分相信從 FileObject->FsContext 裏取得的數據。
正確的方法還是在你傳遞下去 IRP_MJ_CREATE 後從最下層文件系統延設備棧返回到
你這裏後再獲得。



6、加/解密中判斷點

只判斷 IRP_PAGING_IO,IRP_SYNCHRONOUS_PAGING_IO,IRP_NOCACHE 是
沒錯的。如果有問題,相信是自己的問題。關於有人提到在 FILE_OBJECT->Flags
中的 FO_NO_INTERMEDIATE_BUFFERING 是否需要判斷,對此問題的回答是隻要
你判斷了 IRP_NOCACHE 就不用再判斷 FILE_OBJECT 中的,因爲它最終會
設置 IRP->Flags 爲 IRP_NOCACHE。關於你看到的諸如 IRP_DEFER_IO_COMPLETION
等 IRP 不要去管它,因爲它只是一個過程。最終讀寫還是如上所介紹。至於
以上這些 IRP 哪個是由 CC MGR 發送的,哪些是由 I/O MGR 發送和在什麼
時候發送的,這個已經有很多討論了,相信可以找到。



7、舉例說明關於 IRP 傳遞與完成注意事項

只看 Walter Oney 的那本 《Programming the Microsoft Windows driver model》
裏介紹的流程,自己沒有實際的體會還是不夠的,那裏只介紹了基礎概念,讓自己
有了知識。知道如何用,在什麼情況下用,用哪種方法,能夠用的穩定這叫有了技術。
我們從另一個角度出發,把問題分爲兩段來看,這樣利於總結。一個 IRP 在過濾驅
動中,把它分爲需要安裝 CompleteRoutine 的與無需安裝 CompleteRoutine 的。
那麼在不需要安裝 CompleteRoutine 的有以下幾類情況。

(1) 拿到這個 IRP 後什麼都不做,直接調用 IoCompleteRequest() 來返回。
(2) 拿到這個 IRP 後什麼都不做,直接傳遞到底層設備,使用
IoSkipCurrentIrpStackLocation() 後調用 IoCallDriver() 傳遞。
(3) 使用 IoBuildSynchronousFsdRequest() 或 IoBuildDeviceIoControlRequest()
來建立 IRP 的。

以上幾種根據需要直接使用即可,除了一些參數與標誌需要注意外,沒有什麼系統
機制相關的東西需要注意了。那麼再來看需要安裝 CompleteRoutine 的情況。我們
把這種情況再細分爲兩種,一是在 CompleteRoutine 中返回標誌爲
STATUS_MORE_PROCESSING_REQUIRED 的情況。二是返回處這個外的標誌,需要使用函數
IoMarkIrpPending() 的情況。在 CompleteRoutine 中絕大多數就這麼兩種情況,
你需要使用其中的一種情況。那麼爲什麼需要安裝 CompleteRoutine 呢?那是因爲
我們對其 IRP 從上層驅動,經過我們驅動,在經過底層設備棧返回到我們這一層驅
動時需要得到其中內容作爲參考依據的,還有對其中內容需要進行修改的。再有一種
情況是沒有經過上層驅動,而 IRP 的產生是在我們驅動直接下發到底層驅動,而經
過設備棧後返回到我們這一層,且我們不在希望它繼續向上返回的,因爲這個 IRP
本身就不是從上層來的。綜上所述,先來看下 IoMarkIrpPending() 的情況。


(1) 在 CompleteRoutine 中判斷 Irp->PendingReturned 並使用 IoMarkIrpPending()
然後返回。這種方法在沒有使用 KeSetEvent() 的情況下,且不是自建 IRP 發送
到底層驅動返回時使用。也就是說有可能我所做的工作都是在 CompleteRoutine
中進行的。比如加/解密時,我在這裏對下層驅動返回數據的判斷並修改。修改
後因爲沒有使用 STATUS_MORE_PROCESSING_REQUIRED 標誌,它會延設備堆一直向
上返回併到用戶得到數據爲止。這裏一定要注意,在這種情況下 CompleteRoutine
返回後,不要在碰這個 IRP。也就是說如果這個時候你使用了 IoCompleteRequest()
的話會出現一個 MULTIPLE_IRP_COMPLIETE_REQUEST 的 BSOD 錯誤。


(2) 在 CompleteRoutine 中直接返回 STATUS_MORE_PROCESSING_REQUIRED 標誌。這種
情況在使用了 KeSetEvent() 的函數下出現。這裏又有兩個小小的分之。

1) 出現於上層發送到我這裏,當我這裏使用 IoCallDriver() 後,底層返回數
據經過我這一層時,我想讓它暫時停止繼續向上傳遞,讓這個 IRP 稍微歇息
一會,等我對這個 IRP 返回的數據操作完成後(一般是沒有在 CompleteRoutine
中對返回數據進行操作情況下,也就是說等到完成例程返回後再進行操作),由
我來調用 IoCompleteRequest() 讓它延着設備棧繼續返回。這裏要注意,我們
是想讓它返回的,所以調用了 IoCompleteRequest()。這個可不同於下面所講的
自己從頭分配 IRP 時在 CompleteRoutine 中已經調用 IoFreeIrp() 釋放了當前
IRP 的情況。比如我在做一個改變文件大小,向文件頭寫入加密標誌的驅動時,
在上層發來了 IRP_MJ_QUERY_INFORMATION 查詢文件,我想在這個時候獲得文件
信息進行判斷,然後根據我的判斷結果再移動文件指針。注意:上面是兩步,第
一步是先獲得文件大小,那麼在這個時候我就需要用到上述辦法,先讓這個 IRP
傳遞下去,得到我想要的東西后在進行對比。等待適當時機完成這個 IRP,讓數
據繼續傳遞,直到用戶收到爲止。第二步我會結合下面小節來講。

2) 出現於自己從頭建立 IRP,當使用 IoAllocate() 或 IoBuildAsynchronousFsdRequest()
創建 IRP 調用 IoCallDriver() 後,底層返回數據到我這一層時,我不想讓這
個 IRP 繼續向上延設備棧傳遞。因爲這個 IRP 就是在我這層次建立的,上層本
就不知道有這麼一個 IRP。那麼到這裏我就要在 CompleteRoutine 中使用 IoFreeIrp()
來釋放掉這個 IRP,並不讓它繼續傳遞。這裏一定要注意,在 CompleteRoutine
函數返回後,這個 IRP 已經釋放了,如果這個時候在有任何關於這個 IRP 的操作
那麼後果是災難性的,必定導致 BSOD 錯誤。前面 1) 小節給出的例子只完成了第
一步這裏繼續講第二步,第一步我重用這個 IRP 得到了文件大小,那麼這個時候雖
然知道大小,但我還是無法知道這個文件是否被我加過密。這時,我就需要在這裏
自己從頭建立一個 IRP_MJ_READ 的 IRP 來讀取文件來判斷是否我加密過了的文件,
如果是,則要減少相應的大小,然後繼續返回。注意:這裏的返回是指讓第一步的
IRP 返回。而不是我們自己創建的。我們創建的都已經在 CompleteRoutine 中銷
毀了。



8、關於完成 IRP 的動作簡介

當一個底層驅動調用了 IoCompleteRequest() 函數時,基本上所有設備棧相關 IRP 處理工
作都是在它那裏完成的。包括 IRP->Flags 的一些標誌的判斷,對 APC 的處理,拋出
MULTIPLE_IRP_COMPLETE_REQUESTS 錯誤等。當它延設備棧一直調用驅動所安裝的 CompleteRoutine
時,如果發現 STATUS_MORE_PROCESSING_REQUIRED 這個標誌,則會停止向上繼續回滾。這也是
爲什麼在 CompleteRoutine 中使用這個標誌即可暫停 IRP 的原因。



9、關於 ObQueryNameString 的使用

這個函數的使用,在有些環境下會有問題。它的上層函數是 ZwQueryObject()。在某些
情況下會導致系統掛起,或者直接 BSOD。它是從 對象管理器中的 ObpRootDirectoryObject
開始遍歷,通過 OBJECT_HEADER_TO_NAME_INFO 獲得對象名稱。今天問了下 PolyMeta
好象是在處理 PIPE 時會掛啓,這個問題出現在 2000 系統。在 XP 上好象補丁了。


10、關於重入問題

其實這個問題在很久前的 IFS FAQ 裏已經介紹的很清楚,包括處理方法以及每種方法
可能帶來的問題。IFS FAQ 裏的 Q34 一共介紹了四種方法,包括自己從頭建立 IRP
發送,使用 ShadowDevice,使用特徵字符串,根據線程 ID,在 XP 下使用IoCreateFileSpecifyDeviceObjectHint() 函數。並且把以上幾種在不同環境
下使用要處理的問題也做了簡單的介紹。且在 Q33 裏介紹了在 CIFS 碰到的 FILE_COMPLETE_IF_OPLOCKED 問題的解決方法。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章