文章总结: 本文深入分析基于跳转表的虚拟机字节码逆向技术,详细解析了VM取指-执行循环、opcode分发机制及栈式架构实现原理。通过汇编代码逐行剖析,展示了VM如何利用双重栈结构处理输入参数并通过特定指令序列生成加密密钥,揭示了虚拟化保护技术在恶意软件隐蔽和软件保护中的应用价值与对抗难点。 综合评分: 90 文章分类: 二进制安全,逆向分析,恶意软件,漏洞分析,实战经验
“逆向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 解释器核心循环,实现了:
- 取指:从
vm_instruction_pointer指向的位置读取 16 位操作码 - 译码:将操作码作为索引查询
vm_opcode_handlers表 - 执行:跳转到对应的操作码处理函数执行具体逻辑
典型应用场景:
- 软件保护壳(如 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字节码程序”的学习(二)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论