VM虚拟机逆向
VM虚拟机逆向
本文适合
已经能读普通校验逻辑,但遇到自定义 opcode、解释器循环和字节码数组时不知道从哪里下手的学习者。学完你能:识别 VM 解释器,提取字节码,建立 opcode 语义表,并写反汇编器或模拟器还原校验逻辑。
VM 逆向题会把原本直接执行的逻辑转换成自定义字节码,再由解释器执行。分析重点从“读机器指令”变成“理解这个虚拟机的指令集和状态机”。
一句话判断
如果程序有一段字节码数组、一个指令指针、一个分发循环,并根据 opcode 执行不同处理器,就要按自定义 VM 题分析。
VM 题的核心不是把解释器每行都翻译完,而是把 opcode 语义和被解释的字节码程序还原出来。
题目中常见信号
- 代码中有大数组或资源块,被循环逐字节读取。
- 有
pc/ip变量不断递增或跳转。 - 有
switch(opcode)、函数指针表、computed goto 或大 if-else 分发。 - 有自定义寄存器数组、栈、内存数组或 flags。
- 输入没有直接参与普通比较,而是被 VM 指令读取、变换和比较。
- 反编译器看到的是解释器逻辑,不是原始 flag 校验逻辑。
核心概念
VM 题分为两层程序:
- 宿主程序:真实 CPU 执行的解释器。
- 虚拟程序:自定义字节码表达的校验逻辑。
分析重点要从宿主程序切换到虚拟程序。你需要还原:
- 字节码在哪里。
- opcode 长度和操作数格式。
- 每条 opcode 如何修改 VM 栈、寄存器、内存和指令指针。
- 哪些指令读取输入、做比较、跳转或输出结果。
记录 opcode 表是 VM 题最重要的工作产物。
最小分析流程
- 找到解释器分发循环和 opcode 读取位置。
- 定位字节码数组,确认起始地址、长度和是否运行时解密。
- 记录 VM 状态:pc、stack、registers、memory、flags。
- 为每个 opcode 建表:操作数长度、语义、影响的状态。
- 写一个简单反汇编器,把字节码翻译成伪指令。
- 写模拟器或手工跟踪关键路径,定位输入比较和成功条件。
最小验证示例
先确认解释器在读取 opcode:
break *0x401250 # opcode = bytecode[pc] 后的位置
run
p/x $eax # 假设 eax 保存 opcode
p/x *(unsigned char*)$rdi记录前几条字节码:
bytecode = bytes.fromhex("13 01 41 07 0d 0f 20 ff")
for i, b in enumerate(bytecode):
print(i, hex(b))当你确认 0x13=INPUT、0x01=PUSH imm、0x07=XOR、0x0d=CMP 后,就可以写反汇编器和模拟器验证输入如何被检查。
常见利用 / 解题路线
路线总览:
- 分发循环路线:从
switch(opcode)或函数表建立 opcode 到 handler 的映射。 - 字节码提取路线:从
.rodata/.data/assets或运行时解密后内存提取虚拟程序。 - 反汇编器路线:先翻译字节码为伪指令,降低阅读难度。
- 模拟器路线:实现 VM 状态和 opcode,跑输入并对比原程序输出。
- 约束求解路线:把 VM 指令转成 Z3 约束,求满足成功条件的输入。
- 动态 trace 路线:插桩打印每条 opcode、pc、栈顶和比较值,快速定位关键指令。
自定义 VM 是什么
程序里可以自己实现一个解释器。
它读取一串字节码,根据 opcode 执行不同操作。
例如:
01 push
02 xor
03 add
04 cmp
05 jmp真实校验逻辑被编码成字节码,解释器负责逐条执行。
VM 题的组成
一个基础 VM 通常包括:
- 字节码数组。
- 指令指针。
- 寄存器或栈。
- opcode 分发逻辑。
- 每条指令的语义。
- 输入输出或比较逻辑。
分析时要把这些元素分开,不要把解释器和被解释程序混成一团。
分发方式
常见分发方式包括:
switch(opcode)。- 函数表。
- computed goto。
- 大量 if-else。
- 混淆后的跳转表。
找到分发循环,就找到了 VM 的心脏。
还原指令语义
每个 opcode 要回答:
- 它读取哪些操作数。
- 它修改哪些寄存器、栈或内存。
- 它是否改变指令指针。
- 它是否和输入有关。
- 它是否影响最终比较。
记录成表格是 VM 逆向中非常有用的做法。
反编译字节码
当 opcode 语义明确后,可以写一个简单反汇编器,把字节码翻译成更可读的伪指令。
再进一步,可以写解释器模拟执行,或者把约束提取给求解器。
这比盯着原始反编译代码更稳定。
自定义字节码识别
VM 题的第一步是识别字节码的存储位置:
# 在 IDA/Ghidra 中查找:
# 1. 大的字节数组(通常是 .data 或 .rodata 段)
# 2. 程序开头的 memcpy 或初始化函数
# 3. 通过字符串提示找到字节码位置
# Ghidra 中识别字节码:
# - 查看 .data 段的大型数组
# - 查找 switch 跳转表
# - 分析初始化函数
# 常见字节码格式:
# - 纯字节序列:01 02 03 04
# - 带前缀:FF 01 02 03
# - 变长编码:根据 opcode 决定操作数长度提取字节码
def extract_bytecode(binary_path, offset, size):
"""从二进制中提取字节码"""
with open(binary_path, 'rb') as f:
f.seek(offset)
bytecode = f.read(size)
return bytecode
# 使用示例
bytecode = extract_bytecode('./vuln', 0x2000, 256)
print(f"字节码: {bytecode.hex()}")Opcode 处理器映射
建立 opcode 到处理函数的映射表:
def analyze_dispatch(binary_path, dispatch_addr):
"""
分析分发逻辑,建立 opcode 映射
"""
# 使用 angr 或手动分析
p = angr.Project(binary_path)
# 分析 switch 语句
# 每个 case 对应一个 opcode 处理器
opcode_handlers = {
0x01: 'PUSH',
0x02: 'POP',
0x03: 'ADD',
0x04: 'SUB',
0x05: 'MUL',
0x06: 'DIV',
0x07: 'XOR',
0x08: 'AND',
0x09: 'OR',
0x0A: 'NOT',
0x0B: 'SHL',
0x0C: 'SHR',
0x0D: 'CMP',
0x0E: 'JMP',
0x0F: 'JZ',
0x10: 'JNZ',
0x11: 'LOAD',
0x12: 'STORE',
0x13: 'INPUT',
0x14: 'OUTPUT',
0xFF: 'HALT',
}
return opcode_handlers建立处理器映射表
# 创建 opcode 分析表格
opcode_table = """
| Opcode | 操作 | 操作数 | 说明 |
|--------|------|--------|------|
| 0x01 | PUSH | val | 压栈 |
| 0x02 | POP | - | 出栈 |
| 0x03 | ADD | - | 加法 |
| 0x04 | SUB | - | 减法 |
| 0x05 | MUL | - | 乘法 |
| 0x06 | DIV | - | 除法 |
| 0x07 | XOR | - | 异或 |
| 0x08 | AND | - | 与 |
| 0x09 | OR | - | 或 |
| 0x0A | NOT | - | 非 |
| 0x0B | SHL | - | 左移 |
| 0x0C | SHR | - | 右移 |
| 0x0D | CMP | - | 比较 |
| 0x0E | JMP | addr | 跳转 |
| 0x0F | JZ | addr | 零跳转 |
| 0x10 | JNZ | addr | 非零跳转 |
| 0x11 | LOAD | idx | 加载 |
| 0x12 | STORE| idx | 存储 |
| 0x13 | INPUT| - | 输入 |
| 0x14 | OUTPUT| - | 输出 |
| 0xFF | HALT | - | 停机 |
"""编写模拟器
class VMSimulator:
def __init__(self, bytecode, input_data=b''):
self.bytecode = bytecode
self.pc = 0 # 程序计数器
self.stack = [] # 栈
self.registers = [0] * 16 # 寄存器
self.memory = [0] * 256 # 内存
self.input = list(input_data)
self.input_pos = 0
self.output = []
self.halted = False
def fetch(self):
"""取指令"""
if self.pc >= len(self.bytecode):
self.halted = True
return None
opcode = self.bytecode[self.pc]
self.pc += 1
return opcode
def fetch_operand(self):
"""取操作数"""
if self.pc >= len(self.bytecode):
self.halted = True
return None
operand = self.bytecode[self.pc]
self.pc += 1
return operand
def push(self, val):
"""压栈"""
self.stack.append(val & 0xFFFFFFFF)
def pop(self):
"""出栈"""
if not self.stack:
return 0
return self.stack.pop()
def step(self):
"""执行一步"""
opcode = self.fetch()
if opcode is None:
return
if opcode == 0x01: # PUSH
val = self.fetch_operand()
self.push(val)
elif opcode == 0x02: # POP
self.pop()
elif opcode == 0x03: # ADD
a = self.pop()
b = self.pop()
self.push(a + b)
elif opcode == 0x04: # SUB
a = self.pop()
b = self.pop()
self.push(b - a)
elif opcode == 0x05: # MUL
a = self.pop()
b = self.pop()
self.push(a * b)
elif opcode == 0x06: # DIV
a = self.pop()
b = self.pop()
self.push(b // a if a != 0 else 0)
elif opcode == 0x07: # XOR
a = self.pop()
b = self.pop()
self.push(a ^ b)
elif opcode == 0x08: # AND
a = self.pop()
b = self.pop()
self.push(a & b)
elif opcode == 0x09: # OR
a = self.pop()
b = self.pop()
self.push(a | b)
elif opcode == 0x0A: # NOT
a = self.pop()
self.push(~a)
elif opcode == 0x0B: # SHL
a = self.pop()
b = self.pop()
self.push(b << a)
elif opcode == 0x0C: # SHR
a = self.pop()
b = self.pop()
self.push(b >> a)
elif opcode == 0x0D: # CMP
a = self.pop()
b = self.pop()
if a == b:
self.push(0)
elif a < b:
self.push(-1)
else:
self.push(1)
elif opcode == 0x0E: # JMP
addr = self.fetch_operand()
self.pc = addr
elif opcode == 0x0F: # JZ
addr = self.fetch_operand()
val = self.pop()
if val == 0:
self.pc = addr
elif opcode == 0x10: # JNZ
addr = self.fetch_operand()
val = self.pop()
if val != 0:
self.pc = addr
elif opcode == 0x11: # LOAD
idx = self.fetch_operand()
self.push(self.memory[idx])
elif opcode == 0x12: # STORE
idx = self.fetch_operand()
val = self.pop()
self.memory[idx] = val
elif opcode == 0x13: # INPUT
if self.input_pos < len(self.input):
self.push(self.input[self.input_pos])
self.input_pos += 1
else:
self.push(0)
elif opcode == 0x14: # OUTPUT
val = self.pop()
self.output.append(val)
elif opcode == 0xFF: # HALT
self.halted = True
def run(self):
"""运行完整程序"""
while not self.halted:
self.step()
return bytes(self.output)
# 使用示例
vm = VMSimulator(bytecode, input_data=b'flag{test}')
output = vm.run()
print(f"输出: {output}")字节码反汇编器
def disassemble(bytecode):
"""将字节码反汇编为可读指令"""
mnemonics = {
0x01: 'PUSH', 0x02: 'POP', 0x03: 'ADD',
0x04: 'SUB', 0x05: 'MUL', 0x06: 'DIV',
0x07: 'XOR', 0x08: 'AND', 0x09: 'OR',
0x0A: 'NOT', 0x0B: 'SHL', 0x0C: 'SHR',
0x0D: 'CMP', 0x0E: 'JMP', 0x0F: 'JZ',
0x10: 'JNZ', 0x11: 'LOAD', 0x12: 'STORE',
0x13: 'INPUT', 0x14: 'OUTPUT', 0xFF: 'HALT',
}
has_operand = {0x01, 0x0E, 0x0F, 0x10, 0x11, 0x12}
result = []
pc = 0
while pc < len(bytecode):
opcode = bytecode[pc]
mnemonic = mnemonics.get(opcode, f'DATA({opcode:#x})')
if opcode in has_operand and pc + 1 < len(bytecode):
operand = bytecode[pc + 1]
result.append(f"{pc:04x}: {mnemonic} {operand:#x}")
pc += 2
else:
result.append(f"{pc:04x}: {mnemonic}")
pc += 1
return '\n'.join(result)
# 反汇编
print(disassemble(bytecode))VM 检测与反检测
# VM 检测技术(用于反调试)
def detect_vm():
"""
程序可能检测是否运行在虚拟机中
"""
import platform
import subprocess
# 1. 检查特定硬件特征
vm_indicators = ['VMware', 'VirtualBox', 'QEMU', 'Xen']
# 2. 检查 CPUID
try:
result = subprocess.run(['cpuid', '-1'], capture_output=True, text=True)
for indicator in vm_indicators:
if indicator.lower() in result.stdout.lower():
return True
except FileNotFoundError:
pass
# 3. 检查特定设备文件
import os
vm_files = ['/dev/vmware', '/dev/vboxguest', '/proc/xen']
for f in vm_files:
if os.path.exists(f):
return True
# 4. 检查 MAC 地址前缀
mac = ':'.join(['{:02x}'.format((hash(platform.node()) >> i) & 0xff)
for i in range(0, 48, 8)])
vm_macs = ['00:0c:29', '00:50:56', '08:00:27']
for prefix in vm_macs:
if mac.startswith(prefix):
return True
return False
# 反检测
def anti_detect():
"""
绕过 VM 检测
"""
import ctypes
import struct
# 1. 修改 CPUID 返回值(需要内核模块或 hypervisor 支持)
# 简化方案:Hook 检测函数
print("[*] Anti-detect methods:")
print(" 1. 修改 VM 配置文件中的硬件标识")
print(" 2. 使用 -cpu host 传递宿主机 CPUID")
print(" 3. 修改 MAC 地址前缀")
print(" 4. 删除 VM guest tools 特征文件")
print(" 5. 使用 QEMU -machine type=q35 隐藏特征")完整 VM 逆向流程
def vm_reverse_engineering():
# 步骤 1:识别 VM 类型
# - 栈式 VM
# - 寄存器式 VM
# - 混合式
# 步骤 2:提取字节码
bytecode = extract_bytecode('./vuln', offset=0x2000, size=256)
# 步骤 3:分析分发逻辑
# 找到 switch 语句和状态变量
# 步骤 4:建立 opcode 映射
opcode_map = analyze_dispatch('./vuln', dispatch_addr=0x401000)
# 步骤 5:反汇编字节码
asm = disassemble(bytecode)
print(asm)
# 步骤 6:编写模拟器
vm = VMSimulator(bytecode, input_data=b'test')
output = vm.run()
# 步骤 7:分析校验逻辑
# 找到比较、分支等关键指令
# 步骤 8:提取约束或编写求解器
# 使用 Z3 或符号执行
return output常见失败原因
- 一直分析解释器实现:解释器只是执行环境,最终要看字节码程序。
- 没有记录 opcode 语义:没有表格就很难写反汇编器或模拟器。
- 把 VM 寄存器当 CPU 寄存器:VM 的寄存器/栈是程序自己维护的数组或结构体。
- 跳过指令指针变化:条件跳转、调用、返回会改变虚拟 pc,是控制流核心。
- 不检查字节码是否解密:有些 VM 运行前会先解密或重排字节码。
- 模拟器和原程序不一致:先用一小段字节码对比 pc、栈和寄存器状态。
- 急着上 Z3:opcode 语义没还原前,约束求解只会生成错误模型。
迷你案例
程序有 while (pc < len) { op = code[pc++]; switch(op) ... }。提取 code 后,先记录语义:
0x10 INPUT
0x20 PUSH imm
0x30 XOR
0x40 CMP
0x50 JNZ
0xff HALT反汇编字节码得到:
INPUT
PUSH 0x23
XOR
PUSH 0x45
CMP
JNZ fail说明第一个输入字节满足 input[0] ^ 0x23 == 0x45。逐条恢复所有比较后得到 flag。这个案例的闭环是:识别 VM -> 提取字节码 -> 建 opcode 表 -> 反汇编 -> 逆约束。