图解unsortedbin attack & largebin attack

图解unsortedbin attack & largebin attack

M1aoo0bin

(。・ω・。)

malloc 时,不仅仅是从 bin 里面找到并分配合适的内存,还有拿出后重新组织链表,这两种攻击都是在组织链表的时候被改写过的指针骗了,把一块不存在 bin 里的内存当作 free 过后的 chunk 一样组织进去。那么这块内存上就会记录指向 bin 的链表头之类的指针,也就是一个地址,仅当成数据的时候就是一个大数。

unsortedbin attack

unsortedbin 结构

unsortedbin是双向循环链表,FIFO,也就是后进的 chunk 会被组织进“表头”和上一个进入 unsorted bin 里的之间
就像这样:
20240925013408
为了循环的结构看的更加清楚,我把它画成了圈的形式,chunk0 是先被 free 的堆块。

往 unsortedbin 里面打两个 chunk 的话也可以看到他的结构是这样的。
20240925013528
20240925013328

源码分析

然后看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
//size check
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);

//此处省略进入smallbin的情况

/* remove from unsorted list */
//unsorted bin attack
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

// 此处省略exact fit、进入smallbin、进入largebin

}

malloc 的时候会遍历 unsortedbin 且把他们都分配到该分配的地方去(切割分配,exact fit,smallbin,largebin),这个过程会不断解链,把尾节点(对于循环链表这么说不太准确)放到该放的 bin 里去,然后把倒数第二个节点bck链接成一个新的环。

而 bck = victim->bk; 也就是只依赖 bk 来判断谁是现在的倒数第二个 chunk 。

如果我们伪造一个 bk 指向我们希望的地址-0x10作为伪造的 chunk ,那么这个 chunk 就会被当作倒数第二个节点和 bin 的头链接成循环链表。

攻击

具体攻击就如下图这样:
先塞一个 chunk0 ,然后更改 bk 指向fake_chunk
20240928000214

然后malloc一下,就会进入遍历 unsortedbbin 的环节,此时 fake_chunk 被认为是bck, 根据先进先出,chunk0 将从 unsortedbin 中被拿出,然后执行那两句unsortedbin attack的代码,可以看到有两条指针有变化,蓝色的就是我们期待的攻击结果,此时fake_chunkfd指针位置被改成了一个大数。
(以满足exact fit时为例,其他也差不多)
20240928000246

不过在libc2.28及之后
这里的源码变成了:

1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

因为有了安全性检查,所以 unsortedbin attack 失效了,不过如果要是能泄露堆地址以及堆溢出可以改写很多的话或许还是可以绕过的。

largebin attack

先是一坨largebin源码

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
    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 ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
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 ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
// largebin attack 1
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;
}
// largebin attack 2
mark_bin(av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

从bin_at开始讲起

其实这和利用的部分没什么关系,只是在了解后觉得这种省空间的方式很有趣。

数组中的 bin 依次如下

  • unsorted bin 的索引为1。
  • 索引为 2-63 的是 small bin,同一个 small bin 链表中的 chunk 的大小相同。两个相邻索引的 small bin 链表中的 chunk 大小相差的字节数为 2 个机器字长,即 32 位相差 8 字节,64 位相差 16 字节。
  • 索引为 63-126 的是 large bins。large bins 中的每一个 bin 都包含一定范围内的 chunk,其中的 chunk 按 fd 指针的顺序从大到小排列。相同大小的 chunk 同样按照最近使用顺序排列。

bin_at宏定义源码

1
2
3
#define bin_at(m, i) \
(mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) \
- offsetof (struct malloc_chunk, fd))

m 代表的是分配区的地址,配合上不同类型的 bins 的索引就可以找到每个 bin 链表的头。bin_at 的作用就是通过不同 bin 的索引找到链表头。

通过这样的计算就能将对应的 bins() 再往前移两个单元的长度,也就是人为往前移出了prev_size位和size位。

20240928014016
(图中bins(0),bins(1)即为 unsortedbin 的 fd,bk)

对于 bin 的链表头来说,其实prev_sizesize也不是很重要,于是只是把得到的结果强行转化为malloc_chunk*类型,可以有正常的链表链接操作,而实际在内存上并不是 malloc_chunk 结构体。

源码分析

注意上文中注释//largebin attack 1的部分,

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;
}

进入这里的 else 的情况是:largebin 非空;victim 不是 largebin 中最小的堆块;victim 的 size 和原 chunk 大小不相等。
这段的本意就是插入fd_nextsize,bk_nextsize链接的跳表中,但是因为没有检查 fwd 的 bk_nextsize 是否被更改,所以如果提前更改 fwd 的 bk_nextsize 那么也能塞出一条指针

20240929175706
(左为正常插入流程,右为伪造 bk_nextsize)

同理,//largebin attack 2的地方也是伪造了 bk 。

然而同样的,在libc2.30及之后,largebin也加入了相应的校验。

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
else{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
......
else
{
//largebin attack
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)");
}
}
......

攻击

hgame2024 week3 Elden Ring III

(づ′▽`)づ

后记

迟到了非常久,久到我有点忘记了为什么要把他们写在一起,可能是因为都是双向链表,然后都是任意地址非任意写,只是改大数。暑假没写是因为真的要忘光了,然后刚回忆起来一点就又去忙干别的事。总之希望这篇能够讲清楚这两种利用,变成未来的记忆胶囊((。

  • Title: 图解unsortedbin attack & largebin attack
  • Author: M1aoo0bin
  • Created at : 2024-07-23 22:21:52
  • Updated at : 2024-09-29 18:12:13
  • Link: https://redefine.ohevan.com/2024/07/23/really-ez-largebin/
  • License: This work is licensed under CC BY-NC-SA 4.0.