看了 ChuKuangRen 的《
Windows文件系統(tǒng)過(guò)濾驅(qū)動(dòng)開發(fā)教程(第二版).pdf》后,頗有感觸。我想,交流都是建立在平等的基礎(chǔ)上,在抱怨氛圍和環(huán)境不好的同時(shí)應(yīng)該先想一想自己究竟付出了多少?只知索取不愿付出的人也就不用抱怨了,要怪也只能怪自己。發(fā)自己心得的人無(wú)非是兩種目的,一是引發(fā)一些討論,好糾正自己錯(cuò)誤的認(rèn)識(shí),以便從中獲取更多的知識(shí)使自己進(jìn)步的更快。二是做一份備忘,當(dāng)自己遺忘的時(shí)候能夠馬上找到相關(guān)資料。我這里也總結(jié)了近幾年做文件過(guò)濾驅(qū)動(dòng)時(shí)所積累下來(lái)的一些小小經(jīng)驗(yàn),這分筆記也是看了 ChuKuangRen 的教程后,臨時(shí)想到的一小部分而已,是想到哪寫到哪,不是很全,如果以后再回想起什么也會(huì)不斷補(bǔ)充。因其工作原因,近段時(shí)間在 SOLARIS 驅(qū)動(dòng)與 Linux 內(nèi)核方面投入的精力比較多,Windows 下的文件過(guò)濾驅(qū)動(dòng)一直也沒有怎么去碰,所以最后還是那句老話 FIXME。
1、獲得文件全路徑以及判斷時(shí)機(jī)
除在所有 IRP_MJ_XXX 之前自己從頭創(chuàng)建 IRP 發(fā)送到下層設(shè)備查詢?nèi)窂酵?,不要嘗試在 IRP_MJ_Create 以外的地方獲得全路徑,因?yàn)橹挥性?IRP_MJ_Create
中才會(huì)使用 ObCreateObject() 來(lái)建立一個(gè)有效的 FILE_OBJECT。而在 IRP_READ IRP_WRITE 中它們是直接操作 FCB (File Control Block)的。
2、從頭建立 IRP 發(fā)送關(guān)注點(diǎn)
無(wú)論你建立什么樣的 IRP,是 IRP_MJ_Create 也好還是 IRP_MJ_DIRECTORY_CONTROL也罷,最要提醒的就是一些標(biāo)志。不同的標(biāo)志會(huì)代來(lái)不同的結(jié)果,有些結(jié)果是直接返回失敗。這里指的標(biāo)志不光是 IRP->Flags,還要考慮 IO_STACK_LOCATION->Flags還有其它等等。尤其是你要達(dá)到一些特殊目的,這時(shí)候更需要注意,如 IRP_MN_QUERY_DIRECTORY,不同的標(biāo)志結(jié)果有很大的不同。
3、從頭建立 IRP 獲取全路徑注意點(diǎn)
自己從頭建立一個(gè) IRP_MJ_QUERY_INFORMATION 的 IRP 獲取全路徑時(shí)需要注意,不僅在 IRP_MJ_Create 要做區(qū)別處理,在 IRP_MJ_CLOSE 也要做同樣的處理,否則如果目標(biāo)是 NTFS 文件系統(tǒng)的話可能產(chǎn)生 deadlock。如果是 NTFS 那么在 IRP_MJ_CLEANUP 的時(shí)候也需要對(duì) FO_STREAM_FILE 類型的文件做同樣處理。
4、獲得本地/遠(yuǎn)程訪問(wèn)用戶名(域名/SID)
方法只有在 IRP_MJ_Create 中才可用,那是因?yàn)?IO_SECURITY_CONTEXT 只有在 IO_STACK_LOCATION->Parameters.Create.SecurityContext 才會(huì)有效。這樣你才有可能從 IO_SECURITY_CONTEXT->SecurityContext->AccessState->SubjectSecurityContext.XXXToken 中獲得訪問(wèn) TOKEN,從而進(jìn)一步得到用戶名或 SID。記得 IFS 中有一個(gè)庫(kù),它的 LIB 導(dǎo)出一個(gè)函數(shù)可以讓你在獲得以上信息后得到用戶名與域名。但如果你想兼容 NT4 的話,只能自己分析來(lái)得出本地和遠(yuǎn)程的 SID。
5、文件與目錄的判斷
正確的方法在楚狂人的文檔里已經(jīng)說(shuō)過(guò)了,再補(bǔ)充一句。如果你的文件過(guò)濾驅(qū)動(dòng)要兼容所有文件系統(tǒng),那么不要十分相信從 FileObject->FsContext 里取得的數(shù)據(jù)。正確的方法還是在你傳遞下去 IRP_MJ_Create 后從最下層文件系統(tǒng)延設(shè)備棧返回到你這里后再獲得。
6、加/解密中判斷點(diǎn)
只判斷 IRP_PAGING_IO,IRP_SYNCHRONOUS_PAGING_IO,IRP_NOCACHE 是沒錯(cuò)的。如果有問(wèn)題,相信是自己的問(wèn)題。關(guān)于有人提到在 FILE_OBJECT->Flags中的 FO_NO_INTERMEDIATE_BUFFERING 是否需要判斷,對(duì)此問(wèn)題的回答是只要你判斷了 IRP_NOCACHE 就不用再判斷 FILE_OBJECT 中的,因?yàn)樗罱K會(huì)設(shè)置 IRP->Flags 為 IRP_NOCACHE。關(guān)于你看到的諸如 IRP_DEFER_IO_COMPLETION 等 IRP 不要去管它,因?yàn)樗皇且粋€(gè)過(guò)程。最終讀寫還是如上所介紹。至于以上這些 IRP 哪個(gè)是由 CC MGR 發(fā)送的,哪些是由 I/O MGR 發(fā)送和在什么時(shí)候發(fā)送的,這個(gè)已經(jīng)有很多討論了,相信可以找到。
7、舉例說(shuō)明關(guān)于 IRP 傳遞與完成注意事項(xiàng)
只看 Walter Oney 的那本 《Programming the Microsoft Windows driver model》里介紹的流程,自己沒有實(shí)際的體會(huì)還是不夠的,那里只介紹了基礎(chǔ)概念,讓自己有了知識(shí)。知道如何用,在什么情況下用,用哪種方法,能夠用的穩(wěn)定這叫有了技術(shù)。我們從另一個(gè)角度出發(fā),把問(wèn)題分為兩段來(lái)看,這樣利于總結(jié)。一個(gè) IRP 在過(guò)濾驅(qū)動(dòng)中,把它分為需要安裝 CompleteRoutine 的與無(wú)需安裝 CompleteRoutine 的。那么在不需要安裝 CompleteRoutine 的有以下幾類情況。
(1) 拿到這個(gè) IRP 后什么都不做,直接調(diào)用 IoCompleteRequest() 來(lái)返回。
(2) 拿到這個(gè) IRP 后什么都不做,直接傳遞到底層設(shè)備,使用IoSkipCurrentIrpStackLocation() 后調(diào)用 IoCallDriver() 傳遞。
(3) 使用 IoBuildSynchronousFsdRequest() 或 IoBuildDeviceIoControlRequest()來(lái)建立 IRP 的。
以上幾種根據(jù)需要直接使用即可,除了一些參數(shù)與標(biāo)志需要注意外,沒有什么系統(tǒng)機(jī)制相關(guān)的東西需要注意了。那么再來(lái)看需要安裝 CompleteRoutine 的情況。我們把這種情況再細(xì)分為兩種,一是在 CompleteRoutine 中返回標(biāo)志為STATUS_MORE_PROCESSING_REQUIRED 的情況。二是返回處這個(gè)外的標(biāo)志,需要使用函數(shù)IoMarkIrpPending() 的情況。在 CompleteRoutine 中絕大多數(shù)就這么兩種情況,你需要使用其中的一種情況。那么為什么需要安裝 CompleteRoutine 呢?那是因?yàn)槲覀儗?duì)其 IRP 從上層驅(qū)動(dòng),經(jīng)過(guò)我們驅(qū)動(dòng),在經(jīng)過(guò)底層設(shè)備棧返回到我們這一層驅(qū)動(dòng)時(shí)需要得到其中內(nèi)容作為參考依據(jù)的,還有對(duì)其中內(nèi)容需要進(jìn)行修改的。再有一種情況是沒有經(jīng)過(guò)上層驅(qū)動(dòng),而 IRP 的產(chǎn)生是在我們驅(qū)動(dòng)直接下發(fā)到底層驅(qū)動(dòng),而經(jīng)過(guò)設(shè)備棧后返回到我們這一層,且我們不在希望它繼續(xù)向上返回的,因?yàn)檫@個(gè) IRP 本身就不是從上層來(lái)的。綜上所述,先來(lái)看下 IoMarkIrpPending() 的情況。
(1) 在 CompleteRoutine 中判斷 Irp->PendingReturned 并使用 IoMarkIrpPending()然后返回。這種方法在沒有使用 KeSetEvent() 的情況下,且不是自建 IRP 發(fā)送到底層驅(qū)動(dòng)返回時(shí)使用。也就是說(shuō)有可能我所做的工作都是在 CompleteRoutine 中進(jìn)行的。比如加/解密時(shí),我在這里對(duì)下層驅(qū)動(dòng)返回?cái)?shù)據(jù)的判斷并修改。修改后因?yàn)闆]有使用 STATUS_MORE_PROCESSING_REQUIRED 標(biāo)志,它會(huì)延設(shè)備堆一直向上返回并到用戶得到數(shù)據(jù)為止。這里一定要注意,在這種情況下 CompleteRoutine返回后,不要在碰這個(gè) IRP。也就是說(shuō)如果這個(gè)時(shí)候你使用了 IoCompleteRequest()的話會(huì)出現(xiàn)一個(gè) MULTIPLE_IRP_COMPLIETE_REQUEST 的 BSOD 錯(cuò)誤。
(2) 在 CompleteRoutine 中直接返回 STATUS_MORE_PROCESSING_REQUIRED 標(biāo)志。這種情況在使用了 KeSetEvent() 的函數(shù)下出現(xiàn)。這里又有兩個(gè)小小的分之。
1) 出現(xiàn)于上層發(fā)送到我這里,當(dāng)我這里使用 IoCallDriver() 后,底層返回?cái)?shù)據(jù)經(jīng)過(guò)我這一層時(shí),我想讓它暫時(shí)停止繼續(xù)向上傳遞,讓這個(gè) IRP 稍微歇息一會(huì),等我對(duì)這個(gè) IRP 返回的數(shù)據(jù)操作完成后(一般是沒有在 CompleteRoutine中對(duì)返回?cái)?shù)據(jù)進(jìn)行操作情況下,也就是說(shuō)等到完成例程返回后再進(jìn)行操作),由我來(lái)調(diào)用 IoCompleteRequest() 讓它延著設(shè)備棧繼續(xù)返回。這里要注意,我們是想讓它返回的,所以調(diào)用了 IoCompleteRequest()。這個(gè)可不同于下面所講的自己從頭分配 IRP 時(shí)在 CompleteRoutine 中已經(jīng)調(diào)用 IoFreeIrp() 釋放了當(dāng)前IRP 的情況。比如我在做一個(gè)改變文件大小,向文件頭寫入加密標(biāo)志的驅(qū)動(dòng)時(shí),在上層發(fā)來(lái)了 IRP_MJ_QUERY_INFORMATION 查詢文件,我想在這個(gè)時(shí)候獲得文件信息進(jìn)行判斷,然后根據(jù)我的判斷結(jié)果再移動(dòng)文件指針。注意:上面是兩步,第一步是先獲得文件大小,那么在這個(gè)時(shí)候我就需要用到上述辦法,先讓這個(gè) IRP傳遞下去,得到我想要的東西后在進(jìn)行對(duì)比。等待適當(dāng)時(shí)機(jī)完成這個(gè) IRP,讓數(shù)據(jù)繼續(xù)傳遞,直到用戶收到為止。第二步我會(huì)結(jié)合下面小節(jié)來(lái)講。
2) 出現(xiàn)于自己從頭建立 IRP,當(dāng)使用 IoAllocate() 或 IoBuildAsynchronousFsdRequest()創(chuàng)建 IRP 調(diào)用 IoCallDriver() 后,底層返回?cái)?shù)據(jù)到我這一層時(shí),我不想讓這個(gè) IRP 繼續(xù)向上延設(shè)備棧傳遞。因?yàn)檫@個(gè) IRP 就是在我這層次建立的,上層本就不知道有這么一個(gè) IRP。那么到這里我就要在 CompleteRoutine 中使用 IoFreeIrp()來(lái)釋放掉這個(gè) IRP,并不讓它繼續(xù)傳遞。這里一定要注意,在 CompleteRoutine函數(shù)返回后,這個(gè) IRP 已經(jīng)釋放了,如果這個(gè)時(shí)候在有任何關(guān)于這個(gè) IRP 的操作那么后果是災(zāi)難性的,必定導(dǎo)致 BSOD 錯(cuò)誤。前面 1) 小節(jié)給出的例子只完成了第一步這里繼續(xù)講第二步,第一步我重用這個(gè) IRP 得到了文件大小,那么這個(gè)時(shí)候雖然知道大小,但我還是無(wú)法知道這個(gè)文件是否被我加過(guò)密。這時(shí),我就需要在這里自己從頭建立一個(gè) IRP_MJ_READ 的 IRP 來(lái)讀取文件來(lái)判斷是否我加密過(guò)了的文件,如果是,則要減少相應(yīng)的大小,然后繼續(xù)返回。注意:這里的返回是指讓第一步的IRP 返回。而不是我們自己創(chuàng)建的。我們創(chuàng)建的都已經(jīng)在CompleteRoutine 中銷毀了。
8、關(guān)于完成 IRP 的動(dòng)作簡(jiǎn)介
當(dāng)一個(gè)底層驅(qū)動(dòng)調(diào)用了 IoCompleteRequest() 函數(shù)時(shí),基本上所有設(shè)備棧相關(guān) IRP 處理工作都是在它那里完成的。包括 IRP->Flags 的一些標(biāo)志的判斷,對(duì) APC 的處理,拋出MULTIPLE_IRP_COMPLETE_REQUESTS 錯(cuò)誤等。當(dāng)它延設(shè)備棧一直調(diào)用驅(qū)動(dòng)所安裝的 CompleteRoutine時(shí),如果發(fā)現(xiàn) STATUS_MORE_PROCESSING_REQUIRED 這個(gè)標(biāo)志,則會(huì)停止向上繼續(xù)回滾。這也是為什么在 CompleteRoutine 中使用這個(gè)標(biāo)志即可暫停 IRP 的原因。
9、關(guān)于 ObQueryNameString 的使用
這個(gè)函數(shù)的使用,在有些環(huán)境下會(huì)有問(wèn)題。它的上層函數(shù)是 ZwQueryObject()。在某些情況下會(huì)導(dǎo)致系統(tǒng)掛起,或者直接 BSOD。它是從 對(duì)象管理器中的 ObpRootDirectoryObject開始遍歷,通過(guò) OBJECT_HEADER_TO_NAME_INFO 獲得對(duì)象名稱。今天問(wèn)了下 PolyMeta好象是在處理 PIPE 時(shí)會(huì)掛啟,這個(gè)問(wèn)題出現(xiàn)在 2000 系統(tǒng)。在 XP 上好象補(bǔ)丁了。
10、關(guān)于重入問(wèn)題
其實(shí)這個(gè)問(wèn)題在很久前的 IFS FAQ 里已經(jīng)介紹的很清楚,包括處理方法以及每種方法可能帶來(lái)的問(wèn)題。IFS FAQ 里的 Q34 一共介紹了四種方法,包括自己從頭建立 IRP發(fā)送,使用 ShadowDevice,使用特征字符串,根據(jù)線程 ID,在 XP 下使用IoCreateFileSpecifyDeviceObjectHint() 函數(shù)。并且把以上幾種在不同環(huán)境下使用要處理的問(wèn)題也做了簡(jiǎn)單的介紹。且在 Q33 里介紹了在 CIFS 碰到的 FILE_COMPLETE_IF_OPLOCKED 問(wèn)題的解決方法。
(全文完)