0%

Unlink

Unlink
Unlink是什么
在讲那个wiki上被转发烂了的chunk图之前有两个点先解决一下:

unlink是什么
什么时候执行了unlink
这两个点也是我在初期一直都很困惑的地方,直到翻看了libc的源码(可以在这里下载,我下载的是2.23),在malloc.c中找到了unlink。unlink其实是libc中定义的一个宏,定义如下:

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
#define unlink(AV, P, BK, FD) {                                            
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}

那什么时候执行了unlink呢?在执行free()函数时执行了 _int_free()函数,在_int_free()函数中调用了unlink宏,大概的意思如下(注意_int_free()是函数不是宏):

1
2
3
4
5
6
7
8
#define unlink(AV, P, BK, FD)
static void _int_free (mstate av, mchunkptr p, int have_lock)
free(){
_int_free(){
unlink();
}
}

堆释放
好了关于unlink的部分先暂停一下,这里我们需要回顾一下调用free()函数堆释放这部分的知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//gcc -g test.c -o test
#include<stdio.h>

void main(){
long *hollk1 = malloc(0x80);
long *first_chunk = malloc(0x80);
long *hollk3 = malloc(0x80);
long *second_chunk = malloc(0x80);
long *hollk5 = malloc(0x80);
long *third_chunk = malloc(0x80);
long *hollk7 = malloc(0x80);

free(first_chunk);
free(second_chunk);
free(third_chunk);

return 0;
}

举一个例子,这里申请了7个chunk,接着依次释放了first_chunk、second_chunk、third_chunk。这里为什么释放这几个chunk呢,因为地址相邻的chunk释放之后会进行合并,地址不相邻的时候不会合并。由于申请的是0x80的chunk,所以在释放之后不会进fastbin而是先进unsortbin。我们用gdb打开编译好的例子,因为使用了-g参数,所以我们在第17行使用命令b 17下断点,接下来让程序跑起来,使用命令bin我们看一下双向链表中的排列结构:

离近点看! 可以模糊的看到已经有三个chunk_free进入了unsortbin。那么这三个chunk_free从右向左分别对应着first_chunk、second_chunk、third_chunk,我们使用heap命令查看一下这几个chunk:

这几个释放的chunk已经按照unsortbin中的顺序排列。这里主要看每一个chunk的fd、bk:

first_bk -> second
second_fd -> first 、 second_bk -> third
third_fd -> second`

unlink过程及检查
下面呢给出wiki上的链接,说实话wiki上的图和说明我是真的没看懂。。。。。所以我按照字节的例子和理解写出来:

wiki链接:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink-zh/

wiki中unlink关键执行了如下的步骤:

FD = P -> fd = target addr -12
BK = P -> bk = expect value
FD -> bk = BK,即 *(target addr - 12 + 12) = BK = expect value
BK -> fd = FD,即 *(expect value + 8) = FD = target addr - 12
这后面的target、expect什么的我就不知道是啥了,感觉好像没什么用处。。。。。

我自己的理解
在此声明这里是我个人的理解,没有说wiki不对什么的,而且也是在wiki的基础上做了一些更加通俗易懂的变化

还是用前面堆释放的例子,依次释放了first_chunk、second_chunk、third_chunk,也就是说首先释放的是first,然后释放的是second,最后释放的是third。在双链表中的结构如下:

个人的理解unlink其实是想把second_chunk摘掉,那怎么摘呢?

在前面堆释放部分我们讲过fd其实是前一个被释放chunk的prev_size地址,bk是后一个被释放的chunk的prev_size地址,所以:

如果second_chunk被摘掉,那么就会变成下面这样:

由于first_chunk是最开始被释放的,所以first_chunk相对于third_chunk是前一个被释放的块。同样的third_chunk是之后释放的,所以third_chunk相对于first_chunk是后一个被释放的块,所以:

1
2
3
first_bk = third_prev_addr
third_fd = first_prev_addr

所以我的理解中对应着wiki的表现形式就是:

1
2
3
4
5
second_fd = first_prev_addr
second_bk = third_prev_addr
first_bk = third_prev_addr
third_fd = first_prev_addr

※※※※※※※※执行流程有先后顺序※※※※※※※※

chunk状态检查
现在我们用的大多数linux都会对chunk状态进行检查,以免造成二次释放或者二次申请的问题。但是恰恰是这个检查的流程本身就存在一些问题,能够让我们进行利用。回顾一下以往我们做的题,大部分都是顺着原有的执行流程走,但是通过修改执行所用的数据来改变执行走向。unlink同样可以以这种方式进行利用,由于unlink是在free()函数中调用的,所以我们只看chunk空闲时都需要检查写什么

我们还是拿前面的例子来说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 1 //gcc -g test.c -o test
2 #include<stdio.h>
3
4 void main(){
5 long *hollk1 = malloc(0x80);
6 long *first_chunk = malloc(0x80);
7 long *hollk3 = malloc(0x80);
8 long *second_chunk = malloc(0x80);
9 long *hollk5 = malloc(0x80);
10 long *third_chunk = malloc(0x80);
11 long *hollk7 = malloc(0x80);
12
13 free(first_chunk);
14 free(second_chunk);
15 free(third_chunk);
16
17 return 0;
18 }

这次我们在第17行下断点,并且查看一下second_chunk:

检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小

可以看左图绿色框中的内容,上面绿色框中的内容是second_chunk的size大小,下面绿色框中的内容是hollk5的prev_size,这两个绿色框中的数值是需要相等的(忽略P标志位)。在wiki上我记得在基础部分有讲过,如果一个块属于空闲状态,那么相邻高地址块的prev_size为前一个块的大小

检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0

可以看左图蓝色框中的内容,这里是hollk5的size,hollk5的size的P标志位为0,代表着它前一个chunk(second_chunk)为空闲状态

检查3:检查前后被释放chunk的fd和bk

可以看左图红色框中的内容,这里是second_chunk的fd和bk。首先看fd,它指向的位置就是前一个被释放的块first_chunk,这里需要检查的是first_chunk的bk是否指向second_chunk的地址。再看second_chunk的bk,它指向的是后一个被释放的块third_chunk,这里需要检查的是third_chunk的fd是否指向second_chunk的地址

以上三点就是检查chunk是否空闲的三大标准。