VMProtect虛擬機保護分析入門


開始

以前在逆向分析的時候,遇見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;
}

用調試器附加觀察原來只有四條匯編指令:

0ff4358cc6b45c7af9664a1dcca63a8d.png

被虛擬化后成這樣:

55569acc5d065d116c130e958f122a88.png

代碼被虛擬化之后,假如在調試器中單步執行會跳來跳去,一條匯編會變成成百上千條指令,無法判斷他在干什么。

基本原理

經過一番查資料,知道本質來講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 {
&nbsp;&nbsp;&nbsp; uint32_t opcode;
&nbsp;&nbsp;&nbsp; uint32_t r;
&nbsp;&nbsp;&nbsp; uint32_t s;
&nbsp;&nbsp;&nbsp; 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	

運行大致邏輯

經過我對剛才加殼的代碼進行多次單步執行分析,得到被加虛擬機的代碼運行流程如下。

f9f9c87470ad70319643b250d1c906a2.png

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

0d11cafc73116aaaf434f7df8ca450ee.png

每次取的是[esi-1],也就是esi所示的前一個字節

0096439B | 8A46 FF                  | mov al,byte ptr ds:[esi-1]                                  | 

al現在就指向這里

67a4900b0845dba0bc61ca37a341c13d.png

每次算完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,

b2312745e624e3c6d33e7c5cb7fab1ee.png

這個表里的值是一個偏移。要想跳到實際的HANDLE要把這個值+540000然后再-1

比如,要跳到這個表索引為0的handle就是要跳到[963CAD+0 * 4]+540000-1 = 004246D4+540000-1=009646d3,正好是PopReeg4 handle

乍看這一個表,表里有重復的值,不知道是什么意思。

這個 index剛好就是之前的esi的值。也就是這里

29f160bb2eb91f074d738b10055a6bfd.png

那么說明esi指令的這個地方,有兩個用處?

  1. 決定指令流向,因為他代碼一個指令的索引
  2. 寄存器索引,因為他也代碼了一個寄存器索引

這看起來很詭異,因為esi所指向的這個字節他即充當了操作數寄存器的索引,又充當了本條指令handle的索引。

除非是這樣:先把流程弄好,再按排好的流程再填充這個963CAD表。

比如說,本條指定是

VMPop Reg12

則在ESI指向的那塊內存里寫入12 * 4 = 0x30,然后再在esi指向的內存里寫入0x30,然后再在963CAD這個表里的0x30索引的位置寫入VMPop 的HANDLE。

第二條VM指令 立即數壓棧

第二條指令的時候ESI指向這里

8dbbd85b4851f1a72c218eba26f75629.png

所以索引是0x46

這個指令跳到的handle會讀取[esi-4]的一個DWORD。

ee44a2ebe058c3489b6b9bb0c858d231.png

讀的位置也就是這里:

37dce97eb09e0d755c97f1ac9549e4e4.png

轉換成DWORD就是DA94102D,后面又用bswap指令轉成了2D1094DA,所以這個立即數實際上是2D1094DA

執行完又將esi前移4字節

ebed7adedf1e4ed11a3d8e4db6067184.png

由於這個handel有如下代碼

0096206F | 83ED 04                  | sub ebp,4                                                   |
00963B9E | 8945 00                  | mov dword ptr ss:[ebp],eax                                  | eax是立即數

所以說這個是將立即數壓棧的handle

第三條VM指令 加法

34ffc252a8040799b5bb5513e521a7c5.png

所以這個加法的操作是[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>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM