反病毒引擎設計[轉]

本文將對當今先進的病毒/反病毒技術做全面而細緻的介紹,重點當然放在了反病毒上,特別是虛擬機和實時監控技術。文中首先介紹幾種當今較爲流行的病毒技術,包括獲取系統核心態特權級,駐留,截獲系統操作,變形和加密等。然後分五節詳細討論虛擬機技術:第一節簡單介紹一下虛擬機的概論;第二節介紹加密變形病毒,作者會分析兩個著名變形病毒的解密子;第三節是虛擬機實現技術詳解,其中會對兩種不同方案進行比較,同時將剖析一個查毒用虛擬機的總體控制結構;第四節主要是對特定指令處理函數的分析;最後在第五節中列出了一些反虛擬執行技術做爲今後改進的參照。論文的第三章主要介紹實時監控技術,由於win9x和winnt/2000系統機制和驅動模型不同,所以會分成兩個操作系統進行討論。其中涉及的技術很廣泛:包括驅動編程技術,文件鉤掛,特權級間通信等等。本文介紹的技術涉及操作系統底層機制,難度較大。所提供的代碼,包括一個虛擬機C語言源代碼和兩個病毒實時監控驅動程序反彙編代碼,具有一定的研究和實用價值。
關鍵字:病毒,虛擬機,實時監控
文檔內容目錄
1.緒 論

1. 1課題背景

1.2當今病毒技術的發展狀況

1.2.1系統核心態病毒

1.2.2駐留病毒

1.2.3截獲系統操作

1.2.4加密變形病毒

1.2.5反跟蹤/反虛擬執行病毒

1.2.6直接API調用

1.2.7病毒隱藏

1.2.8病毒特殊感染法

2.虛擬機查毒

2.1虛擬機概論

2. 2加密變形病毒

2.3虛擬機實現技術詳解

2.4虛擬機代碼剖析

2.4.1不依賴標誌寄存器指令模擬函數的分析

2.4.2依賴標誌寄存器指令模擬函數的分析

2.5反虛擬機技術

3.病毒實時監控

3.1實時監控概論

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

3.3WIN9X下的病毒實時監控

3.3.1實現技術詳解

3.3.2程序結構與流程

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

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

3.4.1實現技術詳解

3.4.2程序結構與流程

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

結論

致謝

主要參考文獻


1.緒 論
本論文研究的主要內容正如其題目所示是設計並編寫一個先進的反病毒引擎。首先需要對這“先進”二字做一個解釋,何爲“先進”?衆所周知,傳統的反病毒軟件使用的是基於特徵碼的靜態掃描技術,即在文件中尋找特定十六進制串,如果找到,就可判定文件感染了某種病毒。但這種方法在當今病毒技術迅猛發展的形勢下已經起不到很好的作用了。原因我會在以下的章節中具體描述。因此本論文將不對殺毒引擎中的特徵碼掃描和病毒代碼清除模塊做分析。我們要討論的是爲應付先進的病毒技術而必需的兩大反病毒技術--虛擬機和實時監控技術。具體什麼是虛擬機,什麼是實時監控,我會在相應的章節中做詳盡的介紹。這裏我要說明的一點是,這兩項技術雖然在前人的工作中已有所體現(被一些國內外先進的反病毒廠家所使用),但出於商業目的,這些技術並沒有被完全公開,所以你無論從書本文獻還是網路上的資料中都無法找到關於這些技術的內幕。而我會在相關的章節中剖析大量的程序源碼(主要是2.4節中的一個完整的虛擬機源碼)或是逆向工程代碼(3.3.3節和3.4.3節中三個我逆向工程的某著名反病毒軟件的實時監控驅動程序及客戶程序的反彙編代碼),並同時公佈一些我個人挖掘的操作系統內部未公開的機制和數據結構。另外我在文中會大量地提到或引用一些關於系統底層奧祕的大師級經典圖書,這算是給喜愛系統級編程但又苦於找不到合適教材的朋友開了一份書單。下面就開始進入論文的正題。

1.1課題背景
本論文涉及的兩個主要技術,也是當今反病毒界使用的最爲先進的技術中的兩個,究竟是作何而用的呢?首先說說虛擬機技術,它主要是爲查殺加密變形病毒而設計的。簡單地來說,所謂虛擬機並不是個虛擬的機器,說得更合適一些應該是個虛擬CPU(用軟件實現的CPU),只不過病毒界都這麼叫而已。它的作用主要是模擬INTEL X86 CPU的工作過程來解釋執行可執行代碼,與真正的CPU一樣能夠取指,譯碼並執行相應機器指令規定的操作。當然什麼是加密變形病毒,它們爲什麼需要被虛擬執行以及怎樣虛擬執行等問題會在合適的章節中得到解答。再說另一個重頭戲--實時監控技術,它的用處更爲廣泛,不僅侷限於查殺病毒。被實時監控的對象也很多,如中斷(Intmon),頁面錯誤(Pfmon),磁盤訪問(Diskmon)等等。用於殺毒的監控主要是針對文件訪問,在你要對一個文件進行訪問時,實時監控會先檢查文件是否爲帶毒文件,若是,則由用戶選擇是清除病毒還是取消此次操作請求。這樣就給了用戶一個相對安全的執行環境。但同時,實時監控會使系統性能有所下降,不少殺毒軟件的用戶都抱怨他們的實時監控讓系統變得奇慢無比而且不穩定。這就給我們的設計提出了更高的要求,即怎樣在保證準確攔截文件操作的同時,讓實時監控佔用的系統資源更少。我會在病毒實時監控一節中專門討論這個問題。這兩項技術在國內外先進的反病毒廠家的產品中都有使用,雖然它們的源代碼沒有公開,但我們還是可以通過逆向工程的方法來窺視一下它們的設計思路。其實你用一個十六進制編輯器來打開它們的可執行文件,也許就會看到一些沒有剝掉的調試符號、變量名字或輸出信息,這些蛛絲馬跡對於理解代碼的意圖大有裨益。同時,在反病毒軟件的安裝目錄中後綴爲.VXD或.SYS就是執行實時監控的驅動程序,可以拿來逆向一下(參看我在後面分析驅動源代碼中的討論)。相信至此,我們對這兩項技術有了一個大體的瞭解。後面我們將深入到技術的細節中去。

1.2當今病毒技術的發展狀況
要討論怎樣反病毒,就必須從病毒技術本身的討論開始。正是所謂“知己知彼,百戰不殆”。其實,我認爲目前規定研究病毒技術屬於違法行爲存在着很大的弊端。很難想象一個毫無病毒寫作經驗的人會成爲殺毒高手。據我瞭解,目前國內一些著名反病毒軟件公司的研發隊伍中不乏病毒寫作高手。只不過他們將同樣的技術用到了正道上,以‘毒’攻‘毒’。所以我希望這篇論文能起到拋磚引玉的作用,期待着有更多的人會將病毒技術介紹給大衆。當今的病毒與DOS和WIN3.1時代下的從技術角度上看有很多不同。我認爲最大的轉變是:引導區病毒減少了,而腳本型病毒開始氾濫。原因是在當今的操作系統下直接改寫磁盤的引導區會有一定的難度(DOS則沒有保護,允許調用INT13直接寫盤),而且引導區的改動很容易被發現,所以很少有人再寫了;而腳本病毒以其傳播效率高且容易編寫而深得病毒作者的青睞。當然由於這兩種病毒用我上面說過的基於特徵碼的靜態掃描技術就可以查殺,所以不在我們的討論之列。我要討論的技術主要來自於二進制外殼型病毒(感染文件的病毒),並且這些技術大都和操作系統底層機制或386以上CPU的保護模式相關,所以值得研究。大家都知道DOS下的外殼型病毒主要感染16位的COM或EXE文件,由於DOS沒有保護,它們能夠輕鬆地進行駐留,減少可用內存(通過修改MCB鏈),修改系統代碼,攔截系統服務或中斷。而到了WIN9X和WINNT/2000時代,想寫個運行其上的32位WINDOWS病毒絕非易事。由於頁面保護,你不可能修改系統的代碼頁。由於I/O許可位圖中的規定,你也不能進行直接端口訪問。在WINDOWS中你不可能象在DOS中那樣通過截獲INT21H來攔截所有文件操作。總之,你以一個用戶態程序運行,你的行爲將受到操作系統嚴格的控制,不可能再象DOS下那樣爲所欲爲了。另外值得一提的是,WINDOWS下采用的可執行文件格式和DOS下的EXE截然不同(普通程序採用PE格式,驅動程序採用LE),所以病毒的感染文件的難度增大了(PE和LE比較複雜,中間分了若干個節,如果感染錯了,將導致文件不能繼續執行)。因爲當今病毒的新技術太多,我不可能將它們逐一詳細討論,於是就選取了一些重要並具有代表性的在本章的各小節中進行討論。

1.2.1系統核心態病毒
在介紹什麼是系統核心態病毒之前,有必要討論一下核心態與用戶態的概念。其實只要隨便翻開一本關於386保護模式彙編程序設計的教科書,都可以找到對這兩個概念的講述。386及以上的CPU實現了4個特權級模式(WINDOWS只用到了其中兩個),其中特權級0(Ring0)是留給操作系統代碼,設備驅動程序代碼使用的,它們工作於系統核心態;而特權極3(Ring3)則給普通的用戶程序使用,它們工作在用戶態。運行於處理器核心態的代碼不受任何的限制,可以自由地訪問任何有效地址,進行直接端口訪問。而運行於用戶態的代碼則要受到處理器的諸多檢查,它們只能訪問映射其地址空間的頁表項中規定的在用戶態下可訪問頁面的虛擬地址,且只能對任務狀態段(TSS)中I/O許可位圖(I/O Permission Bitmap)中規定的可訪問端口進行直接訪問(此時處理器狀態和控制標誌寄存器EFLAGS中的IOPL通常爲0,指明當前可以進行直接I/O的最低特權級別是Ring0)。以上的討論只限於保護模式操作系統,象DOS這種實模式操作系統則沒有這些概念,其中的所有代碼都可被看作運行在覈心態。既然運行在覈心態有如此之多的優勢,那麼病毒當然沒有理由不想得到Ring0。處理器模式從Ring3向Ring0的切換髮生在控制權轉移時,有以下兩種情況:訪問調用門的長轉移指令CALL,訪問中斷門或陷阱門的INT指令。具體的轉移細節由於涉及複雜的保護檢查和堆棧切換,不再贅述,請參閱相關資料。現代的操作系統通常使用中斷門來提供系統服務,通過執行一條陷入指令來完成模式切換,在INTEL X86上這條指令是INT,如在WIN9X下是INT30(保護模式回調),在LINUX下是INT80,在WINNT/2000下是INT2E。用戶模式的服務程序(如系統DLL)通過執行一個INTXX來請求系統服務,然後處理器模式將切換到核心態,工作於核心態的相應的系統代碼將服務於此次請求並將結果傳給用戶程序。下面就舉例子說明病毒進入系統核心態的方法。

在WIN9X下進程虛擬地址空間中映射共享系統代碼的部分(3G--4G)中除了最上面4M頁表有頁面保護外其它地方可由用戶程序讀寫。如果你用Softice(系統級調試器)的PAGE命令查看這些地址的頁屬性,則你會驚奇地發現U RW位,這說明這些地址可從用戶態直接讀出或寫入。這意味着任何一個用戶程序都能夠在其運行過程中惡意或無意地破壞操作系統代碼頁。由此病毒就可以在GDT(全局描述符表),LDT(局部描述符表)中隨意構造門描述符並藉此進入核心態。當然,也不一定要藉助門描述,還有許多方法可以得到Ring0。據我所知的方法就不下10餘種之多,如通過調用門(Callgate),中斷門(Intgate),陷阱門(Trapgate),異常門(Fault),中斷請求(IRQs),端口(Ports),虛擬機管理器(VMM),回調(Callback),形式轉換(Thunks),設備IO控制(DeviceIOControl),API函數(SetThreadContext) ,中斷2E服務(NTKERN.VxD)。由於篇幅的限制我不可能將所有的方法逐一描述清楚,這裏我僅選取最具有代表性的CIH病毒1.5版開頭的一段代碼。

人們常說CIH病毒運用了VXD(虛擬設備驅動)技術,其實它本身並不是VXD。只不過它利用WIN9X上述漏洞,在IDT(中斷描述符表)中構造了一個DPL(段特權級)爲3的中斷門(意味着可以從Ring3下執行訪問該中斷門的INT指令),並使描述符指向自己私有地址空間中的一個需要工作在Ring0下的函數地址。這樣一來CIH就可以通過簡單的執行一條INTXX指令(CIH選擇使用INT3,是爲了使同樣接掛INT3的系統調試器Softice無法正常工作以達到反跟蹤的目的)進入系統核心態,從而調用系統的VMM和VXD服務。以下是我註釋的一段CIH1.5的源代碼:

  ; *************************************
  ; * 修改IDT以求得核心態特權級 *
  ; *************************************
  push eax
  sidt [esp-02h] ;取得IDT表基地址
  pop ebx
  add ebx, HookExceptionNumber*08h+04h ;ZF = 0
  cli ;讀取修改系統數據時先禁止中斷
  mov ebp, [ebx]
  mov bp, [ebx-04h] ;取得原來的中斷入口地址
  lea esi, MyExceptionHook-@1[ecx] ;取得需要工作在Ring0的函數的偏移地址
  push esi
  mov [ebx-04h], si
  shr esi, 16
  mov [ebx+02h], si ;設置爲新的中斷入口地址
  pop esi
  ; *************************************
  ; * 產生一個異常來進入Ring0 *
  ; *************************************
  int HookExceptionNumber ;產生一個異常
當然,後面還有恢復原來中斷入口地址和異常處理幀的代碼。


剛纔所討論的技術僅限於WIN9X,想在WINNT/2000下進入Ring0則沒有這麼容易。主要的原因是WINNT/2000沒有上述的漏洞,它們的系統代碼頁面(2G--4G)有很好的頁保護。大於0x80000000的虛擬地址對於用戶程序是不可見的。如果你用Softice的PAGE命令查看這些地址的頁屬性,你會發現S位,這說明這些地址僅可從核心態訪問。所以想在IDT,GDT隨意構造描述符,運行時修改內核是根本做不到的。所能做的僅是通過加載一個驅動程序,使用它來做你在Ring3下做不到的事情。病毒可以在它們加載的驅動中修改內核代碼,或爲病毒本身創建調用門(利用NT由Ntoskrnl.exe導出的未公開的系統服務KeI386AllocateGdtSelectors,KeI386SetGdtSelector,KeI386ReleaseGdtSelectors)。如Funlove病毒就利用驅動來修改系統文件(Ntoskrnl.exe,Ntldr)以繞過安全檢查。但這裏面有兩個問題,其一是驅動程序從哪裏來,現代病毒普遍使用一個稱爲“Drop”的技術,即在病毒體本身包含驅動程序二進制碼(可以進行壓縮或動態構造文件頭),在病毒需要使用時,動態生成驅動程序並將它們扔到磁盤上,然後馬上通過在SCM(服務控制管理器)註冊並最終調用StartService來使驅動程序得以運行;其二是加載一個驅動程序需要管理員身份,普通帳號在調用上述的加載函數時會返回失敗(安全子系統要檢查用戶的訪問令牌(Token)中有無SeLoadDriverPrivilege特權),但多數用戶在大多時候登錄時會選擇管理員身份,否則連病毒實時監控驅動也同樣無法加載,所以留給病毒的機會還是很多的。

1.2.2駐留病毒
駐留病毒是指那些在內存中尋找合適的頁面並將病毒自身拷貝到其中且在系統運行期間能夠始終保持病毒代碼的存在。駐留病毒比那些直接感染(Direct-action)型病毒更具隱蔽性,它通常要截獲某些系統操作來達到感染傳播的目的。進入了核心態的病毒可以利用系統服務來達到此目的,如CIH病毒通過調用一個由VMM導出的服務VMMCALL _PageAllocate在大於0xC0000000的地址上分配一塊頁面空間。而處於用戶態的程序要想在程序退出後仍駐留代碼的部分於內存中似乎是不可能的,因爲無論用戶程序分配何種內存都將作爲進程佔用資源的一部分,一旦進程結束,所佔資源將立即被釋放。所以我們要做的是分配一塊進程退出後仍可保持的內存。

病毒寫作小組29A的成員GriYo 運用的一個技術很有創意:他通過CreateFileMappingA 和MapViewOfFile創建了一個區域對象並映射它的一個視口到自己的地址空間中去,並把病毒體搬到那裏,由於文件映射所在的虛擬地址處於共享區域(能夠被所有進程看到,即所有進程用於映射共享區內虛擬地址的頁表項全都指向相同的物理頁面),所以下一步他通過向Explorer.exe中注入一段代碼(利用WriteProcessMemory來向其它進程的地址空間寫入數據),而這段代碼會從Explorer.exe的地址空間中再次申請打開這個文件映射。如此一來,即便病毒退出,但由於Explorer.exe還對映射頁面保持引用,所以一份病毒體代碼就一直保持在可以影響所有進程的內存頁面中直至Explorer.exe退出。

另外還可以通過修改系統動態連接模塊(DLL)來進行駐留。WIN9X下系統DLL(如Kernel32.dll 映射至BFF70000)處於系統共享區域(2G-3G),如果在其代碼段空隙中寫入一小段病毒代碼則可以影響其它所有進程。但Kernel32.dll的代碼段在用戶態是隻能讀不能寫的。所以必須先通過特殊手段修改其頁保護屬性;而在WINNT/2000下系統DLL所在頁面被映射到進程的私有空間(如Kernel32.dll 映射至77ED0000)中,並具有寫時拷貝屬性,即沒有進程試圖寫入該頁面時,所有進程共享這個頁面;而當一個進程試圖寫入該頁面時,系統的頁面錯誤處理代碼將收到處理器的異常,並檢查到該異常並非訪問違例,同時分配給引發異常的進程一個新頁面,並拷貝原頁面內容於其上且更新進程的頁表以指向新分配的頁。這種共享內存的優化給病毒的寫作帶來了一定的麻煩,病毒不能象在WIN9X下那樣僅修改Kernel32.dll一處代碼便可一勞永逸。它需要利用WriteProcessMemory來向每個進程映射Kernel32.dll的地址寫入病毒代碼,這樣每個進程都會得到病毒體的一個副本,這在病毒界被稱爲多進程駐留或每進程駐留(Muti-Process Residence or Per-Process Residence )。

1.2.3截獲系統操作
截獲系統操作是病毒慣用的伎倆。DOS時代如此,WINDOWS時代也不例外。在DOS下,病毒通過在中斷向量表中修改INT21H的入口地址來截獲DOS系統服務(DOS利用INT21H來提供系統調用,其中包括大量的文件操作)。而大部分引導區病毒會接掛INT13H(提供磁盤操作服務的BIOS中斷)從而取得對磁盤訪問的控制。WINDOWS下的病毒同樣找到了鉤掛系統服務的辦法。比較典型的如CIH病毒就是利用了IFSMGR.VXD(可安裝文件系統)提供的一個系統級文件鉤子來截獲系統中所有文件操作,我會在相關章節中詳細討論這個問題,因爲WIN9X下的實時監控也主要利用這個服務。除此之外,還有別的方法。但效果沒有這個系統級文件鉤子好,主要是不夠底層,會丟失一些文件操作。

其中一個方法是利用APIHOOK,鉤掛API函數。其實系統中並沒有現成的這種服務,有一個SetWindowsHookEx可以鉤住鼠標消息,但對截獲API函數則無能爲力。我們能做的是自己構造這樣的HOOK。方法其實很簡單:比如你要截獲Kernel32.dll導出的函數CreateFile,只須在其函數代碼的開頭(BFF7XXXX)加入一個跳轉指令到你的鉤子函數的入口,在你的函數執行完後再跳回來。如下圖所示:

;; Target Function(要截獲的目標函數)
  ……
  TargetFunction:(要截獲的目標函數入口)
  jmp DetourFunction(跳到鉤子函數,5個字節長的跳轉指令)
  TargetFunction+5:
  push edi
  ……
  ;; Trampoline(你的鉤子函數)
  ……
  TrampolineFunction:(你的鉤子函數執行完後要返回原函數的地方)
  push ebp
  mov ebp,esp
  push ebx
  push esi(以上幾行是原函數入口處的幾條指令,共5個字節)
  jmp TargetFunction+5(跳回原函數)
  ……
    但這種方法截獲的僅僅是很小一部分文件打開操作。

在WIN9X下還有一個鮮爲人知的截獲文件操作的辦法,說起來這應該算是WIN9X的一大後門。它就是Kernel32.dll中一個未公開的叫做VxdCall0的API函數。反彙編這個函數的代碼如下:

mov eax,dword ptr [esp+00000004h] ;取得服務代號

pop dword ptr [esp] ;堆棧修正

call fword ptr cs:[BFFC9004] ;通過一個調用門調用3B段某處的代碼

如果我們繼續跟蹤下去,則會看到:

003B:XXXXXXXX int 30h ;這是個用以陷入VWIN32.VXD的保護模式回調

有關VxdCall的詳細內容,請參看Matt Pietrek的《Windows 95 System Programming Secrets》。

當服務代號爲0X002A0010時,保護模式回調會陷入VWIN32.VXD中一個叫做VWIN32_Int21Dispatch的服務。這正說明了WIN9X還在依賴於MSDos,儘管微軟聲稱WIN9X不再依賴於MSDos。調用規範如下:

  my_int21h:push ecx
  push eax ;類似DOS下INT21H的AX中傳入的功能號
  push 002A0010h
  call dword ptr [ebp+a_VxDCall]
  ret
  我們可以將上面VxdCall0函數的入口處第三條遠調用指令訪問的Kernel32.dll數據段中用戶態可寫地址BFFC9004Υ媧⒌?FWORD'六個字節改爲指向我們自己鉤子函數的地址,並在鉤子中檢查傳入服務號和功能號來確定是否是請求VWIN32_Int21Dispatch中的某個文件服務。著名的HPS病毒就利用了這個技術在用戶態下直接截獲系統中的文件操作,但這種方法截獲的也僅僅是一小部分文件操作。

1.2.4加密變形病毒
加密變形病毒是虛擬機一章的重點內容,將放到相關章節中介紹。

1.2.5反跟蹤/反虛擬執行病毒
反跟蹤/反虛擬執行病毒和虛擬機聯繫密切,所以也將放到相應的章節中介紹。

1.2.6直接API調用
直接API調用是當今WIN32病毒常用的手段,它指的是病毒在運行時直接定位API函數在內存中的入口地址然後調用之的一種技術。普通程序進行API調用時,編譯器會將一個API調用語句編譯爲幾個參數壓棧指令後跟一條間接調用語句(這是指Microsoft編譯器,Borland編譯器使用JMP

DWORD PTR [XXXXXXXXh])形式如下:

  push arg1
  push arg2
  ……
  call dword ptr[XXXXXXXXh]
地址XXXXXXXXh在程序映象的導入(Import Section)段中,當程序被加載運行時,由裝入器負責向裏面添入API函數的地址,這就是所謂的動態鏈接機制。病毒由於爲了避免感染一個可執行文件時在文件的導入段中構造病毒體代碼中用到的API的鏈接信息,它選擇運用自己在運行時直接定位API函數地址的代碼。其實這些函數地址對於操作系統的某個版本是相對固定的,但病毒不能依賴於此。現在較爲流行的做法是先定位包含API函數的動態連接庫的裝入基址,然後在其導出段(Export Section)中尋找到需要的API地址。後面一步幾乎沒有難度,只要你熟悉導出段結構即可。關鍵在於第一步--確定DLL裝入地址。其實系統DLL裝入基址對於操作系統的某個版本也是固定的,但病毒爲確保其穩定性仍不能依賴這一點。目前病毒大都利用一個叫做結構化異常處理的技術來捕獲病毒體引發的異常。這樣一來病毒就可以在一定內存範圍內搜索指定的DLL(DLL使用PE格式,頭部有固定標誌),而不必擔心會因引發頁面錯誤而被操作系統殺掉。

由於異常處理和後面的反虛擬執行技術密切相關,所以特將結構化異常處理簡單解釋如下:

共有兩類異常處理:最終異常處理和每線程異常處理。

其一:最終異常處理

當你的進程中無論哪個線程發生了異常,操作系統將調用你在主線程中調用SetUnhandledExceptionFilter建立的異常處理函數。你也無須在退出時拆去你安裝的處理代碼,系統會爲你自動清除。

  PUSH OFFSET FINAL_HANDLER
  CALL SetUnhandledExceptionFilter
  ……
  CALL ExitProcess
  ;************************************
  FINAL_HANDLER:
  ……
  ;(eax=-1 reload context and continue)
  MOV EAX,1
  RET ;program entry point
  ……
  ;code covered by final handler
  ……
  ;code to provide a polite exit
  ……
  ;eax=1 stops display of closure box
  ;eax=0 enables display of the box
  其二:每線程異常處理

FS中的值是一個十六位的選擇子,它指向包含線程重要信息的數據結構TIB,線程信息塊。其的首雙字節指向我們稱爲ERR的結構:

1st dword +0 pointer to next err structure

(下一個err結構的指針)

2nd dword +4 pointer to own exception handler

(當前一級的異常處理函數的地址)

所以異常處理是呈練狀的,如果你自己的處理函數捕捉並處理了這個異常,那麼當你的程序發生了異常時,操作系統就不會調用它缺省的處理函數了,也就不會出現一個討厭的執行了非法操作的紅叉。

下面是cih的異常段:

MyVirusStart:
  push ebp
  lea eax, [esp-04h*2]
  xor ebx, ebx
  xchg eax, fs:[ebx] ;交換現在的err結構和前一個結構的地址
  ; eax=前一個結構的地址
  ; fs:[0]=現在的err結構指針(在堆棧上)
  call @0
  @0:
  pop ebx
  lea ecx, StopToRunVirusCode-@0[ebx] ;你的異常處理函數的偏移
  push ecx ;你的異常處理函數的偏移壓棧
  push eax ;前一個err結構的地址壓棧
  ;構造err結構,記這時候的esp(err結構指針)爲esp0
  ……
  StopToRunVirusCode:
  @1 = StopToRunVirusCode
  xor ebx, ebx ;發生異常時系統在你的練前又加了一個err結構,
                       ;所以要先找到原來的結構地址
  mov eax, fs:[ebx] ; 取現在的err結構的地址eax
  mov esp, [eax] ; 取下個結構地址即eps0到esp
  RestoreSE: ;沒有發生異常時順利的回到這裏,你這時的esp爲本esp0
  pop dword ptr fs:[ebx] ;彈出原來的前一個結構的地址到fs:0
  pop eax ;彈出你的異常處理地址,平棧而已
  1.2.7病毒隱藏
實現進程或模塊隱藏應該是一個成功病毒所必須具備的特徵。在WIN9X下Kernel32.dll有一個可以使進程從進程管理器進程列表中消失的導出函數RegisterServiceProcess ,但它不能使病毒逃離一些進程瀏覽工具的監視。但當你知道這些工具是如何來枚舉進程後,你也會找到對付這些工具相應的辦法。進程瀏覽工具在WIN9X下大都使用一個叫做ToolHelp32.dll的動態連接庫中的Process32First和Process32Next兩個函數來實現進程枚舉的;而在WINNT/2000裏也有PSAPI.DLL導出的EnumProcess可用以實現同樣之功能。所以病毒就可以考慮修改這些公用函數的部分代碼,使之不能返回特定進程的信息從而實現病毒的隱藏。

但事情遠沒有想象中那麼簡單,俗話說“道高一尺,魔高一丈”,此理不謬。由於現在很多逆項工程師的努力,微軟力圖隱藏的許多祕密已經逐步被人們所挖掘出來。當然其中就包括WINDOWS內核使用的管理進程和模塊的內部數據結構和代碼。比如WINNT/2000用由ntoskrnl.exe導出的內核變量PsInitialSystemProcess所指向的進程Eprocess塊雙向鏈表來描述系統中所有活動的進程。如果進程瀏覽工具直接在驅動程序的幫助下從系統內核空間中讀出這些數據來枚舉進程,那麼任何病毒也無法從中逃脫。

有關Eprocess的具體結構和功能,請參看David A.Solomon和Mark E.Russinovich的《Inside Windows2000》第三版。

1.2.8病毒特殊感染法
對病毒稍微有些常識的人都知道,普通病毒是通過將自身附加到宿主尾部(如此一來,宿主的大小就會增加),並修改程序入口點來使病毒得到擊活。但現在不少病毒通過使用特殊的感染技巧能夠使宿主大小及宿主文件頭上的入口點保持不變。

附加了病毒代碼卻使被感染文件大小不變聽起來讓人不可思議,其實它是利用了PE文件格式的特點:PE文件的每個節之間留有按簇大小對齊後的空洞,病毒體如果足夠小則可以將自身分成幾份並分別插入到每個節最後的空隙中,這樣就不必額外增加一個節,因而文件大小保持不變。著名的CIH病毒正是運用這一技術的典型範例(它的大小隻有1K左右)。

病毒在不修改文件頭入口點的前提下要想獲得控制權並非易事:入口點不變意味着程序是從原程序的入口代碼處開始執行的,病毒必須要將原程序代碼中的一處修改爲導向病毒入口的跳轉指令。原理就是這樣,但其中還存在很多可討論的地方,如在原程序代碼的何處插入這條跳轉指令。一些查毒工具掃描可執行文件頭部的入口點域,如果發現它指向的地方不正常,即不在代碼節而在資源節或重定位節中,則有理由懷疑文件感染了某種病毒。所以剛纔討論那種病毒界稱之爲EPO(入口點模糊)的技術可以很好的對付這樣的掃描,同時它還是反虛擬執行的重要手段。

另外值得一提的是現在不少病毒已經支持對壓縮文件的感染。如Win32.crypto病毒就可以感染ZIP,ARJ,RAR,ACE,CAB 等諸多類型的壓縮文件。這些病毒的代碼中含有對特定壓縮文件類型解壓並壓縮的代碼段,可以先把壓縮文件中的內容解壓出來,然後對合適的文件進行感染,最後再將感染後文件壓縮回去並同時修改壓縮文件頭部的校驗和。目前不少反病毒軟件都支持查多種格式的壓縮文件,但對有些染毒的壓縮文件無法殺除。原因我想可能是怕由於某種緣故,如解壓或壓縮有誤,校驗和計算不對等,使得清除後壓縮文件格式被破壞。病毒卻不用對用戶的文件損壞負責,所以不存在這種擔心。

2.虛擬機查毒
2.1虛擬機概論
近些年,虛擬機,在反病毒界也被稱爲通用解密器,已經成爲反病毒軟件中最引人注目的部分,儘管反病毒者對於它的運用還遠沒有達到一個完美的程度,但虛擬機以其諸如"病毒指令碼模擬器"和"Stryker"等多變的名稱爲反病毒產品的市場銷售帶來了光明的前景。以下的討論將把我們帶入一個精彩的虛擬技術的世界中。

首先要談及的是虛擬機的概念和它與諸如Vmware(美國VMWARE公司生產的一款虛擬機,它支持在WINNT/2000環境下運行如Linux等其它操作系統)和WIN9X下的VDM(DOS虛擬機,它用來在32位保護模式環境中運行16實模式代碼)的區別。其實這些虛擬機的設計思想是有淵源可尋的,早在上個世紀60年代IBM就開發了一套名爲VM/370的操作系統。VM/370在不同的程序之間提供搶先式多任務,作法是在單一實際的硬件上模式出多部虛擬機器。典型的VM/370會話,使用者坐在電纜連接的遠程終端前,經由控制程序的一個IPL命令,模擬真實機器的初始化程序裝載操作,於是 一套完整的操作系統被載入虛擬機器中,並開始爲使用者着手創建一個會話。這套模擬系統是如此的完備,系統程序員甚至可以運行它的一個虛擬副本,來對新版本進行除錯。Vmware與此非常相似,它作爲原操作系統下的一個應用程序可以爲運行於其上的目標操作系統創建出一部虛擬的機器,目標操作系統就象運行在單獨一臺真正機器上,絲毫察覺不到自己處於Vmware的控制之下。當在Vmware中按下電源鍵(Power On)時,窗口裏出現了機器自檢畫面,接着是操作系統的載入,一切都和真的一樣。而WIN9X爲了讓多個程序共享CPU和其它硬件資源決定使用VMs(所有Win32應用程序運行在一部系統虛擬機上;而每個16位DOS程序擁有一部DOS虛擬機)。VM是一個完全由軟件虛構出來的東西,以和真實電腦完全相同的方式來回應應用程序所提出的需求。從某種角度來看,你可以將一部標準的PC的結構視爲一套API。這套API的元素包括硬件I/O系統,和以中斷爲基礎的BIOS和MS-DOS。WIN9X常常以它自己的軟件來代理這些傳統的API元素,以便能夠對珍貴的硬件多重發訊。在VM上運行的應用程序認爲自己獨佔整個機器,它們相信自己是從真正的鍵盤和鼠標獲得輸入,並從真正的屏幕上輸出。稍被加一點限制,它們甚至可以認爲自己完全擁有CPU和全部內存。實現虛擬技術關鍵在於軟件虛擬化和硬件虛擬化,下面簡要介紹WIN9X下的DOS虛擬機的實現。

當Windows移往保護模式後,保護模式程序無法直接調用實模式的MS-DOS處理例程,也不能直接調用實模式的BIOS。軟件虛擬化就是用來描述保護模式Windows部件是如何能夠和實模式MS-DOS和BIOS彼此互動。軟件虛擬化要求操作系統能夠攔截企圖跨越保護模式和實模式邊界的調用,並且調整適當的參數寄存器後,改變CPU模式。WIN9X使用虛擬設備驅動(VXD)攔截來自保護模式的中斷,通過實模式中斷向量表(IVT),將之轉換爲實模式中斷調用。做爲轉換的一部分,VXD必須使用置於保護模式擴展內存中的參數,生成出適當的參數,並將之放在實模式(V86)操作系統可以存取的地方。服務結束後,VXD在把結果交給擴展內存中保護模式調用端。16位DOS程序中大量的21H和13H中斷調用就此解決,但其中還存在不少直接端口I/O操作,這就需要引入硬件虛擬化來解決。虛擬硬件的出現是爲了在硬件中斷請求線上產生中斷請求,爲了迴應IN和OUT指令,改變特殊內存映射位置等原因。硬件虛擬化依賴於Intel 80386+的幾個特性。其中一個是I/O許可掩碼,使操作系統可能誘捕(Trap)對任何一個端口的所有IN/OUT指令。另一個特性是:由硬件輔助的分頁機制,使操作系統能夠提供虛擬內存,並攔截對內存地址的存取操作,將Video RAM虛擬化是此很好的例證。最後一個必要的特性是CPU的虛擬8086(V86)模式 ,讓DOS程序象在實模式中那樣地執行。

我們下面討論用於查毒的虛擬機並不是象某些人想象的:如Vmware一樣爲待查可執行程序創建一個虛擬的執行環境,提供它可能用到的一切元素,包括硬盤,端口等,讓它在其上自由發揮,最後根據其行爲來判定是否爲病毒。當然這是個不錯的構想,但考慮到其設計難度過大(需模擬元素過多且行爲分析要藉助人工智能理論),因而只能作爲以後發展的方向。我設計的虛擬機嚴格的說不能稱之爲虛擬機器,而叫做虛擬CPU,通用解密器等更爲合適一些,但由於反病毒界習慣稱之爲虛擬機,所以在下面的討論中我還將延續這個名稱。查毒的虛擬機是一個軟件模擬的CPU,它可以象真正CPU一樣取指,譯碼,執行,它可以模擬一段代碼在真正CPU上運行得到的結果。給定一組機器碼序列,虛擬機會自動從中取出第一條指令操作碼部分,判斷操作碼類型和尋址方式以確定該指令長度,然後在相應的函數中執行該指令,並根據執行後的結果確定下條指令的位置,如此循環反覆直到某個特定情況發生以結束工作,這就是虛擬機的基本工作原理和簡單流程。設計虛擬機查毒的目的是爲了對付加密變形病毒,虛擬機首先從文件中確定並讀取病毒入口處代碼,然後以上述工作步驟解釋執行病毒頭部的解密段(decryptor),最後在執行完的結果(解密後的病毒體明文)中查找病毒的特徵碼。這裏所謂的“虛擬”,並非是創建了什麼虛擬環境,而是指染毒文件並沒有實際執行,只不過是虛擬機模擬了其真實執行時的效果。這就是虛擬機查毒基本原理,具體介紹請參看後面的相關章節。

當然,虛擬執行技術使用範圍遠不止自動脫殼(虛擬機查毒實際上是自動跟蹤病毒入口的解密子將加密的病毒體按其解密算法進行解密),它還可以應用在跨平臺高級語言解釋器,惡意代碼分析,調試器。如劉濤濤設計的國產調試器Trdos就是完全利用虛擬技術解釋執行被調試程序的每條指令,這種調試器比較起傳統的斷點式調試器(Debug,Softice等)具有諸多優勢,如不易被被調試者察覺,斷點個數沒有限制等。

2.2加密變形病毒
前面提到過設計虛擬機查毒的目的是爲了對付加密變形病毒。這一章就重點介紹加密變形技術。

早期病毒沒有使用任何複雜的反檢測技術,如果拿反彙編工具打開病毒體代碼看到的將是真正的機器碼。因而可以由病毒體內某處一段機器代碼和此處距離病毒入口(注意不是文件頭)偏移值來唯一確定一種病毒。查毒時只需簡單的確定病毒入口並在指定偏移處掃描特定代碼串。這種靜態掃描技術對付普通病毒是萬無一失的。

隨着病毒技術的發展,出現了一類加密病毒。這類病毒的特點是:其入口處具有解密子(decryptor),而病毒主體代碼被加了密。運行時首先得到控制權的解密代碼將對病毒主體進行循環解密,完成後將控制交給病毒主體運行,病毒主體感染文件時會將解密子,用隨機密鑰加密過的病毒主體,和保存在病毒體內或嵌入解密子中的密鑰一同寫入被感染文件。由於同一種病毒的不同傳染實例的病毒主體是用不同的密鑰進行加密,因而不可能在其中找到唯一的一段代碼串和偏移來代表此病毒的特徵,似乎靜態掃描技術對此即將失效。但仔細想想,不同傳染實例的解密子仍保持不變機器碼明文(從理論上講任何加密程序中都存在未加密的機器碼,否則程序無法執行),所以將特徵碼選於此處雖然會冒一定的誤報風險(解密子中代碼缺少病毒特性,同樣的特徵碼也會出現在正常程序中),但仍不失爲一種有效的方法。

由於加密病毒還沒有能夠完全逃脫靜態特徵碼掃描,所以病毒寫作者在加密病毒的基礎之上進行改進,使解密子的代碼對不同傳染實例呈現出多樣性,這就出現了加密變形病毒。它和加密病毒非常類似,唯一的改進在於病毒主體在感染不同文件會構造出一個功能相同但代碼不同的解密子,也就是不同傳染實例的解密子具有相同的解密功能但代碼卻截然不同。比如原本一條指令完全可以拆成幾條來完成,中間可能會被插入無用的垃圾代碼。這樣,由於無法找到不變的特徵碼,靜態掃描技術就徹底失效了。下面先舉兩個例子說明加密變形病毒解密子構造,然後再討論怎樣用虛擬執行技術檢測加密變形病毒。

著名多形病毒Marburg的變形解密子:

  00401020: movsx edi,si ;病毒入口
  00401023: movsx edx,bp
  00401026: jmp 00408a99
  ......
  00407400: ;病毒體入口
  加密的病毒主體
  00408a94: ;解密指針初始值
  ......
  00408a99: mov dl,f7
  00408a9b: movsx edx,bx
  00408a9e: mov ecx,cf4b9b4f
  00408aa3: call 00408ac4
  ......
  00408ac4: pop ebx
  00408ac5: jmp 00408ade
  ......
  00408ade: mov cx,di
  00408ae1: add ebx,9fdbd22d
  00408ae7: jmp 00408b08
  ......
  00408b08: add ecx,80c1fbc1
  00408b0e: mov ebp,7fcdeff3 ;循環解密記數器初值
  00408b13: sub cl,39
  00408b16: movsx esi,si
  00408b19: add dword ptr[ebx+60242dbf],9ef42073 ;解密語句,9ef42073是密鑰
  00408b23: mov edx,6fd1d4cf
  00408b28: mov di,dx
  00408b2b: inc ebp
  00408b2c: xor dl,a3
  00408b2f: mov cx,si
  00408b32: sub ebx,00000004 ;移動解密偏移指針,逆向解密
  00408b38: mov ecx,86425df9
  00408b3d: cmp ebp,7fcdf599 ;判斷解密結束與否
  00408b43: jnz 00408b16
  00408b49: jmp 00408b62
  ......
  00408b62: mov di,bp
  00408b65: jmp 00407400 ;將控制權交給解密後的病毒體入口
  著名多形病毒Hps的變形解密子:

  005365b8: ;解密指針初始值和病毒體入口
  加密的病毒主體
  ......
  005379cd: call 005379e2
  ......
  005379e2: pop ebx
  005379e3: sub ebx,0000141a ;設置解密指針初值
  005379e9: ret
  ......
  005379f0: dec edx ;減少循環記數值
  005379f1: ret
  ......
  00537a00: xor dword ptr[ebx],10e7ed59 ;解密語句,10e7ed59是密鑰
  00537a06: ret
  ......
  00537a1a: sub ebx,ffffffff
  00537a20: sub ebx,fffffffd ;移動解密指針,正向解密
  00537a26: ret
  ......
  00537a30: mov edx,74d9cb97 ;設置循環記數初值
  00537a35: ret
  ......
  00537a3f: call 005379cd ;病毒入口
  00537a44: call 00537a30
  00537a49: call 00537a00
  00537a4e: call 00537a1a
  00537a53: call 005379f0
  00537a58: mov esi,edx
  00537a5a: cmp esi,74d9c696 ;判斷解密結束與否
  00537a60: jnz 00537a49
  00537a66: jmp 005365b8 ;將控制權交給解密後的病毒體入口
  以上的代碼看上去絕對不會是用編譯器編譯出來,或是編程者手工寫出來的,因爲其中充斥了大量的亂數和垃圾。代碼中沒有註釋部分均可認爲是垃圾代碼,有用部分完成的功能僅是循環向加密過的病毒體的每個雙字加上或異或一個固定值。這只是變形病毒傳染實例的其中一個,別的實例的解密子和病毒體將不會如此,極度變形以至讓人無法辯識。至於變形病毒的實現技術由於涉及複雜的算法和控制,因此不在我們討論範圍內。

這種加密變形病毒的檢測用傳統的靜態特徵碼掃描技術顯然已經不行了。爲此我們採取的方法是動態特徵碼掃描技術,所謂“動態特徵碼掃描”指先在虛擬機的配合下對病毒進行解密,接着在解密後病毒體明文中尋找特徵碼。我們知道解密後病毒體明文是穩定不變的,只要能夠得到解密後的病毒體就可以使用特徵碼掃描了。要得到病毒體明文首先必須利用虛擬機對病毒的解密子進行解釋執行,當跟蹤並確定其循環解密完成或達到規定次數後,整個病毒體明文或部分已被保存到一個內部緩衝區中了。虛擬機之所以又被稱爲通用解密器在於它不用事先知道病毒體的加密算法,而是通過跟蹤病毒自身的解密過程來對其進行解密。至於虛擬機怎樣解釋指令執行,怎樣確定可執行代碼有無循環解密段等細節將在下一節中介紹。

2.3虛擬機實現技術詳解
有了前面關於加密變形病毒的介紹,現在我們知道動態特徵碼掃描技術的關鍵就在於必須得到病毒體解密後的明文,而得到明文產生的時機就是病毒自身解密代碼解密的完畢。目前有兩種方法可以跟蹤控制病毒的每一步執行,並能夠在病毒循環解密結束後從內存中讀出病毒體明文。一種是單步和斷點跟蹤法,和目前一些程序調試器相類似;另一種方法當然就是虛擬執行法。下面分別分析單步和斷點跟蹤法和虛擬執行法的技術細節。

單步跟蹤和斷點是實現傳統調試器的最根本技術。單步的工作原理很簡單:當CPU在執行一條指令之前會先檢查標誌寄存器,如果發現其中的陷阱標誌被設置則會在指令執行結束後引發一個單步陷阱INT1H。至於斷點的設置有軟硬之分,軟件斷點是指調試器用一個通常是單字節的斷點指令(CC,即INT3H)替換掉欲觸發指令的首字節,當程序執行至斷點指令處,默認的調試異常處理代碼將被調用,此時保存在棧中的段/偏移地址就是斷點指令後一字節的地址;而硬件斷點的設置則利用了處理器本身的調試支持,在調試寄存器(DR0--DR4)中設置觸發指令的線形地址並設置調試控制寄存器(DR7)中相關的控制位,CPU會在預設指令執行時自動引發調試異常。而Windows本身又提供了一套調試API,使得調試跟蹤一個程序變得非常簡單:調試器本身不用接掛默認的調試異常處理代碼,而只須調用WaitForDebugEvent等待系統發來的調試事件;調試器可利用GetThreadContext掛起被調試線程獲取其上下文,並設置上下文中的標誌寄存器中的陷阱標誌位,最後通過SetThreadContext使設置生效來進行單步調試;調試器還可通過調用兩個功能強大的調試API--ReadProcessMemory和WriteProcessMemory來向被調試線程的地址空間中注入斷點指令。根據我逆向後的分析結果,VC++的調試器就是直接利用這套調試API寫成的。使用以上的調試技術既然可以寫出像VC++那樣功能齊全的調試器,那麼沒有理由不能將之運用於病毒代碼的自動解密上。最簡單的最法:創建待查可執行文件爲調試器的調試子進程,然後用上述方法對其進行單步跟蹤,每當收到具有EXCEPTION_SINGLE_STEP異常代碼的事件時就可以分析該條以單步模式執行的指令,最後當判斷病毒的整個解密過程結束後即可調用ReadProcessMemory讀出病毒體明文。

用單步和斷點跟蹤法的唯一一點好處就在於它不用處理每條指令的執行--這意味着它無需編寫大量的特定指令處理函數,因爲所有的解密代碼都交由CPU去執行,調試器不過是在代碼被單步中斷的間隙得到控制權而已。但這種方法的缺點也是相當明顯的:其一容易被病毒覺察到,病毒只須進行簡單的堆棧檢查,或直接調用IsDebugerPresent就可確定自己正處於被調試狀態;其二由於沒有相應的機器碼分析模塊,指令的譯碼,執行完全依賴於CPU,所以將導致無法準確地獲取指令執行細節並對其進行有效的控制。;其三單步和斷點跟蹤法要求待查可執行文件真實執行,即其將做爲系統中一個真實的進程在自己的地址空間中運行,這當然是病毒掃描所不能允許的。很顯然,單步和斷點跟蹤法可以應用在調試器,自動脫殼等方面,但對於查毒卻是不合適的。

而使用虛擬執行法的唯一一點缺點就在於它必須在內部處理所有指令的執行--這意味着它需要編寫大量的特定指令處理函數來模擬每種指令的執行效果,這裏根本不存在何時得到控制權的問題,因爲控制權將永遠掌握在虛擬機手中。用軟件方法模擬CPU並非易事,需要對其機制有足夠的瞭解,否則模擬效果將與真實執行相去甚遠。舉兩個例子:一個是病毒常用的乘法後ASCII調整指令AAM,這條指令因爲存在未公開的行爲從而常常被病毒用來考驗虛擬機設計的優劣。通常情況下AAM是雙字節指令,操作碼爲D4 0A(其實0A隱含代表了操作數10);但也可作爲單字節指令明確地指定第二字節除數爲任意8位立即數,此時操作碼僅爲D4。虛擬機必需考慮到後一種指定除數的情況來保證模擬結果的正確性;還有一個例子是關於處理器響應中斷的方式,即CPU在剛打開中斷後將不會馬上響應中斷,而必須隔一個指令週期。如果虛擬機沒有考慮到該機制則很可能虛擬執行流程會與真實情況不符。但虛擬執行的優點也是很明顯的,同時它正好填補了單步和斷點跟蹤法所力不能及的方面:首先是不可能被病毒覺察到,因爲虛擬機將在其內部緩衝區中爲被虛擬執行代碼設立專用的堆棧,所以堆棧檢查結果與實際執行無二(不會向堆棧中壓入單步和斷點中斷時的返回地址);其次由於虛擬機自身完成指令的解碼和地址的計算,所以能夠獲取每條指令的執行細節並加以控制;最後,最爲關鍵的一條在於虛擬執行確實做到了“虛擬”執行,系統中不會產生代表被執行者的進程,因爲被執行者的寄存器組和堆棧等執行要素均在虛擬機內部實現,因而可以認爲它在虛擬機地址空間中執行。鑑於虛擬執行法諸多的優點,所以將其運用於通用病毒體解密上是再好不過的了。

通常,虛擬機的設計方案可以採取以下三種之一:自含代碼虛擬機(SCCE),緩衝代碼虛擬機(BCE),有限代碼虛擬機(LCE)。

自含代碼虛擬機工作起來象一個真正的CPU。一條指令取自內存,由SCCE解碼,並被傳送到相應的模擬這條指令的例程,下一條指令則繼續這個循環。虛擬機會包含一個例程來對內存/寄存器尋址操作數進行解碼,然後還會包括一個用於模擬每個可能在CPU上執行的指令的例程集。正如你所想到的,SCCE的代碼會變的無比的巨大而且速度也會很慢。然而SCCE對於一個先進的反病毒軟件是很有用的。所有指令都在內部被處理,虛擬機可以對每條指令的動作做出非常詳細的報告,這些報告和啓發式數據以及通用清除模塊將相互參照形成一個有效的反毒系統。同時,反病毒程序能夠最精確地控制內存和端口的訪問,因爲它自己處理地址的解碼和計算。

緩衝代碼虛擬機是SCCE的一個縮略版,因爲相對於SCCE它具有較小的尺寸和更快的執行速度。在BCE中,一條指令是從內存中取得的,並和一個特殊指令表相比較。如果不是特殊指令,則它被進行簡單的解碼以求得指令的長度,隨後所有這樣的指令會被導入到一個可以通用地模擬所有非特殊指令的小過程中。而特殊指令,只佔整個指令集的一小部分,則在特定的小處理程序中進行模擬。BCE通過將所有非特殊指令用一個小的通用的處理程序模擬來減少它必須特殊處理的指令條數,這樣一來它削減了自身的大小並提高了執行速度。但這意味着它將不能真正限制對某個內存區域,端口或其他類似東西的訪問,同時它也不可能生成如SCCE提供的同樣全面的報告。

有限代碼虛擬機有點象用於通用解密的虛擬系統所處的級別。LCE實際上並非一個虛擬機,因爲它並不真正的模擬指令,它只簡單地跟蹤一段代碼的寄存器內容,也許會提供一個小的被改動的內存地址表,或是調用過的中斷之類的東西。選擇使用LCE而非更大更復雜的系統的原因,在於即使只對極少數指令的支持便可以在解密原始加密病毒的路上走很遠,因爲病毒僅僅使用了INTEL指令集的一小部分來加密其主體。使用LCE,原本處理整個INTEL指令集時的大量花費沒有了,帶來的是速度的巨大增長。當然,這是以不能處理複雜解密程序段爲代價的。當需要進行快速文件掃描時LCE就變的有用起來,因爲一個小型但象樣的LCE可以用來快速檢查執行文件的可疑行爲,反之對每個文件都使用SCCE算法將會導致無法忍受的緩慢。當然,如果一個文件看起來可疑,LCE還可以啓動某個SCCE代碼對文件進行全面檢查。

下面開始介紹32位自含代碼虛擬機w32encode(w32encode.cpp,Tw32asm.h,Tw32asm.cpp做爲查毒引擎的一部分和其它搜索清除模塊聯編爲Rsengine.dll)的程序結構和流程。由於這是一個設計完備且複雜的大型商用虛擬機,其中不可避免地包含了對某些特定病毒的特定處理,爲了使虛擬機模型的結構清晰脈絡分明,分析時我將做適當的簡化。

w32encode的工作原理很簡單:它首先設置模擬寄存器組(用一個DWORD全局變量模擬真實CPU內部的一個寄存器,如ENEAX)的初始值,初始化執行堆棧指針(虛擬機用內部的一個數組static int STACK[0x20]來模擬堆棧)。然後進入一個循環,解釋執行指令緩衝區ProgBuffer中的頭256條指令,如果循環退出時仍未發現病毒的解密循環則可由此判定非加密變形病毒,若發現瞭解密循環則調用EncodeInst函數重複執行循環解密過程,將病毒體明文解密到DataSeg1或DataSeg2中。相關部分代碼如下:

W32Encode0中總體流程控制部分代碼:

  for (i=0;i<0x100;i++) //首先虛擬執行256條指令試圖發現病毒循環解密子
  {
  if (InstLoc>=0x280)
  return(0);
  if (InstLoc+ProgSeekOff>=ProgEndOff)
  return(0); //以上兩條判斷語句檢查指令位置的合法性
  saveinstloc(); //存儲當前指令在指令緩衝區中的偏移
  HasAddNewInst=0;
  if (!(j=parse())) //虛擬執行指令緩衝區中的一條指令
  return(0); //遇到不認識的指令時退出循環
  if (j==2) //返回值爲2說明發現瞭解密循環
  break;
  }
  if (i==0x100) //執行過256條指令後仍未發現循環則退出
  return(0);
  PreParse=0;
  ProcessInst();
  if (!EncodeInst()) //調用解密函數重複執行循環解密過程
  return(0);
  jmp中判定循環出現部分代碼:

  if ((loc>=0)&&(loc<InstLoc)) //若轉移後指令指針小於當前指令指針則可能出現循環
  if (!isinstloc(loc)) //在保存的指令指針數組InstLocArray中查找轉移後指
  ...... //令指針值,如發現則可判定循環出現
  else
  {
  ......
  return(2); //返回值2代表發現瞭解密循環
  }
  parse中虛擬執行每條指令的過程較複雜一些:通常parse會從取得指令緩衝區ProgBuffer中取得當前指令的頭兩個字節(包括了全部操作碼)並根據它們的值調用相應的指令處理函數。例如當第一個字節等於0F並且第二個字節位與BE後等於BE時,可判定此指令爲movszx並同時調用movszx進行處理。當執行進入特定指令的處理函數中時,首先要通過判斷尋址方式(調用modregrm或modregrm1)確定指令長度並將控制權交給saveinst函數。saveinst在保存該指令的相關信息後會調用真正指令執行函數W32ExecuteInst。這個函數和parse非常相似,它從SaveInstBuf1中取得當前指令的頭兩個字節並根據它們的值調用相應的指令模擬函數以完成一條指令的執行。相關部分代碼如下:

W32ExecuteInst中指令分遣部分代碼:

  if ((c&0xf0)==0x50)
  {if (ExecutePushPop1(c)) //模擬push和pop
  return(gotonext());
  return(0);
  }
  if (c==0x9c)
  {if (ExecutePushf()) //模擬pushf
  return(gotonext());
  return(0);
  }
  if (c==(char)0x9d)
  {if (ExecutePopf()) //模擬popf
  return(gotonext());
  return(0);
  }
  if ((c==0xf)&&((c2&0xbe)==0xbe))
  {if (i=ExecuteMovszx(0)) //模擬movszx
  return(gotonext());
  return(0);
  }
    2.4虛擬機代碼剖析
總體流程控制和分遣部分的相關代碼,在上一章中都已分析過了。下面分析具體的特定指令模擬函數,這纔是虛擬機的精華之所在。我將指令分成不依賴標誌寄存器和依賴標誌寄存器兩大類分別介紹:

2.4.1不依賴標誌寄存器指令模擬函數的分析
push和pop指令的模擬:

  static int ExecutePushPop1(int c)
  {
  if (c<=0x57)
  {if (StackP<0) //入棧前檢查堆棧緩衝指針的合法性
  return(0);
  }
  else
  if (StackP>=0x40) //出棧前檢查堆棧緩衝指針的合法性
  return(0);
  if (c<=0x57) {
  StackP--;
  ENESP-=4; //如果是入棧指令則在入棧前減少堆棧指針
  }
  switch (c)
  {case 0x50:STACK[StackP]=ENEAX; //模擬push eax
  break;
  ......
  case 0x5f:ENEDI=STACK[StackP]; //模擬push edi
  break;
  }
  if (c>=0x58) {
  StackP++;
  ENESP+=4; //如果是出棧指令則在出棧後增加堆棧指針
  }
  return(1);
  }
  2.4.2依賴標誌寄存器指令模擬函數的分析
CW32Asm類中cmp指令的模擬:

  void CW32Asm:: cmpw(int c1,int c2)
  {
  char FlgReg;
  __asm {
  mov eax,c1 //取得第一個操作數
  mov ecx,c2 //取得第二個操作數
  cmp eax,ecx //比較
  lahf //將比較後的標誌結果裝入ah
  mov FlgReg,ah //保存結果在局部變量FlgReg中
  }
  FlagReg=FlgReg; //保存結果在全局變量FlagReg中
  }
  CW32Asm類中jnz指令的模擬:

  int CW32Asm::JNE()
  {int i;
  char FlgReg=FlagReg; //用保存的FlagReg初始化局部變量FlgReg
  __asm
  {
  mov ah,FlgReg //設置ah爲保存的模擬標誌寄存器值
  pushf //保存虛擬機自身當前標誌寄存器
  sahf //將模擬標誌寄存器值裝入真實標誌寄存器中
  mov eax,1
  jne l //執行jnz
  popf //恢復虛擬機自身標誌寄存器
  xor eax,eax
  l:
  popf //恢復虛擬機自身標誌寄存器
  mov i,eax
  }
  return(i); //返回值爲1代表需要跳轉
  }
    2.5反虛擬機技術
任何一個事物都不是盡善盡美,無懈可擊的,虛擬機也不例外。由於反虛擬執行技術的出現,使得虛擬機查毒受到了一定的挑戰。這裏介紹幾個比較典型的反虛擬執行技術:

首先是插入特殊指令技術,即在病毒的解密代碼部分人爲插入諸如浮點,3DNOW,MMX等特殊指令以達到反虛擬執行的目的。儘管虛擬機使用軟件技術模擬真正CPU的工作過程,它畢竟不是真正的CPU,由於精力有限,虛擬機的編碼者可能實現對整個Intel指令集的支持,因而當虛擬機遇到其不認識的指令時將會立刻停止工作。但通過對這類病毒代碼的分析和統計,我們發現通常這些特殊指令對於病毒的解密本身沒有發生任何影響,它們的插入僅僅是爲了干擾虛擬機的工作,換句話說就是病毒根本不會利用這條隨機的垃圾指令的運算結果。這樣一來,我們可以僅構造一張所有特殊指令對應於不同尋址方式的指令長度表,而不必爲每個特殊指令編寫一個專用的模擬函數。有了這張表後,當虛擬機遇到不認識的指令時可以用指令的操作碼索引表格以求得指令的長度,然後將當前模擬的指令指針(EIP)加上指令長度來跳過這條垃圾指令。當然,還有一個更爲保險的辦法那就是:得到指令長度後,可以將這條我們不認識的指令放到一個充滿空操作指令(NOP)的緩衝區中,接着我們將跳到緩衝區中去執行,這等於讓真正的CPU幫我們來執行這條指令,最後一步當然是將執行後真實寄存器中的結果放回我們的模擬寄存器中。這虛擬執行和真實執行參半方法的好處在於:即便在特殊指令對於病毒是有意義的,即病毒依賴其返回結果的情況下,虛擬機仍可保證虛擬執行結果的正確。

其次是結構化異常處理技術,即病毒的解密代碼首先設置自己的異常處理函數,然後故意引發一個異常而使程序流程轉向預先設立的異常處理函數。這種流程轉移是CPU和操作系統相互配合的結果,並且在很大程度上,操作系統在其中起了很大的作用。由於目前的虛擬機僅僅模擬了沒有保護檢查的CPU的工作過程,而對於系統機制沒有進行處理。所以面對引發異常的指令會有兩種結果:其一是某些設計有缺陷的虛擬機無法判斷被模擬指令的合法性,所以模擬這樣的指令將使虛擬機自身執行非法操作而退出;其二虛擬機判斷出被模擬指令屬於非法指令,如試圖向只讀頁面寫入的指令,則立刻停止虛擬執行。通常病毒使用該技術的目的在於將真正循環解密代碼放到異常處理函數後,如此虛擬機將在進入異常處理函數前就停止了工作,從而使解密子有機會逃避虛擬執行。因而一個好的虛擬機應該具備發現和記錄病毒安裝異常過濾函數的操作並在其引發異常時自動將控制轉向異常處理函數的能力。

再次是入口點模糊(EPO)技術,即病毒在不修改宿主原入口點的前提下,通過在宿主代碼體內某處插入跳轉指令來使病毒獲得控制權。通過前面的分析,我們知道虛擬機掃描病毒時出於效率考慮不可能虛擬執行待查文件的所有代碼,通常的做法是:掃描待查文件代碼入口,假如在規定步數中沒有發現解密循環,則由此判定該文件沒有攜帶加密變形病毒。這種技術之所以能起到反虛擬執行的作用在於它正好利用了虛擬機的這個假設:由於病毒是從宿主執行到一半時獲得控制權的,所以虛擬機首先解釋執行的是宿主入口的正常程序,當然在規定步數中不可能發現解密循環,因而產生了漏報。如果虛擬機能增加規定步數的大小,則很有可能隨着病毒插入的跳轉指令跟蹤進入病毒的解密子,但確定規定步數大小實在是件難事:太大則將無謂增加正常程序的檢測時間;太小則容易產生漏報。但我們對此也不必過於擔心,這類病毒由於其編寫技術難度較大所以爲數不多。在沒有反彙編和虛擬執行引擎的幫助下,病毒很難在宿主體內定位一條完整指令的開始處來插入跳轉,同時很難保證插入的跳轉指令的深度大於虛擬機的規定步數,並且沒有把握插入的跳轉指令一定會被執行到。

另外還有多線程技術,即病毒在解密部分入口主線程中又啓動了額外的工作線程,並且將真正的循環解密代碼放置於工作線程中運行。由於多線程間切換調度由操作系統負責管理,所以我們的虛擬機只能在假定被執行線程獨佔處理器時間,即保證永遠不被搶先,的前提下進行。如此一來,虛擬機對於模擬啓用多線程工作的代碼將很難做到與真實效果一致。多線程和結構化異常處理兩種技術都利用了特定的操作系統機制來達到反虛擬執行的目的,所以在虛擬CPU中加入對特定操作系統機制的支持將是我們今後改進的目標。

最後是元多形技術(MetaPolymorphy),即病毒中並非是多形的解密子加加密的病毒體結構,而整體均採用變形技術。這種病毒整體都在變,沒有所謂“病毒體明文”。當然,其編寫難度是很大的。如果說前幾種反虛擬機技術是利用了虛擬機設計上的缺陷,可以通過代碼改進來彌補的話,那麼這種元多形技術卻使虛擬機配合的動態特徵碼掃描法徹底失效了,我們必須尋求如行爲分析等更先進的方法來解決。

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

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

83003C23:保存guidll中等待查殺打開文件的APC函數地址和當前線程KTHREAD指針。

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

83003C27:保存guidll中等待查殺關閉文件的APC函數地址和當前線程KTHREAD指針。

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

以上列出的IO控制代碼的發出是固定,而當鉤子函數啓動後,還會發出一些隨機的控制代碼:

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函數流程的一個簡單介紹,其中省略了諸如判斷固定驅動器,超時等內容,具體細節請參看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 ds:DeviceIoControl
  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.4WINNT/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有兩部分組成:固定部分(稱爲標題)和一個或多個堆棧單元。固定部分信息包括:請求的類型和大小,是同步請求還是異步請求,用於緩衝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)超時值,因爲非固定驅動器的讀寫時間會稍長些。

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

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

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

以上列出的IO控制代碼的發出是固定,而當鉤子函數啓動後,還會發出一些隨機的控制代碼:

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線程流程的一個簡單介紹,其中省略了諸如判斷固定驅動器,超時等內容,具體細節請參看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映射到相同的物理地址。
      結 論
至此本論文已告撰寫完畢。本論文在介紹了諸多目前較爲流行的病毒技術後着重討論了當今兩大反病毒技術:虛擬機和實時監控。

我參與開發的w32encode是一個功能完備且結構複雜的商用虛擬機,它屬於32位自含指令式虛擬機,與其它搜索清除模塊合併在一起組成了一個功能強大的反病毒引擎。雖然目前它還不能支持所有的386+指令集,但從其查殺毒的運行效果來看結果還是非常令人滿意的:普通的加密變形病毒可以在虛擬機默認的處理常式中查殺;特殊的,如hps,marburg等複雜加密變形病毒則可通過向虛擬機中添加少量的病毒特定處理代碼來完成查殺。由於反虛擬執行技術的出現,所以今後對此虛擬機源代碼的更新--向其中添加更多的對操作系統機制的支持--或者重寫--成爲真正的虛擬機器而非虛擬CPU--將是不可避免的。

同時,我通過逆向工程某反病毒軟件的實時監控程序,在系統原理和驅動編程上又有了新的認識,並且它大大增強了我的反彙編功力。今後我會將註釋的反彙編代碼編寫成C語言版源代碼,並把病毒掃描模塊移到系統核心態下工作,從而使整個工程變爲“主動的與內核無縫連接”式監控。

總之當今反病毒技術的主流發展方向是屏棄傳統的特徵碼掃描,創建智能的監控與行爲分析引擎,這就必然要求更加先進的虛擬機和實時監控技術。

致 謝
在這次畢業設計中,我首先特別要感謝的是我的指導教師趙博士,是他在百忙之中對我耐心的輔導才使這次畢業設計順利完成。

其次,對我的聯繫教師鄧老師表示我的最真誠的感謝。雖然我和鄧老師接觸的時間不是很長,但她的熱心誠懇和認真負責給我留下了深刻的印象。

最後,我還要向北京XX電腦技術開發責任有限公司的幾名同事表示感謝。他們在技術上給予了我很大的支持,並且正是他們提供了病毒樣本才使得本論文中相關部分得以完成。

主要參考文獻
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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章