VMP3源码学习——虚拟化

admin 2025-12-29 00:55:39 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入分析了VMP3源码中的虚拟化实现机制,详细讲解了VM入口初始化、上下文保存、密钥滚动以及Handler如Push、Pop、Call等指令的执行逻辑。文章还剖析了伪指令流的编译过程与业务逻辑包装,为理解商业虚拟化保护技术与逆向分析提供了核心参考。 综合评分: 90 文章分类: 逆向分析,二进制安全,代码审计


cover_image

VMP3源码学习——虚拟化

原创

CrazyHarb

冲鸭安全

2025年11月16日 10:02 北京

VMP3源码学习——虚拟化

背景:

前文中我们已经分析了源码中的变异核心指令,我们书接上文,本文中我们来看一下VMP中的虚拟化相关功能的代码,vmp的运行流程可以参考鸭哥的(VMP3.x内部原理详解与还原思路),链接我放在本文最后的参考中了,说实话,我不是这方面专家,所以本文中涉及的逻辑仅限于windows x64下的PE文件的高级版本VMP

 前文摘要

VMP源码学习(1) 变异分析与代码bug

入口点:

| | | — | | C                    // intel 虚拟化入口                      void IntelFunction::CompileToVM(const CompileContext &ctx); |

HANDLERS分析:

1. entry_command_

这个就是鸭哥文章中提到的vmentry,该代码的最终效果可参考文章中的内容,它在源码中初始化流程为:

1初始化不使用寄存器列表,x64下是R12 ~ R15

1在rbx、rbp、rsi、rdi、R8~R11这几个寄存器之间,不放回取四个寄存器,分别表示密钥寄存器、伪指令寄存器(VIP)、伪栈寄存器(VSP)、跳转寄存器

1初始化入口加密器,其实就是随机几条指令add sub xor inc dec bswap rol ror not neg这几个指令进行随机,指令条数大于100,或者大于3且随机数的最低位为1(我称它为随机数命中),则生成结束。同等道理,如果vmp选项中选中了加密字节码,也会随机一个指令加密器

1开始填入代码,把rax、rcx、rdx、rbx、rbp、rsi、rdi、R8~R15、rflag随机交换顺序后,全部入栈

1随机一个空闲寄存器存储当前段地址,并把该寄存器入栈

1将rsp + 0x90的值赋给伪指令寄存器,0x90代表前面压入栈的16个寄存器+1个随机的空闲寄存器+call的返回地址,共18*8 = 0x12*8 = 0x90,取到的是vm入口入栈的参数

1通过入口加密器解密伪指令寄存器,解密后加上前面的当前的段地址,当然这些运算都是低32位的,如果镜像地址高出32位地址,就把高位地址加上

1把rsp栈指针赋值给伪栈寄存器,并执行 rsp -= 0x180后,将RSP进行对齐操作,即

| | | — | | C                   RSP &= 0xFFFFFFFFFFFFFFF0 |

| | | — | | Assembly language                   示例代码                   push    rdi                   push    r9                   push    rbx                   push    r11                   push    rcx                   push    rax                   push    r12                   push    r8                   push    rsi                   push    rbp                   push    r10                   pushfq                   push    r14                   push    rdx                   push    r15                   push    r13                   ; 以上是全部入栈                   mov     rbp, 7FF50F570000h                   push    rbp; 存储当前段的地址                   mov     rbx, [rsp+90h]; 取到call处的参数, 17 个push一个返回地址   18 * 8 = 0x90                   ; 解密算法开始                   not     ebx                   rol     ebx, 2                   dec     ebx                   xor     ebx, 1DBA2712h                   ; 解密算法结束                   add     rbx, rbp   ; 加上段地址算出来地址                   mov     rax, 100000000h   ; x64 位高位有1,加上                   add     rbx, rax     ; rbx 你就是伪指令寄存器了                   mov     rsi, rsp     ; rsi 你就是伪栈指针寄存器了                   sub     rsp, 180h                   and     rsp, 0FFFFFFFFFFFFFFF0h   ; rsp 分配栈空间 |

2. init_command_

1初始化密钥寄存器,即把伪指令寄存器的值减掉当前的段的地址得到的值赋值给密钥寄存器

1把当前的RIP赋值给跳转寄存器,开始添加EndHandler指令,并跳转到下一个Handler,等…等一下,这个EndHandler是啥?

3. ReadCommand

要了解EndHandler,我们首先来了解一下VMP读取指令的动作,它主要动作是滚动密钥寄存器,读取下一条指令码

1首先会从伪指令寄存器中读取4字节的下一个指令,但这个读取动作会分为两种,分为倒序和正序,这个是随机的,如果是倒序,则先给伪寄存器指令减4,再读取4字节内容;如果是正序,则先读取4字节,再给伪寄存器加4

1读取出来的指令值,先与密钥寄存器进行异或,再根据指令加密器(注意不是寄存器)对数据进行解密

1最后密钥寄存器会与结果进行异或,达到滚动密钥寄存器的目的

1返回解密出来的结果

4. EndHandler

好了,我们已经了解了读取指令的动作,那么HandlerEnd这部分逻辑就简单了,这部分在每一个Handler中都会出现,它主要动作是调用读取指令进行读取,然后将值加到跳转寄存器,并跳转过去

这里面有一个特殊的点,如果参数指定了目标指令地址,那就会跳到目标指令地址中,但是还是会执行一次读取指令的动作

| | | — | | Assembly language                   mov     r10, rbx        ; r10 你就是密钥寄存器了                   mov     rdx, 7FF50F570000h                   sub     r10, rdx        ; 减掉段地址                   lea     r11, [#5]     ; 获取当前rip地址到r11,你就是跳转寄存器了                   ;EndHandler                   ; 下面的指令就是ReadCommand,看来是倒序                   sub     rbx, 4                   mov     eax, [rbx]; 挪动指针,然后读取4字节                   xor     eax, r10d ; 先和r10密钥寄存器进行异或                   ; 下面是解密算法                   dec     eax                   rol     eax, 17h                   sub     eax, 3F47718Ah                   neg     eax                   rol     eax, 1Eh                   ; 回填r10,滚动更新r10密钥寄存器                   push    r10                   xor     [rsp], eax                   pop     r10                   ; ReadCommand 结束                   movsxdrax, eax                   add     r11, rax   ;加到r11 跳转寄存器上                   jmp     r11   ; 跳! |

5. check_stack

别误会,这个Handler并不是对栈做了多么严谨的判断,仅仅判断了一下伪寄存器空间用了是否超过了0x40大小,如果超过了,就把rsp移动到当前栈指针上面0x180的位置,并把原始位置的0x100字节拷贝过去(在golang里面是乘以2,估计这么设计是有什么说法)

| | | — | | Assembly language                   lea     rcx, [rsp+140h]                   ; 由于栈分配了至少0x180的大小,判断伪栈寄存器用的空间是否少于0x40,少于的话,直接进行执行下一条指令                   cmp     rsi, rcx                   ja      @F                   mov     rax, rsp    ; 这个位置证明栈空间不够了,那就把rsp移动到伪寄存器 – 0x180的位置                   mov     rcx, 100h                   lea     r8, [rsi-80h]                   and     r8, 0FFFFFFFFFFFFFFF0h                   sub     r8, rcx                   mov     rsp, r8                   push    rsi                   pushfq                   mov     rsi, rax   ; 把之前的rsp拷贝0x100个字节到新的rsp位置                   mov     rdi, r8                   cld                   rep movsb                   popfq                   pop     rsi                   @@:                   jmp     r11 |

6. push register

| | | — | | Assembly language                   ; ReadCommand Handler                   sub     rbx, 1                   movzx   r8d, byte ptr [rbx]                   xor     r8b, r10b                   ror     r8b, 2                   dec     r8b                   not     r8b                   inc     r8b                   xor     r10b, r8b                   // 获取对应寄存器/局部变量的值                   mov     r9, [rsp+r8]                   // 移动伪寄存器栈的指针,将值写入,完成压入动作                   sub     rsi, 8                   mov     [rsi], r9                   // EndHandler 动作                   sub     rbx, 4                   mov     edx, [rbx]                   xor     edx, r10d                   not     edx                   dec     edx                   bswap   edx                   ror     edx, 5                   inc     edx                   push    r10                   xor     [rsp], edx                   pop     r10                   movsxdrdx, edx                   add     r11, rdx                   jmp     check_stack |

7. pop Register

| | | — | | Assembly language                   // 读取伪栈寄存器的值,并移动伪栈寄存器指针                   mov     rdi, [rsi]                   add     rsi, 8                   // ReadCommand 动作                   sub     rbx, 1                   movzx   ecx, byte ptr [rbx]                   xor     cl, r10b                   add     cl, 32h ; ‘2’                   not     cl                   neg     cl                   dec     cl                   neg     cl                   xor     r10b, cl                   // 写入对应的寄存器/变量中                   mov     [rsp+rcx], rdi                   // EndHandler动作                   sub     rbx, 4                   mov     ecx, [rbx]                   xor     ecx, r10d                   sub     ecx, 7ACA04FBh                   bswap   ecx                   add     ecx, 2B355FD5h                   rol     ecx, 10h                   dec     ecx                   push    r10                   xor     [rsp], ecx                   pop     r10                   movsxdrcx, ecx                   add     r11, rcx                   jmp     r11 |

8. push value

| | | — | | Assembly language                   ; ReadCommand 动作                   sub     rbx, 8                   mov     rcx, [rbx]                   xor     rcx, r10                   inc     rcx                   xor     rcx, 0CCC15C6h                   not     rcx                   inc     rcx                   xor     rcx, 456C3348h                   xor     r10, rcx                   // 移动伪栈指针,将值写入,完成压入动作                   sub     rsi, 8                   mov     [rsi], rcx                   // EndHandler动作                   sub     rbx, 4                   mov     r8d, [rbx]                   xor     r8d, r10d                   not     r8d                   inc     r8d                   not     r8d                   add     r8d, 4FEA55AAh                   push    r10                   xor     [rsp], r8d                   pop     r10                   movsxdr8, r8d                   add     r11, r8                   jmp     checkStackHandler |

9. add

| | | — | | Assembly language                   ; 取出伪栈寄存器上面的两个参数                   mov     rcx, [rsi]                   mov     r9, [rsi+8]                   // 相加                   add     rcx, r9                   // 结果返回结构体                    // {                   //     uint64 rflags;                   //     uint64 result;                   // }                   // 由于输入参数和返回大小一样,所以不会对伪栈寄存器进行任何移动                   mov     [rsi+8], rcx                   pushfq                   pop     qword ptr [rsi]                   // EndHandler 动作                   sub     rbx, 4                   mov     ebp, [rbx]                   xor     ebp, r10d                   bswap   ebp                   add     ebp, 42EC7D95h                   rol     ebp, 12h                   dec     ebp                   push    r10                   xor     [rsp], ebp                   pop     r10                   movsxdrbp, ebp                   add     r11, rbp                   jmp     r11 |

10. call

| | | — | | Assembly language                   ; 这里由于是再次entry_command_过了,所以,伪寄存器已经重新随机了                   ; pcode_register(VIP) —>rbp                   ; jmp_register 跳转寄存器—-> r11                   ; crypt_register_ 加密寄存器—-> r9                   ; stack_register(VSP) 伪堆栈寄存器 —-> ebx                   ; ReadCommand 动作                   sub     rbp, 1                   movzx   edi, byte ptr [rbp+0]                   xor     dil, r9b                   rol     dil, 1                   dec     dil                   ror     dil, 4                   not     dil                   ror     dil, 5                   xor     r9b, dil                   ; 伪寄存器入栈,堆栈寄存器赋值给rbp                   push    rbp                   push    r11                   push    r9                   mov     rbp, rbx                   ; edi 解密出来是参数的个数                   mov     ebx, edi                   mov     edx, ebx                   xor     ecx, ecx                   ; 参数个数小于等于4,则直接跳转                   cmp     ebx, 4                   jbe     lower_or_equal_4                   mov     edx, 4                   lea     ecx, [rbx-4]; 存储入栈的参数                   ;我们的是x64,所以rdx rcx r8 r9 剩下的入栈                   lower_or_equal_4:                   shl     ecx, 3; *8入栈的总大小                   shl     edx, 3; *8计算参数总大小                   mov     rax, rbp                   add     rax, rdx   ; 伪堆栈指针跳过总参数大小                   mov     [rbp-8], rax; 保存原始伪堆栈指针                   mov     [rbp-10h], rsp ; 保存原始rsp                   sub     rsp, rcx     ; 分配入栈参数                   // 对齐rsp                   and     rsp, 0FFFFFFFFFFFFFFF0h                   add     rsp, rcx   ; 堆栈加回去                   test    ebx, ebx                   jz      core_call; 0 个参数直接调用call                   loop_arg_deal:                   mov     rax, [rbp+rbx*8+0]; 取一个参数                   cmp     ebx, 1                    jnz     judge_2; 不是1个参数就跳转                   mov     rcx, rax; 如果只有一个参数就放在rcx中                   jmp     dec_arg_count                   judge_2:                   cmp     ebx, 2                   jnz     judge_3 ; 如果不是2个参数,就跳转                   mov     rdx, rax; 第二个参数放入rdx中                   jmp     dec_arg_count                   judge_3:                   cmp     ebx, 3                   jnz     judge_4 ;如果不是3个参数,就跳转                   mov     r8, rax; 第三个参数放入r8中                   jmp     dec_arg_count                   judge_4:                   cmp     ebx, 4                   jnz     external_args ; 如果大于4个参数,就把当前的入栈                   mov     r9, rax; 第四个参数放入r9中                   jmp     dec_arg_count                   external_args:                   push    qword ptr [rbp+rbx*8+20h]                   dec_arg_count:                   sub     ebx, 1; 剩余参数-1                   jnz     loop_arg_deal                   core_call:                   mov     rax, [rbp+0]; 取出来函数地址                   sub     rsp, 20h; 分配0x20个4参数空间                   call    rax   ; 调用                   mov     rsp, [rbp-10h]; 恢复堆栈                   mov     rbp, [rbp-8]   ; 恢复伪栈寄存器                   mov     [rbp+0], rax   ; 结果存入                   mov     rbx, rbp       ; 伪寄存器恢复                   pop     r9                   pop     r11                   pop     rbp                   ; EndHandler                   sub     rbp, 4                   mov     ecx, [rbp+0]                   xor     ecx, r9d                   sub     ecx, 7D526FFAh                   bswap   ecx                   xor     ecx, 24E27C67h                   not     ecx                   inc     ecx                   bswap   ecx                   push    r9                   xor     [rsp], ecx                   pop     r9                   movsxdrcx, ecx                   add     r11, rcx                   jmp     r11 |

11. push [address]

根据前文的handler总结,规律大差不差,后面的handler我们用简单的伪代码来表示一下核心逻辑

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   mov reg2, [reg1]                   sub stack_registr_, result_size – operand_size                   [stack_registr_] = reg2 |

12. pop [address]

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   mov reg2, [stack_registr_ + 8]                   add stack_registr_, operand_size                   mov [reg1], reg2 |

13. push segment register

| | | — | | Assembly language                   mov reg1, seg                   sub stack_registr_, 2                   mov word ptr [stack_registr_], reg1 |

14. Pop segment register

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   add stack_registr_, operand_size                   mov seg, reg1 |

15. Push debug register

| | | — | | Assembly language                   mov reg1, DrN                   sub stack_registr_, 8                   mov [stack_registr_], reg1 |

16. Pop debug register

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   add stack_registr_, 8                   mov i, reg1 |

17. Push control register

| | | — | | Assembly language                   mov reg1, CrN                   sub stack_registr_, 8                   mov [stack_registr_], reg1 |

18. Pop control register

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   add stack_registr_, 8                   mov CrN, reg1 |

19. Push rsp

| | | — | | Assembly language                   mov reg1, stack_registr_                   sub stack_registr_, 8                   mov [stack_registr_], reg1 |

20. Pop rsp

| | | — | | Assembly language                   mov stack_registr_, [stack_registr_] |

21. Nor

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   mov reg2, [stack_registr_ + mov_size]                   sub stack_registr_, offset                   not reg1                   not reg2                   and reg1, reg2                   mov [stack_registr_ + 8], reg1                   pushfq                   pop [stack_registr_] |

22. Nand

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   mov reg2, [stack_registr_ + mov_size]                   sub stack_registr_, offset                   not reg1                   not reg2                   or reg1, reg2                   mov [stack_registr_ + 8], reg1                   pushfq                   pop [stack_registr_] |

23. shl/shr

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   mov cl, [stack_registr_ + mov_size]                   sub stack_registr_, offset                   shl/shr reg1, cl                   mov [stack_registr_ + 8], reg1                   pushfq                   pop [stack_registr_] |

24. rcl/rcr

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   mov cx, [stack_registr_ + mov_size]                   sub stack_registr_, offset                   shr ch, 1                   rcx/rcr reg1, cl                   mov [stack_registr_ + 8], reg1                   pushfq                   pop [stack_registr_] |

25. shld/shrd

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   mov reg2, [stack_registr_ + size]                   mov cl, [stack_registr_ + size * 2]                   sub stack_registr_, offset                   shld/shrd reg1, reg2, cl                   mov [stack_registr_ + 8], reg1                   pushfq                   pop [stack_registr_] |

26. div/idiv

| | | — | | Assembly language                   mov rax, [stack_registr_]                   mov rcx, [stack_registr_ + mov_size]                   sub rsp, offset                   div/idiv rcx                   mov [stack_registr_], rax                   pushfq                   pop [stack_registr_] |

27. mul/imul

| | | — | | Assembly language                   mov rax, [stack_registr_ + mov_size]                   mov rdx, [stack_registr_]                   sub rsp, offset                   mul/imul rdx                   mov [stack_registr_ + 8], rdx                   mov [stack_registr_ + 8 + mov_size], rax                   pushfq                   pop [stack_registr_] |

28. fild, fld, fadd, fsub, fsubr, fstp, fst, fist, fistp, fdiv, fmul, fcomp, fstcw, fldcw, fstsw

| | | — | | Assembly language                   command [stack_registr_] |

29. wait, fchs, fsqrt, f2xm1, fabs, fclex, fcos, fdecstp, fincstp, finit, fldln2, fldz, fld1, fldpi, fpatan, fprem, fprem1, fptan, frndint, fsin, ftst, fyl2x, fldlg2

| | | — | | Assembly language                   command |

30. ret/iret

| | | — | | Assembly language                   mov rsp, stack_registr_                   pop all register                   ret/iret |

31. popf

| | | — | | Assembly language                   push [stack_registr_]                   add stack_registr_, 8                   popfq |

32. Jmp

| | | — | | Assembly language                   mov reg1, [stack_registr_]  ; 这里读取了但没人用                   add stack_registr_, 8                   jmp xxx |

33. rdtsc

| | | — | | Assembly language                   rdtsc                   sub stack_registr_, 8                   mov [stack_registr_], edx                   mov [stack_registr_ + 4], eax |

34. Cpuid

| | | — | | Assembly language                   mov rax, [stack_registr_]                   mov reg1, stack_registr_                   push rbx                   cpuid                   sub reg1, 12                   mov [reg1 + 12], eax                   mov [reg1 + 8], ebx                   mov [reg1 + 4], ecx                   mov [reg1], edx                   pop rbx                   mov stack_registr_, reg1 |

35. Syscall

| | | — | | Assembly language                   call syscallflag                   return                    syscallflag:                    mov r10, rcx                   syscall                   ret |

36. Lock

| | | — | | Assembly language                   mov reg1, [stack_registr_]                   mov reg2, [stack_registr_ + 8]                   sub/add stack_registr_, offset    和result_size对齐大小                   lock command [reg1], reg2                   ; xchg: mov [stack_registr_], reg2   break;                   ; xadd: mov [stack_registr_], reg2                   pushfq                   pop [stack_registr_] |

VMCommand分析:

前面我们已经掌握了VMP的全部handler,那么,这些handler是如何串起来工作的呢?

通过前文的EndHandler我们可看到,每次跳转到下一个Handler之前都会读取伪指令寄存器中的数据,然后解密,再进行跳转,我们以

| | | — | | C++                   pop rxx |

为例,看一下代码中是如何处理的,入口点为:

| | | — | | C++                   void IntelVirtualMachine::CompileCommand(IntelVMCommand &vm_command) |

首先,代码先对寄存器做了判断,如果不为rsp,则获取,pop rxx的handler(Handlers 中的7)的入口点,并通过GetRegister随机一个寄存器偏移(针对栈),并将该偏移放在虚拟指令集中

在函数的结尾,判断是否是开头第一个指令,如果是,则在开头插入4字节0;判断是否是末尾,如果不是末尾,则在末尾添加4字节0

即,假设有5个pop,例如:

| | | — | | C++                   pop rax                   pop rbx                   pop rcx                   pop rdx                   pop rsi |

那么经过上面的函数,生成的伪指令为:

| | | — | | C++                   00 00 00 00 18 00 00 00 00                   20 00 00 00 00                   28 00 00 00 00                   30 00 00 00 00                   38 |

当然,上面的伪指令是我随便填的,实际上的寄存器存储地址是随机的,通过针对handler的对比可以发现,一个pop读取了5个字节的伪指令寄存器,即(1字节寄存器地址偏移 + 4字节下一条指令地址偏移)

那么为什么第一条指令会多出来4个字节呢?那是因为entry_command_中也存在EndHandler

当然了,这些0是没办法直接跳转的,所以在

| | | — | | C++                   IntelVirtualMachine::CompileBlock |

对跳转地址做了修正

业务逻辑:

看到这,恭喜你已经完成了代码主体中的基本流程,我们假设一下,如果我们的代码是

| | | — | | C++                   cpuid |

那么,我们编译后的代码是34号 handler的 cpuid handler吗?答案是否定的,因为这里面还存在着一层业务逻辑

在函数IntelCommand::CompileToVM中我们可以看到,cpuid指令不仅增加了cpuid handler的逻辑,还在之前把rax寄存器的值入栈,执行结束后把结果出栈,我们回头看一下34号handler的结构,再回想一下handler的运行过程——参数放栈上,结果也放栈上,那么一切就都对的上了。

另外,如果是x64下的代码,vmp还会增加把几个寄存器高位清空的动作。其余的业务逻辑就不再贴出来,有时间大家可以自己copy一下源码

总结:

Handlers中的列表其实并不完整,因为VMP还会初始化几个包含少部分Handlers的虚拟指令列表——我们称为虚拟机,编译的过程中,vmp会随机一个虚拟机进行指令替换,所以可能会出现cpuid在一个地方为被虚拟化的指令,在另一个地方需要退出虚拟化才会执行cpuid,详情可以参考IntelVirtualMachineList::Prepare及BaseFunction::virtual_machine,这里就不再赘述了。

文章有很多不清晰或者不对的地方,希望大家帮忙指出,如果错误太大,请大家多多包涵,正如我开头所说的,我不是这方面的专家。

文章参考:

https://mp.weixin.qq.com/s/CpbOGakvdKkWAyZogxpwvw (VMP3.x内部原理详解与还原思路)

https://mp.weixin.qq.com/s/UU3fL70Jbw4uwGULxZeK6A (VMP源码学习(1) 变异分析与代码bug)


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:冲鸭安全 CrazyHarb《VMP3源码学习——虚拟化》

评论:0   参与:  0