那CTF,那VMre,那些事(三)
字数 2014 2025-08-07 08:22:23
基于堆栈的虚拟机逆向分析技术详解
0x01 前言
本文档详细解析基于堆栈的虚拟机保护逆向技术,通过多个CTF题目实例,深入讲解栈虚拟机的特点、指令实现原理及逆向分析方法。
0x02 栈虚拟机基础
虚拟机架构分类
-
基于堆栈的虚拟机(Stack-based)
- 频繁操作堆栈
- 虚拟寄存器保存在堆栈中
- 每个原子指令handler都需要push/pop操作
-
基于寄存器的虚拟机(Register-based)
- 操作数存放在CPU寄存器中
- 指令需明确包含操作数地址
-
3地址机器
栈虚拟机特点
- 通过IP获取操作数
- 操作数保存在Stack数据结构中(LIFO)
- 典型操作流程:
- POP 20
- POP 7
- ADD 20, 7, result
- PUSH result
常见指令实现
; 虚拟加法指令
Vadd:
Mov eax,[esp+4] ; 取源操作数
Mov ebx,[esp] ; 取目的操作数
Add ebx,eax ; 相加
Add esp,8 ; 平衡堆栈
Push ebx ; 结果压栈
; 寄存器入栈
vPushReg32:
Mov eax,dword ptr[esi] ; 从字节码获取寄存器偏移
Add esi,4
Mov eax,dowrd ptr [edi+eax] ; 获取寄存器值
Push eax
Jmp VMDispatcher
; 立即数入栈
vPushImm32:
Mov eax,dword ptr[esi]
Add esi,4
Push eax
Jmp VMDispatcher
; 出栈到寄存器
vPopReg32:
Mov eax,dword,ptr[esi]
Add esi,4
Pop dword ptr [edi+eax]
Jmp VMDispatcher
; 虚拟跳转
Vjmp:
Mov esi,dword ptr [esp]
Add esp,4
Jmp VMDispatcher
; 虚拟调用
Vcall:
Push all vreg ; 保存所有虚拟寄存器
Pop all reg ; 弹出到真实寄存器
Push 返回地址 ; 设置返回地址
Push 目标函数地址
Retn
0x03 实例分析 - hgame-week4-easyvm
题目分析
-
主函数逻辑
- 输入40字符的flag
- 初始化大数组
- 调用关键函数sub_1400017D0处理
- 与预设数组比较
-
关键函数分析
- 构造栈结构体:
struct StackInfo { QWORD Size; // 栈尺寸 QWORD CurrentStackTop;// 栈顶 QWORD pStack; // 指向分配的栈 } - 动态调试发现处理逻辑:
- 取输入字符串中每三个字符的第二个
- 与固定值异或
- 循环0x20次
- 构造栈结构体:
解题脚本
cipher = [58,84,47,42,47,54,19,1,46,3,53,64,71,14,95,89,1,105,39,8,61,76,51,26,45,11,64,14,75,36,65,39,37,40,41,42,2,2,93,36]
xor = [82,51,78,71,74,77,103,105,71,112,106,54,42,81,54,42,94,54,84,103,78,35,64,117,94,100,51,97,56,75,50,72,86,71,118,79,99,113,36,89]
for i in range(40):
print(chr(cipher[i] ^ xor[i]), end='')
Flag: hgame{this_vm_is__sosososososososo_easy}
0x04 实例分析 - 虎符CTF-vm
虚拟机结构分析
-
关键数据结构
vm_eip: 指令指针vm_sp: 栈指针code: 字节码vm_stack: 操作栈vm_arr: 存储三个数组- arr1(预定义, 50-91)
- arr2(用户输入, 100-141)
- arr3(处理结果, 0-41)
vm_block: 循环计数器存储
-
重要指令解析
| Opcode | 功能描述 |
|---|---|
| 0x1 | getchar()输入 |
| 0x2 | putchar()输出 |
| 0x4 | push data |
| 0x5 | push vm_block[index] |
| 0x7 | push vm_arr[index] |
| 0x8 | pop vm_arr[index] |
| 0x9 | 加法(+) |
| 0xa | 减法(-) |
| 0xb | 乘法(×) |
| 0xd | 取模(%) |
| 0xf | 按位与(&) |
| 0x10 | 按位或(|) |
| 0x12 | 按位取反(~) |
| 0x14 | 等于跳转(==) |
| 0x16 | 小于跳转(<) |
| 0x18 | 大于跳转(>) |
| 0x1d | 无条件跳转 |
字节码分析模式
-
手撕法
- 识别指令组合模式:
0x4 -> 0x8: 数组赋值0x1 -> 0x8: 输入初始化0x5-0x4-0x16: 循环结构
- 识别指令组合模式:
-
脚本翻译法
- 编写字节码翻译脚本自动生成伪代码
关键算法还原
arr1 = [102,78,169,253,60,85,144,36,87,246,93,177,1,32,129,253,54,169,31,161,14,13,128,143,206,119,232,35,158,39,96,47,165,207,27,189,50,219,255,40,164,93]
# 第一部分:双重循环处理
for j in range(7):
for i in range(6):
# 等价于 arr2[6*j+i] ^ ((i+2)*j)
var1 = ~arr2[6*j+i] & ((i+2)*j)
var2 = arr2[6*j+i] & ~((i+2)*j)
arr3[i*7+j] = var1 | var2
# 第二部分:奇偶不同处理
for i in range(1,42):
if i % 2 == 0:
arr[i] = (arr[i] + arr[i-1]) & 0xff
else:
arr[i] = (arr[i] * 0x6b) & 0xff
# 第三部分:校验
for i in range(0x29):
if arr[i] != arr1[i]:
return False
解题脚本
arr1 = [102,78,169,253,60,85,144,36,87,246,93,177,1,32,129,253,54,169,31,161,14,13,128,143,206,119,232,35,158,39,96,47,165,207,27,189,50,219,255,40,164,93]
arr = [0]*42
arr[0] = arr1[0]
flag = ''
# 逆向处理
for i in range(1,42):
if i % 2 == 0:
arr[i] = (arr1[i] - arr1[i-1]) & 0xff
else:
for j in range(0xff):
if (j * 0x6b) & 0xff == arr1[i]:
arr[i] = j
# 逆向双重循环
for j in range(7):
for i in range(6):
flag += chr(arr[i*7+j] ^ ((i+2)*j))
print(flag)
0x05 实例分析 - 2021长安杯-virture
题目特点
- 32元一次方程组求解
- 30多个指令但实际只用到10余个
关键指令
| Opcode | 功能 |
|---|---|
| 0x01 | 加法 |
| 0x02 | 乘法 |
| 0x09 | mov reg, Dword |
| 0x0d | 异或 |
| 0x12 | mov [addr], reg |
| 0x13 | mov reg, Dword |
| 0x15 | putchar() |
| 0x16 | getchar() |
| 0x17 | 比较功能 |
| 0x1b | exit() |
| 0x20 | 条件跳转 |
| 0x21 | jne |
解题思路
- 提取32个方程系数
- 使用z3求解器解方程组
解题脚本
from z3 import *
# 初始化32个变量
v = [Int('v%d'%i) for i in range(32)]
s = Solver()
# 添加方程约束(示例部分)
s.add(62*v[0] + 33*v[1] + 51*v[2] + 104*v[3] + 42*v[4] + 37*v[5] + 80*v[6] + 66*v[7] + 46*v[8] + 119*v[9] + 63*v[10] + 93*v[11] + 40*v[12] + 95*v[13] + 69*v[14] + 105*v[15] + 54*v[16] + 36*v[17] + 93*v[18] + 53*v[19] + 66*v[20] + 67*v[21] + 62*v[22] + 65*v[23] + 80*v[24] + 117*v[25] + 50*v[26] + 43*v[27] + 36*v[28] + 107*v[29] + 108*v[30] + 111*v[31] == 170779)
# 添加其余31个方程...
# 求解
if s.check() == sat:
m = s.model()
flag = ''.join([chr(m[vi].as_long()) for vi in v])
print("flag{" + flag + "}")
else:
print("No solution")
0x06 总结与技巧
逆向分析技巧
-
识别虚拟机结构
- 查找switch-case或while-switch结构
- 识别虚拟寄存器、栈、指令指针等关键数据结构
-
指令分析
- 从简单指令入手(如mov/push/pop)
- 识别指令组合模式(如赋值、循环等)
-
动态调试
- 结合静态分析与动态调试
- 跟踪栈和寄存器变化
-
算法还原
- 识别数学运算模式(如异或、模乘等)
- 注意数据宽度截断(如&0xff)
解题策略
-
对简单VM保护:
- 直接动态调试提取关键算法
-
对复杂VM保护:
- 编写反汇编脚本翻译字节码
- 重点分析加密循环部分
- 使用符号执行或约束求解
-
对数学类题目:
- 提取方程系数
- 使用z3等求解器
注意事项
- 不要被代码量吓倒,实际有效代码可能只占小部分
- 注意虚拟指令与实际指令的对应关系
- 关注数据流而非控制流
- 耐心分析指令组合模式