逆向被虛擬機器所保護的二進位制檔案

wyzsk發表於2020-08-19
作者: 左懶 · 2015/11/24 12:13

From: http://resources.infosecinstitute.com/reverse-engineering-virtual-machine-protected-binaries/

0x00 簡介


在程式碼混淆當中,虛擬機器被用於在一個程式上執行不同機器指令集。例如虛擬機器可以讓32位的x86架構機器上執行ARM指令集。用於程式碼混淆的虛擬機器跟那種普通的,能執行作業系統的虛擬機器完全不同(如:VMware),前者只用於執行有限的指令做一些特定的任務。

在瞭解相關程式碼混淆器的虛擬機器指令集執行機制後,逆向工程一個使用該指令集的虛擬機器所保護程式還是比較容易的。只需要花費少量的時間來研究一下這個架構的指令集操作碼。然而悲劇的是,現在的虛擬機器程式碼混淆器大多數都使用自定義指令集。換句話說,每個指令都被賦予一個自定義的操作碼(通常都是隨機的)和自定義的格式,逆向人員需要逆向解碼每個操作碼的含義。這簡直會玩死人!舉個例子,讓我們來看看32位的x86指令集和我們將在本文中介紹的自定義指令集之間的區別:

很明顯,這些指令都是把第二個運算元指定的記憶體位元組賦值到第一個運算元的暫存器當中。然而這兩條指令的二進位制操作碼錶示卻並不相同,其中第二條指令的0x56操作碼完全是一個隨機數字。兩條指令的第二個位元組都表示操作碼需要用到的暫存器,其中每4位表示一個暫存器。

在進入逆向工程例項之前,我們得先知道基於虛擬機器程式碼混淆技術幕後的工作原理:虛擬機器啟動後首先第一件事便在它的程式虛擬地址空間中申請一塊“address space”,換句話說,它申請所需的記憶體空間,棧和暫存器。然後,虛擬機器載入操作碼檔案並執行。程式碼的執行是由一個VM loop完成的。在這個loop當中,虛擬機器的處理器解析每個預定的操作碼和運算元,然後根據指令集迭代執行。直到VM loop遇到一個指定的退出操作碼。

0x01 舉個例子


為此我花了一些時間用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執行除錯這個程式。

0x02 第一條指令


第一個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

0x03 第二條指令


讓我們來看看下個操作碼(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)。

0x04 第三條指令


接著第三個操作碼(0xC2):

這個操作碼的含義似乎是在讀取棧頂的一個WORD資料。但在讀取之前它先檢查棧是否為空,如果是,則丟擲一個VM異常。因為之前已經有一個值PUSH進去了,所以我們知道這個棧不為空。把棧頂的資料儲存到DX暫存器後,棧指標+1。我們還知道DX的值現在為0x250(屬於程式碼和資料區域的一部分)。隨後,確保棧頂的值不會超過0x1000(address space的尺寸)。接下來把[ESI+DX]指向的字串作為的引數呼叫printf。在我們這個例子當中,vm_file第0x250個位元組儲存的字串是“Password:”,它將被列印到螢幕上。

我們可以得出這樣的結論:0xC2指令需要把字串偏移PUSH到棧上,然後POP出來printf它。

正如你所見,在這完成逆向這些操作碼之後我們已經到達列印“Password:”的程式碼上。你可能已經注意到我們可以用單一的指令簡化每個操作碼所代表的執行動作。接下來,我們不採用逐步的分析方式來分析這些操作碼了。但是現在的你應該能夠逆向工程一個被虛擬機器所保護的程式,甚至打造一個屬於自己的虛擬機器保護程式。

0x05 破解密碼


下面給出如何快速找到正確密碼的方式:

用十六進位制編輯器開啟vm_file檔案,取出0x80到0x17F偏移處的256個隨機位元組,我們可以把它稱之為Random。將使用者輸入的密碼每個位元組都跟Random異或執行,然後跟vm_file檔案在0x240偏移處預定的陣列做比較。

我在下面引用一節中已經給出了一個密碼生成器。編譯執行它便可獲得正確的密碼:

0x06 引用


本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章