# Unlink利用原理

## 原理

一旦涉及到free内存，那么就意味着有新的chunk由allocated状态变成了free状态，此时glibc malloc就需要进行合并操作——向前以及(或)向后合并。这里所谓向前向后的概念如下：将previous free chunk合并到当前free chunk，叫做向后合并；将后面的free chunk合并到当前free chunk，叫做向前合并。

完整的unlink宏如下

```c
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宏如下

```c
/* Take a chunk off a bin list */
define unlink(P, BK, FD) {                                           
  FD = P->fd;                                                          
  BK = P->bk;                                                          
  FD->bk = BK;                                                         
  BK->fd = FD;                                                         
}
```

一、向后合并：

相关代码如下：

```c
    /*malloc.c  int_free函数中*/
/*这里p指向当前malloc_chunk结构体，bck和fwd分别为当前chunk的向后和向前一个free chunk*/
/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
size += prevsize;
  #修改指向当前chunk的指针，指向前一个chunk。
      p = chunk_at_offset(p, -((long) prevsize)); 
      unlink(p, bck, fwd);
}
```

首先检测前一个chunk是否为free，这可以通过检测当前free chunk的PREV\_INUSE(P)比特位知晓。在本例中，当前chunk（first chunk）的前一个chunk是allocated的，因为在默认情况下，堆内存中的第一个chunk总是被设置为allocated的，即使它根本就不存在。

如果为free的话，那么就进行向后合并：

1\)将前一个chunk占用的内存合并到当前chunk; 2)修改指向当前chunk的指针，改为指向前一个chunk。 3)使用unlink宏，将前一个free chunk从双向循环链表中移除(这里最好自己画图理解，学过数据结构的应该都没问题)。

在本例中由于前一个chunk是allocated的，所以并不会进行向后合并操作。

二、向前合并操作：

首先检测next chunk是否为free。那么如何检测呢？很简单，查询next chunk之后的chunk的 PREV\_INUSE (P)即可。相关代码如下：

```c
……
/*这里p指向当前chunk*/
nextchunk = chunk_at_offset(p, size);
……
nextsize = chunksize(nextchunk);
……
if (nextchunk != av->top) { 
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);  #判断nextchunk是否为free chunk
      /* consolidate forward */
      if (!nextinuse) {   #next chunk为free chunk
            unlink(nextchunk, bck, fwd);   #将nextchunk从链表中移除
          size += nextsize;   #p还是指向当前chunk只是当前chunk的size扩大了，这就是向前合并！
      } else
            clear_inuse_bit_at_offset(nextchunk, 0);    

      ……
    }
```

整个操作与”向后合并“操作类似，再通过上述代码结合注释应该很容易理解free chunk的向前结合操作。在本例中当前chunk为first，它的下一个chunk为second，再下一个chunk为top chunk，此时 top chunk的 PREV\_INUSE位是设置为1的(表示top chunk的前一个chunk，即second chunk, 已经使用)，因此first的下一个chunk不会被“向前合并“掉。

介绍完向前、向后合并操作，下面就需要了解执行free（）合并后或者**因为不满足合并条件而没合并**的chunk该如何进一步处理了。在glibc malloc中，会将合并后的chunk放到unsorted bin中(还记得unsorted bin的含义么？)。相关代码如下：

```c
/*
 Place the chunk in unsorted chunk list. Chunks are not placed into regular bins until after they have been given one chance to be used in malloc.
*/  

bck = unsorted_chunks(av);   #获取unsorted bin的第一个chunk
/*
  /* The otherwise unindexable 1-bin is used to hold unsorted chunks. */
    #define unsorted_chunks(M)          (bin_at (M, 1))
*/
      fwd = bck->fd;
      ……
      p->fd = fwd;
      p->bk = bck;
      if (!in_smallbin_range(size))
        {
          p->fd_nextsize = NULL;
          p->bk_nextsize = NULL;
        }
      bck->fd = p;
      fwd->bk = p;  

      set_head(p, size | PREV_INUSE);  #设置当前chunk的size,并将前一个chunk标记为已使用
set_foot(p, size);  #将后一个chunk的prev_size设置为当前chunk的size
/*
   /* Set size/use field */
   #define set_head(p, s)       ((p)->size = (s))
   /* Set size at footer (only when chunk is not in use) */
   #define set_foot(p, s)       (((mchunkptr) ((char *) (p) + (s)))->prev_size = (s))
*/
```

上述代码完成的整个过程简要概括如下：将当前chunk插入到unsorted bin的第一个chunk(第一个chunk是链表的头结点，为空)与第二个chunk之间(真正意义上的第一个可用chunk)；然后通过设置自己的size字段将前一个chunk标记为已使用；再更改后一个chunk的prev\_size字段，将其设置为当前chunk的size；然后更改后一个chunk的size字段的p位，将其设置为0，表示前一个chunk为空闲。

注意：上一段中描述的”前一个“与”后一个“chunk，是指的由chunk的prev\_size与size字段隐式连接的chunk，即它们在内存中是连续、相邻的！而不是通过chunk中的fd与bk字段组成的bin(双向链表)中的前一个与后一个chunk，切记！。

三、Unlink部分安全检测机制

> size大小检测

```c
      #判断nextsize的大小是否是一个正常的值，如果我们fake glibc时将size改成了很大的数，期望达到相应的效果,在nextsize的检测中就会出错
     if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
    || __builtin_expect (nextsize >= av->system_mem, 0))
      {//如果nextsize小于最小的chunk大小，或者大于了整个分配区的内存总量，报错
errstr = "free(): invalid next size (normal)";
goto errout;
      }

  #由于P已经在双向链表中，所以有两个地方记录其大小，所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr ("corrupted size vs. prev_size");
```

> 双链表冲突检测

```c
  #该机制会在执行unlink操作的时候检测链表中前一个chunk的fd与后一个chunk的bk是否都指向当前需要unlink的chunk。这样攻击者就无法替换second chunk的fd与fd了
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      \
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
```

> Double Free检测

```c
/* Lightweight tests: check whether the block is already the top block*/
//判断当前free的chunk是否是top chunk，因为top chunk本身就是一个空闲的chunk，如果是top chunk,造成 double free
if (__glibc_unlikely (p == av->top)){
    errstr = "double free or corruption (top)";
    goto errout;
}
/* Or whether the next chunk is beyond the boundaries of the arena.  */
if (__builtin_expect (contiguous (av)//不是通过mmap分配的，是通过sbrk()分配的
    && (char *) nextchunk   //下一个chunk的地址如果已经超过了top chunk的结束地址，报错
    >= ((char *) av->top + chunksize(av->top)), 0)){
    errstr = "double free or corruption (out)";
    goto errout;
 }
/* Or whether the block is actually not marked used.  */
if (__glibc_unlikely (!prev_inuse(nextchunk))){//如果下一个chunk没有标示将要释放的这个chunk 的p位为0，说明chunk 可能被double free
    errstr = "double free or corruption (!prev)";
    goto errout;
}
```

## 利用

### 通过溢出设置chunk 0数据，伪造fake\_chunk

如图：

```
chunk0                malloc返回的ptr0           chunk1        malloc返回的ptr1
|                     |                        |             |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
|           |         |fake|fake|fake|fake| D  | fake | fake |    |    |      |
|           |         |prev|size| fd | bk | A  | prev | size&|    |    |      |
| prev_size |size&Flag|size|    |    |    | T  | size | flag |    |    |      |
|           |         |    |    |    |    | A  |      |      |    |    |      |
|           |         |    |    |    |    |    |      |      |    |    |      |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
```

我们在malloc返回的ptr0（chunk 0）开始地方构造的数据：

```
p32(0) + p32(81) + p32(&fake_chunk-12) + p32(&fake_chunk-8) + "A"*(80-4*4) + p32(80) + p32(88)
```

这样的话将chunk 0的mem空间伪造成一个fake\_chunk，其中fake\_fd=p32(\&fake\_chunk-12) 代表伪造的chunk头在程序内存空间中的地址-12， fake\_bk=p32(\&fake\_chunk-8) 这样做的话执行unlink操作时

```c
FD=P->fd = &fake_chunk-12 ，
BK=P->bk = &fake_chunk-8 ，
FD->bk ，即 *(&fake_chunk-12+12) = *(&fake_chunk) = buf[0] = fake_chunk = p 
BK->fd ，即*(&fake_chunk-8+8) = *(&fake_chunk) = buf[0] = fake_chunk = p
```

这样就绕过了双向链表检查。

```c
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))             
      malloc_printerr (check_action, "corrupted double-linked list", P);
```

接下来绕过前后size检查，这个就很简单，只需将chunk 1的fake\_prev\_size覆盖为ptr0当前mem空间大小即80，并且fake\_chunk的fake\_size大小必须为ptr0当前mem空间大小加上p标志位=81.

覆盖chunk 1的fake\_size\&flag要正确，为偶数代表前一个chunk为空可合并，大小满足原本chunk大小，这样可以避免错误的size大小以致double free报错.

### free(chunk\[1])，触发unlink

由于在 chunk 1 前面构造了一个伪造的空闲内存块，当free(chunk\[1])时，就会对伪造的空闲内存块进行unlink操作：

```c
F = p -> fd;    #F = &fake_chunk - 12
B = p -> bk;    #B = &fake_chunk- 8
if (F -> bk == p && B -> fd == p){
  F -> bk = B;    #即buf[0] = B = &fake_chunk - 8
  B -> fd = F;    #即buf[0] = F = &fake_chunk -12
}
```

从上可知，unlink后，buf\[0]存的不再是ptr0 的地址了，而是\&fake\_chunk - 12 (即 如下图中的 \&buf-12)。此时我们只关心buf数组的内存，其布局如下：

![](https://firebasestorage.googleapis.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LckC1dmB2Mb2F1Bgx_H%2Fuploads%2FJIkOnhf0sRFiuZJpllyb%2Ffile.png?alt=media)

## 总结

程序中存在堆溢出且长度可观时，很容易构造出unlink；但是当程序没有长度溢出，或者堆大小固定时，我们可以构造chunk错位（伪造）的方式来构造unlink的空闲chunk；还有就是利用合并后被放入unsortedbin中的chunk，利用UAF ，对合并前的堆块进行构造。详细见[2018强网杯silent2](https://bbs.pediy.com/thread-247020.htm)和[网鼎杯Pwn之babyheap](https://b0ldfrev.top/2018/09/02/%E7%BD%91%E9%BC%8E%E6%9D%AFPwn%E4%B9%8Bbabyheap/)

## 参考

> [Linux堆溢出漏洞利用之unlink – 阿里移动安全](http://www.vuln.cn/6327)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://b0ldfrev.gitbook.io/note/pwn/unlink.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
