那CTF,那VMre,那些事(三)
字数 2014 2025-08-07 08:22:23

基于堆栈的虚拟机逆向分析技术详解

0x01 前言

本文档详细解析基于堆栈的虚拟机保护逆向技术,通过多个CTF题目实例,深入讲解栈虚拟机的特点、指令实现原理及逆向分析方法。

0x02 栈虚拟机基础

虚拟机架构分类

  1. 基于堆栈的虚拟机(Stack-based)

    • 频繁操作堆栈
    • 虚拟寄存器保存在堆栈中
    • 每个原子指令handler都需要push/pop操作
  2. 基于寄存器的虚拟机(Register-based)

    • 操作数存放在CPU寄存器中
    • 指令需明确包含操作数地址
  3. 3地址机器

栈虚拟机特点

  • 通过IP获取操作数
  • 操作数保存在Stack数据结构中(LIFO)
  • 典型操作流程:
    1. POP 20
    2. POP 7
    3. ADD 20, 7, result
    4. 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

题目分析

  1. 主函数逻辑

    • 输入40字符的flag
    • 初始化大数组
    • 调用关键函数sub_1400017D0处理
    • 与预设数组比较
  2. 关键函数分析

    • 构造栈结构体:
      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

虚拟机结构分析

  1. 关键数据结构

    • vm_eip: 指令指针
    • vm_sp: 栈指针
    • code: 字节码
    • vm_stack: 操作栈
    • vm_arr: 存储三个数组
      • arr1(预定义, 50-91)
      • arr2(用户输入, 100-141)
      • arr3(处理结果, 0-41)
    • vm_block: 循环计数器存储
  2. 重要指令解析

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 无条件跳转

字节码分析模式

  1. 手撕法

    • 识别指令组合模式:
      • 0x4 -> 0x8: 数组赋值
      • 0x1 -> 0x8: 输入初始化
      • 0x5-0x4-0x16: 循环结构
  2. 脚本翻译法

    • 编写字节码翻译脚本自动生成伪代码

关键算法还原

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

解题思路

  1. 提取32个方程系数
  2. 使用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 总结与技巧

逆向分析技巧

  1. 识别虚拟机结构

    • 查找switch-case或while-switch结构
    • 识别虚拟寄存器、栈、指令指针等关键数据结构
  2. 指令分析

    • 从简单指令入手(如mov/push/pop)
    • 识别指令组合模式(如赋值、循环等)
  3. 动态调试

    • 结合静态分析与动态调试
    • 跟踪栈和寄存器变化
  4. 算法还原

    • 识别数学运算模式(如异或、模乘等)
    • 注意数据宽度截断(如&0xff)

解题策略

  1. 对简单VM保护:

    • 直接动态调试提取关键算法
  2. 对复杂VM保护:

    • 编写反汇编脚本翻译字节码
    • 重点分析加密循环部分
    • 使用符号执行或约束求解
  3. 对数学类题目:

    • 提取方程系数
    • 使用z3等求解器

注意事项

  • 不要被代码量吓倒,实际有效代码可能只占小部分
  • 注意虚拟指令与实际指令的对应关系
  • 关注数据流而非控制流
  • 耐心分析指令组合模式
基于堆栈的虚拟机逆向分析技术详解 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 常见指令实现 0x03 实例分析 - hgame-week4-easyvm 题目分析 主函数逻辑 输入40字符的flag 初始化大数组 调用关键函数sub_ 1400017D0处理 与预设数组比较 关键函数分析 构造栈结构体: 动态调试发现处理逻辑: 取输入字符串中每三个字符的第二个 与固定值异或 循环0x20次 解题脚本 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 : 循环结构 脚本翻译法 编写字节码翻译脚本自动生成伪代码 关键算法还原 解题脚本 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求解器解方程组 解题脚本 0x06 总结与技巧 逆向分析技巧 识别虚拟机结构 查找switch-case或while-switch结构 识别虚拟寄存器、栈、指令指针等关键数据结构 指令分析 从简单指令入手(如mov/push/pop) 识别指令组合模式(如赋值、循环等) 动态调试 结合静态分析与动态调试 跟踪栈和寄存器变化 算法还原 识别数学运算模式(如异或、模乘等) 注意数据宽度截断(如&0xff) 解题策略 对简单VM保护: 直接动态调试提取关键算法 对复杂VM保护: 编写反汇编脚本翻译字节码 重点分析加密循环部分 使用符号执行或约束求解 对数学类题目: 提取方程系数 使用z3等求解器 注意事项 不要被代码量吓倒,实际有效代码可能只占小部分 注意虚拟指令与实际指令的对应关系 关注数据流而非控制流 耐心分析指令组合模式