[机翻] [ABD] 11_VM混淆分析
11. VM 混淆分析(VM Obfuscation Analysis)
⚠️ API 版本说明:本文档中的 Miasm API 代码示例已于 2026-07 更新至 Miasm 0.1.5+ 版本。课程原始材料使用的是 0.1.3 版本(2020年),部分 API 已发生 breaking change。更新处以
【更新】标注,旧版用法以【旧版 v0.1.3】标注。
概述
虚拟化混淆(Virtualization Obfuscation) 是最强的混淆技术之一。它将原始的可执行代码翻译为自定义的、与具体架构无关的字节码(bytecode),然后在一个嵌入二进制中的自定义虚拟机(VM)上解释执行这些字节码。由于字节码格式和 VM 指令集都是攻击者自定义的,分析者无法直接套用标准反汇编工具,必须先逆向工程整个 VM 架构。
本节以 ZeusVM(Zeus 木马的虚拟化变种)为案例,演示如何利用 Miasm 的符号执行能力,自动提取 VM handler 的语义,从而理解虚拟机执行的字节码含义。
ZeusVM 的虚拟化机制
ZeusVM 在原始恶意逻辑外层套了一层 VM 解释器:
- 原始的 x86 指令被翻译为 VM 字节码。
- 二进制中嵌入一个 VM dispatcher(调度器),它读取字节码、查表找到对应的 VM handler、执行 handler、更新 VM 程序计数器(VM_PC),循环往复。
- 每个 VM handler 实现一种虚拟指令语义(如虚拟加法、虚拟赋值、虚拟内存读写等)。
去虚拟化的核心挑战在于:理解每个 handler 究竟做了什么。本节介绍的方法是通过对 handler 进行符号执行,自动推断其语义。
VM 语义建模(核心创新点)
要分析 VM handler,首先必须为 ZeusVM 的虚拟机架构建立一个符号模型——即用 Miasm 的 IR 表达式描述 VM 的内存布局:虚拟寄存器存在哪里、VM_PC 在哪里、立即数如何从字节码中读取等。
ZeusVM 的内存布局模型
ZeusVM 的虚拟机上下文存储在一段连续内存中,由 ECX_init 寄存器指向。关键元素的位置如下:
from miasm.expression.expression import ExprMem, ExprInt, ExprId
from miasm.arch.x86.regs import ECX as ECX_init, ESP as ESP_init
infos = {}
# VM_PC: 虚拟机程序计数器,存储在 [ECX_init]
# 即 ECX_init 指向的内存位置保存当前 VM 字节码的执行地址
infos[ExprMem(regs.ECX_init, 32)] = vm_pc_init # VM_PC_init
# 返回地址: [ESP_init - 4]
# 当 VM 执行完毕后,通过该地址返回原始代码
infos[ExprMem(regs.ESP_init - ExprInt(4, 32), 32)] = ret_addr # RET_ADDR
# 5个虚拟寄存器 REG0-REG4: 存储在 [ECX_init + 4*(i+1)]
# 即虚拟寄存器数组紧跟在 VM_PC 之后,每个占 4 字节
for i in range(0, 5):
infos[ExprMem(regs.ECX_init + ExprInt(4*(i+1), 32), 32)] = ExprId('REG%d' % i, 32)
字节码立即数操作数的符号化
VM handler 从字节码流中读取操作数。这些操作数紧跟在 opcode 之后:
# 立即数操作数: 从 VM_PC+1 读取 (8/16/32位)
# 根据 handler 的不同,操作数可能是不同宽度
expr_imm8 = ExprMem(vm_pc_init + ExprInt(0x1, 32), 8) # imm8 (8位立即数)
expr_imm16 = ExprMem(vm_pc_init + ExprInt(0x1, 32), 16) # imm16 (16位立即数)
expr_imm32 = ExprMem(vm_pc_init + ExprInt(0x1, 32), 32) # imm32 (32位立即数)
# 第二个立即数操作数: 从 VM_PC+2 读取
expr_imm8b = ExprMem(vm_pc_init + ExprInt(0x2, 32), 8) # imm8b (第二个8位立即数)
寄存器索引解码
ZeusVM 字节码中的 imm8 字节同时编码了两个寄存器索引 X 和 Y:低 4 位为索引 X,高 4 位为索引 Y。每个索引乘以 4 再加上基地址偏移 0xC,即得到对应虚拟寄存器在 VM 上下文中的地址。
# 寄存器索引X: 低4位 (imm8 & 0xF) * 4 + 0xC 偏移
# imm8 的低 4 位作为寄存器编号,乘以 4(每个虚拟寄存器 4 字节),
# 加上 0xC 偏移(跳过 VM_PC 和 2 个保留字)得到寄存器地址
base_regx = ECX_init + (imm8.zeroExtend(32) & 0xF) * 4 + 0xC
# 寄存器索引Y: 高4位 (imm8[4:8]) * 4 + 0xC 偏移
# imm8 的高 4 位作为第二个寄存器编号
base_regy = ECX_init + (imm8[4:8].zeroExtend(32)) * 4 + 0xC
关键 API 说明
imm8.zeroExtend(32):将 8 位值零扩展到 32 位,确保算术运算位宽一致。imm8[4:8]:位域切片(slice)操作,提取第 4 到第 8 位(高 4 位)。& 0xF:位与运算,截取低 4 位。* 4:寄存器索引乘以 4,因为每个虚拟寄存器占 4 字节(32 位)。
模型总览
将上述模型综合起来,ZeusVM 的 VM 上下文内存布局如下:
[ECX_init + 0x00] → VM_PC (虚拟机程序计数器)
[ECX_init + 0x04] → REG0 (虚拟寄存器0)
[ECX_init + 0x08] → REG1 (虚拟寄存器1)
[ECX_init + 0x0C] → REG2 (虚拟寄存器2) ← base_regx/base_regy 的起始偏移
[ECX_init + 0x10] → REG3 (虚拟寄存器3)
[ECX_init + 0x14] → REG4 (虚拟寄存器4)
注意:
base_regx的偏移0xC对应REG2的位置(4*(0+1) + 8 = 0xC,即跳过 VM_PC 和前两个寄存器槽位)。这与具体 ZeusVM 实现的内存布局有关,分析不同 VM 时需相应调整。
VM handler 提取流程
建立 VM 语义模型后,接下来提取并分析所有 VM handler 的语义。完整流程如下:
第 1 步:读取 handler 地址数组
ZeusVM 将所有 handler 的入口地址存储在一个数组中。该数组位于固定地址(案例中为 0x427018)。使用 upck32() 从二进制字节流中解包 32 位地址。
from miasm.core.utils import upck32
mnemonic_array_addr = 0x427018 # handler 地址数组的起始地址
# 读取 69 个 handler 的入口地址
handler_addrs = []
for i in range(69):
addr_bytes = bs.getbytes(mnemonic_array_addr + 4 * i, 4)
handler_addr = upck32(addr_bytes)
handler_addrs.append(handler_addr)
print("Handler %d: 0x%x" % (i, handler_addr))
upck32():将 4 字节解包为 32 位无符号整数(小端序)。bs.getbytes(addr, size):从二进制字节流中读取指定地址处的若干字节。
第 2 步:反汇编并生成 IRCFG
对每个 handler 地址进行反汇编,将其汇编指令转换为 Miasm 的 IR 控制流图(IRCFG)。
from miasm.analysis.machine import Machine
machine = Machine("x86_32")
mdis = machine.dis_engine(bs, loc_db=loc_db)
# 【旧版 v0.1.3】ir_arch = machine.ira(loc_db)
# 【更新 v0.1.5+】machine.ira() → machine.lifter_model_call(),变量 ir_arch → lifter
lifter = machine.lifter_model_call(loc_db)
for idx, handler_addr in enumerate(handler_addrs):
# 反汇编 handler
asmcfg = mdis.dis_multiblock(handler_addr)
# 生成 IRCFG
# 【旧版 v0.1.3】ircfg = ir_arch.new_ircfg()
# 【更新 v0.1.5+】ir_arch.new_ircfg() → lifter.new_ircfg()
ircfg = lifter.new_ircfg()
# 【旧版 v0.1.3】ir_arch.add_asmblock_to_ircfg(asmcfg, ircfg)
# 【更新 v0.1.5+】ir_arch.add_asmblock_to_ircfg() → lifter.add_asmblock_to_ircfg()
lifter.add_asmblock_to_ircfg(asmcfg, ircfg)
# 进入第 3 步:符号执行探索
# 【旧版 v0.1.3】analyze_handler(ir_arch, ircfg, handler_addr, idx)
# 【更新 v0.1.5+】ir_arch → lifter
analyze_handler(lifter, ircfg, handler_addr, idx)
第 3 步:符号执行探索每个 handler 的行为
将前面建立的 VM 语义模型(infos 字典)作为符号初始状态,对 handler 执行符号执行,观察它对 VM 上下文(VM_PC、虚拟寄存器、内存)产生了什么修改,从而推断 handler 的语义。
from miasm.ir.symbexec import SymbolicExecutionEngine
# 【旧版 v0.1.3】def analyze_handler(ir_arch, ircfg, handler_addr, idx):
# 【更新 v0.1.5+】参数 ir_arch → lifter
def analyze_handler(lifter, ircfg, handler_addr, idx):
"""对单个 VM handler 执行符号执行,推断其语义。"""
# 初始化符号状态:使用 VM 语义模型
# 【旧版 v0.1.3】sb = SymbolicExecutionEngine(ir_arch, infos)
# 【更新 v0.1.5+】第一个参数 ir_arch→lifter,不再传 infos
sb = SymbolicExecutionEngine(lifter)
# 执行 handler
sb.run_at(ircfg, handler_addr, lbl_start=handler_addr)
# 进入第 4 步:dump 结果
dump_state(sb, infos, idx)
第 4 步:dump_state() 过滤并映射结果
dump_state() 函数对符号执行后的状态进行过滤和人类可读化:
- 过滤初始状态:只显示那些发生变化的寄存器/内存位置,排除未修改的初始值。
- 过滤附加信息:排除临时符号变量等内部辅助信息。
- 映射回 VM 语义:将内存地址翻译回 VM 概念(如
REG2、imm8、VM_PC等),而非显示原始地址表达式。
def dump_state(sb, infos, idx):
"""过滤初始状态和附加信息,只显示变化部分,并映射回 VM 语义。"""
print("=== Handler %d 语义 ===" % idx)
# 收集符号执行后所有符号值
for expr, final_value in sb.symbols.items():
# 跳过未变化项(与初始 infos 相同)
if expr in infos and final_value == infos[expr]:
continue
# 将地址表达式映射回 VM 语义名称
# 例如: ExprMem(ECX_init + 0xC, 32) → "REG2"
vm_name = map_to_vm_semantic(expr, infos)
if vm_name:
print(" %s = %s" % (vm_name, final_value))
else:
print(" %s = %s" % (expr, final_value))
print()
通过上述流程,每个 handler 的语义会以"VM_PC 更新方式 + 虚拟寄存器修改"的形式输出。例如,某个 handler 的输出可能是:
=== Handler 5 语义 ===
VM_PC = VM_PC_init + 0x2 (PC 前进 2 字节:opcode + imm8)
REG2 = REG3 + imm32 (虚拟加法: REG2 = REG3 + imm32)
据此即可判断该 handler 实现的是"加法"虚拟指令。
solved/zeus_get_ir.ipynb
solved/zeus_get_ir.ipynb 是课程提供的更底层实现版本,展示了符号执行的细节管理。相比高层 API 封装,该 notebook 手动管理符号状态,能更精细地控制执行过程。
关键特性
1. 手动管理符号状态(使用 frozenset 做状态快照)
高层 API 自动管理状态,而底层实现需要手动维护符号状态的不可变快照,以便回溯和分支探索:
# 使用 frozenset 创建符号状态的不可变快照
# 便于在分支探索时保存/恢复状态
symbols_snapshot = frozenset(sb.symbols.items())
# ... 执行一段时间后 ...
# 恢复到快照状态
sb.symbols = dict(symbols_snapshot)
frozenset 的不可变性保证了快照不会被后续操作意外修改,是状态管理的基础。
2. 处理 ExprCond 分支条件
VM handler 内部可能包含条件分支(如根据标志位选择不同路径)。符号执行遇到分支时,会产生 ExprCond(条件表达式),底层实现需要显式处理:
from miasm.expression.expression import ExprCond
# 当符号执行结果中包含 ExprCond 时,表示存在条件分支
# 需要分别对两个分支进行探索
if isinstance(result, ExprCond):
# result 形如: ExprCond(cond, src1, src2)
# cond: 条件表达式
# src1: cond 为真时的值
# src2: cond 为假时的值
explore_branch_true(result.src1, cond)
explore_branch_false(result.src2, cond)
3. 循环执行最多 20 个块
为防止符号执行在复杂 handler 中无限循环或陷入路径爆炸,限制最大执行的基本块数量:
MAX_BLOCKS = 20
block_count = 0
while block_count < MAX_BLOCKS:
# 执行下一个基本块
next_addr = sb.run_block_at(ircfg, current_addr)
block_count += 1
if next_addr is None:
break # 执行结束
current_addr = next_addr
if block_count >= MAX_BLOCKS:
print("警告: handler 执行达到最大块数限制,可能未完全分析")
这种限制是符号执行工程实践的常见做法,在分析深度和性能之间取得平衡。
Miasm API 速查
本节涉及的 Miasm API 汇总如下:
| API | 所属模块 | 用途 |
|---|---|---|
regs |
miasm.arch.x86.regs |
x86 寄存器定义(ECX、ESP、EAX 等) |
upck32() |
miasm.core.utils |
将 4 字节解包为 32 位整数(小端序) |
ExprMem |
miasm.expression.expression |
内存访问表达式 |
ExprInt |
miasm.expression.expression |
整数常量表达式 |
ExprId |
miasm.expression.expression |
标识符(符号变量)表达式 |
ExprCond |
miasm.expression.expression |
条件表达式 |
zeroExtend(n) |
Expr 方法 |
零扩展到 n 位 |
[start:stop] |
Expr 切片 |
位域提取(如 imm8[4:8] 取高 4 位) |
SymbolicExecutionEngine |
miasm.ir.symbexec |
符号执行引擎。【更新 v0.1.5+】构造函数第一个参数 ir→lifter,不再传 symbols |
machine.lifter_model_call() |
Machine 方法 |
【更新 v0.1.5+】创建 Lifter(【旧版 v0.1.3】machine.ira()) |
add_asmblock_to_ircfg() |
LifterModelCall 方法 |
将汇编块加入 IR 控制流图。【更新 v0.1.5+】调用对象 ir_arch→lifter(【旧版 v0.1.3】IRA 方法) |
dis_multiblock() |
dis_engine 方法 |
反汇编生成多块 AsmCFG |
位域操作详解
from miasm.expression.expression import ExprInt, ExprMem
# 假设 imm8 是一个 8 位表达式
imm8 = ExprMem(vm_pc_init + ExprInt(0x1, 32), 8)
# 切片 [4:8]: 提取第 4~7 位(高 4 位)
high_nibble = imm8[4:8] # 结果为 4 位表达式
# 零扩展到 32 位,用于参与 32 位地址计算
high_nibble_32 = high_nibble.zeroExtend(32)
# 位与运算截取低 4 位
low_nibble_32 = imm8.zeroExtend(32) & ExprInt(0xF, 32)
这些位操作是 VM 字节码解码的核心工具:opcode 字节通常被拆分为多个字段(寄存器索引、操作模式等),需要灵活使用切片和位运算来提取。
技术要点总结
-
VM 语义建模是去虚拟化的核心创新点:通过用 IR 表达式描述 VM 的内存布局(VM_PC、虚拟寄存器、立即数位置),将"理解 VM"转化为"对 handler 做符号执行并观察副作用"的自动化问题。
-
符号执行自动推断 handler 语义:相比纯人工逆向每个 handler(极其耗时且易错),符号执行能自动计算出 handler 对 VM 状态的修改,大幅提升效率。
-
dump_state()的过滤与映射:将原始的符号表达式结果翻译回人类可读的 VM 语义(REGx、imm8 等),是连接机器分析与人类理解的关键桥梁。 -
底层实现(
solved/zeus_get_ir.ipynb)的价值:手动状态管理(frozenset快照)、ExprCond分支处理、块数限制等,展示了符号执行的工程细节,便于在高层 API 不够用时进行定制化扩展。 -
位域操作是字节码解码的基础:
[4:8]切片、zeroExtend()、位与运算等,是解析自定义 opcode 格式的必备工具。

浙公网安备 33010602011771号