关于如何理解Glibc堆管理器(Ⅲ——从DoubleFree深入理解Bins)

First Post:

Last Update:

 本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。

若有图片及文稿引用,将在本篇结尾处著名来源。

环境与工具:

        Ubuntu16.4 / gcc / (gdb)pwn-dbg

        范例:howtoheap2

搭建调试环境:

1
2
3
git clone https://github.com/shellphish/how2heap.git
cd how2heap
make

        对于GitHub可能速度过慢的情况,可以尝试使用Gitee拷贝仓库,再从Gitee处克隆仓库

fastbin_dup_into_stack:

我们有必要使用glibc2.23版本下的环境来进行这种调试。在更高版本中,已经修复了这个漏洞。这当然对系统来说是好事,但对于试图理解其原理的学习者来说,少一些限制往往能够更加快速的理解。

        如下为源代码:(我并未做出删减,以方便让说明与调试过程相统一)

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

int main()
{
fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
"returning a pointer to a controlled location (in this case, the stack).\n");

unsigned long long stack_var;

fprintf(stderr, "The address we want malloc() to return is %p.\n", 8+(char *)&stack_var);

fprintf(stderr, "Allocating 3 buffers.\n");
int *a = malloc(8);
int *b = malloc(8);
int *c = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", a);
fprintf(stderr, "2nd malloc(8): %p\n", b);
fprintf(stderr, "3rd malloc(8): %p\n", c);

fprintf(stderr, "Freeing the first one...\n");
free(a);

fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
// free(a);

fprintf(stderr, "So, instead, we'll free %p.\n", b);
free(b);

fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
free(a);

fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
"We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
unsigned long long *d = malloc(8);

fprintf(stderr, "1st malloc(8): %p\n", d);
fprintf(stderr, "2nd malloc(8): %p\n", malloc(8));
fprintf(stderr, "Now the free list has [ %p ].\n", a);
fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
"so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
"so that malloc will think there is a free chunk there and agree to\n"
"return a pointer to it.\n", a);
stack_var = 0x20;

fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\n", malloc(8));
fprintf(stderr, "4th malloc(8): %p\n", malloc(8));
}

调试阶段:

1
2
3
4
5
6
7
8
9
10
11
12
test@ubuntu:~/how2heap/glibc_2.23$ gdb fastbin_dup_into_stack 
gdb-peda$ b 14
Breakpoint 1 at 0x40071d: file glibc_2.23/fastbin_dup_into_stack.c, line 14.
gdb-peda$ b 23
Breakpoint 2 at 0x4007bc: file glibc_2.23/fastbin_dup_into_stack.c, line 23.
gdb-peda$ b 29
Breakpoint 3 at 0x400806: file glibc_2.23/fastbin_dup_into_stack.c, line 29.
gdb-peda$ b 32
Breakpoint 4 at 0x40082f: file glibc_2.23/fastbin_dup_into_stack.c, line 32.
gdb-peda$ b 36
Breakpoint 5 at 0x40086a: file glibc_2.23/fastbin_dup_into_stack.c, line 36.
gdb-peda$ run

        可以在如上位置下断点,然后开始调试程序。

         通过continue和n运行到24行,也就是第一次执行free函数的位置,输入bins可以查看当前bins中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ bins
fastbins
0x20: 0x603000 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

        可以看见,此时,fastbins中已经有了第一个节点。继续往下,直到第二次free结束时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ bins
fastbins
0x20: 0x603020 —▸ 0x603000 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

        可以看见,此时第二个节点也挂进fastbins链表的头部了。继续往下调试,直到第三个free函数被执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ bins
fastbins
0x20: 0x603000 —▸ 0x603020 ◂— 0x603000
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

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
gdb-peda$ heap
0x603000 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x603020,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x603020 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x603000,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x21
}
0x603040 FASTBIN {
prev_size = 0x0,
size = 0x21,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x20fa1
}
0x603060 PREV_INUSE {
prev_size = 0x0,
size = 0x20fa1,
fd = 0x0,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

        可以看见,此时堆中的两个chunk在FastBins的链表中形成了一个闭环。

        这是一个非常反直觉的行为,因为我们执行了两次free(a),并且系统并没有报错

       (注:笔者在Kali2021版本中以相同代码进行调试则会出现报错,在该版本中已经存在Tcache Bins,示例中的free函数会将chunk放入Tcache Bins中而不是Fast Bins,因此调试失败)

         联系上一章内容,Fast Bins中的chunk并不是真正处于释放状态,因此系统在执行free函数的时候检查当前chunk的状态时会发现它仍然在被使用,因此我们可以多次进行free(a)的操作而不出现错误

        但这并不意味着系统不会做出检查:下方代码摘自ctf-wik,有删减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Lightweight tests: check whether the block is already the
top block. */
// 当前free的chunk不能是top chunk
if (__glibc_unlikely(p == av->top)) {
errstr = "double free or corruption (top)";
goto errout;
}
// 当前要free的chunk的使用标记没有被标记,double free
/* Or whether the block is actually not marked used. */
if (__glibc_unlikely(!prev_inuse(nextchunk))) {
errstr = "double free or corruption (!prev)";
goto errout;
}

        根据注释可知,free函数的检查只判断当前目标是否处于Top chunk,也就是链表的头部。由于我们free(b)的执行,此时Top Chunk为chunk b,并且由于处在Fast Bins中,因此也绕过了第二个检查,所以成功对 a 进行了两次free操作

        接下来的内容请根据如下代码进行调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main()
{
int *a = malloc(8);
int *b = malloc(8);
free(a);
free(b);
free(a);
int *c = malloc(8);
int *d = malloc(8);
int *e = malloc(8);
int *f = malloc(8);
return 0;
}

1
2
3
4
gcc -g heap2.c -o heap2
gdb heap2
b 13
run

        我删处了很多不必要的说明以方便我们更加直观的看到DoubleFree的效果

        直接运行到第13行,并且完成接下来的四次malloc操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
───────────────────────────────[ SOURCE (CODE) ]────────────────────────────────
In file: /home/giantbranch/Desktop/class/heap2.c
8 int *a = malloc(8);
9 int *b = malloc(8);
10 free(a);
11 free(b);
12 free(a);
► 13 int *c = malloc(8);
14 int *d = malloc(8);
15 int *e = malloc(8);
16 int *f = malloc(8);
17 return 0;
18 }

        当我们运行到第17行时再查看如下变量: 

1
2
3
4
5
6
7
8
gdb-peda$ p c
$5 = (int *) 0x602010
gdb-peda$ p d
$6 = (int *) 0x602030
gdb-peda$ p e
$7 = (int *) 0x602010
gdb-peda$ p f
$8 = (int *) 0x602030

        我们发现,不论怎么申请都只会得到这两个地址了。它们交错出现,只要我们申请的内存能够从这个Bin中取出,那么我们现在就只能得到这两个地址了。

        从malloc的角度来说,它会取出Bins的第一个节点,并将其他节点往上挂入头节点中。

        而在回环的链表中,取出第一个节点后,第二个节点成为新的第一个节点,而新的第二个节点则又是第一个节点(这样说十分绕口,建议手动调试一下),因此没办法像平常操作那样取出目标了

        而从这个出现顺序也能够猜出,Fast Bins是先进后出的结构

fastbin_dup_consolidate:

        我没有使用范例给出的程序,而是自己写了更加方便调试的类似的代码

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

int main()
{
void* p1 = malloc(0x40);
void* p2 = malloc(0x40);
free(p1);
void* p3 = malloc(0x400);
free(p1);
void* p4 = malloc(0x40);
void* p5 = malloc(0x40);
}

        同样编译后运行到第9行,此时的bins:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x602000 ◂— 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

         此时,chunk p1已经被放入Fast Bins中,当我们再次申请一块超出Fast Bins能够服务的chunk时,即执第9行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0x50: 0x602000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x602000
largebins
empty

        发生了合并consolidate,并将chunk p1送入Small Bins中,。继续往下执行第10行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb-peda$ bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x602000 ◂— 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0x50 [corrupted]
FD: 0x602000 ◂— 0x0
BK: 0x602000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x602000
largebins
empty

         第11行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gdb-peda$ bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0x50 [corrupted]
FD: 0x602000 ◂— 0x0
BK: 0x602000 —▸ 0x7ffff7dd1bb8 (main_arena+152) ◂— 0x602000
largebins
empty

        第12行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty

        这个实际结果与上一章所述相同。在第12行代码中,堆管理器检查Small Bins发现可用,分割该chunk分配给 p5,并将该chunk取出Bins。

引用:

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/implementation/free/#_3

插画ID:90981187