史萊姆論壇

返回   史萊姆論壇 > 教學文件資料庫 > 應用軟體使用技術文件
忘記密碼?
論壇說明

歡迎您來到『史萊姆論壇』 ^___^

您目前正以訪客的身份瀏覽本論壇,訪客所擁有的權限將受到限制,您可以瀏覽本論壇大部份的版區與文章,但您將無法參與任何討論或是使用私人訊息與其他會員交流。若您希望擁有完整的使用權限,請註冊成為我們的一份子,註冊的程序十分簡單、快速,而且最重要的是--註冊是完全免費的!

請點擊這裡:『註冊成為我們的一份子!』

Google 提供的廣告


 
 
主題工具 顯示模式
舊 2004-08-11, 03:48 PM   #1
psac
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設 反病毒引擎設計之既時監控篇

目錄

3.1既時監控概論
3.2病毒既時監控實現技術概論
3.3WIN9X下的病毒既時監控
3.3.1實現技術詳解
3.3.2程序結構與流程
3.3.3HOOKSYS.VXD逆向工程程式碼剖析
3.3.3.1鉤子函數入口程式碼
3.3.3.2取得當前行程名稱程式碼
3.3.3.3通信部分程式碼
3.4 WINNT/2000下的病毒既時監控
3.4.1實現技術詳解
3.4.2程序結構與流程
3.4.3HOOKSYS.SYS逆向工程程式碼剖析
3.4.3.1取得當前行程名稱程式碼
3.4.3.2啟動鉤子函數工作程式碼
3.4.3.3映射系統記憶體至用戶空間程式碼

3.病毒既時監控

3.1既時監控概論

既時監控技術其實並非什麼新技術,早在DOS編程時代就有之。只不過那時人們沒有給這項技術冠以這樣專業的名字而已。早期在各大專院校機房中普遍使用的硬碟寫保護軟體正是利用了既時監控技術。硬碟寫保護軟體一般會將自身寫入硬碟零磁頭開始的幾個扇區(由0磁頭0磁柱1扇最開始的64個扇區是保留的,DOS訪問不到)並修改原來的硬碟分區表以使啟動時硬碟寫保護程序可以取得控制權。引導時取得控制權的硬碟寫保護程序會修改INT13H的中斷向量指向自身已駐留於記憶體中的鉤子程式碼以便隨時攔截所有對磁牒的操作。鉤子程式碼的作用當然是很明顯的,它主要負責由判斷中斷入口參數,包括功能號,磁牒目標位址等來決定該類型操作是否被允許,這樣就可以實現對某一特定區域的寫操作保護。後來又誕生了在此基礎之上進行改進了的磁牒恢復卡之類的產品,其利用將寫操作重轉發IP至目標區域外的臨時分區並儲存磁牒先前狀態等技術來實現允許寫入並可隨時恢復之功能。不管怎麼改進,這類產品的核心技術還是對磁牒操作的既時監控。對此有興趣的朋友可參看高雲慶著《硬碟保護技術手冊》。DOS下還有許多通過駐留並截獲一些有用的中斷來實現某種特定目的的程序,我們通常稱之為TSR(終止並等待駐留terminate-and-stay-resident,此種程序不容易編好,需要大量的關於硬體和Dos中斷的知識,還要解決Dos重入,tsr程序重入等問題,搞不好就會當機)。在WINDOWS下要實現既時監控決非易事,普通用戶態程序是不可能監控系統的活動的,這也是出於系統安全的考慮。HPS病毒能在用戶態下直接監控系統中的文件操作其實是由於WIN9X在設計上存在漏洞。而我們下面要討論的兩個病毒既時監控(For WIN9X&WINNT/2000)都使用了驅動編程技術,讓工作於系統核心態的驅動程式去攔截所有的文件訪問。當然由於工作系統的不同,這兩個驅動程式無論從結構還是工作原理都不盡相同的,當然程序寫法和編譯環境更是千差萬別了,所以我們決定將其各自分成獨立的一節來詳細地加以討論。上面提到的病毒既時監控其實就是對文件的監控,說成是文件監控應該更為合理一些。除了文件監控外,還有各種各樣的既時監控工具,它們也都具有各自不同的特點和功用。這裡向大家推薦一個關於WINDOWS系統內核編程的站點:[url]www.sysinternals.com。在其上可以找到很多既時監控小工具,比如能夠監視註冊表訪問的Regmon︴/url]]通過修改系統使用表中註冊表相關服務入口),可以既時地觀察TCP和UDP活動的Tdimon(通過hook系統傳輸協定驅動Tcpip.sys中的dispatch函數來截獲tdi clinet向其傳送的請求),這些工具對於瞭解系統內部運作細節是很有裨益的。介紹完有關的背景情況後,我們來看看關於病毒 既時監控的具體實現技術的情況。

3.2病毒既時監控實現技術概論

正如上面提到的病毒既時監控其實就是一個文件監視器,它會在文件開啟,關閉,清除,寫入等操作時檢查文件是否是病毒攜帶者,如果是則根據用戶的決定選項不同的處理方案,如清除病毒,禁止訪問該檔案,刪除該檔案或簡單地忽略。這樣就可以有效地避免病毒在本機電腦上的感染傳播,因為可執行文件裝入器在裝入一個文件執行時首先會要求開啟該檔案,而這個請求又一定會被既時監控在第一時間截獲到,它確保了每次執行的都是乾淨的不帶毒的文件從而不給病毒以任何執行和發作的機會。以上說的僅是病毒既時監控一個粗略的工作程序,詳細的說明將留到後面相應的章節中。病毒既時監控的設計主要存在以下幾個難點:

其一是驅動程式的編寫不同於普通用戶態程序的寫作,其難度很大。寫用戶態程序時你需要的僅僅就是使用一些熟知的API函數來完成特定的目的,比如開啟文件你只需使用CreateFile就可以了;但在驅動程式中你將無法使用熟悉的CreateFile。在NT/2000下你可以使用ZwCreateFile或NtCreateFile(native API),但這些函數通常會要求執行在某個IRQL(中斷請求級)上,如果你對如中斷請求級,延遲/異步程序使用,非分頁/分頁記憶體等概念不是特別清楚,那麼你寫的驅動將很容易導致顯示藍色當機(BSOD),Ring0下的異常將往往導致系統崩潰,因為它對於系統總是被信任的,所以沒有相應處理程式碼去捕獲這個異常。在NT下對KeBugCheckEx的使用將導致顯示藍色的出現,接著系統將進行轉儲並隨後重啟。另外驅動程式的偵錯不如用戶態程序那樣方便,用象VC++那樣的偵錯器是不行的,你必須使用系統級偵錯器,如softice,kd,trw等。

其二是驅動程式與ring3下客戶程序的通信問題。這個問題的提出是很自然的,試想當驅動程式截獲到某個文件開啟請求時,它必須通知位於ring3下的查毒模組檢查被開啟的文件,隨後查毒模組還需將查毒的結果通過某種方式傳給ring0下的監控程序,最後驅動程式根據返回的結果決定請求是否被允許。這裡面顯然存在一個雙向的通信程序。寫過驅動程式的人都知道一個可以用來向驅動程式傳送設備I/O控制資訊的API使用DeviceIoControl,它的接頭在MSDN中可以找到,但它是單向的,即ring3下客戶程序可以通過使用DeviceIoControl將某些資訊傳給ring0下的監控程序但反過來不行。既然無法找到一個現成的函數實現從ring0下的監控程序到ring3下客戶程序的通信,則我們必須採用迂迴的辦法來間接做到這一點。為此我們必須引入異步程序使用(APC)和事件對象的概念,它們就是實現特權級間喚醒的關鍵所在。現在先簡單介紹一下這兩個概念,具體的用法請參看後面的每子章中的技術實現細節。異步程序使用是一種系統用來當條件合適時在某個特定線程的上下文中執行一個程序的機制。當向一個線程的APC貯列排隊一個APC時,系統將發出一個軟體中斷,當下一次線程被調度時,APC函數將得以執行。APC分成兩種:系統新增的APC稱為內核模式APC,由應用程式新增的APC稱為用戶模式APC。另外只有當線程處於可報警(alertable)狀態時才能執行一個APC。比如使用一個異步模式的ReadFileEx時可以指定一個用戶自訂的回調函數FileIOCompletionRoutine,當異步的I/O操作完成或被取消並且線程處於可報警狀態時函數被使用,這就是APC的典型用法。Kernel32.dll中匯出的QueueUserAPC函數可以向指定線程的貯列中增加一個APC對象,因為我們寫的是驅動程式,這並不是我們要的那個函數。很幸運的是在Vwin32.vxd中匯出了一個同名函數QueueUserAPC,監控程序攔截到一個文件開啟請求後,它馬上使用這個服務排隊一個ring3下客戶程序中需要被喚醒的函數的APC,這個函數將在不久客戶程序被調度時被使用。這種APC喚醒法適用於WIN9X,在WINNT/2000下我們將使用全局共享的事件和信號量對像來解決互相喚醒問題。有關WINNT/2000下的對象組織結構我將在3.4.2節中詳細說明。NT/2000版監控程序中我們將利用KeReleaseSemaphore來喚醒一個在ring3下客戶程序中等待的線程。目前不少反病毒軟體已將驅動使用的查毒模組移到ring0,即如其所宣傳的「主動與操作系統無縫連接」,這樣做省卻了通信的消耗,但把查毒模組寫成驅動形式也同時會帶來一些麻煩,如不能使用大量熟知的API,不能與用戶既時交互,所以我們還是選項剖析傳統的反病毒軟體的監控程序。

其三是驅動程式所佔用資源問題。如果由於監控程序頻繁地攔截文件操作而使系統效能下降過多,則這樣的程序是沒有其存在的價值的。本論文將對一個成功的反病毒軟體的監控程序做徹底的剖析,其中就包含有分析其用以提高自身效能的技巧的部分,如設定歷史記錄,內裝檔案類型過濾,設定等待超時等。

3.3WIN9X下的病毒既時監控

3.3.1實現技術詳解

WIN9X下病毒既時監控的實現主要依賴於虛擬設備驅動(VXD)編程,可安裝文件系統鉤掛(IFSHook),VXD與ring3下客戶程序的通信(APC/EVENT)三項技術。

我們曾經提到過只有工作於系統核心態的驅動程式才具有有效地完成攔截系統範圍文件操作的能力,VXD就是適用於WIN9X下的虛擬設備驅動程式,所以正可當此重任。當然,VXD的功能遠不止由IFSMGR.vxd提供的攔截文件操作這一項,系統的VXDs幾乎提供了所有的底層操作的接頭--可以把VXD看成ring0下的DLL。虛擬機管理器本身就是一個VXD,它匯出的底層操作接頭一般稱為VMM服務,而其他VXD的使用接頭則稱為VXD服務。

二者ring0使用方法均相同,即在INT20(CD 20)後面緊跟著一個服務識別碼,VMM會利用服務識別碼的前半部分設備標識--Device Id找到對應的VXD,然後再利用服務識別碼的後半部分在VXD的服務表(Service Table)中定位服務函數的游標並使用之:

CD 20 INT 20H
01 00 0D 00 DD VKD_Define_HotKey



這條指令第一次執行後,VMM將以一個同樣6字元間接使用指令替換之(並不都是修正為CALL指令,有時會利用JMP指令),從而省卻了查詢服務表的工作:

FF 15 XX XX XX XX CALL [$VKD_Define_HotKey]



必須注意,上述使用方法只適用於ring0,即只是一個從VXD中使用VXD/VMM服務的ring0接頭。VXD還提供了V86(虛擬8086模式),Win16保護模式,Win32保護模式使用接頭。其中V86和Win16保護模式的使用接頭比較奇怪:

XOR DI DI
MOV ES,DI
MOV AX,1684 ;INT 2FH,AX = 1684H-->取得設備入口
MOV BX,002A ;002AH = VWIN32.VXD的設備標識
INT 2F
MOV AX,ES ;現在ESI中應該包含著入口
OR AX,AX
JE failure
MOV AH,00 ;VWIN32 服務 0 = VWIN32_Get_Version
PUSH DS
MOV DS,WORD PTR CS:[0002]

MOV WORD PTR [lpfnVMIN32],DI
MOV WORD PTR [lpfnVMIN32+2],ES ;儲存ES和DI
CALL FAR [lpfnVMIN32] ;call gate(使用門)
ESI指向了3B段的一個保護模式回調:
003B:000003D0 INT 30 ;#0028:C025DB52 VWIN32(04)+0742



INT30強迫CPU從ring3提升到ring0,然後WIN95的INT30處理函數先檢查使用是否發自3B段,如是則利用引發回調的CS:IP索引一個保護模式回調表以求得一個ring0位址。本例中是0028:C025DB52 ,即所需服務VWIN32_Get_Version的入口位址。

VXD的Win32保護模式使用接頭我們在前面已經提到過。一個是DeviceIoControl,我們的ring3客戶程序利用它來和監控驅動進行單向通信;另一個是VxdCall,它是Kernel32.dll的一個未公開的使用,被系統頻繁使用,對我們則沒有多大用處。

你可以參看WIN95DDK的說明 ,其中對每個系統VXD提供的使用接頭均有詳細說明,可按照需要選項相應的服務。

可安裝文件系統鉤掛(IFSHook)就源自IFSMGR.VXD提供的一個服務IFSMgr_InstallFileSystemApiHook,利用這個服務驅動程式可以向系統註冊一個鉤子函數。以後系統中所有文件操作都會經過這個鉤子的過濾,WIN9X下文件讀寫具體流程如下:

在讀寫操作進行時,首先通過未公開函數EnterMustComplete來增加MUSTCOMPLETECOUNT變數的記數,告訴操作系統本操作必須完成。該函數設定了KERNEL32模組裡的內部變數來顯示現在有個關鍵操作正在進行。有句題外話,在VMM裡同樣有個函數,函數名也是EnterMustComplete。那個函數同樣告訴VMM,有個關鍵操作正在進行。防止線程被殺掉或者被掛起。

接下來,WIN9X進行了一個_MapHandleWithContext(又是一個未公開函數)操作。該操作本身的具體意義尚不清楚,但是其操作卻是得到HANDLE所指對象的游標,並且增加了引用計數。

隨後,進行的乃是根本性的操作:KERNEL32發出了一個使用VWIN32_Int21Dispatch的VxdCall。陷入VWIN32後,其 檢查使用是否是讀寫操作。若是,則根據文件句柄切換成一個FSD能識別的句柄,並使用IFSMgr_Ring0_FileIO。接下來工作就轉到了IFS MANAGER。

IFS MANAGER產生一個IOREQ,並跳轉到Ring0ReadWrite內部例程。Ring0ReadWrite檢查句柄有效性,並且獲取FSD在新增文件句柄時返回的CONTEXT,一起傳入到CallIoFunc內部例程。CallIoFunc檢查IFSHOOK的存在,如果不存在,IFS MANAGER產生一個預設的IFS HOOK,並且使用相應的VFatReadFile/VFatWriteFile例程(因為目前 MS本身僅提供了VFAT驅動);如果IFSHOOK存在,則IFSHOOK函數得到控制權,而IFS MANAGER本身就脫離了文件讀寫處理。然後,使用被層層返回。KERNEL32使用未公開函數LeaveMustComplete,減少MUSTCOMPLETECOUNT計數,最終回到使用者。

由此可見通過IFSHook攔截本機文件操作是萬無一失的,而通過ApiHook或VxdCall攔截文件則多有遺漏。著名的CIH病毒正是利用了這一技術,實現其駐留感染的,其中的程式碼片段如下:

lea eax, FileSystemApiHook-@6[edi] ;取得欲安裝的鉤子函數的位址
push eax
int 20h ;使用IFSMgr_InstallFileSystemApiHook
IFSMgr_InstallFileSystemApiHook = $
dd 00400067h
mov dr0, eax ;儲存前一個鉤子的位址
pop eax



正如我們看到的,系統中安裝的所有鉤子函數呈鏈狀排列。最後安裝的鉤子,最先被系統使用。我們在安裝鉤子的同時必須將使用返回的前一個鉤子的位址暫存以便在完成處理後向下傳遞該請求:

mov eax, dr0 ;取得前一個鉤子的位址
jmp [eax] ; 跳到那裡繼續執行



對於病毒既時監控來說,我們在安裝鉤子時同樣需要儲存前一個鉤子的位址。如果文件操作的對象攜帶了病毒,則我們可以通過不使用前一個鉤子來簡單的取消該檔案請求;反之,我們則需及時向下傳遞該請求,若在鉤子中滯留的時間過長--用於等待ring3級查毒模組的處理反饋--則會使用戶明顯感覺系統變慢。

至於鉤子函數入口參數結構和怎樣從參數中取得操作類型(如IFSFN_OPEN)和檔案名(以UNICODE形式存儲)請參看相應的程式碼剖析部分。

我們所需的另一項技術--APC/EVENT也是源自一個VXD匯出的服務,這便是著名的VWIN32.vxd。這個奇怪的VXD匯出了許多與WIN32 API對應的服務:如_VWIN32_QueueUserApc,_VWIN32_WaitSingleObject,_VWIN32_ResetWin32Event,_VWIN32_Get_Thread_Context,_VWIN32_Set_Thread_Context 等。這個VXD叫虛擬WIN32,大概名稱即是由此而來的。雖然服務的名稱與WIN32 API一樣,但使用規則卻大相逕庭,千萬不可用錯。_VWIN32_QueueUserApc用來註冊一個用戶態的APC,這裡的APC函數當然是指我們在ring3下以可告警狀態睡眠的待查毒線程。ring3客戶程序首先通過IOCTL把待查毒線程的位址傳給驅動程式,然後當鉤子函數攔截到待查文件時使用此服務排隊一個APC,當ring3客戶程序下一次被調度時,APC例程得以執行。_VWIN32_WaitSingleObject則用來在某個對象上等待,從而使當前ring0線程暫時掛起。我們的ring3客戶程序先使用WIN32 API--CreateEvent新增一組事件對象,然後通過一個未公開的API--OpenVxdHandle將事件句柄轉化為VXD可辯識的句柄(其實應是指向對象的游標)並用IOCTL發給ring0端VXD,鉤子函數在排隊APC後使用_VWIN32_WaitSingleObject在事件的VXD句柄上等待查毒的完成,最後由ring3客戶程序在查毒完畢後使用WIN32 API--SetEvent來解除鉤子函數的等待。

當然,這裡面存在著一個很可怕的問題:如果你按照的我說的那樣去做,你會發現它會在一端時間內工作正常,但時間一長,系統就被掛起了。就連驅動編程大師Walter Oney在其著作《System Programming For Windows 95》的配套源碼的說明中也稱其APC例程在某些時候工作會不正常。而微軟的工程師聲稱文件操作請求是不能被中斷掉的,你不能在驅動中阻斷文件操作並依賴於ring3的反饋來做出回應。網上關於這個問題也有一些討論,意見不一:有人認為當系統DLL--KERNEL32在其使用ring0處理文件請求時擁有一個互斥量(MUTEX),而在某些情況下為了處理APC要擁有同樣的互斥量,所以死鎖發生了;還有人認為儘管在WIN9X下32位線程是搶先多工作的,但Win16子系統是以協作多工作來執行的。為了能平滑的執行老的16位程序,它引入了一個全局的互斥量--Win16Mutex。任何一個16位線程在其整個生命週期中都擁有Win16Mutex而32位線程當它轉化成16位程式碼也要攫取此互斥量,因為WIN9X內核是16位的,如Knrl386.exe,gdi.exe。如果來自於擁有Win16Mutex的線程的文件請求被阻塞,系統將陷入死鎖狀態。這個問題的正確答案似乎在沒有得到WIN9X源碼的之前永遠不可能被證實,但這是我們既時監控的關鍵,所以必須解決。

我通過跟蹤WIN95文件操作的流程,並反覆做實驗驗證,終於找到了一個比較好的解決辦法:在攔截到文件請求還沒有排隊APC之前我們通過Get_Cur_Thread_Handle取得當前線程的ring0tcb,從中找到TDBX,再在TDBX中取得ring3tcb根據其結構,我們從偏移44H處得到Flags域值,我發現如果它等於10H和20H時容易導致死鎖,這只是一個實驗結果,理由我也說不清楚,大概是這樣的文件請求多來自於擁有Win16Mutex的線程,所以不能阻塞;另外一個根本的解決方法是在使用_VWIN32_WaitSingleObject時指定超時,如果在指定時間裡沒有收到ring3的喚醒信號,則自動解除等待以防止死鎖的發生。

以上對WIN9X下的既時監控的主要技術都做了詳細的闡述。當然,還有一部分關於VXD的結構,編寫和編譯的方法由於篇幅的關係不可能在此一一說明。需要瞭解更詳細內容的,請參看Walter Oney的著作《System Programming For Windows 95》,此書尚有台灣候俊傑翻譯版《Windows 95系統程式設計》。

3.3.2程序結構與流程

以下的程序結構與流程分析來自一著名反病毒軟體的WIN9X既時監控虛擬設備驅動程式Hooksys.vxd:

1.當VXD收到來自VMM的ON_SYS_DYNAMIC_DEVICE_INIT消息--需要注意這是個動態VXD,它不會收到系統虛擬機啟始化時傳送的Sys_Critical_Init, Device_Init和Init_Complete控制消息--時,它開始啟始化一些全局變數和資料結構,包括在堆上分配記憶體(HeapAllocate),新增備用,歷史記錄,開啟文件,等待操作,關閉文件5個雙向循環鏈表及用於鏈表操作互斥的5個信號量(使用Create_Semaphore),同時將全局變數_gNumOfFilters即檔案名過濾項個數設定為0。

2.當VXD收到來自VMM的ON_W32_DEVICEIOCONTROL消息時,它會從入口參數中取得用戶程序利用DeviceIoControl傳送進來的IO控制程式碼(IOCtlCode),以此判斷用戶程序的意圖。和Hooksys.vxd協同工作的ring3級客戶程序guidll.dll會依次向Hooksys.vxd傳送IO控制請求來完成一系列工作,具體次序和程式碼含義如下:

83003C2B:將guidll取得的操作系統版本傳給驅動(儲存在iOSversion變數中),根據此變數值的不同,從ring0tcb結構中提取某些域時將採用不同的偏移,因為操作系統版本不同會影響內核資料結構。

83003C1B:啟始化後備鏈表,將guidll傳入的用OpenVxdHandle轉換過的一組事件游標儲存在每個鏈表元素中。

83003C2F:將guidll取得的驅動器類型值傳給驅動(儲存在DriverType變數中),根據此變數值的不同,使用VWIN32_WaitSingleObject設定不同的等待超時值,因為非類BIOS驅動器的讀寫時間可能會稍長些。

83003C0F:儲存guidll傳送的用戶指定的攔截文件的類型,其實這個類型過濾器在查毒模組中已存在,這裡再設定顯然是為了提高處理效率:它確保不會將非指定類型文件送到ring3級查毒模組,節省了通信的預先配置。經過解析的各檔案類型過濾塊游標將儲存在_gaFileNameFilterArra陣列中,同時更新過濾項個數_gNumOfFilters 變數的值。

83003C23:儲存guidll中等待查殺開啟文件的APC函數位址和當前線程KTHREAD游標。

83003C13:安裝系統檔案鉤子,啟動攔截文件操作的鉤子函數FilemonHookProc的工作。

83003C27:儲存guidll中等待查殺關閉文件的APC函數位址和當前線程KTHREAD游標。

83003C17:卸載系統檔案鉤子,停止攔截文件操作的鉤子函數FilemonHookProc的工作。

以上列出的IO控制程式碼的發出是類BIOS,而當鉤子函數啟動後,還會發出一些隨機的控制程式碼:

83003C07:驅動將開啟文件鏈表的頭元素即最先的請求開啟的文件刪除並插入到等待鏈表尾部,同時將元素的用戶空間位址傳送至ring3級等待查殺開啟文件的APC函數中處理。

83003C0B:驅動將關閉文件鏈表的頭元素即最先的請求關閉的文件刪除並插入到備用鏈表尾部,同時將元素中的檔案名串傳送至ring3級等待查殺關閉文件的APC函數中處理

83003C1F:當查得關閉文件是病毒時,更新歷史記錄鏈表。

下面介紹鉤子函數和guidll中等待查殺開啟文件的APC函數協同工作流程,寫文件和關閉文件的處理與之類似:

當文件請求進入鉤子函數FilemonHookProc後,它先從入口參數中取得被執行的函數的代號並判斷其是否為開啟操作(IFSFN_OPEN 24H),若非則馬上將這個IRQ向下傳遞,即構造入口參數並使用儲存在PrevIFSHookProc中前一個鉤子函數;若是則程序流程轉向開啟文件請求的處理分支。分支入口處首先要判斷當前行程是否是我們自己,若是則必須放過去,因為查毒模組中要頻繁的進行文件操作,所以攔截來自自身的文件請求將導致嚴重的系統死鎖。接下來是從堆棧參數中取得完整的文件路徑名並通過儲存的檔案類型過濾陣列檢查其是否在攔截類型之列,如通過則進一步檢查文件是否是以下幾個須放過的文件之一:SYSTEM.DAT,USER.DAT,\PIPE\。然後尋找歷史記錄鏈表以確定該檔案是否最近曾被檢查並記錄過,若在歷史記錄鏈表中找到關於該檔案的記錄並且記錄未失效即其時間戳和當前系統時間之差不得大於1F4h,則可直接從記錄中讀取查毒結果。至此才進入真正的檢查開啟文件函數_RAVCheckOpenFile,此函數入口處先從備用,等待或關閉鏈表頭部摘得一空閒元素(_GetFreeEntry)並填充之(文件路徑名域等)。接著通過一內核未公開的資料結構中的值(ring3tcb->Flags)判斷可否對該檔案請求排隊APC。如可則將空閒元素加入開啟文件鏈表尾部並排隊一個ring3級檢查開啟文件函數的APC。然後使用_VWIN32_WaitSingleObject在空閒元素中儲存的一個事件對像上等待ring3查毒的完成。當鉤子函數掛起不久後,ring3的APC函數得到執行:它會向驅動發出一IO控制碼為83003C07的請求以取得開啟文件鏈表頭元素即儲存最先提交而未決的文件請求,驅動可以將內核空間中元素的虛擬位址直接傳給它而不必考慮將之重新映射。實際上由於WIN9X內核空間沒有頁保護因而ring3級程序可以直接讀寫之。接著它使用RsEngine.dll中的fnScanOneFile函數進行查毒並在元素中設定查毒結果位,完畢後再對元素中儲存的事件對像使用SetEvent喚醒在此事件上等待的鉤子函數。被喚醒的鉤子函數檢查被ring3查毒程式碼設定的結果位以此決定該檔案請求是被採納即繼續向下傳遞還是被取消即在EAX中放入-1後直接返回,同時增加歷史記錄。

以上只是鉤子函數與APC函數流程的一個簡單介紹,其中省略了諸如判斷類BIOS驅動器,超時等內容,具體細節請參看guidll.dll和hooksys.vxd的反彙編程式碼註釋。

3.當VXD收到來自VMM的ON_SYS_DYNAMIC_DEVICE_EXIT消息時,它釋放啟始化時分配的堆記憶體(HeapFree),並清除5個用於互斥的信號量(Destroy_Semaphore)。

3.3.3HOOKSYS.VXD逆向工程程式碼剖析

在剖析程式碼之前有必要介紹一下逆向工程的概念。逆向工程(Reverse Engineering)是指在沒有來源碼的情況下對可執行文件進行反彙編試突理解機器碼本身的含義。逆向工程的用途很多,如摘掉軟體保護,窺視其設計和編寫技術,發掘操作系統內部奧秘等。本文中我們用到的不少未公開資料結構和服務就是利用逆向的方法得到的。逆向工程的難度可想而知:一個1K大小的exe文件反彙編後就有1000行左右,而我們要逆向的3個文件加起來有80多K,總程式碼量是8萬多行。所以必須掌握一定的逆向技巧,否則工作起來將是非常困難的。

首先要完成逆向工作,必須選項優秀的反彙編及偵錯跟蹤工具。IDA(The Interactive Disassembler)是一款功能強大的反彙編工具:它以交互能力強而著稱,允許使用者增加標籤,註釋及定義變數,函數名稱;另外不少反彙編工具對於特殊處理的反逆向文件,如匯入節損壞等顯得無能為力,但IDA仍可勝任之。當文件被加過殼或插入了干擾指令時 就需要使用偵錯工具進行動態跟蹤。Numega公司的Softice是偵錯工具中的佼佼者:它支持所有類型的可執行文件,包括vxd和sys驅動程式,能夠用熱鍵既時呼出,可對程式碼執行,記憶體和連接阜訪問設定斷點,總之功能非常之強大以至於連微軟總裁比爾蓋茨對此都驚歎不已。

其次需要對編譯器常用的編譯結構有一定瞭解,這樣有助於我們理解程式碼的含義。

如下程式碼是MS編譯器常用的一種編譯進階語言函數的形式:

0001224A push ebp ;儲存基址暫存器
0001224B mov ebp, esp
0001224D sub esp, 5Ch ;在堆棧留出局部變數空間
00012250 push ebx
00012251 push esi
00012252 push edi
......
0001225B lea edi, [ebp-34h] ;引用局部變數
......
0001238D mov esi, [ebp+08h] ;引用參數
......
00012424 pop edi
00012425 pop esi
00012426 pop ebx
00012427 leave
00012428 retn 8 ;函數返回
如下程式碼是MS編譯器常用的一種編譯進階語言取串長度的形式:
0001170D lea edi, [eax+1Ch] ;串首位址游標
00011710 or ecx, 0FFFFFFFFh ;將ecx置為-1
00011713 xor eax, eax ;掃瞄串結束符號(NULL)
00011715 push offset 00012C04h ;編譯器最佳化
0001171A repne scasb ;掃瞄串結束符號位置
0001171C not ecx ;取反後得到串長度
0001171E sub edi, ecx ;恢復串首位址游標



最後一點是必須要有堅忍的毅力和清晰的頭腦。逆向工程本身是件痛苦的工作:進階語言來源碼中使用的變數和函數名字在這裡僅是一個位址,需要反覆偵錯琢磨才能確定其含義;另外編譯器最佳化更為我們理解程式碼增加了不少障礙,如上例中那句壓棧指令是將後面函數使用時參數入棧提前放置。所以毅力和頭腦二者缺一不可。

以下進入hooksys.vxd程式碼剖析,由於程式碼過於龐大,我只選項有代表性且精彩的部分進行介紹。程式碼中的變數和函數及標籤名是我分析後自己增加的,可能會與原作者的意圖有些出入。

3.3.3.1鉤子函數入口程式碼

C00012E0 push ebp
C00012E1 mov ebp, esp
C00012E3 sub esp, 11Ch
C00012E9 push ebx
C00012EA push esi
C00012EB push edi
C00012EC mov eax, [ebp+arg_4] ; 被執行的函數的代號
C00012EF mov [ebp+var_11C], eax
C00012F5 cmp [ebp+var_11C], 1 ; IFSFN_WRITE
C00012FC jz writefile
C0001302 cmp [ebp+var_11C], 0Bh ; IFSFN_CLOSE
C0001309 jz closefile
C000130F cmp [ebp+var_11C], 24h ; IFSFN_OPEN
C0001316 jz short openfile
C0001318 jmp irqpassdown
鉤子函數入口處,堆棧參數分佈如下:
ebp+00h -> 儲存的EBP值.
ebp+04h -> 返回位址.
ebp+08h -> 提供這個API要使用的FSD函數的的位址
ebp+0Ch -> 提供被執行的函數的代號
ebp+10h -> 提供了操作在其上執行的以1為基準的驅動器代號(如果UNC為-1)
ebp+14h -> 提供了操作在其上執行的資源的種類。
ebp+18h -> 提供了用戶串傳遞其上的程式碼頁
ebp+1Ch -> 提供IOREQ結構的游標。



鉤子函數利用[ebp+0Ch]中儲存的被執行的函數的代號來判斷該請求的類型。同時它利用[ebp+0Ch]中儲存的IOREQ結構的游標從該結構中偏移0ch處path_t ir_ppath域取得完整的文件路徑名稱。

3.3.3.2取得當前行程名稱程式碼

C0000870 push ebx
C0000871 push esi
C0000872 push edi
C0000873 call VWIN32_GetCurrentProcessHandle ;在eax中返回ring0 PDB(行程資料庫)
C0000878 mov eax, [eax+38h] ;HTASK W16TDB
;偏移38h處是Win16工作資料庫選項子
C000087B push 0 ;DWORD Flags
C000087D or al,
C000087F push eax ;DWORD Selector
C0000880 call Get_Sys_VM_Handle@0
C0000885 push eax ;取得系統VM的句柄 VMHANDLE hVM
C0000886 call _SelectorMapFlat ;將選項子基址映射為平坦模式的線形位址
C000088B add esp, 0Ch
C000088E cmp eax, 0FFFFFFFFh ;映射錯誤
C0000891 jnz short loc_C0000899
......
C0000899 lea edi, [eax+0F2h] ;從偏移0F2h取得模組名稱
;char TDB_ModName[8]



3.3.3.3通信部分程式碼

hooksys.vxd中程式碼:
C00011BC push ecx ;客戶程序的ring0線程句柄
C00011BD push ebx ;傳入APC的參數
C00011BE push edx ;ring3級APC函數的平坦模式位址
C00011BF call _VWIN32_QueueUserApc ;排隊APC
C00011C4 mov eax, [ebp+0Ch] ;事件對象的ring0句柄
C00011C7 push eax
C00011C8 call _VWIN32_ResetWin32Event;設定事件對像為無信號態
......
C00011E7 mov eax, [ebp+0Ch]
C00011EA push 3E8h ;超時設定
C00011EF push eax ;事件對象的ring0句柄
C00011F0 call _VWIN32_WaitSingleObject ;等待ring3查毒的完成
guidll.dll中程式碼:

APC函數入口:
10001AD1 mov eax, hDevice ;取得設備句柄
10001AD6 lea ecx, [esp+4]
10001ADA push 0
10001ADC push ecx ;返回字元數
10001ADD lea edx, [esp+8]
10001AE1 push 4 ;輸出緩衝區大小
10001AE3 push edx ;輸出緩衝區游標
10001AE4 push 0 ;輸入緩衝區大小
10001AE6 push 0 ;輸入緩衝區游標
10001AE8 push 83003C07h ;IO控制程式碼
10001AED push eax ;設備句柄
10001AEE call dseviceIoControl
10001AF4 test eax, eax
10001AF6 jz short loc_10001B05
10001AF8 mov ecx, [esp+0] ;得到開啟文件鏈表頭元素
10001AFC push ecx
10001AFD call ScanOpenFile ;使用查毒函數
ScanOpenFile函數中:

1000185D call ds:fnScanOneFile ;使用真正查毒庫匯出函數
10001863 mov edx, hMutex
10001869 add esp, 8
1000186C mov esi, eax ;查毒結果
1000186E push edx
1000186F call ds:ReleaseMutex
10001875 test esi, esi ;檢查結果
10001877 jnz short OpenFileIsVirus ;如發現病毒則跳到OpenFileIsViru進一步處理
10001879 mov eax, [ebp+10h] ;事件對象的ring3句柄
1000187C mov byte ptr [ebp+16h], 0 ;設定元素中的結果位為無病毒
10001880 push eax
10001881 call ds:SetEvent ;設定事件對像為有信號態喚醒鉤子函數



3.4 WINNT/2000下的病毒既時監控

3.4.1實現技術詳解

WINNT/2000下病毒既時監控的實現主要依賴於NT內核模式驅動編程,攔截IRP,驅動與ring3下客戶程序的通信(命名的事件與信號量對像)三項技術。程序的設計思法和大體流程與前面介紹的WIN9X下病毒既時監控非常相似,只是在實現技術由於執行環境的不同將呈現很大的區別。

WINNT/2000下不再支持VXD,我將在後面剖析的hooksys.sys其實是一種稱為NT內核模式設備驅動的驅動程式。這種驅動程式無論從其結構還是工作方式都與VXD有很大不同。比較而言,NT內核模式設備驅動的編寫比VXD難度更大:因為它要求編程者熟悉WINNT/2000的整體架構和執行機制,NT/2000是純32位微內核操作系統,與WIN9X有很大區別;能靈活使用內核資料結構,如驅動程式對象,設備對象,文件對象,IO請求包,執行體行程/線程塊,系統服務調度表等。另外編程者在編程時還需注意許多重要事項,如當前系統執行的IO請求級,分頁/非分頁記憶體等。

這裡首先介紹幾個重要的內核資料結構,它們在NT內核模式設備驅動的編程中經常被用到,包括文件對象,驅動程式對象,設備對象,IO請求包(IRP),IO堆棧單元(IO_STACK_LOCATION):

文件明顯符合NT中的對象標準:它們是兩個或兩個以上用戶態行程的線程可以共享的系統資源;它們可以有名稱;它們被關於對象的安全性所保護;並且它們支持同步。對於用戶態受保護的子系統,文件對像通常代表一個文件,設備目錄,或磁碟區的開啟實例;而對於設備和中間型驅動,文件對像通常代表一個設備。文件對像結構中的域大部分是透明的驅動可以訪問的域包括:

PDEVICE_OBJECT DeviceObject:指向文件於其上被開啟的設備對象的游標。

UNICODE_STRING FileName:在設備上被開啟的文件的名字,如果當由DeviceObject代表的設備被開啟時此串長度(FileName.Length)為0。

驅動程式對像代表可裝載的內核模式驅動的映像,當驅動被載入至系統中時,有I/O管理器負責新增。指向驅動程式對象的游標將作為一個輸入參數傳送到驅動的啟始化例程(DriverEntry),再啟始化例程(Reinitialize routines)和卸載例程(Unload routine)。驅動程式對像結構中的域大部分是透明的,驅動可以訪問的域包括:

PDEVICE_OBJECT DeviceObject:指向驅動新增的設備對象的游標。當在啟始化例程中成功使用IoCreateDevice後這個域將被自動更新。當驅動卸載時,它的卸載例程將使用此域和設備對像中NextDevice域使用IoDeleteDevice來清除驅動新增的每個設備對象。

PDRIVER_INITIALIZE DriverInit:由I/O管理器設定的啟始化例程(DriverEntry)入口位址。該例程負責新增驅動程式操作的每個設備的設備對象,需要的話還可以在設備名稱和設備對用戶態可見名稱間新增符號連接。同時它還把驅動程式各例程入口點填入驅動程式對像相應的域中。

PDRIVER_UNLOAD DriverUnload:驅動程式的卸載例程入口位址。

PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION+1]:一個或多個驅動程式調度例程入口位址陣列。每個驅動必須在此陣列中為驅動處理的IRP_MJ_XXX請求集設定至少一個調度入口,這樣所有的IRP_MJ_XXX請求都會 被I/O管理器匯入同一個調度例程。當然,驅動程式也可以為每個IRP_MJ_XXX請求設定獨立的調度入口。

當然,驅動程式中可能包含的例程將遠不止以上列出的。比如啟動I/O例程,中斷服務例程(ISR),中斷服務DPC例程,一個或多個完成例程,取消I/O例程,系統關閉通知例程,錯誤記錄例程。只不過我們將要剖析的hooksys.sys中只用到例程中很少一部分,故其餘的不予詳細介紹。

設備對像代表已裝載的驅動程式為之處理I/O請求的一個邏輯,虛擬或物理設備。每個NT內核模式驅動程式必須在它的啟始化例程中一次或多次使用IoCreateDevice來新增它支持的設備對象。例如tcpip.sys在其DriverEntry中就新增了3個共用此驅動的設備對像:Tcp,Udp,Ip。目前有一種比較流行的稱為WDM(Windows Driver Model)的驅動程式,在大多數情況下,其二進制映像可以相容WIN98和WIN2000(32位版本)。WDM與NT內核模式驅動程式的主要區別在於如何新增設備:在WDM驅動程式中,即插即用(PnP)管理器告知何時向系統中增加一個設備,或者從系統中刪除設備。WDM驅動程式有一個特殊的AddDevice例程,PnP管理器為共用該驅動的每個設備實例使用該函數;而NT內核模式驅動程式需要做大量額外的工作,它們必須探測自己的硬體,為硬體新增設備對像(通常在DriverEntry中),配置並啟始化硬體使其正常工作。設備程序對像結中的域大部分是透明的,驅動可以訪問的域包括:

PDRIVER_OBJECT DriverObject:指向代表驅動程式裝載映像的驅動程式對象的游標。

所有I/O都是通過I/O請求包(IRP)驅動的。所謂IRP驅動,是指I/O管理器負責在系統的非分頁記憶體中分配一定的空間,當接受用戶發出的指令或由事件引發後,將工作指令按一定的資料結構置於其中並傳遞到驅動程式的服務例程。換言之,IRP中包含了驅動程式的服務例程所需的資訊指令。IRP有兩部分組成:類BIOS部分(稱為標題)和一個或多個堆棧單元。類BIOS部分資訊包括:請求的類型和大小,是同步請求還是異步請求,用於緩衝I/O的指向緩衝區的游標和由於請求的進展而變化的狀態資訊。

PMDL MdlAddress:指向一個記憶體描述符表(MDL),該表描述了一個與該請求關聯的用戶模式緩衝區。如果頂級設備對象的Flags域為DO_DIRECT_IO,則I/O管理器為IRP_MJ_READ或IRP_MJ_WRITE請求新增這個MDL。如果一個IRP_MJ_DEVICE_CONTROL請求的控制程式碼指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,則I/O管理器為該請求使用的輸出緩衝區新增一個MDL。MDL本身用於描述用戶模式虛擬緩衝區,但它同時也含有該緩衝區鎖定記憶體頁的物理位址。

PVOID AssociatedIrp.SystemBuffer:SystemBuffer游標指向一個資料緩衝區,該緩衝區位於內核模式的非分頁記憶體中於IRP_MJ_READ和IRP_MJ_WRITE操作,如果頂級設備指定DO_BUFFERED_IO標誌I/O管理器就新增這個資料緩衝區。對於IRP_MJ_DEVICE_CONTROL操作,如果I/O控制功能程式碼指出需要緩衝區,則I/O管理器就新增這個資料緩衝區。I/O管理器把用戶模式程序傳送給驅動程式的資料複製到這個緩衝區,這也是新增IRP程序的一部分。這些資料可以是與WriteFile使用有關的資料,或者是DeviceIoControl使用中所謂的輸入資料。對於讀請求,設備驅動程式把讀出的資料填到這個緩衝區,然後I/O管理器再把緩衝區的內容複製到用戶模式緩衝區。對於指定了METHOD_BUFFERED的I/O控制操作,驅動程式把所謂的輸出資料放到這個緩衝區, 然後I/O管理器再把資料複製到用戶模式的輸出緩衝區。

IO_STATUS_BLOCK IoStatus:IoStatus(IO_STATUS_BLOCK)是一個僅包含兩個域的結構,驅動程式在最終完成請求時設定這個結構。IoStatus.Status域將收到一個NTSTATUS程式碼。

PVOID UserBuffer:對於METHOD_NEITHER方式的IRP_MJ_DEVICE_CONTROL請求,該域包含輸出緩衝區的用戶模式虛擬位址。該域還用於儲存讀寫請求緩衝區的用戶模式虛擬位址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO標誌的驅動程式,其讀寫例程通常不需要訪問這個域。當處理一個METHOD_NEITHER控制操作時,驅動程式能用這個位址新增自己的MDL。

任何內核模式程序在新增一個IRP時,同時還新增了一個與之關聯的IO_STACK_LOCATION結構陣列:陣列中的每個堆棧單元都對應一個將處理該IRP的驅動程式,另外還有一個堆棧單元供IRP的新增者使用。堆棧單元中包含該IRP的類型程式碼和參數資訊以及完成函數的位址。

UCHAR MajorFunction:該IRP的主功能碼。這個程式碼應該為類似IRP_MJ_READ一樣的值,並與驅動程式對像中MajorFunction表的某個派遣函數游標相對應。

UCHAR MinorFunction:該IRP的副功能碼。它進一步指出該IRP屬於哪個主功能類。

PDEVICE_OBJECT DeviceObject:與該堆棧單元對應的設備對象的位址。該域由IoCallDriver函數負責填寫。

PFILE_OBJECT FileObject:內核文件對象的位址,IRP的目標就是這個文件對象。

下面簡要介紹一下WINNT/2000下I/O請求處理流程。先看對單層驅動程式的同步的I/O請求:I/O請求經過子系統DLL子系統DLL使用I/O管理器中相應的服務。I/O管理器以IRP的形式給設備驅動程式傳送請求。驅動程式啟動I/O操作。在設備完成了操作並且中斷CPU時,設備驅動程式服務於中斷。最後I/O管理器完成I/O請求。以上六步只是一個非常粗略的描述,其中的中斷處理和I/O完成階段比較複雜。

當設備完成了I/O操作後,它將發出中斷請求服務。設備中斷髮生時,處理器將控制權交給內核陷阱處理程序,內核陷阱處理程序將在它的中斷調度表(IDT)中定位用於設備的ISR。驅動程式的ISR例程獲得控制權後,它通常只在設備IRQL上停留獲得裝置狀態所必需的一段時間,然後停止設備中斷,接著它排隊一個DPC並清除中斷退出操作。IRQL降低至Dispatch/DPC級之前,所有中間優先級中斷因而可以得到服務。當DPC例程得到控制時,它將啟動設備貯列中下一個I/O請求,然後完成中斷服務。

當驅動的DPC例程執行完後,在I/O請求可以考慮結束之前還有一些工作要做。如某些情況下,I/O系統必須將存儲在系統記憶體中的資料複製到使用者的虛擬位址空間中,如將操作結果記錄在使用者提供的I/O狀態塊中或執行緩衝I/O的服務將資料返回給使用線程。這樣當DPC例程使用I/O管理器完成原始I/O請求後,I/O管理器會為使用線程使用線程排隊一個核心態APC。當線程被調度執行時,掛起的APC被交付。它將把資料和返回狀態複製到使用者的位址空間,釋放代表I/O操作的IRP,並將使用者的文件句柄或使用者提供的事件或I/O完成連接阜設定為有信號狀態。如果使用者用異步I/O函數ReadFileEx和WriteFileEx指定了用戶態APC,則此時還需要將用戶態APC排隊。最後可以考慮完成I/O。在文件或其它對像句柄上等待的線程將被釋放。

關於文件系統設備的I/O請求處理程序與此是基本相同的,主要區別在於增加一個或多個附加的處理層。例如讀文件操作,用戶應用程式使用子系統庫Kernel32.dll中的API函數ReadFile,ReadFile接著使用系統庫Ntdll.dll中的NtReadFile,NtReadFile通過一個陷入指令(INT2E)將處理器模式提升至ring0。然後Ntoskrnl.exe中的系統服務調度程序KiSystemService將在系統服務調度表中定位Ntoskrnl.exe中的NtWReadFile並使用之,同時解除中斷。此服務例程是I/O管理器的一部分。它首先檢查傳遞給它們的參數以保護系統安全或防止用戶模式程序非法存取資料,然後新增一個主功能程式碼為IRP_MJ_READ的IRP,並將之送到文件系統驅動程式的入口點。以下的工作會由文件系統驅動程式與磁牒驅動程式分層來完成。文件系統驅動程式可以重用一個IRP或是針對單一的I/O請求新增一組並行工作的關聯(associated)IRP。執行IRP的磁牒驅動程式最後可能會訪問硬體。對於PIO方式的設備,一個IRP_MJ_READ操作將導致直接讀取設備的連接阜或者是設備實現的記憶體暫存器。儘管執行在內核模式中的驅動程式可以直接與其硬體會話,但它們通常都使用硬體抽像層(HAL)訪問硬體:讀操作最終會使用Hal.dll中的READ_PORT_UCHAR例程來從某個I/O口讀取單字元資料。

WINNT/2000下設備和驅動程式的有著明顯堆棧式層次結構:處於堆棧最底層的設備對像稱為物理設備對象,或簡稱為PDO,與其對應的驅動程式稱為總線驅動程式。在設備對像堆棧的中間某處有一個對像稱為功能設備對象,或簡稱FDO,其對應的驅動程式稱為功能驅動程式。在FDO的上面和下面還會有一些過濾器設備對象。位於FDO上面的過濾器設備對像稱為上層過濾器,其對應的驅動程式稱為上層過濾器驅動程式;位於FDO下面(但仍在PDO之上)的過濾器設備對像稱為下層過濾器,其對應的驅動程式稱為下層過濾器驅動程式。這種棧式結構可以使I/O請求程序更加明瞭。每個影響到設備的操作都使用IRP。通常IRP先被送到設備堆棧的最上層驅動程式,然後逐漸過濾到下面的驅動程式。每一層驅動程式都可以決定如何處理IRP。有時,驅動程式不做任何事,僅僅是向下層傳遞該IRP。有時,驅動程式直接處理完該IRP,不再向下傳遞。還有時,驅動程式既處理了IRP,又把IRP傳遞下去。這取決於設備以及IRP所攜帶的內容。

通過上面的介紹可得知:如果我們想攔截系統的文件操作,就必須攔截I/O管理器發向文件系統驅動程式的IRP。而攔截IRP最簡單的方法莫過於新增一個上層過濾器設備對象並將之加入文件系統設備所在的設備堆棧中。具體方法如下:首先通過IoCreateDevice新增自己的設備對象,然後使用IoGetDeviceObjectPointer來得到文件系統設備(Ntfs,Fastfat,Rdr或Mrxsmb,Cdfs)對象的游標,最後通過IoAttachDeviceToDeviceStack將自己的設備放到設備堆棧上成為一個過濾器。

這是攔截IRP最常用也是最保險的方法,Art Baker的《Windows NT設備驅動程式設計指南》中有詳細介紹,但用它實現病毒既時監控卻存在兩個問題:其一這種方法是將過濾器放到堆棧的最上層,當存在其它上層過濾器時就不能保證過濾器正好在文件系統設備之上;其二由於過濾器設備需要表現的和文件系統設備一樣,這樣其所有特性都需從文件系統設備中複製。另外文件系統驅動對像中調度例程過濾器驅動必須都支持,這就意味著我們無法使過濾器驅動中的調度例程供自己的ring3級客戶程序所專用,因為原本發往文件系統驅動調度例程的IRP現在都會先從過濾器驅動的調度例程中經過。

所以Hooksys.sys沒有使用上述方法。它的方法更簡單且更為直接:它先通過ObReferenceObjectByName得到文件系統驅動對象的游標。然後將驅動對像中MajorFunction陣列中的開啟,關閉,清除,設定文件資訊,和寫入調度例程入口位址改為Hooksys.sys中相應鉤子函數的入口位址來達到攔截IRP的目的。具體操作細節請參看程式碼剖析一節。

下面介紹驅動與ring3下客戶程序的通信技術。與WIN9X下驅動與ring3下客戶程序通信技術相同,NT/2000仍然支持使用DeviceIoControl實現從ring3到ring0的單向通信,但從ring0通過排隊APC來喚醒ring3線程的方法卻無法使用了。原因是我沒有找到一個公開的函數來實現(Walter Oney的書中說存在一個未公開的函數實現從ring0排隊APC)。其實不通過APC我們也可以通過命名的事件/信號量對像來實現雙向喚醒,而且這可能比APC更為可靠些。

對像管理器在Windows NT/2000內核中佔了極其重要的位置,其一個最主要職能是組織管理系統內核對象。在Windows NT/2000中,內核對像管理器大量引入了C++面向對象的思想,即所有內核對象都封裝在對像管理器內部,除對像管理器自己以外,對其他所有想引用內核對像結構成員的子系統都是不透明的,也即都需通過對像管理器訪問這些結構。Microsoft極力推薦內核驅動程式碼遵循這一原則(用戶態程式碼根本不能直接訪問這些資料),它提供了一系列以Ob開頭的例程供我們使用。

內核已命名對像存於系統全局命名內核區,與傳統的DOS目錄和文件組織方式相似,對像管理器也採用樹狀結構管理這些對象,這樣可以快速檢索內核對象。當然使用這種樹狀結構組織內核已命名對象,還有另一個優點,那就是使所有已命名對像組織的十分有條理,如設備對像處於\Device下,而對像類型名稱處於\ObjectTypes下等等。再者這樣也能達到使用戶態行程僅能訪問\??與\BaseNamedObjects下的對象,而內核態程式碼則沒有任何限制的目的。至於系統內部如何組織管理這些已命名對象,其實Windows NT/2000內部由內核變數ObpRootDirectoryObject指向的Directory對像代表根目錄,使用哈希表(HashTable)來組織管理這些命名內核對象。

Hooksys.sys中使用命名的信號量來喚醒ring3級線程。具體做法如下:首先在guidll.dll中使用CreateSemaphore新增一個命名信號量Hookopen並設為無信號狀態,同時使用CreateThread新增一個線程。線程程式碼的入口處通過使用WaitForSingleObject在此信號量上等待被ring0鉤子函數喚醒查毒。驅動程式這邊則在啟始化程序中通過未公開的例程ObReferenceObjectByName(\BaseNamedObjects\Hookopen)得到命名信號量對像Hookopen的游標,當它攔截到文件開啟請求時使用KeReleaseSemaphore將Hookopen置為有信號狀態喚醒ring3級等待檢查開啟文件的線程。其實guidll.dll共新增了兩個命名信號量,還有一個Hookclose用於喚醒ring3級等待檢查關閉文件的線程。

guidll.dll中使用命名的事件來喚醒暫時掛起等待查毒完畢的ring0鉤子函數。具體做法如下:Hooksys.sys在其啟始化程序中通過ZwCreateEvent函數新增一組命名事件對像(此處必須合理設定安全描述符,否則ring3線程將無法使用事件句柄)並得到其句柄,同時通過ObReferenceObjectByHandle得到句柄引用的事件對象的游標。然後Hooksys.sys將這一組事件句柄和游標對以及事件名儲存在備用鏈表的每個元素中:ring3使用句柄,ring0使用游標。當鉤子函數攔截到文件請求時它首先喚醒ring3查毒線程,然後馬上使用KeWaitForSingleObject在一個事件\BaseNamedObjects\Hookxxxx上等待查毒的完成。而被喚醒的ring3查毒線程通過OpenEventA函數由事件名字得到其句柄,在結束查毒後發出一個SetEvent使用將事件置為有信號狀態從而喚醒ring0掛起的鉤子函數。當然,以上討論僅限於開啟文件操作,鉤子函數在攔截到其它文件請求時並不使用KeWaitForSingleObject等待查毒的完成,而是喚醒ring3查毒線程後直接返回;相應的ring3查毒線程也就不必在查毒完成後使用SetEvent進行遠端喚醒。

另外在編寫NT內核模式驅動程式時還必須注意一些事項。首先是中斷請求級(IRQL),這是在進行NT驅動編程時特別值得注意的問題。每個內核例程都要求在一定的IRQL上執行,如果在使用時不能確定當前IRQL在哪個級別,則可使用KeGetCurrentIrql獲取當前的IRQL值並進行判斷。例如欲獲得指向當前行程Eprocess的游標可以考慮先判斷當前的IRQL,如大於等於DISPATCH_LEVEL時可使用IoGetCurrentProcess;而當IRQL小於調度/延遲程序使用級別時(DISPATCH_LEVEL/DPC)則可使用PsGetCurrentProcessId和PsLookupProcessByProcessId。其次要注意的問題是分頁/非分頁記憶體。由於執行在提升的IRQL級上時系統將不能處理頁故障,因為系統在APC級處理頁故障,因而這裡總的原則是:執行在高於或等於DISPATCH_LEVEL級上的程式碼絕對不能造成頁故障。這也意味著執行在高於或等於DISPATCH_LEVEL級上的程式碼必須存在於非分頁記憶體中。此外,所有這些程式碼要訪問的資料也必須存在於非分頁記憶體中。最後是同步互斥問題,這對於如病毒既時監控等系統範圍共享的驅動程式尤顯重要。雖然在Hooksys中沒有新增多線程(PsCreateSystemThread),但由於它掛接了系統檔案鉤子,系統中所有線程的文件請求都會從Hooksys中經過。當一個線程的文件請求被處理程序中Hooksys會去訪問一些全局共享的資料,如過濾器,歷史記錄等,有可能在訪問進行到一半時該線程由於某種原因被搶佔了,結果是其它線程的文件請求經過時Hooksys訪問的共享資料將是錯誤的。為此驅動程式必須合理使用自旋鎖,互斥量,資源等內核同步對像對共享全局資料的所有線程進行同步。

3.4.2程序結構與流程

以下的程序結構與流程分析來自一著名反病毒軟體的WINNT/2000既時監控NT內核模式設備驅動程式Hooksys.sys:

1.啟始化例程(DriverEntry):使用_GetProcessNameOffset取得行程名在Eprocess中的偏移。啟始化備用,開啟文件等待操作,關閉文件,歷史記錄5個雙向循環鏈表及用於鏈表操作互斥的4把自旋鎖和1個快速互斥量。將全局變數_IrqCount(IRP記數)設定為0。新增卸載保護用事件對象。為檔案名過濾陣列啟始化同步用資源變數。在系統全局命名內核區中檢索Hookopen和Hookclose兩個命名信號量( _CreateSemaphore)。為備用(_AllocateBuffer)和歷史記錄(_AllocatHistoryBuf)鏈表在系統非分頁池中分配空間,同時新增一組命名事件對像Hookxxxx並儲存至備用鏈表的每個元素中(_CreateOneEvent)。新增設備,設定驅動例程入口,為設備建立符號連接。新增磁碟機設備對像游標(_QuerySymbolicLink)和文件系統驅動程式對像游標(_HookSys)列表。

2.開啟例程(IRP_MJ_CREATE):將備用鏈表用系統非分頁記憶體(首位址儲存在_SysBufAddr中)映射到用戶空間中(儲存在_UserBufAddr)以便從用戶態可以直接訪問這段記憶體(_MapMemory)。

3.設備控制例程(IRP_MJ_DEVICE_CONTROL):它會從入口IRP當前堆棧單元中取得用戶程序利用DeviceIoControl傳送進來的IO控制程式碼(IoControlCode),以此判斷用戶程序的意圖。和Hooksys.sys協同工作的ring3級客戶程序guidll.dll會依次向Hooksys.sys傳送IO控制請求來完成一系列工作,具體次序和程式碼含義如下:

83003C2F:將guidll取得的驅動器類型值傳給驅動(儲存在DriverType變數中),根據此變數值的不同,設定不同的等待(KeWaitForSingleObject)超時值,因為非類BIOS驅動器的讀寫時間會稍長些。

83003C0F:儲存guidll傳送的用戶指定的攔截文件的類型,其實這個類型過濾器在查毒模組中已存在,這裡再設定顯然是為了提高處理效率:它確保不會將非指定類型文件送到ring3級查毒模組,節省了通信的預先配置。經過解析的各檔案類型過濾塊游標將儲存在_gaFileNameFilterArra陣列中,同時更新過濾項個數_gNumOfFilters變數的值。

83003C13:修改文件系統驅動程式對像調度例程入口,啟動攔截文件操作的鉤子函數的工作。

83003C17:恢覆文件系統驅動程式原調度例程入口,停止攔截文件操作的鉤子函數工作。

以上列出的IO控制程式碼的發出是類BIOS,而當鉤子函數啟動後,還會發出一些隨機的控制程式碼:

83003C07:驅動將開啟文件鏈表的頭元素即最先的請求開啟的文件刪除並插入到等待鏈表尾部,同時將元素的用戶空間位址傳送至ring3級等待查殺開啟文件的線程中處理。

83003C0B:驅動將關閉文件鏈表的頭元素即最先的請求關閉的文件刪除並插入到備用鏈表尾部,同時將元素中的檔案名串傳送至ring3級等待查殺關閉文件的線程中處理

83003C1F:當查得關閉文件是病毒時,更新歷史記錄鏈表。

下面介紹鉤子函數_HookCreateDispatch和guidll中等待查殺開啟文件的線程協同工作流程,而關閉,清除,設定文件資訊,和寫入操作的處理與此大同小異:

當文件請求進入鉤子函數_HookCreateDispatch後,它首先從入口IRP中定位當前的堆棧單元並從中取得代表此次請求的文件對象。然後判斷當前行程是否為我們自己,若是則必須放過去,因為查毒模組中要頻繁的進行文件操作,所以攔截來自ravmon的文件請求將導致嚴重的系統死鎖。接下來利用堆棧單元中的文件對像取得完整的文件路徑名並確保文件不是:\PIPE\,\IPC。之後尋找歷史記錄鏈表以確定該檔案是否最近曾被檢查並記錄過,若在歷史記錄鏈表中找到關於該檔案的記錄並且記錄未失效即其時間戳和當前系統時間之差不得大於1F4h,則可直接從記錄中讀取查毒結果。如歷史鏈表中沒有該檔案的記錄則利用儲存的檔案類型過濾陣列檢查文件是否在被攔截的檔案類型之列。至此才進入真正的檢查開啟文件函數_RAVCheckOpenFile,此函數入口處先從備用,等待或關閉鏈表頭部摘得一空閒元素(_GetFreeEntry)並填充之,如文件路徑名域等。接著將空閒元素加入開啟文件鏈表尾部並釋放Hookopen信號量喚醒ring3下等待檢查開啟文件的線程。然後使用KeWaitForSingleObject在空閒元素中儲存的一個事件對像上等待ring3查毒的完成。當鉤子函數掛起後,ring3查毒線程得到執行:它會向驅動發出一IO控制碼為83003C07的請求以取得開啟文件鏈表頭元素即儲存最先提交而未決的文件請求,驅動會將元素映射到用戶空間中的偏移位址直接傳給它。接著它使用RsEngine.dll中的fnScanOneFile函數進行查毒並在元素中設定查毒結果位,完畢後再對元素中儲存的事件對像使用SetEvent喚醒在此事件上等待的鉤子函數。被喚醒的鉤子函數檢查被ring3查毒程式碼設定的結果位以此決定該檔案請求是被採納即使用儲存的原調度例程還是被取消即使用IofCompleteRequest直接返回,同時增加歷史記錄。

以上只是鉤子函數與ring3線程流程的一個簡單介紹,其中省略了諸如判斷類BIOS驅動器,超時等內容,具體細節請參看guidll.dll和hooksys.sys的反彙編程式碼註釋。

4.關閉例程(IRP_MJ_CLOSE):停止鉤子函數工作,恢覆文件系統驅動程式原調度入口(_StopFilter)。解除到用戶空間的記憶體映射。

5.卸載例程(DriverUnload):停止鉤子函數工作,恢覆文件系統驅動程式原調度入口。刪除設備和符號連接。刪除啟始化時新增的一組命名事件對像Hookxxxx,包括解除游標引用,關閉開啟的句柄。釋放為MDL(_pMdl),備用鏈表(_SysBufAddr),歷史記錄鏈表(_HistoryBuf)和過濾器分配的記憶體空間。刪除為檔案名過濾陣列訪問同步設定的資源變數(_FilterResource)。解除對系統全局命名內核區中Hookopen和Hookclose兩個命名信號量的游標引用。

3.4.3HOOKSYS.SYS逆向工程程式碼剖析

3.4.3.1取得當前行程名稱程式碼

啟始化例程中取得行程名在Eprocess中偏移

00011889 call ds:__imp__IoGetCurrentProcess@0 ;
得到當前行程System的Eprocess游標
0001188F mov edi, eax ;Eprocess基位址
00011891 xor esi, esi ;啟始化偏移為0
00011893 lea eax, [esi+edi] ;掃瞄游標
00011896 push 6 ;行程名長度
00011898 push eax ;掃瞄游標
00011899 push offset $SG8452 ; "System" ;行程名串
0001189E call ds:__imp__strncmp ;比較掃瞄游標處是否為行程名
000118A4 add esp, 0Ch ;恢復堆棧
000118A7 test eax, eax ;測試比較結果
000118A9 jz short loc_118B9 ;找到則跳出循環
000118AB inc esi ;增加偏移量
000118AC cmp esi, 3000h ;在12K範圍中掃瞄
000118B2 jb short loc_11893 ;在範圍之內則繼續比較
鉤子函數開始處取得當前行程名

00010D1E call ds:__imp__IoGetCurrentProcess@0 ;得到當前行程System的Eprocess游標
00010D24 mov ecx, _ProcessNameOffset ;取得儲存的行程名偏移量
00010D2A add eax, ecx ;得到指向行程名的游標



3.4.3.2啟動鉤子函數工作程式碼

000114F4 push 4 ;預先將文件系統驅動對像個數壓棧
000114F6 mov esi, offset FsDriverObjectPtrList ;
取得文件系統驅動對像游標列表偏移位址
000114FB pop edi ;用EDI做記數器,初始值為4
000114FC mov eax, [esi] ;取得第一個驅動對象的游標
000114FE test eax, eax ;測試是否合法
00011500 jz short loc_11548 ;不合法則繼續下一個修改驅動對像
00011502 mov edx, offset _HookCreateDispatch@8 ;
取得自己的鉤子函數的偏移位址
00011507 lea ecx, [eax+38h] ;取得對像中開啟調度例程(IRP_MJ_CREATE)偏移
0001150A call @InterlockedExchange@8 ;
原子操作,替換驅動對像中開啟調度例程的入口為鉤子函數的偏移位址
0001150F mov [esi-10h], eax ;儲存原開啟調度例程的入口



3.4.3.3映射系統記憶體至用戶空間程式碼

0001068E push esi ;系統記憶體大小
0001068F push _SysBufAddr ;系統記憶體基位址
00010695 call ds:__imp__MmSizeOfMdl@8 ;計算描述系統記憶體所需記憶體描述符表(MDL)大小
0001069B push 206B6444h ;偵錯用標籤
000106A0 push eax ;MDL大小
000106A1 push 0 ;在系統非分頁記憶體池中分配
000106A3 call ds:__imp__ExAllocatePoolWithTag@12 ;為MDL分配記憶體
000106A9 push esi ;系統記憶體大小
000106AA mov _pMdl, eax ;儲存MDL游標
000106AF push _SysBufAddr ;系統記憶體基位址
000106B5 push eax ;MDL游標
000106B6 call ds:__imp__MmCreateMdl@12 ;啟始化MDL
000106BC push eax ;MDL游標
000106BD mov _pMdl, eax ;儲存MDL游標
000106C2 call ds:__imp__MmBuildMdlForNonPagedPool@4
;填寫MDL後物理頁面陣列
000106C8 push 1 ;訪問模式
000106CA push _pMdl ;MDL游標
000106D0 call ds:__imp__MmMapLockedPages@8 ;映射MDL描述的實體記憶體頁面
......
000106DB mov _UserBufAddr, eax ;儲存映射後的用戶空間位址
_UserBufAddr 和_SysBufAddr映射到相同的物理位址。



主要參考文獻

David A. Solomon, Mark Russinovich 《Inside Microsoft Windows 2000》September 2000

David A. Solomon 《Inside Windows NT》 May 1998

Prasad Dabak,Sandeep Phadke,Milind Borate 《Undocumented Windows NT》October 1999

Matt Pietrek 《Windows 95 System Programming Secrets》 March 1996

Walter Oney 《System Programming for Windows 95》 March 1996

Walter Oney 《Programming the Windows Driver Model》 1999

陸麟 《WINDOWS9X文件讀寫Internal》2001
psac 目前離線  
送花文章: 3, 收花文章: 1631 篇, 收花: 3205 次
 



發表規則
不可以發文
不可以回覆主題
不可以上傳附加檔案
不可以編輯您的文章

論壇啟用 BB 語法
論壇啟用 表情符號
論壇啟用 [IMG] 語法
論壇禁用 HTML 語法
Trackbacks are 禁用
Pingbacks are 禁用
Refbacks are 禁用


所有時間均為台北時間。現在的時間是 09:42 PM


Powered by vBulletin® 版本 3.6.8
版權所有 ©2000 - 2024, Jelsoft Enterprises Ltd.


SEO by vBSEO 3.6.1