虛擬機虛擬環境與代碼動態變形技術_第1頁
虛擬機虛擬環境與代碼動態變形技術_第2頁
虛擬機虛擬環境與代碼動態變形技術_第3頁
虛擬機虛擬環境與代碼動態變形技術_第4頁
虛擬機虛擬環境與代碼動態變形技術_第5頁
已閱讀5頁,還剩10頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

1、虛擬機、虛擬環境與代碼動態變形技術1. 簡介虛擬機保護是這兩年頗為流行的軟件保護技術。這個詞源于俄羅斯的著名軟件保護軟件"VmProtect",以此軟件為開端引起了軟件保護殼領域的革命。各大軟件保護殼開發團隊都將虛擬機保護這一新穎的技術加入到自己的產品中。如今針對虛擬機保護領域的文章大多是對"VmProtect"的逆向和分析。沒有過多的資料與文章來談談它的設計與構建。此篇文章源自我自己的軟件保護殼對于虛擬機保護這一部分的解決方案。其中借鑒了一部分"VmProtect"的技術,這方面的主要資料來源于"看雪論壇"以及與

2、朋友們的討論。其中又加入了我的一些個人對于虛擬機保護的理解。設計中我采取了三種不同的虛擬機來構成一套完整虛擬機保護系統。文中基本描述了我的虛擬機保護所采用的技術與原理,但具體算法與數據結構并不涉及。文中的第二個重要的議點是當今流行的保護技術-亂序與替換。有人也喜歡把替換稱為抽取,這里只是覺的替換更加直觀。并把這兩部分合稱為代碼混淆,在文章的最后討論了對于花指令生產器的設計。單純的亂序與替換是不能夠起到迷惑Cracker的作用,唯有與隨機生成的花指令聯合起來才可以達到延長破解者逆向時間的作用。2. 虛擬機保護的構建2.1. 整體的設計在我解決方案中設計了三種不同的虛擬機來達到保護的目的。從保護殼

3、大體來講我構建了一臺現實生活中不存在的計算機與它的匯編器和調試器,并用它編寫了保護殼的大部分重要代碼。第三方虛擬機保護在一些crackme中可以找尋到它的影子,主要是為了保護注冊算法不被逆向。在局部方面我定義了兩種虛擬機保護,一種是使用x86體系結構衍生出的虛擬機保護。因為x86的編碼是可識別的,所以防御的重點就是對于x86編碼的轉換與加解密。第二種保護是來源與SSCON2008 著名程序員劉濤濤的扭曲加密變換和"VmProtect"的一些設計思路。其中一些設計又不同于這兩者。2.2. 保護對象-函數在進行保護之前,首先有一個重要的問題就是保護對象,虛擬機保護并不是殼。在穩

4、定性的前提下很難滿足使用虛擬機進行全程序保護又能保持高穩定性。主要影響穩定性的原因是以下三種情況:1. 重定位問題2. 模擬程度的問題3. 各種猥瑣編碼技巧引起不可預知的錯誤對于重定位的引起的不穩定,似乎是一個不可修復的硬傷。由于本文討論的技術都有一個共同的BUG就是重定位。所以對它的討論,另外有一節專門講述。對于第二種情況是顯而易見的,我們設計的畢竟是虛擬機。在有限的開發周期與資源的情況下并不太可能開發出可以模擬當時完整運行情況的虛擬機,那樣的虛擬機并不是用來做虛擬機保護的。例如大名鼎鼎的bochs。內存訪問,異常處理,未模擬的指令,都是造成不穩定的因素。最后一種情況是這樣的。當一個程序被一

5、個殼保護,或者在編寫這個軟件的過程中應用了反逆向的編程手段,或者是我們要保護的目標程序是一個已經經過某個殼保護了的程序。這個殼應用了大量非常規的編碼手段。還有可能是目標程序使用的編譯器生成或者優化的代碼比較特殊。最后一種情況也屬于殼穩定性討論范圍,畢竟沒有一款殼可以號稱兼容所有的程序。即便是簡單的添加新節后XOR。所以,為了把兼容性減小到最低,我們虛擬機保護的對象定位在函數級別上。這里要注意的是使用虛擬機保護的人必須了解要保護軟件的內部構造,知道要保護哪一部分,哪些部分是可以穩定的進行保護。而不是盲目的使用殼隨意對任意函數進行保護,這樣會造成兼容性降低。當我們確定了我們的保護對象,現在首先面臨

6、的問題就是如何找出我們的保護對象。從一個PE文件的代碼節中找出特定函數幀并不是非常困難的問題。但要做到100%的識別出所有函數還是比較困難的,這里就涉及了很多問題。先讓我們看一個標準的函數框架55 push ebp8BEC mov ebp, esp. 任意代碼.8BE5 mov esp, ebp5D pop ebpC3 retn對于識別這種標準框架的流程很簡單。通過反匯編引擎從代碼開頭開始遍歷。先找函數開頭的標記后,接下來找函數結尾,找到后與前一個開頭標記進行閉合并記錄。遇到其余指令則直接略過。依次循環下去。但是事實并非想象的那樣美好,優化過后的代碼往往不夠那么標準,加上編譯器的不同對于函數的

7、建立也有所不同。還有就是加入反逆向代碼的程序。所以即便是著名的反匯編軟件IDA也不能達到完全正確的識別。這里采用的解決方案是,1.建立一套大多數函數框架的特征碼庫。2.讓用戶指定要保護代碼的起始地址與長度。2.3. x86虛擬機保護2.3.1. 為什么要使用x86編碼指令x86的編碼畢竟是公開的,任何反匯編器或者調試器都可以對它進行分析。我為什么還要使用x86虛擬機作保護呢?原因有三點:1. 開發周期短2. 穩定性高3. 避免的重定位修復基于開發周期和成本的考慮,x86虛擬機保護是所有虛擬機保護中成本開銷最小的虛擬機了。基于編碼復雜度的考慮,由于本身就是用x86虛擬機所以代碼長度并不用進行修改

8、。這樣就避免了函數中跳轉偏移的修復。由于x86編碼是公開的,而我只是要借用x86虛擬機的便利性,所以就提出了以下四種解決方案對x86虛擬機解釋進行保護。2.3.2. 隨機OPCODE映射表x86編碼在大多數情況下是由兩張表來進行指定,每張表256個字節,第二張表由第一張表的0F字節引出。換句話說就是當遇到一個x86指令如果OPCODE的第一個字節是0F那么這條指令的OPCODE段就有第二個字節。否則指令的OPCODE段為一個字節。具體x86的編碼方式不在本文的討論范圍之內,請參照IA-32 Architecture Instruction Set。隨機OPCODE映射表的原理就是將這兩張表進行

9、替換。簡單的說如果原來50機器碼對應的是push eax指令,那么在映射之后65機器碼才是對應的push eax。并且保證保護要保護的每個函數都有自己的一套對應規則。隨機映射表的生成與下一個節的字節碼加解密有著很大的聯系。隨機映射表映射的過程實際上也是對每條指令進行加密的過程。而映射表的每個字節表示了解碼所需要的算法。2.3.3. 字節碼的加解密如何保護好字節碼也是重要的問題。雖然OPCODE(包括了前綴段)表進行了隨機映射。但是在x86編碼中還存在ModRM/SIB段和常數段。對ModRM/SIB解析規則進行重新規則應該是最好的解決手段。對常數段的保護可以直接對其進行加密。在ModRM/SI

10、B中進行解密(ModRM/SIB段是什么參見IA-32 Architecture Instruction Set)我在這里的解決方案是采用加密的原則。加密的原則不采用整體加密原則,而是采用局部加密原則,把解密算法放到每條指令做解析的過程當中,當然每條指令的加解密算法不相同,并且加解密算法跟隨OPCODE表的映射進行變換隨之變換。這里需要提到的是多態加解密算法生成器,因為不在本文討論范圍之內,所以不做解釋。當然關于ModRM/SIB也做了同樣的處理。每個被保護的函數的ModRM/SIB解析函數內的解密函數都不相同。而密鑰的選擇也與VMProtect類似采用可以防止外部篡改字節碼的原則。我采用了取

11、當前指令HASH值作為隨后指令解密的密鑰的方式,詳細說明一下:加密后指令1 -> decoder(隨機的密鑰) -> 指令1 -> HASH(指令1) = key1加密后指令2 -> decoder(key1) -> 指令2 -> HASH(指令2) = key2加密后指令3 -> decoder(key2)-> 指令3 -> HASH(指令3) = key3. 以此類推直到解密完成這里需要解釋的是,加密是加密哪一部分。如果是單字節指令不需要進行解密,OPCODE隨機的生成就是對它的一種加密。當一條指令有多個字節時就需要對指令進行加密。加密

12、是這個部分。當遇到要有處理ModRM/SIB段的指令時,此加密結果是在已經加密過后的ModRM/SIB上進行再一次的加密。當進入ModRM/SIB處理程序后,它會對自身的字節再一次進行解密。而對于帶有常數的ModRM/SIB也是經過了兩次加解密。2.3.4. 變形后的指令處理句柄在我閱讀逆向VMProtect的文章時,發現大多數逆向虛擬機的過程都是找OPCODE與處理Handler之間的對應。如果每次的對應都不相同,那么會給逆向帶來很大的麻煩。在上一節中我把OPCODE的對應進行隨機。每個被保護函數都有一套自己的OPCODE表。現在就是把自己的處理句柄進行隨機化。每個被保護的函數都有一套自己的

13、Handler。我們為每個被保護的函數都編寫一套指令執行規則顯然是不現實的。那么我們只能尋求變形的方法。在每次要保護一個函數時,將最原始的Handler函數進行添加花指令或者采用下一個章節提到的指令擴展的方式。由于原始Handler是自己編寫。所以在做指令擴展時可以保證完全的兼容性。(由指令擴展引起的不兼容在指令擴展章節討論)。2.3.5. 虛擬機上下文結構隨機上下文結構隨機的思路也取自于VMProtect。這個設計是這樣的當每次要執行虛擬機時必定有一個結構的某一個字段保存了模擬寄存器的數組。例如如下結構:typedef struct _VM_CONTENT / 寄存器數組 DWORD Reg

14、isters8; / 其他字段定義. VM_CONTENT, *PVM_CONTENT#define R_EAX0#define R_ECX1#define R_EDX2/ 其他寄存器索引定義.在代碼執行的過程中我們可以通過索引分析出此時虛擬機操作的是哪個寄存器。所以在我的設計中采用了VMProtect寄存器隨機這一原則。并將它擴大到隨機上下文結構。他的目的是在于不讓逆向者分析出VM_CONTENT這個結構。讓每條指令都有一個自己上下文結構并不困難,難點在于如何判別隨機化后的上下文結構。所以就又衍生出了另一個結構用于描述如何還原隨機化后的結構。這樣當執行一條指令后將當前上下文結構進行傳遞并且與

15、之一同傳遞的還有一個虛擬機執行上下文重定位結構此結構用于描述前一個上下文結構的修改,此結構的目的是告訴下一條指令的處理句柄,如何將上一條指令的上下文結構還原為原始的上下文結構,便于按照原始的結構構造自己的上下文結構。2.3.6. 加載流程在進入虛擬機保護之前,我們的虛擬機保護加載器需要模擬出一段內存用作虛擬機本身的棧段,所以就要有一個切棧操作,使得原始的棧指針(esp, ebp)歸虛擬機字節碼程序擁有以便達到最大的穩定性。而虛擬機本身做解釋執行的棧段要重新進行模擬。這里只需要在進入虛擬機之前保存原始ebp, esp 并將ebp, esp重新指向一段事先分配好的內存。在虛擬機推出之前再做一此切棧

16、操作。以恢復程序的正常運行。當剛進入被保護的函數中(被保護的函數處的代碼被修改為虛擬機的加載代碼)流程如下:1.將當前運行的上線文設定到虛擬機的VM_CONTENT結構中2.保存esp, ebp 并將esp, ebp 指向全新的內存3.跳入虛擬機中執行這里存在兩種設計,一種就是將此處要保護的代碼的字節地址一同傳遞給虛擬機保護解釋器初始化函數,另外一種設計就是每段要保護的代碼都擁有一個唯一的初始化函數這里我的選擇是后者。2.3.7. 異常的模擬關于在虛擬機保護中需要注意的異常有以下兩種1. 內存訪問權限不足2. 未知指令其中最平凡是內存訪問權限的問題,第二種的多數是因為虛擬機本身沒有模擬完足夠的

17、指令。第二種情況可以通過增加新的指令進行修復。如果本身就是x86的不可解析的指令,那么把控制權交還給SEH鏈就好了。而這種控制權的移交是被動的。這里最頻繁的異常主要是第一種情況。由于被保護程序運行環境,用戶輸入,程序本身等等會造成這樣那么的錯誤。如果發生了內存訪問錯誤。同樣也不需要管它,WINDWS的SHE異常處理會自動接管它。2.4. 指令擴展保護2.4.1. 源自扭曲變形加密在去年(SSCON 2008)上著名程序員劉濤濤提出了扭曲加密變換的理論(詳細解釋參考扭曲加密變換技術一文)簡單介紹一下此項技術的核心例如一條指令 add eax, 1我們可以將它轉化為call ADD_EAX_1Jm

18、p ADD_EAX_1_END; 這里為垃圾代碼ADD_EAX_1:push ebpmov ebp, espsub esp, 0x08push ebxmov dword ptr ebp-0x04, 5mov ebx, 4sub dword ptr ebp-0x04, ebxadd eax, dword ptr ebp-0x04mov esp, ebppop ebpRetADD_EAX_1_END:1條 簡單的指令可以擴展為一個函數,這樣的擴展只局限與想象力。如果整篇代碼進行這樣的擴展是逆向的難度是不可想象的。其中他的做法是采用OBJ文件作為中間文件,當然這樣做用途是避免重定位的問題,而我要保護

19、的對象是已經生成出來的PE文件,其中大多數都是不帶重定位表的(PE文件結構與OBJ文件結構這里不做解釋,請參考相關文檔)。這就需要我們像上一節x86虛擬機保護那樣讓用戶有選擇的進行保護并且在保護之后對代碼進行重定位修復。具體做法下文中有討論。2.4.2. 指令模板化因為我們要擴展我們的代碼例如像上文提到的一樣將add eax, 1變成一個函數的調用,所以我們要有一整套模板。記錄了常用指令要變化的映射,并在生成代碼時進行隨機篩選。在生成時還可以隨機插入一些花指令讓變形的力度更大。這里值得一提的是VMProtect關于and or not xor這些邏輯運算的替換。在電路門中有種成為萬用邏輯閘的東

20、西,具體原理就是使用not not and或者not not or 實現對以上的邏輯運算的模擬。以下公式以及圖參見(破解vmp程序的關鍵點(海風月影)一貼)這樣我們可以把在要保護代碼中的邏輯運算用此來替代。設:P(a, b) = a & bNot (a) = P(a, a)And (a, b) = P(P(a, a), P(b, b)Or(a, b) = P(P(a, b), P(a, b)Xor(a, b) = P(P(P(a, a), P(b, b), P(a, b)這5條算是基礎指令,如果把這些指令進行展開,在通過隨機算法控制這個展開的過程,那么幾條簡單的邏輯運算指令就可以變的非

21、常復雜。2.4.3. 重定位修復重定位問題是以上所有問題需要涉及的為什么會產生重定位的問題。讓我們看一個實際的問題。原始代碼插入代碼后并修復的情況在這兩幅圖的對比之后會發現由于中間插入了代碼,所以像JMP,CALL,JCC這樣的指令都需要進行修復。修復的原則為1.重定位模塊的設計重定位模塊是整個代碼集成的關鍵模塊。它依靠反匯編引擎來識別代碼。并且也負責修訂病毒體偏移的任務。先讀取代碼節,以代碼節的各個跳轉指令為節點進行分段。記錄它的偏移長度以及跳轉方向。分析跳轉指令之間產生的空隙。按空隙的大小對要插入的代碼進行分段。在跳轉中插入代碼后,修訂跳轉的偏移量2.重定位模塊何時插入在將病毒代碼插入到宿

22、主代碼中最理想的位置是兩段跳轉指令之間,這樣可以不用修復任何跳轉偏移。修復偏移只有在插入到跳轉指令的集合內,如果有跳轉指令有交集則修復所有跳轉指令的偏移。3.修復內存跳轉在遇到如call dword ptr 404340,jmp dword ptr 404340等代碼,需要拆解ModRM,SIB兩個位得到。由于call,jmp由內存指定值跳轉的編碼同為FF,擴展編碼為call:02,jmp:04。由于我們編寫的病毒在32位下運行,所以16位的編碼不進行考慮。查INTEL手冊編碼得到兩組可以跳轉的情況。單ModRm情況:當為call,ModRM取15h,為jmp,ModRM取25h.具有SIB的

23、情況:Mod為00h.查看R/M為04h RO為2或者4,所以得下兩種情況ModRM為14h,24h。檢查SIB,SIB的情況僅與Index,Base字段有關。當Index = 04h,Base = 05h下的情況全部可以重定位。從指向的內存中取出要跳轉的地址,進行偏移修復。4.指令的變換某些跳轉指令的立即數是單字節,這些指令無論向上還是向跳轉的空間只有127個字節,所以當插入代碼長度與偏移的和大于這個數字時就要利用相同作用但偏移量更大的指令替換。在擴展指令后,指令的長度進行了增大,這時如果此指令在其他跳轉集合內需要再進行一次重定位修復。5.重定位模塊的缺陷重定位模塊只能修訂以偏移量作為跳轉情

24、況 如果是動態跳轉例如:Jmp dword ptr ebxCall eax 等類似的代碼重定位模塊則不能修復。 以上描述引用自我在看雪論壇關于重定位模塊一貼。具體連接為:2.5. 第三方虛擬機2.5.1. 殼的虛擬化第三方虛擬機的在軟件保護中的應用,可以在某些crackme中見到。在我的殼的設計當中有這樣一臺虛擬機,它控制著整個殼的運行流程,例如重建引入表。加解密運算等。整個殼重要的部分都由此虛擬機進行編碼,與x86虛擬機保護相同的是OPCODE表的隨機化和對指令的加解密。讓殼本身就是處于虛擬機保護的狀態。2.5.2. 用戶的自定義功能擴展引入第三方虛擬機的另一個好處是可以讓用戶擴展殼的功能。

25、只要虛擬機本身提供足夠強大的SDK。引入用戶擴展的另一項功能是為了彌補殼在添加功能上的不便理性。例如反調試模塊。這里展示一副在我的殼中已經進行工作的第三方虛擬機工作圖。目前還只是匯編語言階段。沒有在此基礎上開發出基于此匯編語言的高級語言編譯器。殼本身帶有一個此虛擬機的調試器用來讓用戶調試編寫的代碼。2.6. 外部函數動態跳轉引發的狀況除第三方虛擬機外,無論是x86虛擬機保護還是指令擴展式保護都有一個嚴重的硬傷。如果有一段被保護的函數A,以及一段函數B,B中有一跳轉指令形如:Mov eax, A函數地址Add eax, 某個偏移Jmp eax此時eax的結果為A函數中的某段地址。類似的情況我們是

26、無法進行重定位已經處理的。為了避免有其他函數跳入到被保護函數體內而不是頭地址的情況,在被保護后,我修復了這段代碼節除被保護函數代碼區域的所有偏移性跳轉。以達到最大修復的目的。這種修復可用于代碼擴展。但是對于x86虛擬機保護以及其他解析性質的虛擬機保護來說,此方法無效。3. 代碼混淆3.1. 代碼亂序亂序這種技術已經在大量殼中得以應用。實現技術簡單,穩定性高。并且在此基礎上可以構建出很多有趣的花樣。代碼亂序的目的有兩個。1.是可以增加逆向工程的難度。2.可以讓殼重新獲得對程序的控制權。亂序的原理十分的簡單,具體的流程分為以下幾個步驟:1. 分析要保護文件的代碼節,并遍歷找出所有的CALL或者JM

27、P的代碼2. 修改CALL或者JMP其后的偏移將它指向到自己所指定的地址3. 在新的地址處填充花指令或者自行定義的代碼,在代碼結尾處跳入到原始的目標地址這里給出兩段代碼予以說明,下面的代碼是正在對32位偏移跳轉的指令進行記錄把這條指令當前的地址與要跳轉的目標地址進行記錄。這樣做的目的是流程的第一步找出所有的CALL或者JMP的代碼。ud_obj是u86diasm反匯編引擎的結構體。 if (ud_obj.inp_sess0 = 0x0F) / 32位 (*pCodeFlowNode)->dwBits = Jmp_Bit_32; (*pCodeFlowNode)->dwInsLen

28、= ud_obj.inp_ctr; DWORD dwOffset = ud_obj.operand0.lval.udword; (*pCodeFlowNode)->dwOffset = dwOffset; if (dwOffset >= 0x80000000) (*pCodeFlowNode)->bGoDown = FALSE; dwOffset = dwOffset; dwOffset+; dwOffset -= ud_obj.inp_ctr; (*pCodeFlowNode)->dwGotoMemoryAddress = (*pCodeFlowNode)->d

29、wMemoryAddress - dwOffset; DWORD dwRaw = PeDiy.Rva2Raw(pMem, (DWORD)(*pCodeFlowNode)->dwGotoMemoryAddress - dwImageBase); (*pCodeFlowNode)->pGotoFileAddress = pMem + dwRaw; else (*pCodeFlowNode)->bGoDown = TRUE; dwOffset += ud_obj.inp_ctr; (*pCodeFlowNode)->dwGotoMemoryAddress = (*pCodeF

30、lowNode)->dwMemoryAddress + dwOffset; DWORD dwRaw = PeDiy.Rva2Raw(pMem, (DWORD)(*pCodeFlowNode)->dwGotoMemoryAddress - dwImageBase); (*pCodeFlowNode)->pGotoFileAddress = pMem + dwRaw; /* end else */接下來的事情就是遍歷這個記錄好的結構。給目標程序填充新節,將要亂序的偏移進行修復并指向新的空間。亂序的流程圖3.2. 代碼替換這里的替換是指的自動的替換。用戶指定一段要保護的內存后,我們

31、的殼程序將這段內存處CALL/JMP/JCC之間的代碼自動的進行抽取。并將其移動到另一篇區域,然后將進入這片代碼的地方使用JMP語句跳入到新的區域內。在新的區域內執行完畢后跳入到原來執行完畢的地址。以下是流程描述: 1.找到兩個JMP/CALL/JCC之間的代碼段,這個代碼段符合以下要求,長度要大于等于5個字節(以便放置 跳轉指令)這段指令沒有其他跳轉跳入(如果能排除動態跳轉最好,但是不太可能,這也是產生不能修復的 BUG的原因) 1-找到的這個代碼段可以不忽略有其他跳轉跳入 2.添加一個新節以便儲存這些被替換的代碼。 3.遍歷這個結構,把要替換的代碼段搬運到新的空間,在新的空間末尾添加一個跳入原先代碼段結束的 地址。 4.將原先的代碼塊的首5個字節添加一個JMP跳入到搬運后的地址。并填充同樣長度的花指令。 5.將這個代碼塊除了首5個字節其余字節以花指令填充。 5-x.將這個代碼

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

最新文檔

評論

0/150

提交評論