VMProtect虚拟机保护分析入门

VMProtect虚拟机保护分析入门开始以前在逆向分析的时候,遇见VMP的代码就束手无策,只能跳过。最近在分析的时候又遇见vmp,准备研究一下。我这次遇见的VMP用查壳工具看是VMProtect(1.60-2.05)[-]。所以本次选

大家好,又见面了,我是你们的朋友全栈君。

开始

以前在逆向分析的时候,遇见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>

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/154687.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 第十六天-企业应用架构模式-离线并发模式[通俗易懂]

    第十六天-企业应用架构模式-离线并发模式[通俗易懂]第十六天-企业应用架构模式-离线并发模式

    2022年4月21日
    50
  • java的json解析几种方法_java读取json文件并解析

    java的json解析几种方法_java读取json文件并解析微信搜索关注“咖啡遇上代码”公众号,查看更多一、什么是JSONJSON是一种轻量级的数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得JSON成为理想的数据交换语言。易于阅读和编写,同时也易于解析和生成,并有效地提升网络传输效率。二、JSON语法(1)数据在名称/值对中(2)数据由逗号分隔(3)大括号保存对象(4)中括号保存数…

    2022年10月5日
    3
  • 但是,在通过移动数组的上升周期中找到指定元素

    但是,在通过移动数组的上升周期中找到指定元素

    2022年1月7日
    44
  • idea2021.12.13激活码_在线激活

    (idea2021.12.13激活码)这是一篇idea技术相关文章,由全栈君为大家提供,主要知识点是关于2021JetBrains全家桶永久激活码的内容IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html4C3L012EAA-eyJsa…

    2022年3月30日
    57
  • sql prompt插件的安装

    sql prompt插件的安装这里写自定义目录标题UnabletoconnecttotheRedgateClientService解决方法新手小白,记录一下第一次安装MicrosoftSQLServerManagementStudio(以下简称为ssms),以及它的插件sqlprompt遇到的问题。UnabletoconnecttotheRedgateClientService首先,成功安装了ssms,然后下载了sqlprompt插件的安装包。可以去它的官网下载,懒得找的话我也帮忙找到了它的

    2022年7月15日
    12
  • 手机里实现图片文字识别的实用方法[通俗易懂]

    手机里实现图片文字识别的实用方法[通俗易懂]突然接到老板给的一个任务——把一篇文章排版出来,你会怎样做?是一个字一个字手动手动输入呢?还是语音识别呢?当然,这两种方法都可行,但是不够简单方便。手动输入太慢,语音识别又有点麻烦,如果普通话不好,识别很可能会出错。那什么方法,实现图片文字识别最简单,最方便,还很精确呢?今天就来和大家分享一下,手机里的逆天黑科技,实现图片文字识别,只需5秒钟!方法一:打开QQ,左上角【扫一扫】——手机…

    2022年6月5日
    39

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号