本章主要针对逆向中最复杂的vmp虚拟机/保护技术进行解析.
核心思想:
vmp只不过是积累了大量的混淆/膨胀 罢了

关于还原:
体力活罢了

vmp学习笔记

  1. 本课程主要负责 讲解vmp相关的知识点
  2. 通过本课程的学习可以掌握 vmp逆向相关的技术要点
  3. 可以做到还原简单的 vmp程序

0x1 文件结构

  • vmp 会对程序的 pe结构进行大幅度更改
  • 基于vmp 3.4

0x1.1 PE 头变更

  • pe头是可执行程序中的一个特殊的文件结构
  • pe头中保存着 可执行程序的一些附加信息:编译器版本,编译版本,文件标识,代码体积,代码入口点等信息
  • vmp会直接修改程序的 pe头信息,对其中一些信息进行修改

这里是windows对pe头的数据结构定义

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
    } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
  • pe标志肯定是4550,没有改变

image-20200709095632556.png

  • 文件头部只修改了解表数量

image-20200709100520137.png

  • 可选头部中进行了大量的更改,核心是修改了程序入口点AddressOfEntryPoint

image-20200709100801644.png

  • 数据目录也进行了大量的修改
  • 抹除了debug信息
  • 修改了 导入目录,导出目录,资源目录

image-20200709102052397.png

0x1.2 区段变更

  • 通常情况下加了vmp壳后的程序,多了vmp0,vmp1两个区段,区段名是可以修改的
  • text段实际还是原程序的text段,rdata也是原程序的rdata段,data段也是原程序的data段, rsrc也是原程序的rsrc段
  • 但是如果资源加密了,那么rsrc段只会保留原图标,原版权资源
  • vmp0段是vmp的虚拟机段,vmp1段是外壳段,也是保护后入口处

    1. vmp0 可以理解为功能代码实现的地方
    2. vmp1 是存放vmp寄存器等数据存储的地方

这是程序原始的区段

image-20200709094620017.png

这是加密后的区段

image-20200709094644386.png

仔细观察可以发现:

  1. vmp将 一些节表的大小(text,rdata,data)映射为0
  2. 唯一有用的数据是节表vmp1
  3. vmp1才是程序的入口点,负责解析释放资源,运行虚拟机

image-20200709103321241.png

0x2 反调试

  1. 由于vmp是一个虚拟机程序,本身就有很强的反调试性能,因此vmp本身并没有花太多精力在反调试上
  2. 使用一些常见的反调试插件就可以实现反调试了,这里推荐使用插件SharpOD
  3. 接下来主要介绍一下 vmp 反调试的原理,作为了解即可
  • kernel32.IsDebuggerPresent

    检测返回值
  • kernel32.CheckRemoteDebuggerPresent

    检测返回值
  • ntdll.ZwQueryInformationProcess

    传入不存在的ProcessInformationClass参数,0x1e,检测返回值
  • ntdll.ZwSetInformationThread

    参数ThreadInformationClass传入0x11,对调试器隐藏线程
  • ntdll.ZwQuerySystemInformation 2次

    内核调试器,枚举内核模块
  • kernel32.CloseHandle

    传入无效handle,有调试器的情况下会抛出异常

0x3 虚拟机检测

  1. vmp直接使用 汇编指令cpuid来检测虚拟机
  2. cpuid可以返回当前是否是虚拟机,甚至是直接解析出虚拟机的字符串名称
  3. 这里只给出一个简单试验demo以及对策,有需要的话后面会有专门的章节讲这一个知识点

实现代码

bool check_by_cpuid ()
{
    bool IsUnderVM = false;
    __asm {
        xor eax, eax;
        inc    eax;
        cpuid;
        bt     ecx, 0x1f;
        jc     UnderVM;
        nop;
        jmp     NotUnderVM;
UnderVM:;
        mov    IsUnderVM, 0x1;
NotUnderVM:
        nop;
    }

    return IsUnderVM;
}

解决方案:直接修改vmware的虚拟机指令屏蔽掉 cpuid指令即可

cpuid.1.ecx="0---:----:----:----:----:----:----:----"

0x4 逻辑门

  • vmp 中大量使用 逻辑门电路来实现 膨胀混淆
  • 这里主要讲解一些基础的逻辑门知识点

0x4.1 基本法则

一下3个是逻辑运算的基本算法:

  1. 与 ->同真 则真
  2. 或 ->有真 则真
  3. 非 ->真假 互换

下面两个是常用的比较复杂的组合门电路

名称符号作用
或非门nor先进行运算,然后对结果进行运算
与非门nand先进行运算,然后对结果进行运算
异或xor相异为1
同或xnor相同为1

0x4.2 代换公式

或非代换:

$ nor(a,b)=!(a|b)= !a\ \&\ !b $

$ not(a)= (!a)\ \&\ (!a)=nor(a,a)$

$ and(a,b) = a \& b = !(!a) \& !(!b) = nor(!a, !b) = nor(nor(a,a), nor(b,b)) $

$ or(a,b) = !(!(a|b)) = !(nor(a,b)) = nor(nor(a,b),nor(a,b)) $

xor.png

与非代换:

nand(a,b)=!(a\&b)=!a+!b= (!a) | (!b)$

$not(a) = !a = (!a) | (!a) = nand(a,a)$

$and(a,b) = a \& b = !(!(a\&b)) = !(nand(a,b)) = nand(nand(a,b),nand(a,b))$

$or(a,b) = a | b = !(!a) | !(!b) = nand(!a, !b) = nand(nand(a,a), nand(b,b))$
xor2.png

0x5 关于资源

  • vmp重新定义了PE资源的结构
  • vm化了系统的资源函数,相关的资源操作都会进入vmp自 己的定义里
  • 下面是资源相关的API
BeginUpdateResource
CopyImage
EndUpdateResource
EnumResLangProc
EnumResNameProc
EnumResourceLanguages
EnumResourceLanguagesEx
EnumResourceNames
EnumResourceNamesEx
EnumResourceTypes
EnumResourceTypesEx
EnumResTypeProc
FindResource
FindResourceEx
FreeResource
LoadImage
LoadResource
LockResource
SizeofResource
UpdateResource
FormatMessage
LoadAccelerators
LoadBitmap
LoadCursor
LoadIcon
LoadImage
LoadMenu
LoadString

0x6 eflag/rflag 标志位

  1. eflag/rflag 是处理器用于保存计算状态的寄存器
  2. eflag/rflag 在常规的汇编编程中属于底层操作,一般直接由专门的运算指令自动处理
  3. vmp处理后的程序会大量使用 eflag寄存器来达到混淆的目的
  4. 完整系统的学习eflag寄存器是逆向vmp的必要条件

0x6.1 标志位定义

这是intel关于 eflag的说明

image-20200715141953228.png

这里给一张对应的中文说明(这就是传说中的那张图)

flag.png

这是与运算相关的6个标志位

名称位置标志意义
CFbit 0Carry flag进位标志,运算时产生进位或借位,CF=1
PFbit 2Parity flag奇/偶校验标志,奇校验PF=1
AFbit 4Auxiliary Carry flag辅助进位标志,如低4位往高4位有进位或借位,AF=1
ZFbit 6Zero flag零标志,运算结果为0,ZF=1
SFbit 7Sign flag符号标志,运算结果为负,SF=1
OFbit 11Overflow flag溢出标志

DF: (bit 10)方向标志位

  1. 用来控制字符操作指令
  2. 置标志位,使字符操作指令 由起始地址自动递减
  3. 清除标志位,使字符操作指令由起始地址自动递增
  4. 注意此处DF的值是由程序员进行设定的 cld命令是将DF设置为0,std命令是将DF设置为1

TF:调试标志位

  • 当TF=1时,处理器每次只执行一条指令,即单步执行

IF:中断允许标志位

  • 它用来控制8086是否允许接收外部中断请求
  • 若IF=1,8086能响应外部中断,反之则屏蔽外部中断;

系统标志位和IOPL:TF,IF,IOPL,NT,RF,VM,AC,VIF,VIP,ID

  1. 这些都是系统级标志位
  2. 应用层不常用
  3. 不需要关心这些

RFLAG为64位的标志寄存器

  1. 在64位中,eflag扩展到64为并且叫做rflag
  2. rflag的高32位保留未使用,低32位 和eflag是一样的.

0x6.2 实例

  • 这里列举一些 实例来展示 标志位是如何变化的

ZF:

mov eax,10
sub eax,10
;结果eax=0,ZF=1

SF:

mov eax,10
sub eax,11 
;结果eax= -1,SF=1

CF:

mov eax,ffffffff
add eax,1
;结果eax=0,CF=1

系统判断CF标志时,把eax的初始值当作无符号数

OF:

mov eax,7fffffff
add eax,1 
;结果eax=80000000,OF=1

系统判断CF标志时,把eax的初始值当作有符号数

0x6.3 标志位 控制指令

  1. 在这些状态标志中,只有CF标志可以直接修改,使用STCCLCCMC指令
  2. 除此之外位指令(BTBTSBTRBTC)可以将指定的位复制到CF标志中
  3. 状态标志允许单个算术操作生成三种不同数据类型的结果:无符号整数、有符号 整数和BCD整数

    1. 如果算术运算的结果被视为无符号整数,则CF标志表示超出范围的条件(进位或借用)
    2. 如果被视为有符号整数(两个补数),则OF标志 表示进位或借用
    3. 如果被视为BCD数字,AF标志表示进位或借位。SF标志表示 有符号整数的符号。 ZF标志表示有符号或无符号整数零
    4. 当对整数执行多精度算术时,CF标志与带进位(ADC)的加法和带借位(SBB)指令 的减法一起使用,以将进位或借用从一个计算传播到下一个计算

这里是详细的标志位控制指令

指令影响标志位作用
STCCF设置进位标志
CLCCF清除进位标志
CMCCF取反进位标志
CLDDF清除方向标志
STDDF设置方向标志
LAHFEFLAG将eflag的低16位加载进AH寄存器中
SAHFEFLAG将AH寄存器的值存储到eflag的低16位中
PUSHF/PUSHFDEFLAG将标志寄存器压入栈中
POPF/POPFDEFLAG将栈中的数据推出到标志寄存器中
STIIF设置中断标志
CLIIF清除中断标志
BTCF=Bit(BitBase, BitOffset)设置cf为指定位的值。 ZF标志不受影响。 未定义OF、SF、AF和PF标 志。
BTSCF=Bit(BitBase, BitOffset);
Bit(BitBase, BitOffset)=1;
设置cf为指定位的值,并且设置指定位的值为1。 ZF标志不受影响。 未定义OF、SF、AF 和PF标志。
BTRCF=Bit(BitBase, BitOffset);<br/>Bit(BitBase, BitOffset)=0;设置cf为指定位的值,并且设置指定位的值为0。 ZF标志不受影响。 未定义OF、SF、AF 和PF标志。
BTCCF=Bit(BitBase,BitOffset);<br/>Bit(BitBase,BitOffset)=NOT Bit(BitBase, BitOffset)设置cf为指定位的值,指定位的值取反。 ZF标志不受影响。 未定义OF、SF、AF 和PF标志。
BSFBSF(Bit Scan Forward)位扫描找1, 低 -> 高,找到是 1 的位后, 把位置数给参数一并置 ZF=0,找不到1时, 置 ZF=1
BSRBSR(Bit Scan Reverse)位扫描找1, 高 -> 低,找到是 1 的位后, 把位置数给参数一并置 ZF=0,找不到1时, 置 ZF=1

这里给一段 bsf/bsr的案例

__asm
{
    pushad
    mov ebx, 000011100b
    bsf eax, ebx; eax=2
    bsr eax, ebx; eax=4
    popad
}

2表示从低到高找,0,1,2位找到1
4表示从高到低找,最高位1在第4位(0,1,2,3,4)

0x6.4 标志位 判断指令

  1. setxx是一系列专用指令,用于获取标志位状态
  2. 工作原理: 根据EFLAGS寄存器中状态标志(CF、SF、OF、ZF和PF)的设置,将目标操作数设置为0或1

相关术语:

  • 术语"above"和"below"与CF标志相关联,并引用两个无符号整数值之间的 关系
  • 术语"greater"和"less"与SF和OF标志相关联,比较有符号操作数
  • "小于","大于",缩写"l","g"
  • "below","above"比较无符号操作数,"低","高",缩写"b","a"

交替指令:

  1. 许多SETCC指令操作码都有交替的助记符
  2. 例如,SETG(如果更大则设置字节)和SETNLE(如果不小于或相等则设置字节)具有相同的操作码,并对相同的条件进行测试:ZF等于0,SF等于OF
  3. 提供这些交替助记符以使代码更易理解
  4. 一些语言将逻辑语言表示为具有所有位集的整数。 这种表示可以通过为 SETCC指令选择逻辑相反的条件,然后递减来获得。
指令作用备注
SETE/SETZSet byte if equal (ZF=1)
SETNE/SETNZSet byte if not equal (ZF=0).
SETA/SETNBESet byte if above (CF=0 and ZF=0).
SETAE/SETNB/SETNCSet byte if above or equal (CF=0).
SETB/SETNAE/SETCSet byte if below (CF=1).
SETBE/SETNASet byte if below or equal (CF=1 or ZF=1).
SETG/SETNLESet byte if greater (ZF=0 and SF=OF).
SETGE/SETNLSet byte if greater or equal (SF=OF).
SETL/SETNGESet byte if less (SF≠ OF)
SETLE/SETNGSet byte if less or equal (ZF=1 or SF≠ OF).
SETSSet byte if sign (SF=1).
SETNSSet byte if not sign (SF=0).
SETOSet byte if overflow (OF=1)
SETNOSet byte if not overflow (OF=0).
SETPE/SETPSet byte if parity (PF=1).
SETPO/SETNPSet byte if not parity (PF=0).
TEST逻辑比较

0x6.5 标志位 条件赋值

  1. CMOVxx是条件复制指令,本质上仍然是赋值指令
  2. 区别在于:条件复制指令根据 标志位自动判断是否执行 赋值
  3. 这些指令可以将16位、32位或64位值从内存移动到通用寄存器或从一个通用 寄存器移动到另一个通用寄存器。 不支持8位寄存器操作数的条件移动。
  4. 条件复制指令一般成对出现。例如,CMOVA(如果高), CMOVNBE(如果不低于等于)指令是操作码0F47H的备用助记符
  5. 有些处理器并不支持 这些指令,在程序中可以使用cpuid指令来判断
指令作用备注
CMOVE/CMOVZMove if equal (ZF=1).
CMOVNE/CMOVNZMove if not equal (ZF=0).
CMOVA/CMOVNBEMove if above (CF=0 and ZF=0).
CMOVAE/CMOVNBMove if above or equal (CF=0).
CMOVB/CMOVNAEMove if below (CF=1).
CMOVBE/CMOVNAMove if below or equal (CF=1 or ZF=1)
CMOVG/CMOVNLEMove if greater (ZF=0 and SF=OF)
CMOVGE/CMOVNLMove if greater or equal (SF=OF).
CMOVL/CMOVNGEMove if less (SF≠ OF).
CMOVLE/CMOVNGMove if less or equal (ZF=1 or SF≠ OF).
CMOVCMove if carry (CF=1).
CMOVNCMove if not carry (CF=0).
CMOVOMove if overflow (OF=1).
CMOVNOMove if not overflow (OF=0).
CMOVSMove if sign (SF=1).
CMOVNSMove if not sign (SF=0).
CMOVP/CMOVPEMove if parity even (PF=1).
CMOVNP/CMOVPOMove if not parity (PF=0).

0x6.6 标志位 条件跳转

  1. jcc 条件跳转指令根据 标志位 判断是否进行跳转
  2. 目标指令用相对偏移(相对于EIP寄存器中指令指针当前值的有符号偏移)指定跳转地址
  3. 由于状态标志的特定状态有时可以用两种方式解释,因此为一些操作码定义 了两个助记符

    例如,JA(跳转如果高)指令和JNBE( 如果不低于或相等)指 令是操作码77H的备用助记符。
  4. CC指令不支持远跳(跳转到其他代码段)
指令作用备注
JE/JZJump short if equal (ZF=1).
JNE/JNZJump short if not equal (ZF=0).
JA/JNBEJump short if above (CF=0 and ZF=0).
JAE/JNBJump short if above or equal (CF=0).
JB/JNAEJump short if below (CF=1).
JBE/JNAJump short if below or equal (CF=1 or ZF=1).
JG/JNLEJump short if greater (ZF=0 and SF=OF).
JGE/JNLJump short if greater or equal (SF=OF).
JL/JNGEJump short if less (SF≠ OF).
JLE/JNGJump short if less or equal (ZF=1 or SF≠ OF).
JCJump short if carry (CF=1).
JNCJump short if not carry (CF=0).
JOJump short if overflow (OF=1).
JNOJump short if not overflow (OF=0).
JSJump short if sign (SF=1).
JNSJump short if not sign (SF=0).
JPO/JNPJump short if parity odd (PF=0).
JPE/JPJump short if parity (PF=1).
LOOPZ/LOOPE Decrement count;jump short if count ≠ 0 and ZF = 1.
LOOPNZ/LOOPNE Decrement count;jump short if count ≠ 0 and ZF = 0.

0x7 VMP 核心原理

  • vmp 1.09 作为研究对象

目前主流的软件虚拟机技术分为以下三类:

  1. 基于堆栈的虚拟化技术

    实际情况中 的使用频率比较高
  2. 基于寄存器的虚拟化技术
  3. 3地址代码

    代码例子: x = y op z

    一种中间语言,编译器使用它来改进代码转换效率

    每个三地址码指令,都可以被分解为一个四元组(4-tuple):(运算符,操作数1,操作数2,结果)

    因为每个陈述都包含了三个变量,所以它被称为三地址码

关于加密/混淆:

  • vmp利用大量 位运算,逻辑 运算指令 来解密数据,每个版本都有自己的特色,核心就是利用偏门的方法来模拟基本运算

关于指令序列:

  • 在二进制层面观察所有指令集的定义,可以发现所有指令都可以分解为: 指令码 操作数的格式
  • vmp的虚拟指令 也遵守这个规则

0x7.1 实现逻辑

本程序源码如下

#include "stdafx.h"
#include <Windows.h>

void AddVMTest()
{
    __asm mov eax,1
}

int AddCalc(int a,int b)
{
    return a + b;
}


void ApiTest()
{
    MessageBoxA(0,"Hello","VMPTest",0);
}

int main(int argc, char* argv[])
{
    AddVMTest();
    AddCalc(2,3);
    ApiTest();
    getchar();
    return 0;
}

0x7.1.1 加密代码块

使用 VMP_1.09 的最强加密,尝试对如下汇编代码进行加密

mov eax,[ebp+0x8]
add eax,[ebp+0xc] 

如下图选择加密块

image-20201221135624783.png

选择最强加密

最强加密:所有 指令/handle/操作码/操作数/... 都需要解密才能使用,还有大量的混淆代码

最弱加密: 去除了栈混淆,解密算法

image-20201221135733440.png

0x7.1.2 vm 入口代码

函数原始的完整汇编代码

00401070  | 55             | push ebp                         |
00401071  | 8BEC           | mov ebp,esp                      |
00401073  | 83EC 40        | sub esp,40                       |
00401076  | 53             | push ebx                         |
00401077  | 56             | push esi                         | esi:"minkernel\\ntdll\\ldrinit.c"
00401078  | 57             | push edi                         | edi:"LdrpInitializeProcess"
00401079  | 8D7D C0        | lea edi,dword ptr ss:[ebp-40]    |
0040107C  | B9 10000000    | mov ecx,10                       |
00401081  | B8 CCCCCCCC    | mov eax,CCCCCCCC                 |
00401086  | F3:AB          | rep stosd                        |
00401088  | 8B45 08        | mov eax,dword ptr ss:[ebp+8]     |;目标1
0040108B  | 0345 0C        | add eax,dword ptr ss:[ebp+C]     |;目标2
0040108E  | 5F             | pop edi                          | edi:"LdrpInitializeProcess"
0040108F  | 5E             | pop esi                          | esi:"minkernel\\ntdll\\ldrinit.c"
00401090  | 5B             | pop ebx                          |
00401091  | 8BE5           | mov esp,ebp                      |
00401093  | 5D             | pop ebp                          |
00401094  | C3             | ret                              |

处理后的汇编代码

00401070      | 55                 | push ebp                           |
00401071      | 8BEC               | mov ebp,esp                        |
00401073      | 83EC 40            | sub esp,40                         |
00401076      | 53                 | push ebx                           |
00401077      | 56                 | push esi                           | esi:EntryPoint
00401078      | 57                 | push edi                           | edi:EntryPoint
00401079      | 8D7D C0            | lea edi,dword ptr ss:[ebp-40]      | edi:EntryPoint
0040107C      | B9 10000000        | mov ecx,10                         | ecx:EntryPoint
00401081      | B8 CCCCCCCC        | mov eax,CCCCCCCC                   |
00401086      | F3:AB              | rep stosd                          |
00401088      | E9 E9B10200        | jmp vmtest.vmp.42C276              |;修改1
0040108D      | 4A                 | dec edx                            |;修改2
0040108E      | 5F                 | pop edi                            | edi:EntryPoint
0040108F      | 5E                 | pop esi                            | esi:EntryPoint
00401090      | 5B                 | pop ebx                            |
00401091      | 8BE5               | mov esp,ebp                        |
00401093      | 5D                 | pop ebp                            |
00401094      | C3                 | ret                                |

观察后发现:

原始汇编代码

00401088  | 8B45 08        | mov eax,dword ptr ss:[ebp+8]     |;目标1
0040108B  | 0345 0C        | add eax,dword ptr ss:[ebp+C]     |;目标2

被修改为

00401088      | E9 E9B10200        | jmp vmtest.vmp.42C276              |;修改1,这里跳转到 vmp1
0040108D      | 4A                 | dec edx                            |;修改2

汇编代码 jmp vmtest.vmp.42C276 就是跳转到vmp1流程中的关键跳转

接下来流程转移到如下代码

0042C276      | 68 69C14200        | push vmtest.vmp.42C169             |;保存 第一个解密密钥,key1
0042C27B      | E9 67F1FFFF        | jmp vmtest.vmp.42B3E7              |;这跳转到vmp0
0042C280      | 47                 | inc edi                            | edi:EntryPoint

指令解析:

  1. 0042C276处的push是第一个解密密钥,用于解密下一个指令流
  2. 0042C27B最终跳转到 vmp0中执行相关代码/功能,这里就是虚拟机的和核心实现

0x7.1.3 vm 解析器/handler

虚拟机实现的关键代码如下

0042B3E7      | 9C                 | pushfd                             |;保存eflag寄存器
0042B3E8      | 60                 | pushad                             |;保存通用寄存器
0042B3E9      | 68 00000000        | push 0                             |;第二个解密密钥,key2
0042B3EE      | 8B7424 28          | mov esi,dword ptr ss:[esp+28]      |;这里获取到的就是之前 push的第一个密钥,key1
0042B3F2      | BF 00B04200        | mov edi,vmtest.vmp.42B000          |;指向vmp 上下文,俗称 vm_context
0042B3F7      | FC                 | cld                                |
0042B3F8      | 89F3               | mov ebx,esi                        |; ebx=key1
0042B3FA      | 033424             | add esi,dword ptr ss:[esp]         |; esi+=[esp] -> esi+=key2 ->esi=key1+key2

0042B3FD      | AC                 | lodsb                              |
; 将[esi]指向的那一个字节放在 al中,al=byte [esi]=byte [key1+key2]=byte [42C169]=27
; 同时 执行 esi += 1
; 此时eax=cccc cc27
; 这里取出来的 27 其实就是 vmp虚拟指令的 操作码1

; 这开始 解析/解密 操作码 
0042B3FE      | 00D8               | add al,bl                          |
0042B400      | 04 E1              | add al,E1                          |
0042B402      | 34 FE              | xor al,FE                          |
0042B404      | C0C0 03            | rol al,3                           |
0042B407      | 04 2B              | add al,2B                          |
; 这里操作码2
; 此时eax=cccc cca7


0042B409      | 00C3               | add bl,al                          |;更新 bl所代表的key  ebx=key1+al
; 这条指令运行前  eax=cccc cca7, ebx=0042 C169
; 注意是 bl+al,这种加法溢出后不会影响高位
; 最终 ebx=0042 C110


0042B40B      | 0FB6C0             | movzx eax,al                       |
; 将al扩展成4字节,高位补0,eax=0000 00a7
; 这里获得的eax 才是真正的操作码

0042B40E      | 8D0C85 AFB44200    | lea ecx,dword ptr ds:[eax*4+42B4AF]|;handler表
; 内存地址 42B4AF 处保存的是 地址表
; 这个表中的地址就是 所谓 的 handler
; handler: vmp指令实现的具体过程
; [eax*4+42B4AF] 的目的就是获取到 操作码(eax)对应的 指令实现地址
; 此时 ecx=0042B19B

0042B415      | FF21               | jmp dword ptr ds:[ecx]             |;跳转到指令地址
0042B417      | D9EB               | fldpi                              |
....
  • 后面一大段膨胀混淆的代码就不贴出来了
  • 核心是函数头部的pushfd,pushad,用于保存原始的 处理器状态
  • 0042B3E9处的push 0 是第二个解密密钥
  • 0042B3E9开始的代码用于初始化vm 以及寄存器

handle表的具体内容如下

;内存地址  数值
0042B4AF 0042C0A4 ¤ÀB. 
0042B4B3 0042B8CB ˸B. 
0042B4B7 0042B46C l´B. 
......
0042B74B 0042B19B .±B. ;目标 handler
  • handler中的每一项都是 vmp虚拟机的一个虚拟指令的实现

总结一下关于寄存器的使用

  • esi指向的是 指令流,相当于VM 的 EIP

    指令中一些操作数也会保存在这里

    最强加密中 这里取出的数据都是加密的,需要配合 bl 解密

  • al暂时保存加密中的 指令,通过[esi]获取
  • bl用于解密操作码

    解密当前 指令 和准备下次解密用的 bl 都是在一起进行的
  • eax保存的是解密后的指令码
  • handler是一个地址数组,保存的vm虚拟出的指令实现(取决于eax.有时候是一个函数的实现)
  • edi指向VM_context,实际上就是vm虚拟的寄存器

0x7.1.4 handler 虚拟机

承接上面的代码,这里直接分析 handler函数 0042B19B的实现

代码如下

0042B19B      | 0FB606             | movzx eax,byte ptr ds:[esi]        |;取出下一个 操作指令/操作数
0042B19E      | 00D8               | add al,bl                          |

0042B1A0      | 83C6 01            | add esi,1                          |;操作指令下标+1


;这里一段代码负责解密al
0042B1A3      | FEC8               | dec al                             |
0042B1A5      | F6D0               | not al                             |
0042B1A7      | C0C8 04            | ror al,4                           |
0042B1AA      | FEC8               | dec al                             |

0042B1AC      | 00C3               | add bl,al                          |
;更新下一次 的解密key
;此时eax=0000 0002

0042B1AE      | 8F0487             | pop dword ptr ds:[edi+eax*4]       |
;这里弹出的就是之前 push的 key2,代码 0042B3E9 处
;将数据 pop 到 edi[eax]
; edi 里 保存的是vm的虚拟寄存器,vm context
;==至此第一个 handle 执行结束 ==


0042B1B1      | E9 47020000        | jmp vmtest.vmp.42B3FD              |
; 重新跳转到 虚拟机入口
; 位于 0x7.1.3 虚拟机入口
; 到这里 可以发现  地址 42B3FD 开始的代码就是 vmp 循环解析指令的过程
; 这里也可以 作为一个 虚拟指令的运算周期结束

...

0x7.1.5 vm_context

在代码0042B1AE处将edi指向的 vm context dump出来

; 内存地址 数值     文本
0042B000 48E43409 .4äH 
0042B004 41E10EF8 ø.áA 
0042B008 345D7A8F .z]4      ;-->最终将数据保存到这里,为 0000 0000
0042B00C 1867F86D møg. 
0042B010 B9E6D0A3 £Ðæ¹ 
0042B014 1F2B6620  f+. 
0042B018 297F5C4A J\.) 
0042B01C 9B703C43 C<p. 
0042B020 4BB0A9C0 À©°K 
0042B024 4ECD5359 YSÍN 
0042B028 2F1828C6 Æ(./ 
0042B02C 9B6C8370 p.l. 
0042B030 1AD14840 @HÑ. 
0042B034 AF0D41A4 ¤A.¯ 
0042B038 7AA21721 !.¢z 
0042B03C C3E2C8F6 öÈâà 
0042B040 66C98C66 f.Éf 

同时将此时栈中的数据dump出来

; 地址     数值
0019FE58  00000000 
0019FE5C  0019FED0 
0019FE60  00401520 vmtest.vmp.EntryPoint
0019FE64  0019FED0 
0019FE68  0019FE7C 
0019FE6C  00233000 
0019FE70  02470DB0 &"ALLUSERSPROFILE=C:\\ProgramData"
0019FE74  00000000 
0019FE78  CCCCCCCC 
0019FE7C  00000206 
0019FE80  0042C169 vmtest.vmp.0042C169 ;这里就是 一开始 push的 key1

;从这里开始就不用看了
0019FE84  0019FF30 
0019FE88  00401520 vmtest.vmp.EntryPoint
0019FE8C  00233000 
0019FE90  CCCCCCCC 
0019FE94  CCCCCCCC 
0019FE98  CCCCCCCC 

0x7.1.5 指令流/循环解析

  • 目前为止,已经对 vmp的原理和模式有一个认知了
  • 根据上文所讲,esi中保存的是指令流标号 决定了使用哪个handler
  • 现在对 esi指向的指令流 进行一下总结

esi, 0042C169完整的数据表

0042C169 27 C0 7E F7 D3 0C 24 AD 7A 73 C7 B0 13 5C 65 BE 'À~÷Ó.$.zsÇ°.\e¾ 
0042C179 B8 21 0C B5 2B 25 80 8E 4F 80 CF 4F 31 D9 4D BD ¸!.µ+%..O.ÏO1ÙM½ 
0042C189 E3 FC 99 84 EC 19 25 4E B7 64 9D 73 10 B0 1C 62 ãü..ì.%N·d.s.°.b 
0042C199 27 3D 2C 89 AE 7C 65 8E 70 54 0A E0 8B 41 0E C5 '=,.®|e.pT.à.A.Å 
0042C1A9 CA 24 8B 46 D8 CA D1 DA 6F 28 16 AC 1B 11 7E AF Ê$.FØÊÑÚo(.¬..~¯ 
0042C1B9 F3 E5 FB E0 87 39 EF 3C F4 03 EF FE 2A 99 02 69 óåûà.9ï<ô.ïþ*..i 
0042C1C9 4D 91 A8 F7 BA E0 8A C4 76 4A 57 AC 9F 67 F2 EF M.¨÷ºà.ÄvJW¬.gòï 
0042C1D9 A5 62 C0 FD 21 08 C5 2B ED 28 4E 24 46 E2 3B 7B ¥bÀý!.Å+í(N$Fâ;{ 
0042C1E9 5D 70 57 C1 1A 82 76 DF D7 D6 12 9F CE D5 66 86 ]pWÁ..vß×Ö..ÎÕf. 
0042C1F9 B5 60 CC 64 CD E5 42 19 A9 3A 8B 2D 60 88 52 B3 µ`ÌdÍåB.©:.-`.R³ 
0042C209 F9 4C 3C F3 F8 1A BE 1D D5 1F B5 C2 6E ED 18 7C ùL<óø.¾.Õ.µÂní.| 
0042C219 71 8F EF 17 3C 46 F1 59 1A 9D 1E 8A 03 A5 52 8B q.ï.<FñY.....¥R. 
0042C229 61 FE 13 A6 B9 9F C0 8F A0 AE E1 22 4C 42 7B 7B aþ.¦¹.À. ®á"LB{{ 
0042C239 D1 19 13 39 8B 97 90 BC 01 87 C7 0D 6B D3 A9 9B Ñ..9...¼..Ç.kÓ©. 
0042C249 C1 97 34 04 3E 68 6E 85 77 7E 8C B3 BF 29 91 75 Á.4.>hn.w~.³¿).u 
0042C259 2E 68 66 96 FF 57 0B 4C 17 4D 80 89 F8 F1 B4 8A .hf.ÿW.L.M..øñ´. 
0042C269 5C D2 FE 74 A0 B6 07 E0 7C 95 F5 2E 20 68 69 C1 \Òþt ¶.à|.õ. hiÁ 
0042C279 42 00 E9 67 F1 FF FF 47 00 00 00 00 00 00 00 00 B.égñÿÿG........ 

  1. 首先执行esi[0]=27,解密后eax=a7 对应 handler 0042B19B,是一个pop指令
  2. handler 0042B19B 中取esi[1]=c0,解密后eax=2,对应pop需要的操作数
  3. handler 0042B19B 的pop指令运行完成后,重新jmp到0042B3FD
  4. 代码 0042B3FD就是vmp内部的一个指令解析器
  5. 接下去取出指令esi[2]=7e,解密后eax=a7,因此这里会取得和第一步一样的handler 0042B19B
  6. handler 0042B19B 中取esi[3]=f7,解密后eax=???,对应pop需要的操作数
  7. handler 0042B19B 的pop指令运行完成后,重新jmp到0042B3FD
  8. 这里就是一个完整的循环了
  9. 0042B3FD 就是所谓的解析器 vm_translation

0x7.1.6 context对照表

  • 上文已经提到:vm会用虚拟出的vm_context来充当原始指令中的寄存器
  • 要注意的是,这里的顺序不是固定的
  • 下面这张表格是给后面分析程序时打草稿用的
内存地址下标作用
0042B0000
0042B0041
0042B0082
0042B00C3
0042B0104
0042B0145
0042B0186
0042B01C7
0042B0208
0042B0249
0042B02810
0042B02C11
0042B03012
0042B03413
0042B03814
0042B03C15
0042B04016

0X7.2 加法还原

  • 使用ida/dbg 对 vmp 最强加密后的 exe文件进行 分析
  • 需要还原出核心的逻辑结构
  • 在ida中写出详细的注释
  • 核心是对 vm_translation=0042B415下断点,捕捉handler

0x7.2.1 push 3

esi=0042C1BE时,对应的handle0042B93A,此handler负责将第1个参数3保存到栈中

image-20201223132707238.png

0x7.2.2 push 2

esi=0042C1BF时,对应的handle0042B91A,此handler负责将第2个参数2保存到栈中

image-20201223132157242.png

0x7.2.3 执行 add

eax=00000076时可以发现原来的两个参数已经进入到当前栈中了,此时esi=0042C1C1

image-20201223131056576.png

继续跟进到当前的handler 地址是0042C099

image-20201223131056576.png

可以发现当前handle就是负责处理 原始的加法运算

运行完成之后可以发现 运算的结果已经保存到栈中了

image-20201223131540596.png

接下去几个handle肯定是将栈中的5保存到vm_context

0x7.3 msgbox还原

  • 我们这里直接使用x64的日志功能来确认 具体实现的handler

通过分析已经得知地址0042B2D8就是当前程序的 handler分发器

直接在这个地址下如下的条件断点

image-20201223142534931.png

其中 日志语法为eax={eax};esi={esi};handler={[eax*4+0x42B63B]},可以同时捕获对应的handler地址

设置完成后直接运行程序,当出现 弹窗时 从日志窗口中可以取得如下日志

eax=56;esi=42C081;handler=42BAFB
eax=56;esi=42C083;handler=42BAFB
eax=56;esi=42C085;handler=42BAFB
eax=56;esi=42C087;handler=42BAFB
eax=56;esi=42C089;handler=42BAFB
eax=56;esi=42C08B;handler=42BAFB
eax=56;esi=42C08D;handler=42BAFB
eax=56;esi=42C08F;handler=42BAFB
eax=56;esi=42C091;handler=42BAFB
eax=56;esi=42C093;handler=42BAFB
eax=6A;esi=42C095;handler=42B404
eax=8F;esi=42C097;handler=42B4D3
eax=7F;esi=42C09C;handler=42B4E7
eax=89;esi=42C09E;handler=42C042
eax=7F;esi=42C09F;handler=42B4E7
eax=8F;esi=42C0A1;handler=42B4D3
eax=89;esi=42C0A6;handler=42C042
eax=6A;esi=42C0A7;handler=42B404
eax=7F;esi=42C0A9;handler=42B4E7
eax=8F;esi=42C0AB;handler=42B4D3
eax=89;esi=42C0B0;handler=42C042
eax=8F;esi=42C0B1;handler=42B4D3
eax=7F;esi=42C0B6;handler=42B4E7
eax=70;esi=42C0B8;handler=42C042
eax=84;esi=42C0B9;handler=42B525
eax=7F;esi=42C0BA;handler=42B4E7
eax=7F;esi=42C0BC;handler=42B4E7
eax=7F;esi=42C0BE;handler=42B4E7
eax=7F;esi=42C0C0;handler=42B4E7
eax=7F;esi=42C0C2;handler=42B4E7
eax=7F;esi=42C0C4;handler=42B4E7
eax=7F;esi=42C0C6;handler=42B4E7
eax=7F;esi=42C0C8;handler=42B4E7
eax=7F;esi=42C0CA;handler=42B4E7
eax=7F;esi=42C0CC;handler=42B4E7
eax=6E;esi=42C0CE;handler=42B37B

因此esi=42C0CE时对应的handler=42B37B就是messagebox的实现

image-20201223143155813.png

观察右下角的堆栈,保存着原始数据

0x7.4 api调用总结

这里以 messagebox作为例子:

  1. 在执行你的API调用之前,跟普通PE一样,输入表是Ready的,也就是MessageBox地址是准备好的
  2. 从字节码中拿出MessageBox在输入表中位置,然后经过一次重定位(VMProtect自己的重定位方式),获取到MessageBox的真实地址
  3. 真实堆栈压入一个进入VMP的函数地址
  4. 真实堆栈压入MessageBox地址
  5. popad popfd还原前面进入虚拟机的现场
  6. 退出虚拟机,ret方式 跳入MessageBox
  7. MessageBox执行完毕之后,MessageBox内部的Ret 直接就返回到了之前准备好的进入虚拟机函数的入口地址
  8. 进入虚拟机,继续执行虚拟代码

0x8 vmp2

  • 本章主要讲解一些插件的使用,在分析vmp1.x/2.x的时候有很大的帮助
  • 这里以vmp 2.3.18作为典型
  • 仍然是使用 esi作为指令流的ip寄存器

以下几点是高版本vmp改动的地方:

  1. 增加大量膨胀,混淆指令
  2. 栈存放的方式改为 x80386
  3. 基本原理不变,整体结构发生改变
  4. 在重定位上做手脚

0x8.1 逻辑总结

这里先对vmp 2.3.18相对于v1的改变进行总结:

  1. 首先是esi指向的指令流,前后需要解密 3次
  2. 其次是eax(al)代表的指令/操作码,前后需要解密4次
  3. ebx(bl)代表的decode key参与 操作码eax(al),以及操作数esi[?]的解密过程
  4. 解密后的操作码eax(al)同时也参与 ebx(bl) decode key的下一轮解密过程
  5. handler中的地址数据是被加密的,加密的 handler临时保存在ecx中,解密后通过push ;return指令来完成跳转
  6. esi[?]中保存的 操作数也是被加密的
  7. edi指向vmp虚拟的寄存器
  8. ebp被vmp当作虚拟机的esp来使用
  9. 即使原始exe没有重定位信息(随机基址),vmp也会分配vm_relloc不过值为0

关键流程如下:

  1. 保存key1,key2
  2. 利用key1 解密出 deCodeKey
  3. 用deCodeKey解密操作码
  4. 用解密完成的操作码更新deCode
  5. 获取handler
  6. 解密handler
  7. push ret跳转到handler
  8. 取我们的操作数(加密的)
  9. 用deCodeKey解密操作数
  10. 用解密完成的操作数更新deCode
  11. 真正执行我们的handler代码
  12. 重新跳转到 vm 解析器

这张图介绍了 vmp的栈结构

image-20201229161722051.png

0x8.2 新增混淆

0x8.2.1 临近混淆

先看下面一段代码

0042DAC8      | E9 00000000        | jmp $+5                             |
0042DACD      | E9 00000000        | jmp $+5                             |
0042DAD2      | 68 B76A7692        | push 92766AB7                       |
0042DAD7      | E8 00000000        | call $+5                            | call $0
0042DADC      | C70424 E5852550    | mov dword ptr ss:[esp],502585E5     |
  • 汇编代码中会有$+num这类的地址代码
  • 指的是根据当前指令的地址,动态偏移num得到新地址

    可以理解为 address=eip+num
  • 但是 jmp/call 这类指令的长度本身就只有5字节
  • 因此jmp $+5指的是跳转到下一条指令
  • 同样call $+5 指的是call 下一条指令的地址

0x8.2.2 栈混淆

.vmp0:0042DAE3                 pushf                   ; 这里开始就是混淆代码
.vmp0:0042DAE4                 mov     byte ptr [esp+58h+var_58], dh
.vmp0:0042DAE7                 pushf
.vmp0:0042DAE8                 mov     [esp+5Ch+var_58], cx
.vmp0:0042DAED                 lea     esp, [esp+8]    ; 修复之前混淆的 栈

新版vmp特别喜欢用这类代码来删除栈

lea     esp, [esp+number]

0x8.2.3 esp混淆

.vmp0:0042B30B                 mov     [esp+28], ecx
.vmp0:0042B30F                 pusha                   ; 这里实际上执行的是
.vmp0:0042B30F                                         ; sub esp,4*8
.vmp0:0042B30F                                         ; 本意并不一定是保存寄存器
.vmp0:0042B310                 lea     esp, [esp+60]   ; 这条指令和上一个 pusha 是一堆混淆指令
.vmp0:0042B310                                         ; 用于混淆esp
.vmp0:0042B310                                         ; 相当于 lea esp,[esp+28]

0x8.3 fkvmp

  • 这是一个基于 od的插件
  • 针对目标:VMProtect 1.x 2.x (大概到 VMProtect 2.09)
  • 使用方法简单,在虚拟机入口处(push/call 或 jmp)右键 FKVMP -> start 即可
  • 可以所有Handler已经为指令都已经识别出来,初始化压栈的寄存器顺序也会打印出来。
  • 本工具(可能是第1次完成)字节码的还原的实现(其实可以认为是虚拟机指令的反汇编)使对 VMProtect 的人工分析成为可能
  • 缺点是只支持单一的基本块,每次分析到 SetEIP 指令(实际就是跳转指令)就会停止,需要人工操作,去分析下一基本块。

0x8.4 zeus

  • 这是一个全自动脱壳插件
  • 可以理解为 fkvmp的一个国内分支
  • 这是一个基于 od的插件
  • 适用于 vmp2
  • 脱壳后,需用UIF跟REC修复,才能实现跨平台
  • 推荐使用

首先分析汇编代码,找到 vm的入口点,比如下面这段代码中地址004010BA就是vm的入口点

.text:004010A0                 push    ebp
.text:004010A1                 mov     ebp, esp
.text:004010A3                 sub     esp, 40h
.text:004010A6                 push    ebx
.text:004010A7                 push    esi
.text:004010A8                 push    edi
.text:004010A9                 lea     edi, [ebp+var_40]
.text:004010AC                 mov     ecx, 10h
.text:004010B1                 mov     eax, 0CCCCCCCCh
.text:004010B6                 rep stosd
.text:004010B8                 mov     esi, esp
.text:004010BA                 jmp     vm_entry        ; 进入vm虚拟机

接下来在od中选中这行代码,然后右击选择分析

image-20201224150403259.png

接下来打开od的日志窗口就可以看到相关信息了

image-20201224150517701.png

0x8.5 VMP分析插件v1.4

  • 针对目标:VMProtect 1.x 2.x
  • 类型:Ollydbg / Immunity Debugger 插件
  • 功能:Handler 识别、虚拟字节码调试、表达式化简

可以认为是 FKVMP 的超级加强版,在同类工具之中功能最为强度的工具。

首先该工具支持了跨基本块的分析,可以一次性将全部虚拟机字节码提取出来。除此外,还有许多亮点功能:

  • 虚拟指令级别的调试。可以像调试汇编一样调试虚拟机指令,可以单步运行一条虚拟指令,并查看虚拟寄存器、虚拟栈的信息。
  • 表达式转化及化简。本功能会在虚拟指令级别进行数据流和控制流的分析,进行字节码的收缩。 VMProtect 是栈机结构,同时有 NOR 逻辑的混淆膨胀,字节码的收缩还原一直是研究的重点,本工具可以完成字节码收缩过程,输出收缩后的表达式。唯一不同是没有转化成原始的 x86 代码。但在分析上,已经可以提供极为有力的参考。
  • 支持字节码的 Patch。由于加密的存在,字节码 Patch 一直是十分痛苦的过程,本工具可以像 Patch 普通指令一样 Patch 虚拟指令。
  • 支持自定义模板。模板包括 Handler 识别模板和表达式化简模板。该工具提供了模板文件及模板修改工具。理论上,可以通过对模板文件的修改使及兼容所有版本的 VMProtect 1.x 2.x。( 3.x 因为虚拟机结构变化不能支持)
  • 下图是工具使用截图。该工具以插件的形式,实现了一个与原生OD非常相似的VMP调试界面,寄存器、栈的内容可以实时查看
  • 使用方法简单,在虚拟机入口处(push/call 或 jmp)右键 VMP分析插件 -> 分析虚拟程序 完成分析(f9)。然后可以打开插件的虚拟指令窗口、调试窗口查看字节码并进行调试分析。

image-20201224165519889.png

这个插件的核心是将vmp相关的膨胀混淆指令按照语义还原成简单的类x86指令,可能会出现很多不认识的指令,这时候打开编辑虚拟指令信息就可以查看详细的注释

image-20201225092143635.png

0x8.6 编译器还原

核心原理:

  1. VMP分析插件v1.4或者xxdisasm生成的虚拟汇编还原成c语言

    虚拟寄存器之类的 可以还原成局部变量
  2. 利用编译器 编译/优化
  3. 最后利用ida打开处理后的二进制文件即可

0x9 vmp sdk 使用

  • vmp 安装完成后会带有sdk开发包
  • 使用这个sdk可以完成一些比较高级的加密功能
  • 这里主要介绍 如何在程序开发中引入vmp的sdk进行保护VMProtectSDK.h
  • 注意ddk/sys是驱动文件
  • 这里以加密字符串的功能作为例子
  • 参考链接

0x9.1 sdk 结构

安装目录如下

+---Include
|   +---ASM
|   |       VMProtectSDK.inc
|   |
|   +---C
|   |       VMProtectDDK.h
|   |       VMProtectSDK.h    //c语言程序主要使用这个 头文件开发
|   |
|   \---Pascal
|           VMProtectSDK.pas
|
\---Lib                         //运行时需要使用的库文件
    |   VMProtectDDK32.sys
    |   VMProtectDDK64.sys
    |   VMProtectSDK32.dll
    |   VMProtectSDK64.dll
    |
    +---COFF                    //编译时需要连接的lib文件
    |       VMProtectDDK32.lib
    |       VMProtectDDK64.lib
    |       VMProtectSDK32.lib
    |       VMProtectSDK64.lib
    |
    \---OMF
            VMProtectSDK32.lib

下面这个是一份VMProtectSDK.hdemo

#pragma once

#ifdef _WIN64
    #pragma comment(lib, "VMProtectSDK64.lib")
#else
    #pragma comment(lib, "VMProtectSDK32.lib")
#endif

#ifdef __cplusplus
extern "C" {
#endif

// protection
__declspec(dllimport) void __stdcall VMProtectBegin(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginVirtualization(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginMutation(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginUltra(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginVirtualizationLockByKey(const char *);
__declspec(dllimport) void __stdcall VMProtectBeginUltraLockByKey(const char *);
__declspec(dllimport) void __stdcall VMProtectEnd(void);

// utils
__declspec(dllimport) BOOL __stdcall VMProtectIsDebuggerPresent(BOOL);
__declspec(dllimport) BOOL __stdcall VMProtectIsVirtualMachinePresent(void);
__declspec(dllimport) BOOL __stdcall VMProtectIsValidImageCRC(void);
__declspec(dllimport) char * __stdcall VMProtectDecryptStringA(const char *value);
__declspec(dllimport) wchar_t * __stdcall VMProtectDecryptStringW(const wchar_t *value);
__declspec(dllimport) BOOL __stdcall VMProtectFreeString(void *value);

// licensing
enum VMProtectSerialStateFlags
{
    SERIAL_STATE_FLAG_CORRUPTED            = 0x00000001,
    SERIAL_STATE_FLAG_INVALID            = 0x00000002,
    SERIAL_STATE_FLAG_BLACKLISTED        = 0x00000004,
    SERIAL_STATE_FLAG_DATE_EXPIRED        = 0x00000008,
    SERIAL_STATE_FLAG_RUNNING_TIME_OVER    = 0x00000010,
    SERIAL_STATE_FLAG_BAD_HWID            = 0x00000020,
    SERIAL_STATE_FLAG_MAX_BUILD_EXPIRED    = 0x00000040,
};
#pragma pack(push, 1)
typedef struct
{
    WORD            wYear;
    BYTE            bMonth;
    BYTE            bDay;
} VMProtectDate;
typedef struct
{
    INT                nState;                // VMProtectSerialStateFlags
    wchar_t            wUserName[256];        // user name
    wchar_t            wEMail[256];        // email
    VMProtectDate    dtExpire;            // date of serial number expiration
    VMProtectDate    dtMaxBuild;            // max date of build, that will accept this key
    INT                bRunningTime;        // running time in minutes
    BYTE            nUserDataLength;    // length of user data in bUserData
    BYTE            bUserData[255];        // up to 255 bytes of user data
} VMProtectSerialNumberData;

#pragma pack(pop)
__declspec(dllimport) INT  __stdcall VMProtectSetSerialNumber(const char * SerialNumber);
__declspec(dllimport) INT  __stdcall VMProtectGetSerialNumberState();
__declspec(dllimport) BOOL __stdcall VMProtectGetSerialNumberData(VMProtectSerialNumberData *pData, UINT nSize);
__declspec(dllimport) INT  __stdcall VMProtectGetCurrentHWID(char * HWID, UINT nSize);

// activation
enum VMProtectActivationFlags
{
    ACTIVATION_OK = 0,
    ACTIVATION_SMALL_BUFFER,
    ACTIVATION_NO_CONNECTION,
    ACTIVATION_BAD_REPLY,
    ACTIVATION_BANNED,
    ACTIVATION_CORRUPTED,
    ACTIVATION_BAD_CODE,
    ACTIVATION_ALREADY_USED,
    ACTIVATION_SERIAL_UNKNOWN,
    ACTIVATION_EXPIRED
};

__declspec(dllimport) INT __stdcall VMProtectActivateLicense(const char *code, char *serial, int size);
__declspec(dllimport) INT __stdcall VMProtectDeactivateLicense(const char *serial);
__declspec(dllimport) INT __stdcall VMProtectGetOfflineActivationString(const char *code, char *buf, int size);
__declspec(dllimport) INT __stdcall VMProtectGetOfflineDeactivationString(const char *serial, char *buf, int size);


#ifdef __cplusplus
}
#endif

0x9.2 字符串加密解析

  • 下面这个 代码demo 用于 测试 vmp的字符串加密功能的实现
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include "VMProtectSDK.h"

int main(int argn,char ** args,char ** env)
{
    
    //获取解密后的字符串
    char * str_decode=VMProtectDecryptStringA("被加密字符");
    
    MessageBox(0,str_decode,"标题",MB_OK);
    
    //释放内存
    VMProtectFreeString(str_decode);
    return 0;
}

这是原始代码

.text:00401077                 push    ebp
.text:00401078                 mov     ebp, esp
.text:0040107A                 sub     esp, 10h
.text:00401080                 mov     [ebp+lpMem], 0
.text:00401087                 mov     [ebp+var_8], 0
.text:0040108E                 mov     [ebp+var_C], esp
.text:00401091                 push    offset unk_47B2F8       ;对应原始的参数
.text:00401096                 call    VMProtectDecryptStringA ;vm加密函数
.text:0040109B                 cmp     [ebp+var_C], esp
.text:0040109E                 jz      short loc_4010B7

这是被vmp处理后的代码

.text:00401077                 push    ebp
.text:00401078                 mov     ebp, esp
.text:0040107A                 sub     esp, 10h
.text:00401080                 mov     [ebp+var_4], 0
.text:00401087                 mov     [ebp+var_8], 0
.text:0040108E                 mov     [ebp+var_C], esp
.text:00401091                 jmp     loc_4B792C            ;修改1,虚拟机入口点,可以用 vmp分析插件直接对这里进行分析
.text:00401096                 call    sub_45A4E0            ;修改2
.text:0040109B                 cmp     [ebp-0Ch], esp
.text:0040109E                 jz      short loc_4010B7
  • 这里虚拟机的核心是利用 大量 逻辑运算进行解密
  • 没必要深究 逻辑运算是如何实现的,每个版本都不一样
  • 要注意的是返回值肯定是解密后的字符串

0x? vmp3.4 iat调用

  1. vmp 会对所有 iat 调用进行混淆
  2. 这里要注意的是无论怎么混淆 最终都会得出 目标函数的 函数地址

首先来看一下原始程序中 关于iat调用的代码:

image-20200715104840734.png

  • 直接获取函数LoadStringA的函数地址放在edi
  • 随后通过 call edi的方式来调用

接下来我们看一下vmp处理后的代码:

image-20200715111102915.png

  • 原来的iat调用变成了一个 ppo;call调用
  • 这个call会调转到vmp0
  • 直接将496b3e处的实现代码提取出来
00496B3E 90              nop
00496B3F E9 EE571000     jmp 0059C332

0059C332 873C24          xchg dword ptr ss:[esp],edi
0059C335 E9 1FBFF4FF     jmp 004E8259

004E8259 57              push edi
004E825A 66:0F40F9       cmovo di,cx ;无关指令
004E825E 50              push eax
004E825F 9F              lahf        ;无关指令
004E8260 B8 D2144000     mov eax,0x4014D2
004E8265 E9 A3751B00     jmp 0069F80D

0069F80D 8B80 6A9E1800   mov eax,dword ptr ds:[eax+0x189E6A]
0069F813 0F40FB          cmovo edi,ebx;无关指令
0069F816 66:8BFF         mov di,di;无关指令
0069F819 8D80 543D9053   lea eax,dword ptr ds:[eax+0x53903D54]
0069F81F 8BF8            mov edi,eax
0069F821 66:0FB6C7       movzx ax,bh;无关指令
0069F825 86C4            xchg ah,al;无关指令
0069F827 58              pop eax
0069F828 E9 4422D6FF     jmp 00401A71

00401A71 C3              retn

对以上代码进行简化,去除无关指令获得以下代码

0059C332 873C24           xchg dword ptr ss:[esp],edi ;将虚拟机的 返回地址应用到真机中
004E8259 57               push edi
004E825E 50               push eax
004E8260 B8 D2144000      mov eax,0x4014D2
0069F80D 8B80 6A9E1800    mov eax,dword ptr ds:[eax+0x189E6A]
0069F819 8D80 543D9053    lea eax,dword ptr ds:[eax+0x53903D54]
0069F81F 8BF8             mov edi,eax ;计算出 目标api的地址,保存到寄存器 edi中
0069F827 58               pop eax
00401A71 C3               retn        ;跳转到 虚拟机指定的 返回地址与  0x0059C332 对应
  • 首先 api的调用会被vmp转换成pop a;call b的形式
  • a就是vmp虚拟机中的返回地址,利用代码0x0059C332将返回地址应用到真机中
  • 代码0x00401A71负责跳转到 a代表返回地址
Last modification:November 22, 2022
如果觉得我的文章对你有用,请随意赞赏