介紹VMP虛擬化原理之前,先簡單介紹一下計算機運行的原理。總所周知,現代計算機的核心部件是CPU、內存、磁盤、鍵盤、顯示器等;最最最核心的就屬CPU、內存和磁盤了。用戶按開機鍵,CPU會把OS從磁盤加載到內存運行。由於CPU只能識別並執行二進制文件,所以代碼、數據等都是以二進制存放在磁盤和內存的。
1、為了在軟件層面“虛擬化”出底層的硬件,讓OS順利在虛擬機運行,虛擬機也要提供CPU、內存和磁盤的“虛擬”環境,讓二進制代碼得以順利執行。目前主流的虛擬化技術是intel/amd推出的VT技術,讓VMware、virtualBox這種虛擬機可以“直接”讓硬件CPU執行二進制代碼,極大提升了虛擬機的效率和速度,減少了模擬的性能損耗。但這種虛擬機的功能相對較重,並且直接讓硬件CPU執行虛擬機的二進制代碼,達不到保護、混淆代碼的目的,所以這種硬件虛擬化是不適合做代碼保護的,只能考慮通過純軟件模擬虛擬機執行代碼指令。
為了在軟件層面模擬CPU執行二進制的代碼指令,需要有以下關鍵點:
- 虛擬機的寄存器,用來存放各種臨時數據
- 虛擬機的堆棧,用來做各種數據交換
- 虛擬機的指令。x86架構下,CPU一旦讀取到0x55指令,就知道執行push ebp;一旦讀取到0x8BEC,就知道執行mov ebp,esp; 同理,虛擬機也需要有自己的指令集,虛擬CPU才知道自己要干啥。一般虛擬機的指令要么是操作寄存器,要么是操作堆棧,要么做各種算數運算,虛擬機指令的handler都要模擬這些功能。那么問題來了,虛擬機的指令集能不能和物理CPU一樣了? 顯然是不行的! 兩個原因:(1)如果一樣,還要純軟件虛擬機干啥? (2)如果一樣,達不到混淆指令的目的
- 虛擬機的EIP,用來指明虛擬CPU當前執行的代碼
為了滿足以上關鍵點,VMP采取的方案:
- 虛擬機的寄存器:在內存開辟一段連續的區域當成虛擬機的寄存器,業界稱之為VM_CONTEXT,某些版本的VMP用EDI指向這個區域
- 虛擬機的堆棧: 這個和物理機是一樣的,直接在內存開辟就好。VMP還是用EBP指向棧頂
- 虛擬機的指令:不同版本VMP的指令是不一樣的,這樣可以在一定程度上防止VMP本身被破解,業界俗稱VM_DATA
- 虛擬機的EIP:業界俗稱vEIP,某些版本的VMP用ESI替代,指向VM_DATA,用以讀取虛擬CPU需要執行的指令;
2、VMP虛擬機的執行流程
(1)想想啟動VT時,是不是要先開辟一段內存空間,把當前guestOS部分寄存器的值保存好?VMP也一樣,先保存物理寄存器的值,后續退出VM后才能還原
(2)讓vEIP從VM_DATA讀取虛擬機的指令
(3)由於虛擬機的指令和物理CPU完全不同,那么在指令讀取后,該怎么去執行了?舉個栗子:比如0x1表示入棧,0x2表示出棧,0x3表示寄存器之間互相傳數據(當然實際的指令可能不會這么簡單,VMP每個版本的指令集都不同),這些指令該怎么執行了?在VMP中,有個概念叫handler,專門根據不同的指令執行不同的操作(當然這些操作VMP事先都定義好了)。這個和VT中VMX的handler作用類似:根據不同的異常有不同的處理方法(我個人猜測VMP的作者肯定借鑒了VT的原理和思路);
為了達到這種不同指令執行不同handler分支的效果,編碼實現層面通常用switch+case實現,用於將不同的指令跳轉到不同的分支執行,業界俗稱dispatcher。具體到匯編代碼,switch+case一般的匯編形式為:mov ecx,dword ptr ds:[eax*4+base] (注意寄存器可能會變成其他的,但這 xxx*4+基址的形式不會變), 這是比較明顯的特征,用以用來定位VMP的dispatcher。
(4)執行完一個handler,vEIP接着指向下VM_DATA的下一個指令,然后重復(2)-(4)這幾個步驟;
整個過程展示如下:

(5)綜上所述,要想全面了解、分析和掌控VMP,必須要找准這么幾個點:
- VM_DATA:虛擬機的指令都集中在這了
- VM_CONTEXT:虛擬寄存器都保存在這里
- diapatcher:所有指令都從這里路由到對應的handler執行(可以簡單理解為管理層派發活的,不過3.x版本的VMP貌似去掉了統一的dispatcher,由上個handler直接跳轉到下個handler,有點P2P、區塊鏈去中心化的感覺)
- handler:具體模擬執行虛擬指令的分支(可以簡單理解為具體干活的工具人)。handler之間跳轉通過jmp esi 或 push esi ,ret等指令實現(不同版本使用的寄存器可能不同,但跳轉實現的方式就這些);
- vEIP:當前執行的指令,需要明確是由那個物理寄存器保存的
- vStack:存放了臨時數據用於各種交換,目前版本還是由
3、上面講了大段各種理論,下面說說具體怎么找這些關鍵點(注意: 下面OD分析的截圖不是一次調試截取的,事實上我測試時反復調試了十幾次,每次用OD打開樣本的地址都不同,但不影響代碼執行的順序和關鍵點的分析)。
(1)demo代碼如下:這里模擬一個密碼、lisence之類的場景:正確的密碼是123,輸入后輸出ok;輸入錯誤的密碼就輸出fail;
#include <stdio.h> #include <stdlib.h> #include <Windows.h> char buf[1204]; void main() { while (1) { scanf("%s", buf); if (!strcmp(buf, "123")) printf("ok\n"); else printf("fail\n"); } }
(2)這里只把main函數虛擬保護即可:

(3)虛擬化后的結果:從40K增加到602K,增加了15倍;

用OD打開:VMP0段564K,膨脹的部分都集中在這里了;

(4)剛進入OD,看到的全是jmp。根據之前的理論分析,這些jmp構成了handler表:

在剛才那個內存視圖,選中vmp0段,右鍵選擇內存訪問斷點,后續只要訪問這個段就會立即斷下來。然后開始運行,斷到下面:push xxxx,call xxxx這是非常明顯的VMP3.xx版本的殼特征:

不停F7單步進入后來到這里:看到了好多push 寄存器的指令,這里就是保存原始物理寄存器的值的,為后續進入VM加殼做准備;可以看到在push指令之間加載着大量沒用的垃圾指令;

這些push指令執行完后,棧如右圖所示;注意這里一行關鍵代碼已經標紅: esp+0x28處的值賦給了edi,edi指向了VM_DATA,也就是虛擬指令集(后面會進一步解密edi);

經過下面add edi,edx后,得到真正的VM_DATA地址:

繼續往下:又有一條關鍵指令:sub esp,0xC0; 這里把esp往上開辟192byte的空間,作為VM_CONTEXT(也就是虛擬存器)。后續會用esp+edx來讀取這些虛擬寄存器;此時堆棧圖如右邊:下面保存的是物理寄存器,上面是VM_CONTEXT,中間以ebp隔開;

繼續F7(后續能看到大量這類似的代碼):通過ebp從棧取原物理寄存器的值,然后通過edi取指令,接着根據指令對取出的數做各種運算:

計算完畢后寫回VM_CONTEXT:

接着繼續取指令和棧的值:

計算完畢后繼續寫回VM_CONTEXT:

期間還檢查虛擬棧是否填滿:

棧頂和棧底比較,看看誰大:
如果棧夠用就通過jmp繼續執行下個handler:

如此往復好多次,都快把VM_CONTEXT填滿為止,終於到了while循環。這時不知道密碼是多少,先隨便輸入,看到程序輸出了fail,突破點就在這了: 通過ASCII在內存中找fail,然后下個訪問的斷點,成功斷下來:

這里回溯棧:在棧中保存了函數的調用關系,這里肯定有while循環、判斷對比的函數:這里把password在棧上所有的函數都輪詢個遍(kernerl32、ucrtbase這些windows系統自帶的API就沒必要了),挨個下斷點,一共下了十幾個:

這里用辨識度比較高的字符串"aaaaaaaaaaaaa"輸入,然后挨個斷點地檢查,終於在一個斷點處找到了密碼字符:這個大概率是strcmp函數

總結:1、大段代碼被人為切割成一小塊一小塊地執行,代碼之間通過jmp、call、push+ret方式跳轉
2、統一的dispatcher是真的沒有了,handler倒是有一個大表(整個程序的入口點也改到這里了),里面全是jmp語句,對應不同的handler分支
3、esp指向VM_CONTEXT,edx是虛擬寄存器的偏移,通過esp+edx定位虛擬寄存器的位置;讀取的虛擬寄存器保存在ecx,各種處理后通過ecx寫回虛擬寄存器;
ebp指向虛擬棧頂,通過ebp+偏移挨個讀寫虛擬棧的數據;
4、取指令、解密、執行、跳轉到下一個handler:很多代碼都是一樣的,重復生成了好多次
參考:1、https://bbs.pediy.com/thread-225262.htm 新手篇VMProtect 1.81 Demo
2、https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458296943&idx=1&sn=8ba937d6216a37025d5f97d8a4989f4a VMProtect 3.3.1虛擬機&代碼混淆機制入門
3、https://bbs.pediy.com/thread-225803.htm 如何分析虛擬機(2):進階篇 VMProtect 2.13.8
4、https://bbs.pediy.com/thread-224732.htm 談談VMP的爆破
