——————————————————————————————————————————————————————
在寫 filter driver 或 rootkit 時,經常需要 attach 到設備棧中的目標設備,來攔截途經的 IRP(I/O Request Packet),實現過濾功能。
首先要獲悉目標設備向 Windows Object Manager 維護的全局名稱空間注冊的 _DEVICE_OBJECT 名,此類信息可以通過像是 WinObj.exe 的工具獲取。
接着調用 ObReferenceObjectByName(),該函數把獲取的目標對象地址存儲到它的最后一個參數(指針)中,然后返回給調用者。
實戰時我們會發現,引用 _DRIVER_OBJECT 幾乎總是成功;而引用 _DEVICE_OBJECT,則不一定會成功,返回的 NTSTATUS 狀態碼一般以兩種居多:
1 C0000022(STATUS_ACCESS_DENIED) 2 C0000024(STATUS_OBJECT_TYPE_MISMATCH)
第一種情況通常是由於創建目標 _DEVICE_OBJECT 時指定的 session id 與當前的 session id 不一致,或者目標對象持有特殊的安全訪問令牌/安全屬性,所以我們無法以常規方式獲取,而且這種錯誤頻繁出現在 IoGetDeviceObjectPointer() 調用時,偏偏多數講過濾驅動和 rootkit 的書籍都用 IoGetDeviceObjectPointer() 作為示例代碼的一部分,真是有點誤人子弟的意味。
第二種情況普遍出現在通過 ObReferenceObjectByName() 引用某些 _DEVICE_OBJECT 的場景中,緣由與 ObReferenceObjectByName() 利用其它執行體組件例程,在全局名稱空間中執行的名字查找邏輯密切相關,后面會解釋。
需要指出,既然通過 ObReferenceObjectByName() 引用絕大多數 _DRIVER_OBJECT 都會成功,而且 _DRIVER_OBJECT.DeviceObject 又指向該驅動創建的設備鏈中第一個 _DEVICE_OBJECT,那么這就是最穩當的方法。不過我們還是要知道 STATUS_OBJECT_TYPE_MISMATCH 的原因。
ObReferenceObjectByName() 是一個未公開的例程,在 MSDN 中沒有文檔描述,另一方面,包含的 ntddk.h 或 wdm.h 頭文件中也沒有相關原型聲明;
但是內核映像 ntoskrnl.exe 和其它的版本,的確導出了它的符號,換言之,我們只需要告訴鏈接器把這個函數名作為外部符號來解析即可。
此外,ObReferenceObjectByName() 的第五個參數也是一個未文檔化的數據類型(POBJECT_TYPE),所以相關的聲明是必須的,如下圖所示:
—————————————————————————————————————————————————————————————
請注意,我們聲明了一個指向類型“POBJECT_TYPE”的指針——IoDeviceObjectType——而“POBJECT_TYPE”自身又是指向類型“OBJECT_TYPE”的指針,所以在傳入第五個參數時,一定要謹慎,使用操作符 “*” 解引 IoDeviceObjectType,才會與它的形參類型(POBJECT_TYPE)匹配,否則會導致 ObReferenceObjectByName() 失敗,干擾我們對返回的 NTSTATUS 原因判斷!
假設我們自己的驅動要獲得“\Device\QQProtect”對應的 _DEVICE_OBJECT 指針,然后檢查返回的 NTSTATUS 狀態碼,如下圖所示:
(“\Device\QQProtect”是與即時通信軟件 QQ 一同安裝的兩個過濾驅動之一:QQProtect.sys 創建的設備對象名,
它也是我們稍后的 IRP Dispatch Routine Hook 實驗目標!)
可以看到,在虛擬機中測試時,DbgPrint() 打印返回的狀態碼為 C0000024(STATUS_OBJECT_TYPE_MISMATCH),也就是對象類型不匹配,如下圖所示:
剛好手邊有一份 NT 5.2 版內核的源碼,它用來編譯 Windows XP/Server 2003 使用的內核,盡管與我的測試機器的 NT 6.1 版內核有所差異,不過
還是姑且來看下 ObReferenceObjectByName() 內部究竟干了些什么。ObReference*() 系列的例程多數放在內核源碼的“obref.c” 與“obdir.c”
文件內。通過對相關調用鏈的分析,如下圖所示:
上圖中有兩處關鍵點:其一是 ObpLookupObjectName() 中,檢查目標對象類型的初始化設定(用 _OBJECT_TYPE_INITIALIZER 結構表示)中,是否指定了 ParseProcedure 例程,對於“設備”類對象,該函數值指針總是為 IopParseDevice() ,最終導致調用 IopParseDevice()。
仔細觀察前面的圖片可知,從最初我調用 ObReferenceObjectByName() 開始,就為它的第七個參數 ParseContext 傳入 NULL,而 ParseContext 會在調用鏈中一路往下傳遞,最終由 IopParseDevice() 接受並對該參數進行驗證,如果它為空,就返回 STATUS_OBJECT_TYPE_MISMATCH。
現在你知道為啥 ObReferenceObjectByName() 引用目標設備總是讓人如此蛋疼,關鍵就在需要分配並初始化那個 ParseContext。。。
———————————————————————————————————————————
我在源碼中提取了相關代碼片段,如下面這些圖所示,最好能把它與上面的流程圖對比加深理解,
后面我會拿虛擬機上的 Windows 7(基於 NT 6.1 版內核)調試,你會驚訝地發現,追蹤棧回溯信息時,
竟然與 NT 5.2 版內核源碼中的調用鏈非常相似,這說明版本之間的遷移並沒有讓對象名查找和驗證邏輯改動太大。
(至少從 Windows XP 到 7 而言是如此,之后的版本由於沒測試過,就不清楚了!)
從 IopParseDevice() 內部的那段注釋,我依稀得到了繞過調用源檢測的思路——那就是跟蹤 NtCreateFile() ,看看 OPEN_PACKET 具體是在哪里
分配並初始化的;由於 IopParseDevice() 會檢測 POPEN_PACKET 結構實例的一些字段來保證 ObReferenceObjectByName() 調用
是從 NtCreateFile() 發起的,NtCreateFile() 實現在 NT 5.2 版內核源碼的 creater.c 中,它只是簡單地執行調用鏈
IoCreateFile()->IopCreateFile()(此兩例程都實現在源碼的 iosubs.c 中),而具體由 IopCreateFile() 分配並初始化 OPEN_PACKET 結構。
所以我們只要在引用目標設備對象前,仿照 IopCreateFile() 的相關邏輯來分配並初始化 OPEN_PACKET,並作為 ObReferenceObjectByName()
的參數傳入,就會繞過 IopParseDevice() 的“調用源檢測”邏輯。
這部分 Patch 就留待后面的隨筆再發表。我們當前先要驗證“設備”類對象的“ParseProcedure”確實為 IopParseDevice()。。。。。
——————————————————————————————————————————————————
在雙擊內核調試環境中,首先通過設備名稱“\Device\QQProtect”取得相應對象的信息:
得到對象頭地址后,格式化並轉儲其中的字段,我們感興趣的是“TypeIndex”字段,它用來索引“對象類型表”中的相應“對象類型”:
WInodws 內核使用一個數據結構——ObTypeIndexTable 存放有關各種“對象類型”的信息,本質上 ObTypeIndexTable 是一個指針數組,在 32 位體系結構上,每個指針大小 4 字節,而我們得到的索引號為(下標從 0 開始)19,下圖中的兩條表達式據此計算出該“對象類型”的地址:
由此可知,相應“對象類型”結構的地址為 0x855cef78——Windows 內核用數據結構 _OBJECT_TYPE 來表示“對象類型”的概念,所以再次
格式化並轉儲其中的字段,我們感興趣的字段為“TypeInfo”,如前所述,它是一個“對象類型初始化設定”結構,內核用
_OBJECT_TYPE_INITIALIZER 來表示“對象類型初始化設定”的概念。需要注意,TypeInfo 偏移它的母結構起始地址 0x28 字節,所以要加上這個
offset 再查看,如你所見,其中的“ParseProcedure”為 IopParseDevice()。
下篇文章將討論如何繞過 IopParseDevice() 的調用源檢測,並調試我們的成果,將其應用於 rootkit 開發技術中。