Large Bin Attack 学习记录

注意
本文最后更新于 2024-02-11,文中内容可能已过时。

这篇文章是在石墨上编写的,可能会存在格式上的一些问题,导致阅读不够流畅。 如果有需要格式较为完善的文件,可以发邮件找我要 PDF 格式的文件。

Largebin Attack 是我一直以来未能攻克的一道难题,不过终于在最近把他弄懂了,其中也少不了长时间的琢磨。 根据我自己浏览各个文章后得到的经验,我建议师傅在学习 Large Bin Attck 的内容时可以对其中的结构进行绘图,并且手动模拟一遍源码的流程以加深理解。

Large Bin Attack

利用范围

Large Bin Attack 是一种较为困难的攻击方式,他对攻击的条件要求较多,实现也较为复杂,通常和 Unsorted Bin 打配合来实现 house of storm,来达到提升影响力的作用。

glibc2.29 及以下版本,可以利用 Large Bin Attack 来写两个地址。

但是在 glibc2.30 及以上版本中,只能利用 Large Bin Attack 来写一个地址。

git commit:

利用原理

我们在 unsorted bin attack 的时候就已经了解过,当一个块将要 malloc 的时候,堆管理器会根据返回去不同的地方查找是否有空闲的 chunk,其中一个地方就是 unsorted bin,而在遍历 unsorted bin 堆块的时候会先将当前遍历的堆块进行脱链,脱链根据大小放入 small bin 或者 largebin 当中去。

而 largebin attack 就发生在堆块中 unsorted bin 放入 largebin 的过程当中去。

这个过程较为复杂,所以我们需要先来了解一下 largebin 是什么?有什么?

largebin 是什么?

堆块管理器中最慢的一种管理方式,largebin 的范围是 size > 0x400(x64)

large bin 采用双链表结构,里面的 chunk 从头结点的 fd 指针开始,按大小顺序进行排列。

largebin 有什么?

在 bins 中占据 64 到 126 这个范围的位置,

index公差
64[0x400,0x440) 0x40
65[0x440,0x480) 0x40
0x40(共 32 个)
970x200
980x200
0x200(共 16 个)
1120x1000
1130x1000
0x1000(共 8 个)
1200x8000
1210x8000
1220x8000
1230x8000(共 4 个)
1240x40000
1250x40000(共 2 个)
126> 0x40000

根据上表可以知道,就算在同一个 index 中,其大小也有差别,这和 fastbin 是不一样的。

所以堆管理器就利用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/*
  This struct declaration is misleading (but accurate and necessary).
  It declares a "view" into memory allowing access to necessary
  fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;
  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

这个结构中的 fd_nextsizebk_nextsize 来链接到下一个 size 的堆块头部和上一个 size 的堆块头部。 然后在相同 size 的堆块内部再通过 fdbk 来进行内部的管理。

为了表述方便,我们在下述描述中,称用 fd_nextsizebk_nextsize 链接的链表为横向链表,称用 fdbk 链接的链表为纵向链表,有心的师傅可以画个图理解一下。

而且,为了管理的高效,在横向链表中,堆管理器维护一个循环的单调链表,由最大的 size(在这个 index 下的最大 size)作为表头,最小的 size 作为表尾,且首尾相连。

而 large bin attack 就发生在,malloc 过程中把某个之前不存在的 size 插入到横向链表的过程中。

PS:如果之前存在,那么就直接加入到对应 size 的纵向链表即可。

下面来看这一块的代码,这一块的代码也在遍历 unsorted bin 堆块的过程中,所以对一些变量的理解可以参照那个的来看。

 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
if (in_smallbin_range(size)) 
{
  victim_index = smallbin_index(size);//获取size对应的smallbin的index
  bck = bin_at(av, victim_index);//bck指向size对应的smallbin的链表头
  //fwd指向size对应的smallbin的链表中的新加入的chunk(small bin使用头插法)
  fwd = bck->fd;
}
else//如果不再smallbin的范围,也就是说在large bin 的范围
{
  victim_index = largebin_index(size);//获取size对应的large bin的index
  bck = bin_at(av, victim_index);//bck指向size对应的large bin的链表头
  fwd = bck->fd;//fwd指向size对应的large bin的链表中的新加入的chunk
  
  //如果large bin 非空,在largbin进行按顺序插入
  if (fwd != bck) {
	  /* Or with inuse bit to speed comparisons */
	  size |= PREV_INUSE;
	  assert((bck->bk->size & NON_MAIN_ARENA) == 0);//默认不启用assert
	  /*
		large bin中的chunk是按从大到小排列的,如果size < large bin 
		的最后一个chunk,说明size是这个large bin中的最小的,我们把它
		加入到此large bin尾部。
	  */
	  if ((unsigned long) (size) < (unsigned long) (bck->bk->size)) {
		  
		  fwd = bck;
		  bck = bck->bk;
		  
		  /*
		  large bin 中size最小的chunk的fd_nextsize会指向size最大的
		  那个chunk,也就是首部的chunk。同样,large bin 中size最大的
		  chunk的bk_nextsize会指向size最小的那个chunk。
		  victim的bk_nextsize指向large bin原来最小的chunk,它的
		  bk_nextsize指向最大的那个chunk。那么原来的最小的就成了第二小的了。
		  把它fd_nextsize和bk_nextsize都修正。
		  */
		  victim->fd_nextsize = fwd->fd;
		  victim->bk_nextsize = fwd->fd->bk_nextsize;
		  //最大size的chunk的bk_nextsize,和原来最小chunk的bk_nextsize都指向victim
		  fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
	  } 
	  else //如果victim不是large bin 中最小的chunk
	  {
		  assert((fwd->size & NON_MAIN_ARENA) == 0);//默认不启用assert
		  //从大到小(从头到尾)找到合适的位置
		  while ((unsigned long) size < fwd->size) {
			  fwd = fwd->fd_nextsize;
			  assert((fwd->size & NON_MAIN_ARENA) == 0);
		  }
	//如果size刚好相等,就直接加入到其后面省的改fd_nextsize和bk_nextsize了
		  if ((unsigned long) size == (unsigned long) fwd->size)
			  fwd = fwd->fd;
		  else 
		  {
			  //size不相等,即size>fwd->size,把victim加入到纵向链表中
			  victim->fd_nextsize = fwd;
			  victim->bk_nextsize = fwd->bk_nextsize;
			  fwd->bk_nextsize = victim;
			  victim->bk_nextsize->fd_nextsize = victim;
		  }
		  bck = fwd->bk;
	  }
  } 
  else //如果large bin 为空,将victim加入到纵向列表
	victim->fd_nextsize = victim->bk_nextsize = victim;
}
//#define mark_bin(m, i)    ((m)->binmap[idx2block (i)] |= idx2bit (i))
mark_bin(av, victim_index); //把victim加入到的bin的表示为非空
//把victim加入到large bin的链表中
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

这一块内容摘选自:https://www.cnblogs.com/Rookle/p/13140339.html 这位师傅对这些内容的了解非常深入,推荐通过他的文章来学习。

从上述代码中可以看出,如果我们可以构造一个 unsorted bin:X;构造一个 largebin:Y。

那么我们需要满足的情况下,这时候申请一块堆块,就会触发 unsorted bin 的脱链,让 X 插入到 largebin 中,那么如果满足 size(X) > size(Y) && index(size(X)) == index(size(Y)),那么 X 就会插入到 Y 的前面,从而触发这一串代码进行插入。

1
2
3
4
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;

强烈建议可以画一个图理清变量之前的关系,手动模拟一下。 这里我推算出的关系是,victim 就是当前要插入的 unsorted bin 的 chunk,fwd 是依次查找到的小于 size(victim),这个 chunk 就是我们构造中的 Y。

这里只需要重点关注这一行,如果我们对 victim->bk_nextsize 进行伪造,那么就可以控制程序写一个堆块地址到目标位置。

1
victim->bk_nextsize->fd_nextsize = victim;

而下面我们还注意到了,他做了一个类似 unsorted bin attack 的操作

我们只需要重点关注这一行,如果我们可以控制 fwd->bk 位置,那么就可以写一个堆块地址到目标位置

1
bck->fd = victim;

在上面的构造位置中,bck 也就是 fwd->bk

glibc2.30 的检测

1
2
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
    malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");

在 glibc2.29 中,增加了对双向链表完整性的检查,这样的检查方式正如同 glibc2.29 中增加的对 unsorted bin 类似。但是与其不同的是,这个检查只存在于插入的 unsorted chunk size 大于 chunk 时候,也就是说,如果我们构造一个小于所有 largebin 中堆块的 unsorted chunk,那么就可以成功利用上面那个分支操作。 代码来源:https://code.woboq.org/userspace/glibc/malloc/malloc.c.html

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

如上代码所示,在插入之前首先会判定一下该 chunk 的大小是否小于 largebin 中的最小块,如果小于那么就会走上面的分支,如果大于那么就会走下面的分支,而检测则只存在于下面的分支中。 所以我们绕过这个检测的最好方法就是,不经过这个检测。[感觉这个检测方法是不是有些敷衍啊]

我们通过上面分支的这些代码来达到写出地址的目的。

1
2
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;

以上两行代码,我们可以需要的其实只有一行,我为了便于理解稍加更改

1
fwd->fd->bk_nextsize->fd_nextsize = victim;

其中,fwd->fd 就是 largebin chunk,我们只需要修改他的 bk_nextsize 为目标地址 - 0x20,那么就可以对目标地址进行写入,进而达到利用的目的。 但是由于少了一块地址的写入就不能用于 house of storm

利用方向

目前只了解一个 house of storm,之后学到新的知识后会回来补充。

总结

可以利用 largebin attack 干很多和 unsorted bin attack 类似的事情,而且我们还可以利用 largebin attack 的 fd_nextsize 和 bk_nextsize 来泄露堆地址(例如 house of orange)。

总的来说,这种方法还有很多操作没有被开发利用(或者是我还没有了解),相信在之后这个方向的题目会出的越来越多!掌握它也成为了非常必要的事情。

参考链接

[1] house_of_storm 详解 - Rookie - 博客园 https://www.cnblogs.com/Rookle/p/13140339.html

0%