開始
以前在逆向分析的時候,遇見VMP的代碼就束手無策,只能跳過。最近在分析的時候又遇見vmp,准備研究一下。我這次遇見的VMP用查殼工具看是VMProtect(1.60-2.05)[-]。所以本次選用的殼版本是VMP1.8
VMP介紹
VMP全稱VMProtect,號稱目前軟件保護最扣一道防線。為了防止逆向分析人員對軟件的逆向分析,VMP最主要的是對指定關鍵代碼進行虛擬化,同時再加一些亂序跳轉和大量的廢指令,反調試,內存保護,導入表保護,使逆向分析人員無法分析執行的代碼,經過VMP虛擬機的代碼被膨脹好多倍。本次學習只研究VMP最關鍵和最難的部分:虛擬化
初步對比
我在visual stdio里寫了下面代碼,並對加殼時TestVmpFunc函數選擇虛擬化。本都得使用的調試器是x64dbg
#include <iostream>
_declspec(naked) void TestVmpFunc()
{
__asm
{
mov eax,0x100
mov ebx,0x1000
add eax,ebx
retn
}
}
int main()
{
//下面這是特征碼,用於在調試器里定位自己的這段代碼
__asm {
mov eax,eax
mov eax,eax
}
while (true) {
__asm {
pushad
mov eax, TestVmpFunc
call eax
popad
}
system("pause");
}
std::cout << "完成了" << std::endl;
return 0;
}
用調試器附加觀察原來只有四條匯編指令:
被虛擬化后成這樣:
代碼被虛擬化之后,假如在調試器中單步執行會跳來跳去,一條匯編會變成成百上千條指令,無法判斷他在干什么。
基本原理
經過一番查資料,知道本質來講VMP是一個基於堆棧機的intel指令模擬器,對過編譯把原來的intel指令編譯成精心設計的一組虛擬指令,然后用自己的一套引擎來解釋執行。VMP加殼后,他會將原來的代碼進行刪除,導致基本完全無法進行還原。
VMP是防止別人逆向分析自己的代碼,逆向分析的目的是分析代碼,了解代碼邏輯和代碼的目的,然后加以利用。看樣子,目前只能通過對虛擬機引擎的分析,來搞懂虛擬機引擎,然后理清代碼流程,達到逆向分析的目的。
自己實現一個簡單的虛擬機加深了解
定義寄存器和內存
這里第8個寄存器為指令指針寄存器類似x86的eip
uint32_t g_regs[8];//8個寄存器
uint32_t g_mem[1000];//1000個內存空間
這里為了簡單,規定每條指令都有三個操作數(哪怕某一條指令用不到三個參數)
指令格式為:OPCODE r,s,t
//指令操作數
struct Instruct {
uint32_t opcode;
uint32_t r;
uint32_t s;
uint32_t t;
};
聲明OPCode
enum OP_CODE {
opSTOP,/*停止執行 忽略r,s,t參數*/
opIN,/*讀入一個值放到reg[r]里*/
opOUT,/*將reg[r]的值輸入*/
opADD,/*regs[r] = regs[s] + regs[t]*/
opLD,//regs[r]=dmem[regs[s] + t]
opST,//dmem[regs[s] + t] = regs[r]
opLDA,//regs[r]= regs[s]+t
opLDC,//regs[r]=t
};
std::vector<instruct> g_instruct_list;//指令列表
初始化
void Init()
{
memset(g_regs, 0, sizeof(g_regs));
g_instruct_list.clear();
}
加載代碼
void LoadCode(const std::string & file_name)
{
//代碼文件為txt文件
//每行模式為opcode,r,s,t
//例如:1,0,0,0
std::ifstream file(file_name);
if (!file.is_open()) {
return;
}
auto GetOneInstruct = [&file](Instruct & instruct) {
char elem;
uint32_t values[4] = { 0 };
bool success = true;
for (int i = 0; i < 4 ; i++) {
file >> values[i];
if (file.fail()) {
success = false;
break;
}
if (i < 4 - 1) {
file >> elem;
}
}
if (!success) {
return false;
}
instruct = { values[0],values[1],values[2],values[3] };
return true;
};
Instruct instruct;
while (GetOneInstruct(instruct)) {
g_instruct_list.push_back(instruct);
}
}
運行指令
bool RunInstruct(const Instruct& instruct)
{
switch (instruct.opcode) {
case opSTOP:
return false;
case opIN:
Handle_opIN(instruct);
break;
case opOUT:
Handle_opOUT(instruct);
break;
case opADD:
Handle_opADD(instruct);
break;
default:
throw std::logic_error("Invalid Op Code:" + std::to_string(instruct.opcode));
break;
}
return true;
}
void RunCode() {
while (true) {
uint32_t eip = g_regs[7];
if (eip > g_instruct_list.size() - 1) {
break;
}
const Instruct& instruct = g_instruct_list.at(eip);
if (!RunInstruct(instruct)) {
break;
}
g_regs[7]++;
}
}
// handle處理
void Handle_opIN(const Instruct& instruct);
void Handle_opOUT(const Instruct& instruct);
void Handle_opADD(const Instruct& instruct);
void Handle_opLD(const Instruct& instruct);
void Handle_opST(const Instruct& instruct);
void Handle_opLDA(const Instruct& instruct);
void Handle_opLDC(const Instruct& instruct);
測試
int main()
{
Init();
LoadCode("asm.txt");
RunCode();
return -1;
}
初步分析
虛擬機入口
00952380 | 68 95514200 | push 425195 |
00952385 | E8 FC220100 | call testvmp.vmp.964686 |
push 425195的作用
經過對后面的流程進行分析,得知這里的425195在虛擬機跳轉銜接上起到了關鍵的作用。VMP為了防止逆向分析的一個重要的干擾就是亂序,運行幾行匯編就各種jump,VMP使用的jump方法是JXX指令和CALL,RET來進行。
如下代碼使用了push和ret組合實現跳轉:
00963A35 | FF7424 34 | push dword ptr ss:[esp+34] |
00963A39 | C2 3800 | ret 38 |
上面的這段代碼,假如不知道[esp+34]的值,不知道會跳轉到哪里。所以靜態分析工具例如ida是就無法分析。然而425195這個值充當了一個Key的作用。VMP巧妙的運用這個值來進行實時計算要跳轉的地方。
虛擬機初始化
單步進入就會看到虛擬機初始化的代碼。
初始化充斥着許多垃圾指令,注意看注釋。
push 45FFB40D
mov byte ptr ss:[esp],C0
call testvmp.vmp.962149
mov dword ptr ss:[esp+4],edx
mov byte ptr ss:[esp],22
pushfd
mov dword ptr ss:[esp+4],edi
jmp testvmp.vmp.9633F4
mov word ptr ss:[esp],cx
mov dword ptr ss:[esp],eax
pushad
jmp testvmp.vmp.9641DB
pushfd
mov dword ptr ss:[esp+20],esi
call <testvmp.vmp.sub_963725>
mov dword ptr ss:[esp+20],ebx
mov dword ptr ss:[esp+8],5870296F
mov dword ptr ss:[esp+1C],eax
pushfd
push esi 保存寄存器ESI
pushfd
pop dword ptr ss:[esp+20]
push A9CEAE65
pushad
push dword ptr ss:[esp+4]
mov byte ptr ss:[esp],49
lea esp,dword ptr ss:[esp+48] 彈棧
jmp testvmp.vmp.9636DA
bt ax,3
bswap di
cmc
and dh,dh
push ebp 保存寄存器EBP
xadd si,di
movsx bp,al
not edi
push ecx 保存寄存器ECX
ror esi,5
clc
push dword ptr ds:[962430]
inc si
push 540000 這個值與之前PUSH來的KEY共同計算指令handle下一跳地址
jmp testvmp.vmp.963343
test cl,F7
rcr si,cl
pushad
mov esi,dword ptr ss:[esp+50]
sbb ebp,23A52066
ror di,1
lea ebp,dword ptr ss:[esp+20]
sar di,cl
bsr dx,bp
inc edi
sub esp,A0 分配棧空間
shl dh,6
ror dx,cl
dec edi
mov al,dl
mov edi,esp VM寄存器指針
push ebx
call testvmp.vmp.964391
bswap edx
add esi,dword ptr ss:[ebp] 重定位
add esp,8
運行大致邏輯
經過我對剛才加殼的代碼進行多次單步執行分析,得到被加虛擬機的代碼運行流程如下。
EBP為虛擬機自己的棧頂地址類似x86的esp
EDI為虛擬機寄存器基地址
詳細分析
下面對各個關鍵點通過匯編和數據進行詳細分析
ESI的邏輯
代碼流是通過ESI來進行的
ESI先來自那個Push進來的Key
0096334A | 8B7424 50 | mov esi,dword ptr ss:[esp+50] | var_4 進虛擬機push的Key
再加那個540000的偏移
00964393 | 0375 00 | add esi,dword ptr ss:[ebp] | esi+= 540000
本次VMP版本ESI是每次累減而不是累加
ESI操作完現在是00965195
每次取的是[esi-1],也就是esi所示的前一個字節
0096439B | 8A46 FF | mov al,byte ptr ds:[esi-1] |
al現在就指向這里
每次算完edx(下一跳地址)之后esi還會-1
00964785 | 83EE 01 | sub esi,1 | esi:sub_9650C6+CF
第一條VM指令VMPop Reg
實際上ESI指向的2C是寄存器索引
00964241 | 891407 | mov dword ptr ds:[edi+eax],edx | Handle eax是root esi指的那個字節
2C/4 = B 所以本次VMP指令就是
VMPop Reg11
從第一條VM指令看Handle跳轉代碼的邏輯
每次要跳到哪個HANDLE取決於這行匯編代碼
009643B0 | 8B1485 AD3C9600 | mov edx,dword ptr ds:[eax*4+<sub_963cad>] | 這里的EDX決定着后面ret 38 ret到 [963CAD + Index * 4]+540000-1 edx-1+540000
可以看到這里有一個表,那就是963CAD,
這個表里的值是一個偏移。要想跳到實際的HANDLE要把這個值+540000然后再-1
比如,要跳到這個表索引為0的handle就是要跳到[963CAD+0 * 4]+540000-1 = 004246D4+540000-1=009646d3,正好是PopReeg4 handle
乍看這一個表,表里有重復的值,不知道是什么意思。
這個 index剛好就是之前的esi的值。也就是這里
那么說明esi指令的這個地方,有兩個用處?
- 決定指令流向,因為他代碼一個指令的索引
- 寄存器索引,因為他也代碼了一個寄存器索引
這看起來很詭異,因為esi所指向的這個字節他即充當了操作數寄存器的索引,又充當了本條指令handle的索引。
除非是這樣:先把流程弄好,再按排好的流程再填充這個963CAD表。
比如說,本條指定是
VMPop Reg12
則在ESI指向的那塊內存里寫入12 * 4 = 0x30,然后再在esi指向的內存里寫入0x30,然后再在963CAD這個表里的0x30索引的位置寫入VMPop 的HANDLE。
第二條VM指令 立即數壓棧
第二條指令的時候ESI指向這里
所以索引是0x46
這個指令跳到的handle會讀取[esi-4]的一個DWORD。
讀的位置也就是這里:
轉換成DWORD就是DA94102D,后面又用bswap指令轉成了2D1094DA,所以這個立即數實際上是2D1094DA
執行完又將esi前移4字節
由於這個handel有如下代碼
0096206F | 83ED 04 | sub ebp,4 |
00963B9E | 8945 00 | mov dword ptr ss:[ebp],eax | eax是立即數
所以說這個是將立即數壓棧的handle
第三條VM指令 加法
所以這個加法的操作是[ebp+4]=[ebp]+[ebp+4]
完整逆向VMP結果
VMPop Reg11
VMPushDWORD 2D1094DA
VMAdd [EBP+4]=[EBP]+[EBP+4]
VMPop Reg5
VMPop Reg6
VMPop Reg14
VMPop Reg2
VMPop Reg7
VMPop Reg5
VMPop Reg4
VMPop Reg0
VMPop Reg3
VMPop Reg10
VMPop Reg15
VMPop Reg9
VMPop Reg0
VMPush WORD 0x100
VMPUsh WORD 0x1000
VMPop Reg9
VMPop Reg8
VMPush Reg15
VMPhsh Reg9
VMPush Reg8
VmAdd
VMPopReg R13
VMPopReg R12
VMPopReg R10
VmPush Reg3
VmPUsh Reg0
VMPush Reg9
VMPush Reg12
VMPush Reg13
VmPUsh Reg2
VMPush Reg14
VmPUsh Reg3
VmPUsh Reg9
下一步要做的
下一步就是要寫腳本對更復雜的代碼進行自動解析。</sub_963cad></testvmp.vmp.sub_963725>