From: http://resources.infosecinstitute.com/reverse-engineering-virtual-machine-protected-binaries/
在代碼混淆當中,虛擬機被用于在一個程序上運行不同機器指令集。例如虛擬機可以讓32位的x86架構機器上運行ARM指令集。用于代碼混淆的虛擬機跟那種普通的,能運行操作系統的虛擬機完全不同(如:VMware),前者只用于執行有限的指令做一些特定的任務。
在了解相關代碼混淆器的虛擬機指令集執行機制后,逆向工程一個使用該指令集的虛擬機所保護程序還是比較容易的。只需要花費少量的時間來研究一下這個架構的指令集操作碼。然而悲劇的是,現在的虛擬機代碼混淆器大多數都使用自定義指令集。換句話說,每個指令都被賦予一個自定義的操作碼(通常都是隨機的)和自定義的格式,逆向人員需要逆向解碼每個操作碼的含義。這簡直會玩死人!舉個栗子,讓我們來看看32位的x86指令集和我們將在本文中介紹的自定義指令集之間的區別:
很明顯,這些指令都是把第二個操作數指定的內存字節賦值到第一個操作數的寄存器當中。然而這兩條指令的二進制操作碼表示卻并不相同,其中第二條指令的0x56操作碼完全是一個隨機數字。兩條指令的第二個字節都表示操作碼需要用到的寄存器,其中每4位表示一個寄存器。
在進入逆向工程實例之前,我們得先知道基于虛擬機代碼混淆技術幕后的工作原理:虛擬機啟動后首先第一件事便在它的進程虛擬地址空間中申請一塊“address space”,換句話說,它申請所需的內存空間,棧和寄存器。然后,虛擬機加載操作碼文件并執行。代碼的執行是由一個VM loop完成的。在這個loop當中,虛擬機的處理器解析每個預定的操作碼和操作數,然后根據指令集迭代執行。直到VM loop遇到一個指定的退出操作碼。
為此我花了一些時間用C語言寫了一個自定指令集的虛擬機,完整的源代碼可以在這篇文章最后獲得。正如你所猜的,單獨一個虛擬機是做不了任何事情的。這也是我為什么還寫了這么一個CrackMe小程序。另外我誠摯邀請大家也給這個小家伙添加更多的功能吧!
在前言中提到過的,這個虛擬機使用一套自定的指令集,并由虛擬機在初始化階段后把操作碼文件加載到“address space”。
讓我們確保操作碼文件和虛擬機在同一個目錄,然后執行。隨便輸入一串密碼可以看到:
密碼驗證失敗!
我們現在的目標就是給這個程序找出正確的密碼。先從看一下這個操作碼文件(vm_file)開始,用十六進制編輯器打開它:
可以看到,在vm_file文件有諸如”Right pass!“,”Wrong pass!“和”Password:“的字符串。接下來開始逆向這個虛擬機,用IDA打開它。
IDA打開虛擬機之后我們直接定位到VM loop的虛擬地址:0x00401334。下圖顯示了這個程序相當龐大,但既然找到正確的入口點那我們肯定有辦法搞定它。
讓我們來看看入口函數執行了哪些指令:
push ebp
push edi
push esi
push ebx
sub esp, 2Ch
mov esi, [esp+3Ch+arg_0]
mov ebx, [esp+3Ch+arg_4]
mov ax, [ebx+0Ah]
lea ebp, [esi+1200h]
loc_40134D: ; This is where the loop starts
movzx edx, ax
mov cl, [esi+edx]
lea edx, [eax+1]
mov [ebx+0Ah], dx
sub ecx, 10h
cmp cl, 0E1h ; switch 226 cases
jbe short loc_40136C
”mov cl, [esi+edx]“指令讀取一個字節到CL,很顯然CL寄存器只包含了操作碼。該操作碼是通過ESI和EDX兩個寄存器進行定位的。從前面可以清楚地看到EDX只包含了一個WORD(16 bit)而ESI包含了DWORD(32 bit)。所以,ESI實際上是指向VM代碼段,而DX指向我們的虛擬機當前指令的指針(文件中當前操作碼的index)。
在正確讀入字節之后我們注意到DX寄存器的值被保存到[EBX + 0AH]。這位置是虛擬機分配的寄存器空間。我們現在知道EBX寄存器指示著ESI寄存器所指向的文件數據在內存中的位置。
在比較之前,我們注意到編譯器使用了編譯優化:在訪問switch table之前的每個操作碼的值減去0x10。
loc_40136C:
movzx ecx, cl
jmp ds:switchTable[ecx*4] ; switch jump
該switch table雖然相當大,但它可以更快地計算出動態地址。你可以在Win32下用OllyDbg或IDA運行調試這個程序。
第一個switch帶我們跳到一個小過程當中:
我們現在處于“case 0x18”這個操作碼,因為編譯器增加了一個減法操作優化這段代碼。如果你現在回去檢查一下vm_file的話可以發現第一個字節就是0x18。這個操作碼似乎需要一些操作數,所以VM多讀取一個字節到DX寄存器。下一步,VM的指令指針[EBX+0AH]更新為EAX+2,這意味著IP(instruction pointer)指向下一個字節。之后,讀取到的字節跟3比較,如果大于3則離開循環并拋出一個異常。但在我們的例子中是不會拋出異常的,因為二進制文件中該操作數等于0x01,因此程序不會發生跳轉。接著我們到達這里:
提醒你一下,EBX是指向虛擬機的寄存器數組的指針,所以第一條指令把[EBX+1*2](第二個寄存器)初始化為0。
現在,我們有足夠的信息可以判斷VM包含了4個寄存器,我們可以稱之為R0,R1,R2,R3。
剩下的代碼從文件加載2個字節(大端序的0x250)的數據存放到R1寄存器中。接著,VM的指令指針會指向下一條指令,也就是在文件的0x04偏移處。最后,jmp跳轉到loc_40134D的VM loop循環處開始下一條指令的執行。
直到現在,我們只能知道第一條指令是什么,它只是一條簡單的mov指令。這條指令可以重寫為下面的格式:
MOV R1, 250H
讓我們來看看下個操作碼(0xAF):
第一個代碼塊跟之前的mov指令一樣。顯然,這是需要用到一個寄存器作為操作數的典型代碼,在我們的例子當中,它使用的是R1(0x01)寄存器。下一步它訪問[EBX+0CH]的寄存器。我們知道這個寄存器肯定不是R0,R1,R2,R3。因為R3被存儲在[EBX+6]。我們也知道這不是IP指令指針,因為它位于[EBX+0AH]。所以要弄清楚這個寄存器是什么,我們需要回去檢查它在主函數中的初始化:
.text:00402703 mov word ptr [eax+0Ch], 256
回到我們分析處,我們注意到獲取到這個寄存器的值后做減一操作,然后再跟0xFFFF做比較。因為該寄存器被初始化為256,所以在該寄存器的值為0并減一之前是不會等于0xFFFF的。如果該寄存器等于0xFFFF,那么VM將退出循環。因為我們這是第一次執行,所以斷定[EBX+0CH]肯定等于255。
接下來兩條指令讀取R1(0x250)的值保存到DX寄存器。接著就可以看到一條有趣的指令:
mov [esi+eax*2+1000h], dx
如果你還記得的話,ESI是指向代碼和數據區域的基址。此外,ESI+1000H跨度了4K的地址空間。因此,我們可以假設,ESI+1000H是指向一個不同的“section”的VM“address space”。
我們可以用偽代碼重述一下這個操作:
#!c
WORD section[256];
[…]
section[ –reg ] = R1;
看起來這似乎是一個棧結構,R1寄存器的值被保存到棧指針減一后所在的位置。我們可以大膽地假設0xAF操作碼代表PUSH指令。因此,這條操作碼指令的含義可以理解為:PUSH R1。
現在我們知道[EBX+0CH]是VM的棧指針,棧空間為256*sizeof(uint16_t)。此外,如果你想把VM的棧指針和x86架構的機器棧指針做比較,可以看到VM的棧指針僅僅是一個array的index,而x86機制的棧指針是一個寄存器(ESP)。
接著第三個操作碼(0xC2):
這個操作碼的含義似乎是在讀取棧頂的一個WORD數據。但在讀取之前它先檢查棧是否為空,如果是,則拋出一個VM異常。因為之前已經有一個值PUSH進去了,所以我們知道這個棧不為空。把棧頂的數據保存到DX寄存器后,棧指針+1。我們還知道DX的值現在為0x250(屬于代碼和數據區域的一部分)。隨后,確保棧頂的值不會超過0x1000(address space的尺寸)。接下來把[ESI+DX]指向的字符串作為的參數調用printf。在我們這個例子當中,vm_file第0x250個字節保存的字符串是“Password:”,它將被打印到屏幕上。
我們可以得出這樣的結論:0xC2指令需要把字符串偏移PUSH到棧上,然后POP出來printf它。
正如你所見,在這完成逆向這些操作碼之后我們已經到達打印“Password:”的代碼上。你可能已經注意到我們可以用單一的指令簡化每個操作碼所代表的執行動作。接下來,我們不采用逐步的分析方式來分析這些操作碼了。但是現在的你應該能夠逆向工程一個被虛擬機所保護的程序,甚至打造一個屬于自己的虛擬機保護程序。
下面給出如何快速找到正確密碼的方式:
用十六進制編輯器打開vm_file文件,取出0x80到0x17F偏移處的256個隨機字節,我們可以把它稱之為Random。將用戶輸入的密碼每個字節都跟Random異或運行,然后跟vm_file文件在0x240偏移處預定的數組做比較。
我在下面引用一節中已經給出了一個密碼生成器。編譯執行它便可獲得正確的密碼: