“逆向VM字节码程序”的学习(二)

admin 2026-01-31 23:40:13 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入分析基于跳转表的虚拟机字节码逆向技术,详细解析了VM取指-执行循环、opcode分发机制及栈式架构实现原理。通过汇编代码逐行剖析,展示了VM如何利用双重栈结构处理输入参数并通过特定指令序列生成加密密钥,揭示了虚拟化保护技术在恶意软件隐蔽和软件保护中的应用价值与对抗难点。 综合评分: 90 文章分类: 二进制安全,逆向分析,恶意软件,漏洞分析,实战经验


cover_image

“逆向VM字节码程序”的学习(二)

原创

MicroPest MicroPest

MicroPest

2026年1月30日 22:52 安徽

把关键点藏进VM字节码,这是个非常有创意的想法,如果用在病毒上则是非常地隐蔽。分析者必须先逆解释器,再逆字节码,无形中加大了隐蔽性和破解的难度。

就这个问题,我们这篇再继续细细研究下。

1、字节码结构和内容(位于0x40A140):

{regions:[{addr:0x40A140,size:64}]}

[{“addr”:”0x40A140″,”data”:”0x0 0x0 0x21 0x0 0x2 0x0 0x0 0x0 0x91 0x0 0x8 0x0 0x0 0x0 0x16 0x0 0x0 0x0 0xc 0x0 0x9 0x0 0xa 0x0 0xb 0x0 0x0 0x0 0x0 0x0 0xc 0x0 0x2 0x0 0xc 0x0 0x0 0x0 0x0 0x0 0x1d 0x0 0xa 0x0 0xb 0x0 0x0 0x0 0x0 0x0 0x63 0x0 0x2 0x0 0xc 0x0 0x0 0x0 0x0 0x0 0x18 0x0 0x6 0x0″}]

2、VM执行函数VMFetchAndExecuteNextOpcode:

这段汇编代码实现了一个典型的 虚拟机(VM)指令分派器(Dispatcher),也就是解释器的核心取指-执行循环。它的功能可以分解为以下几个步骤:

.text:00401540 VMFetchAndExecuteNextOpcode proc near

.text:00401540 var_4 = word ptr -4

.text:00401540     push    ebp

.text:00401541     mov     ebp, esp

.text:00401543     push    ecx

;标准的函数入口,创建一个栈帧,并在栈上分配一个 16 位(word)的局部变量 var_4 用来暂存当前操作码。

.text:00401544     movzx   eax, vm_instruction_pointer

.text:0040154B     mov     cx, ds:word_40A140[eax*2]

.text:00401553     mov     [ebp+var_4], cx

;取指Fetch

  • 从 vm_instruction_pointer 读取当前指令位置
  • word_40A140 是 字节码/操作码数组(Opcode Array)
  • 由于索引乘以 2(eax*2),说明每个操作码是 16 位(2 字节)
  • 读取的操作码存入 CX(并保存到栈上变量)

.text:00401557     movzx   edx, [ebp+var_4]

.text:0040155B     mov     eax, vm_opcode_handlers[edx*4]

.text:00401562     call    eax ; vm_opcode_handlers

; 译码与分派

  • vm_opcode_handlers 是 跳转表/函数指针表(Jump Table)
  • 由于索引乘以 4(edx*4),说明是 32 位指针数组(x86 架构)
  • 根据操作码值查表得到对应的处理函数地址,然后间接调用

.text:00401564     mov     esp, ebp

.text:00401566     pop     ebp

.text:00401567     retn

.text:00401567 VMFetchAndExecuteNextOpcode endp

这是一个 基于跳转表(Jump Table)的 VM 解释器核心循环,实现了:

  1. 取指:从 vm_instruction_pointer 指向的位置读取 16 位操作码
  2. 译码:将操作码作为索引查询 vm_opcode_handlers 表
  3. 执行:跳转到对应的操作码处理函数执行具体逻辑

典型应用场景

  • 软件保护壳(如 VMProtect、 Themida 等)的虚拟化保护
  • 脚本语言解释器(如早期的 Lua、Python 字节码解释器)
  • 模拟器/仿真器核心

3、VM的主循环和入口点函数sub_401610:

.text:00401610 sub_401610      proc near

.text:00401610     push    ebp

.text:00401611     mov     ebp, esp

.text:00401613     call    InitializeVMOpcodeHandlers

.text:00401618     xor     eax, eax

.text:0040161A     mov     word_40DF18, ax

.text:00401620     xor     ecx, ecx

.text:00401622     mov     word_40DF1A, cx

.text:00401629     mov     edx, 9

.text:0040162E     mov     word_40DF1C, dx

.text:00401635     xor     eax, eax

.text:00401637     mov     vm_instruction_pointer, ax

.text:0040163D __vm_execution_loop:

.text:0040163D     movzx   ecx, vm_instruction_pointer

.text:00401644     movzx   edx, vm_code_size

.text:0040164B     cmp     ecx, edx

.text:0040164D    jge     short __last_vm_instruction_reached

.text:0040164F     call    VMFetchAndExecuteNextOpcode

.text:00401654     jmp     short __vm_execution_loop

.text:00401656 __last_vm_instruction_reached:

.text:00401656      mov     ax, word_40DF18

.text:0040165C      pop     ebp

.text:0040165D      retn

.text:0040165D sub_401610      endp

4、opcode处理器的初始化函数和处理器表:

.text:00401570 InitializeVMOpcodeHandlers proc near

.text:00401570     push    ebp

.text:00401571     mov     ebp, esp

.text:00401573     mov     dword_40DEE0, offset VMOpcodeHandler_push

.text:0040157D     mov     dword_40DEE4, offset VMOpcodeHandler_pop

.text:00401587     mov     dword_40DEE8, offset VMOpcodeHandler_add

.text:00401591     mov     dword_40DEEC, offset VMOpcodeHandler_sub

.text:0040159B     mov     dword_40DEF0, offset VMOpcodeHandler_RotateRight

.text:004015A5     mov     dword_40DEF4, offset VMOpcodeHandler_RotateLeft

.text:004015AF     mov     dword_40DEF8, offset VMOpcodeHandler_xor

.text:004015B9     mov     dword_40DEFC, offset VMOpcodeHandler_not

.text:004015C3     mov     dword_40DF00, offset VMOpcodeHandler_eq

.text:004015CD    mov     dword_40DF04, offset VMOpcodeHandler_sel

.text:004015D7    mov     dword_40DF08, offset VMOpcodeHandler_jmp

.text:004015E1     mov     dword_40DF0C, offset VMOpcodeHandler_load

.text:004015EB     mov     dword_40DF10, offset VMOpcodeHandler_store

.text:004015F5     mov     dword_40DF14, offset VMOpcodeHandler_nop

.text:004015FF     pop     ebp

.text:00401600     retn

.text:00401600 InitializeVMOpcodeHandlers endp

5、opcode处理字节码(举例):

.text:00401030 VMOpcodeHandler_push proc near

.text:00401030 immediate = word ptr -4

.text:00401030     push    ebp

.text:00401031     mov     ebp, esp

.text:00401033     push    ecx

.text:00401034     mov     ax, vm_instruction_pointer

.text:0040103A     add     ax, 1

.text:0040103E     mov     vm_instruction_pointer, ax

.text:00401044     movzx   ecx, vm_instruction_pointer

.text:0040104B     mov     dx, ds:word_40A140[ecx*2]

.text:00401053     mov     [ebp+immediate], dx

.text:00401057     movzx   eax, [ebp+immediate]

.text:0040105B     push    eax

.text:0040105C     call    VMStack_push

.text:00401061     add     esp, 4

.text:00401064     mov     cx, vm_instruction_pointer

.text:0040106B     add     cx, 1

.text:0040106F     mov     vm_instruction_pointer, cx

.text:00401076     mov     esp, ebp

.text:00401078     pop     ebp

.text:00401079     retn

.text:00401079 VMOpcodeHandler_push endp

这段汇编就是“虚拟机里  push imm16  指令”的执行过程——把字节码流里的 16 位常数取出来,压到 VM 自己的小栈上,再把指令指针往前挪 2 字节。逐句翻译如下:

| | | | | — | — | — | | 汇编 | 伪代码 | 说明 | | mov ax, vm_instruction_pointer | ax = ip | 取当前字节码偏移 | | add ax, 1 | ax++ | 跳过 opcode 字节 | | mov vm_instruction_pointer, ax | ip = ax | 更新 ip | | movzx ecx, vm_instruction_pointer | ecx = ip | 零扩展成 32 位索引 | | mov dx, word_40A140[ecx*2] | dx = bytecode[ip] | 读下一个 16 位立即数 | | push eax / call VMStack_push | stack_push(dx) | 把常数压进 VM 栈 | | 再次 ip++ | ip += 1 | 跳过刚才读完的 2 字节立即数 |

一句话:“取 2 字节常数 → 压栈 → 指令指针 +2,继续取下一条指令”——这就是 VM 世界里  push imm16  的全部工作。

6、VM字节码执行可视化追踪

完整执行时间线

假设输入:smokestack.exe SECRET1234

时间轴:程序执行的每个关键步骤

══════════════════════════════════════

;具体代码如下:

; 循环变量 i 在 [ebp+var_4]

; argv[1] 指针在 [ebp+argv]

loc_402FA5:

    mov     edx, [ebp+var_4]        ; edx = i

    add     edx, 1                  ; i++

    mov     [ebp+var_4], edx        ; 保存i

loc_402FAE:

    cmp     [ebp+var_4], 0Ah        ; 比较 i < 10

    jge     short loc_402FCF        ; 如果 i >= 10,跳出循环

    mov     eax, [ebp+argv]         ; eax = argv

    mov     ecx, [eax+4]            ; ecx = argv[1](第一个参数字符串)

    mov     edx, [ebp+var_4]        ; edx = i

    movsx   ax, byte ptr [ecx+edx]  ; ax = argv[1][i](字符,符号扩展)

    mov     ecx, [ebp+var_4]        ; ecx = i

    mov     word_40DF20[ecx*2], ax  ; VM栈[i] = argv[1][i]

    jmp     short loc_402FA5        ; 继续循环

loc_402FCF:

    call    sub_401610              ; 调用VM主函数

    mov     word ptr [ebp+var_1C], ax  ; 保存VM返回值(ax寄存器)

========================

内存布局示意图

关键洞察

(1). 双重栈结构:

   – 程序有自己的调用栈(存储main等函数的栈帧)

   – VM有自己的数据栈(word_40DF20数组)

(2). 输入作为初始状态:

   – 用户输入不是VM指令的一部分

   – 而是VM执行时的初始数据

(3). 寄存器模拟:

   – ax、bp、sp、ip都是普通变量

   – 但VM将它们当作寄存器使用

(4). 字节码与数据分离:

   – 字节码在 word_40A140

   – 数据(栈)在 word_40DF20

   – 这是冯·诺依曼架构的变体(哈佛架构)

(5). 返回值传递:

   – VM通过ax寄存器返回结果

   – main函数读取ax的值继续处理

7、字节码和argv[1]之间的关系?

字节码(0x40A140)通过 VM 的 pop 指令把 word_40DF20[0..9] 当输入,而这 10 个槽位正是 main 写入的 argv[1][0..9];VM 算出的 word_40DF18 又被拼回 argv[1] 形成 12 字节种子,最终决定 RC4 解密结果。

(1) argv[1] 怎么“喂”给字节码(VM)

  • main (0x402F30)要求 argc>1 && strlen(argv[1])>=10 ,只取 argv[1] 的前 10 个字节 。

  • 这 10 个字节被写入 VM 的“栈内存”数组 word_40DF20[i] (0x40DF20):

  • 写入点在 main 的循环: word_40DF20[i] = argv[1][i]; (0x402FB4 附近)

  • sub_401610 (0x401610)启动 VM 前把栈指针 word_40DF1C 设为 9 (0x401629~0x401635),并把 vm_instruction_pointer 置 0:

  • 这意味着 VM 一开始就把 word_40DF20[0..9] 当作 预置栈内容 ,其“栈顶”正好是 word_40DF20[9] = argv[1][9] 。

(2) 字节码如何“使用” argv[1]

  • VM 的字节码在 word_40A140 (0x40A140),取指在 VMFetchAndExecuteNextOpcode (0x401540):

  • opcode = word_40A140[vm_instruction_pointer] ,再用 vm_opcode_handlers[opcode] 分发执行。

  • 关键点:VM 的各种运算 opcode(add/sub/xor/rol/ror/eq/sel…)内部都会调用 VMStack_pop() (0x401080)从 word_40DF20[word_40DF1C] 取数并 word_40DF1C– 。

  – 因为初始化时 word_40DF1C=9 , 第一次 pop 读到的就是 argv[1][9],然后 argv[1][8] … 逆序被消费 (除非中途 push 了常量/中间值)。

  • VM 最终的“输出”是全局 word_40DF18 , sub_401610 执行完字节码后返回它( return word_40DF18; ,0x401656)。

(3) VM 输出如何与 argv[1] 结合(形成后续解密的关键材料)

  • main 在跑完 VM 后得到 v10 = sub_401610() (16-bit)。

  • 然后构造 12 字节 Src :

  • memcpy(Src, argv[1], 0xA) :前 10 字节仍是 argv[1][0..9]

  • Src[5] = v10 :把 VM 计算出的 2 字节追加到末尾(等价于 Src[10..11]=v10 )

  • sub_402EE0(Src, 0xCu, &v4) 会对这 12 字节做一次变换,然后再对输出 迭代 0x100000 次 (100 万次)生成 16 字节( &v4 )——这是典型的“慢哈希/拉伸”用法。

  • sub_401880(byte_40D008, dword_40D108, &v4, 16) 用这 16 字节做 RC4 密钥 ( sub_401660 =KSA, sub_401770 =PRGA),去解密 byte_40D008 指向的密文,长度由 dword_40D108 给出(这里值为 256 ,地址 0x40D108),最后 printf(“\n%s\n”, byte_40D008) 输出结果。


免责声明:

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

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

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

本文转载自:MicroPest MicroPest MicroPest《“逆向VM字节码程序”的学习(二)》

评论:0   参与:  0