[轉]VMP虛擬機加殼的原理學習

VMP虛擬機加殼的原理學習 - .Little Hann

很久沒有到博客寫文章了,9月份開學有點忙,參加了一個上海的一個CHINA SIG信息比賽,前幾天又無錫南京來回跑了幾趟,簽了阿里巴巴的安全工程師,準備11月之後過去實習,這以前就好好待在學校學習了。html

這段時間斷斷續續把《加密與解碼 第三版》給看完了,雖然對逆向仍是隻知其一;不知其二,不過對VMP虛擬機加殼這塊有了一點新的認識。這裏分享一些筆記和想法,沒有新的東西,都是書上還KSSD裏看來的,權當筆記和分享,大神勿噴。java

準備分紅3部分講一下web

1. VM虛擬機簡介算法

2. VM虛擬指令和x86彙編的映射原理sql

3. 用VM虛擬指令構造一段小程序小程序

1. VM虛擬機簡介windows

虛擬機保護技術就是將基於x86彙編系統的可執行代碼轉換爲字節碼指令系統的代碼,以達到保護原有指令不被輕易逆向和修改的目的,這種指令也能夠叫僞指令,和VB的pcode有點相似。安全

從本質上講,虛擬指令系統就是對本來的x86彙編指令系統進行一次封裝,將本來的彙編指令轉換爲另外一種表現形式。多線程

例如:框架

push uType
push lpCaption
push lpText
push hWnd,
call MessageBox

這是一段x86的彙編指令,編譯器在翻譯的時候會產生一個固定模式的彙編代碼(在同一個CPU指令集下)。
但若是咱們對本來的C代碼使用VMP SDK進行虛擬化,那麼在編譯這段代碼的時候就會使用等效的VM虛擬指令來達到一樣的功能。
vPushMem uType
vPushMem lpCaption
vPushMem lpText
vPushMem hWnd,
vCall vCode

注意,虛擬指令的也有本身的機器碼,但和本來的x86彙編機器碼徹底不同,並且經常是一堆無心義的代碼,它們只能由VM虛擬解釋器(Dispatcher)來解釋並執行(關於虛擬解釋器接下來會詳細解釋),
因此,咱們在用OD等工具進行反彙編分析的時候,看到的就是一大堆的無心義的代碼,甚至還有大量的junk code,jmp code等,致使咱們沒法從反編譯層面分析出本來的代碼流向,天然也就沒法輕易的進行
算法逆向分析了

咱們在逆向虛擬機加殼後的程序中看到的彙編代碼其實不是x86彙編代碼,而是字節碼(僞指令)它是由指令執行系統定義的一套指令和數據組成的一串數據流。
java的JVM、.NET或其餘動態語言的虛擬機都是靠解釋字節碼來執行的,但它們的字節碼(中間語言IL)之間不通用,由於每一個系統設計的字節碼都是爲本身使用的,並不兼容其餘的系統。
因此虛擬機的脫殼很難寫出一個通用的脫殼機,原則上只要虛擬指令集一變更,那本來的僞指令的解釋就會發生變化。
我我的理解要逆向被VM SDK保護起來的原始代碼,只有手工分析這段虛擬指令,找到虛擬指令和原始彙編的對應關係,而後重寫出原始程序的代碼,完成算法的逆向和分析。

這張圖是一個虛擬機執行時的整圖概述,VStartVM部分初始化虛擬機,VMDispatcher負責調度這些Handler,Handler能夠理解爲一個個的子函數(功能代碼),它是每個僞指令
對應的執行功能代碼,爲何要出現一條僞指令對應着一個Handler執行模塊呢?這和虛擬機加殼的指令膨脹有關,被虛擬機加殼後,一樣一條指令被翻譯成了虛擬僞指令,
一條虛擬僞指令每每對應着好幾倍的等效代碼,當中可能還加入了花指令,整個Handler加起來可能就等效爲本來的一條x86彙編指令。
Bytecode就是虛擬僞指令,在程序中,VMDispatcher每每是一個類while結構,不斷的循環讀取僞指令,而後執行。
Virtual Machine Loop Start
...
...
...
--> Decode VM Instruction's
...
--> Execute the Decoded VM Instruction's
...
...
--> Check Special Conditions
...
...
...
Virtual Machine Loop End


在理解Handler(VM虛擬指令和x86彙編的映射)以前,有必要先理解一下VM的啓動框架和調用約定,每種代碼執行模式都須要有啓動框架和調用約定。
在C語言中,在進入main函數以前,會有一些C庫添加的啓動函數,它們負責對棧區,變量進行初始化,在main函數執行完畢以後再收回控制權,這就叫作啓動框架。而
C CALL,StdCall等約定就是調用約定,它們規定了傳參方式和堆棧平衡方式。

一樣,對與VM虛擬機,咱們也須要有一種啓動框架和調用約定,來保證虛擬指令的正確執行以及虛擬和現實代碼之間的切換。

1. 調度器VStartVM
VStartVM過程將真實環境壓入後有一個VMDispatcher標籤,當handler執行完畢以後會跳回到這裏造成了一個循環,因此VStartVM過程也能夠叫作Dispatcher(調度器)
VStartVM首先將全部的寄存器的符號壓入堆棧,而後esi指向字節碼的起始地址,ebp指向真實的堆棧,edi指向VMContext,esp再減去40h(這個值能夠變化)就是VM用的堆棧地址了。
換句話說,這裏將VM的環境結構和堆棧都放在了當前堆棧之下200h處的位置上了。
由於堆棧是變化的,在執行完跟堆棧有關的指令時總應該檢查一下真實的堆棧是否已經接近本身存放的數據了,若是是,那麼再將本身的結構往更地下移動。
而後從 movzx eax, byte ptr [esi]這句開始,讀字節碼,而後在jump表中尋找相應的handler,並跳轉過去繼續執行。

VStartVM
push eax
push ebx
push ecx
push edx
push esi
push edi
push ebp
pushfd
mov esi, [esp+0x20] ;字節碼開始的地址(寄存器佔了32byte,從32byte開始就是剛纔push的字節碼的位置)
mov ebp, esp ;ebp就是堆棧了
sub esp, 0x200
mov edi, esp ;edi就是VMContext
sub esp, 0x40 ;這時的esp就是VM用的堆棧了
VMDispatcher
movzx eax, byte ptr [esi] ;得到bytecode
lea esi, [esi+1] ;跳過這個字節
jmp dword ptr [eax*4 + JUMPADDR] ;跳到handler執行處

調用方法
push 指向字節碼的起始地址
jmp VStartVM

這裏有幾個約定
edi = VMContext
esi = 當前字節碼地址
ebp = 真實堆棧

在整個虛擬機代碼執行過程當中,必需要遵照一個事實。
1. 不能將edi,esi,ebp寄存器另作他用
2. edi指向的VMContext存放在棧中而沒有存放在其餘固定地址或者申請的堆空間中,是由於考慮到多線程程序的兼容



2. 虛擬環境 VMContext
VMContext即虛擬環境結構,存放了一些須要用到的值
struct VMContext
{
DWORD v_eax;
DWORD v_ebx;
DWORD v_ecx;
DWORD v_edx;
DWORD v_esi;
DWORD v_edi;
DWORD v_ebp;
DWORD v_efl; 符號寄存器
}


3. 平衡堆棧 VBegin和VCheckEsp
VMStartVM將全部的寄存器都壓入了堆棧,因此,首先應該使堆棧平衡才能開始執行真正的代碼(這是書上寫的)。個人理解是這裏要先將現實的代碼中的寄存器複製切換到虛擬代碼中

Vebing:
mov eax, dword ptr [ebp] ;ebp指向真實的堆棧
mov [edi+0x1C], eax ;edi指向VMContext
add esp, 4

mov eax, dword ptr [ebp] ;ebp指向真實的堆棧
mov [edi+0x18], eax ;v_ebp
add esp, 4

mov eax, dword ptr [ebp] ;ebp指向真實的堆棧
mov [edi+0x14], eax ;v_edi
add esp, 4

mov eax, dword ptr [ebp] ;ebp指向真實的堆棧
mov [edi+0x10], eax ;v_esi
add esp, 4

mov eax, dword ptr [ebp] ;ebp指向真實的堆棧
mov [edi+0x0C], eax ;v_edx
add esp, 4

mov eax, dword ptr [ebp] ;ebp指向真實的堆棧
mov [edi+0x08], eax ;v_ecx
add esp,

mov eax, dword ptr [ebp] ;ebp指向真實的堆棧
mov [edi+0x04], eax ;v_ebx
add esp, 4

mov eax, dword ptr [ebp] ;ebp指向真實的堆棧
mov [edi], eax ;v_eax
add esp, 4

add esp, 4 ;釋放參數
jmp VMDispatcher

執行這個"Handler"以後,堆棧就平衡了,就能夠開始"繼續"執行真正的僞代碼了。

還有一個問題,由於將VMContext結構存放在當前使用的堆棧(EBP)更低地址的區域(初始時VMContext距離棧頂EBP有0x200的空間),當堆棧被push壓入數據時,
總會在某條指令以後改寫VMContext的內容,爲了不這種狀況,就須要對VMContext進行動態移位。


VCheckEsp:
lea eax, dword ptr [edi+0x100] ;edi指向VMContext
cmp eax, ebp ;小於則繼續正常運行
jl VMDispatcher

mov edx, edi ;不然,則進行移位
mov ecx, esp
sub ecx, edx ;計算一下須要搬移的字節空間,做爲循環複製的次數

push esi ;保存ip指針
mov esi, esp
sub esp, 0x60
mov edi, esp
push edi ;保存新的edi VMContext基址
sub esp, 0x40

cld
rep movsb ;複製
pop edi
pop esi
jmp VMDispatcher

一些設計到堆棧的Handler在執行後跳到VCheckEsp判斷esp是否接近VMContext所在的位置,若是是就將VMContext結構複製到更低的地方去




2. VM虛擬指令和x86彙編的映射原理
這是VM虛擬機的核心部分,即把每條x86指令或每一類x86彙編指令轉換爲等效的僞指令Handler,能夠說,不一樣的虛擬機都有本身的一套爲指令集,不一樣的爲指令集對原始的
x86彙編指令的翻譯都是不一樣的

handler分兩大類:
1. 輔助handler,指一些更重要的、更基本的指令,如堆棧指令
2. 普通handler,指用來執行普通的x86指令的指令,如運算指令

1. 輔助handler
輔助handler除了VBegin這些維護虛擬機不會致使崩潰的handler以外,就是專門用來處理堆棧的handler了。

vPushReg32:
mov eax, dword ptr [esi] ;從字節碼中獲得VMContext中的寄存器偏移
add esi, 4
mov eax, dword ptr [edi+eax] ;獲得寄存器的值
push eax ;壓入寄存器
jmp VMDispatcher

vPushImm32:
mov eax, dword ptr [esi]
add esi, 4
push eax
jmp VMDispatcher

vPushMem32:
mov eax, 0
mov ecx, 0
mov eax. dword ptr [esp] ;第一個寄存器偏移
test eax, eax
cmovge edx, dword ptr [edi+eax] ;若是不是負數則賦值
mov eax, dword ptr [esp+4] ;第二個寄存器偏移
test eax, eax
cmovge ecx, dword ptr [edi+eax] ;若是不是負數則賦值
imul ecx, dword ptr [esp+8] ;第二個寄存器的乘積
add ecx, dword ptr [esp+0x0C] ;第三個爲內存地址常量
add edx, ecx
add esp, 0x10 ;釋放參數
push edx ;插入參數
jmp VMDispatcher

vPopReg32:
mov eax, dword ptr [esi] ;獲得reg偏移
add esi, 4
pop dword ptr [edi+eax] ;彈回寄存器
jmp VMDispatcher

vFree:
add esp, 4
jmp VMDispatcher


2. 普通Handler

add指令的形式一般有
add reg,imm
add reg,reg
add reg,mem
add mem,reg
等寫法....
若是將操做數都先交給堆棧handler處理,那麼執行到vadd handler時,已是一個當即數存在堆棧中了,vadd handler沒必要去管它從哪裏來,只須要用這個當即數作加法操做便可。
也就是說,將輔助指令和普通指令配合起來來一塊兒完成x86指令到僞指令的轉換

vadd:
mov eax, [esp+4] ;取源操做數
mov ebx, [esp] ;取目的操做數
add ebx, eax
add esp, 8 ;平衡堆棧
push ebx ;壓入堆棧


下面的指令轉換爲僞代碼:
add esi, eax
轉換爲
vPushReg32 eax_index ;eax在VMContext下的偏移
vPushReg32 esi_index
vadd
vPopReg32 esi_index

add esi, 1234
轉換爲
vPushImm32 1234
vPushReg32 esi_index
vadd
vPopReg32 esi_index


add esi, dword ptr [401000]
轉換爲
vPushImm32 401000
vPushImm32 -1 ;scale
vPushImm32 -1 ;reg_index
vPushImm32 -1 ;reg2_index
vPushMem32 ;壓入內存地址的值
vPushReg32 esi_index
vadd
vPopReg32 esi_index

這就是add指令的多種實現,咱們能夠發現不管是哪種形式,均可以使用vadd來執行,只是使用了不一樣的堆棧handler,這就是Stack_Based虛擬機的方便之處。



標誌位的問題:
在相關的handler執行前恢復標誌位,執行後保存標誌位。以stc命令來講,stc指令是將標誌的CF位置爲1
VStc:
push [edi+0x1C]
popfd
stc
pushfd
pop [edi+0x1C]
jmp VMDispatcher

這樣操做以後就能保證代碼中的標誌不會被虛擬機引擎所執行的代碼所改變



轉移指令:
轉移指令有條件轉移、無條件轉移、call和retn
實現時能夠將esi指向當前字節碼的地址,esi指針就比如真實CPU中的eip寄存器,能夠經過改寫esi寄存器的值來更改流程。無條件跳轉jmp的handler比較簡單

vJmp:
mov esi, dword ptr [esp] ;[esp]指向要跳轉到的地址
add esp, 4
jmp VMDispatcher

條件轉移jcc指令稍微有一點麻煩,由於它要經過測試標誌位來判斷視爲須要更改流程


基本上全部條件跳轉指令都有相應的CMOV指令來匹配。
vJne:
cmovne esi, [esp]
add esp, 4
jmp VMDispatcher

vJa:
cmova esi, [esp]
add esp, 4
jmp VMDispatcher

vJae:
cmovae esi, [esp]
add esp, 4
jmp VMDispatcher

vJb:
cmovb esi, [esp]
add esp, 4
jmp VMDispatcher

vJbe:
cmovbe esi, [esp]
add esp, 4
jmp VMDispatcher

je:
cmove esi, [esp]
add esp, 4
jmp VMDispatcher

jg:
cmovg esi, [esp]
add esp, 4
jmp VMDispatcher


call指令:
call和retn指令雖然也是轉移指令,可是它們的功能不太同樣。
首先,虛擬機設計爲只在一個堆棧層次上運行

mov eax, 1234
push 1234
call anotherfunc
theNext:
add esp, 4

其中第124條指令都是在當前堆棧層次上執行的,而call anotherfunc是調用子函數,會將控制權移交給另外的代碼,這些代碼是不受虛擬機控制的。因此碰到call指令,必須退出虛擬機,讓子函數在真實CPU中執行完畢後再交回給虛擬機執行下一條指令。
vcall:
push theNext
jmp anotherfunc

若是想在推出虛擬機後讓anotherfunc這個函數返回後再次拿回控制權,能夠更改返回地址,來達到繼續接管代碼的操做。在一個地址上寫上這樣的代碼:
theNextVM:
push theNextByteCode
jmp VStartVM

這是一個從新進入虛擬機的代碼,theNextByteCode表明了theNext以後的代碼字節碼。只需將theNext的地址改成theNextVM的地址,便可完美地模擬call指令了。當虛擬機外部的代碼執行完畢後ret回來的時候就會執行theNextVM的代碼,從而使虛擬機繼續接管控制權。

vcall:
push all vreg ;全部虛擬寄存器
poo all reg ;彈出到真實寄存器中
push 返回地址 ;
push 要調用的函數的地址
retn


ret指令:
retn指令和其餘普通指令不太同樣,retn在這裏被虛擬機認爲是一個推出函數。retn有兩種寫法:一種是不帶操做數的,另外一種是帶操做數的。
retn
retn 4
第一種retn形式先獲得當前esp中存放的返回地址,而後再釋放返回地址的堆棧並跳轉到返回地址
第二種在釋放返回地址的堆棧時再釋放操做數的空間

vRetn:
xor eax, eax
mov ax, word ptr [esi] ;retn的操做數是word型的,因此最大隻有0xFFFF
add esi, 2
mov ebx, dword ptr [ebp] ;獲得要返回的地址
add esp, 4 ;釋放空間
add esp, eax ;若是有操做數,一樣釋放
push ebx ;壓入返回地址
push ebp ;壓入堆棧指針
push [edi+0x1C]
push [edi+0x18]
push [edi+0x14]
push [edi+0x10]
push [edi+0x0C]
push [edi+0x08]
push [edi+0x04]
push [edi]
pop eax
pop ebx
pop ecx
pop edx
pop esi
pop edi
pop ebp
popfd
pop esp ;還原堆棧指針到esp中,而VM_CONTEXT也算是自動銷燬了
retn



不可識別指令:
在這裏任何不能識別的指令均可將其劃分爲不可模擬指令,碰到這類指令時,只能與vcall使用相同的方法,即先退出虛擬機,執行這個指令,
而後再壓入下一字節碼(虛擬指令)的地址,從新進入虛擬機。






3. 用VM虛擬指令構造一段小程序
上面闡述了一些關於虛擬機運行和虛擬指令的一些原理和理解,接下來從實踐的調度來實現一個最簡單的基於虛擬指令的小程序。
即咱們的執行代碼是咱們自定義的一些僞指令,調度器在執行的時候不斷的從僞指令字節碼中取指令,而後到Handler地址表中尋址對應的執行體Handler,進行執行,執行完畢後
再跳回到調度器回收控制權,依次循環,知道執行完全部的虛擬指令。

------------------------------
code:

#include "stdafx.h" #include "windows.h" /* 下面是虛擬指令 咱們只模擬了2條指令 */ //push 0x12345678 push一個4字節的數 #define vPushData 0x10 //call 0x12345678 call一個4字節的地址 #define vCall 0x12 //結束符 #define vEnd 0xff //一個字符串 char *str = "Hello World"; /* 這是咱們構造的虛擬指令, 數據還不 在mian裏面咱們進行了修改 push 0 push offset str push offset str ;把字符串的地址入棧 push 0 call MessageBoxA ; */ BYTE bVmData[] = { vPushData, 0x00, 0x00, 0x00,0x00, vPushData, 0x00, 0x00, 0x00,0x00, vPushData, 0x00, 0x00, 0x00, 0x00, vPushData, 0x00, 0x00, 0x00,0x00, vCall, 0x00, 0x00, 0x00, 0x00, vEnd}; //這就是簡單的虛擬引擎了 _declspec(naked) void VM(PVOID pvmData) { __asm { //取vCode地址放入ecx mov ecx, dword ptr ss:[esp+4] __vstart: //取第一個字節到al中 mov al, byte ptr ds:[ecx] cmp al, vPushData je __vPushData cmp al, vCall je __vCall cmp al, vEnd je __vEnd int 3 __vPushData: inc ecx mov edx, dword ptr ds:[ecx] push edx add ecx, 4 jmp __vstart __vCall: inc ecx mov edx, dword ptr ds:[ecx] call edx add ecx, 4 jmp __vstart __vEnd: ret } } int main(int argc, char* argv[]) { //修改虛擬指令的數據

  *(DWORD *)(bVmData+5 + 1) = (DWORD)str; *(DWORD *)(bVmData+10 + 1) = (DWORD)str; *(DWORD *)(bVmData+20 + 1) = (DWORD)MessageBoxA; //執行虛擬指令 VM(bVmData); return 0; }

這個程序中的__vstart就至關於前面說的VMDispatcher,對僞指令進行識別判斷,採起調度策略使程序流跳轉到相應的Handler裏去。
每一個Handler在執行完以後都有一句相同的代碼:jmp __vstart 用於回收控制權到VMDispatcher中。以便下一次調度。
最後,當全部的僞指令都執行完以後,程序識別到vEnd,就ret退出程序。

基本上,這個虛擬機的原理就是這樣了。




4. 總結
這裏有一點關鍵點其實沒有弄清楚,就是x86彙編指令到虛擬機僞指令的轉換問題,上面的小程序咱們是直接自定義了一套僞指令和映射規則,實際狀況中如VMProtect加殼軟件要
解決的是根據本來的彙編指令動態的轉換爲僞指令再回寫到原程序的二進制文件中。

還有一個問題沒搞明白的就是,若是要對虛擬機的程序進行逆向分析或脫殼。個人理解就只能是想辦法找到目標程序僞指令和x86彙編之間的映射關係,而後手工分析這段代碼
(由於考慮到效率問題,通常的程序都是隻對一些核心算法進行了虛擬化),將這段代碼重寫出來,以達到逆向分析或脫殼的目的。

不知道理解的對不對,逆向這塊感受算法分析和虛擬機是個難點,之後能夠針對這個問題進行一些更深刻的研究
 
相關推刊
  • 《匿名收藏》 6
我來評幾句
登陸後評論

已發表評論數(0)

相關文章
相關標籤/搜索