目錄
2.1虛擬機概論
2.2加密變形病毒
2.3虛擬機實現技術詳解
2.4虛擬機代碼剖析
? 2.4.1不依賴標志寄存器指令模擬函數的分析
? 2.4.2依賴標志寄存器指令模擬函數的分析
2.5反虛擬機技術
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),即病毒中并非是多形的解密子加加密的病毒體結構,而整體均采用變形技術。這種病毒整體都在變,沒有所謂“病毒體明文”。當然,其編寫難度是很大的。如果說前幾種反虛擬機技術是利用了虛擬機設計上的缺陷,可以通過代碼改進來彌補的話,那么這種元多形技術卻使虛擬機配合的動態特征碼掃描法徹底失效了,我們必須尋求如行為分析等更先進的方法來解決。【未完待續】
主要參考文獻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