ctfwiki--堆的基础操作

参考链接:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/implementation/basic-zh/#__comments

堆的基础操作

unlink

unlink 用来将一个双向链表(只存储空闲的 chunk)中的一个元素取出来,可能在其他地方使用。

(1)malloc

  1. 从恰好合适的 large bin 中获取 chunk
    需注意:fastbin 和 small bin 没有使用 unlink,这就是为什么漏洞会经常出现在它们这里的原因。
    依次遍历处理 unsorted bin 时也没有使用 unlink 的。
  2. 从比请求 chunk 所在的 bin 大的 bin 中取 chunk

(2)free

  1. 后向合并,合并物理相邻低地址空闲 chunk
  2. 前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)

(3)malloc_consolidate

  1. 后向合并,合并物理相邻低地址空闲 chunk
  2. 前向合并,合并物理相邻高地址空闲 chunk(除了 top chunk)

(4)realloc

	前向扩展,合并物理相邻高地址空闲 chunk(除了 top chunk)

由于 unlink 使用非常频繁,因此 unlink 被实现了一个宏:

/* Take a chunk off a bin list */
// unlink p
#define unlink(AV, P, BK, FD) {                                            \
    // 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr ("corrupted size vs. prev_size");               \
    FD = P->fd;                                                                      \
    BK = P->bk;                                                                      \
    // 防止攻击者简单篡改空闲的 chunk 的 fd 与 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;                                                              \
        // 下面主要考虑 P 对应的 nextsize 双向链表的修改
        if (!in_smallbin_range (chunksize_nomask (P))                              \
            // 如果P->fd_nextsize为 NULL,表明 P 未插入到 nextsize 链表中。
            // 那么其实也就没有必要对 nextsize 字段进行修改了。
            // 这里没有去判断 bk_nextsize 字段,可能会出问题。
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {                      \
            // 类似于小的 chunk 的检查思路
            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);                                              \
            // 这里说明 P 已经在 nextsize 链表中了。
            // 如果 FD 没有在 nextsize 链表中
            if (FD->fd_nextsize == NULL) {                                      \
                // 如果 nextsize 串起来的双链表只有 P 本身,那就直接拿走 P
                // 令 FD 为 nextsize 串起来的
                if (P->fd_nextsize == P)                                      \
                  FD->fd_nextsize = FD->bk_nextsize = FD;                      \
                else {                                                              \
                // 否则我们需要将 FD 插入到 nextsize 形成的双链表中
                    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;                      \
              }                                                                      \
          }                                                                      \
      }                                                                              \
}

这里以 small bin 的 unlink 为例子介绍一下,对于 large bin 的 unlink, 与其类似,只是多个 nextsize的处理。
在这里插入图片描述
可以看出:P最后的fd 和 bk 指针并没有发生变化。但是当我们去遍历整个双向链表时,已经遍历不到对应的链表了。
这一点没有变化还是有用处的,因为我们有时候可以使用这个方法来泄露地址:

  1. libc 地址
    P 位于双向链表头部,bk 泄露
    P 位于双向链表尾部,fd 泄露
    双向链表只包含一个空闲 chunk 时,P 位于双向链表中,fd 和 bk 均可泄露

  2. 堆地址,双向链表包含多个空闲 chunk
    P 位于双向链表头部,fd 泄露
    P 位于双向链表中,fd 和 bk 均可泄露
    P 位于双向链表尾部,bk 泄露

注意:
这里的头部指的是 bin 的 fd 指向的 chunk, 即双向链表中最新加入的 chunk
这里的尾部指的是 bin 的 bk 指向的 chunk, 即双向链表中最先加入的 chunk

同时,无论是 fd bk 还是 fd_nextsize, bk_nextsize,程序都会检测 fd 和 bk 是否满足对应的要求

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

  // next_size related
              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);

注意:
堆的第一个 chunk 所记录的 prev_inuse 位默认为1.

malloc_printerr

当 glibc malloc 检测到错误时,会调用 malloc_printerr 函数

static void malloc_printerr(const char *str) {
  __libc_message(do_abort, "%s\n", str);
  __builtin_unreachable();
}

主要会调用 __libc_message 来执行 abort 函数,如下:

  if ((action & do_abort)) {
    if ((action & do_backtrace))
      BEFORE_ABORT(do_abort, written, fd);

    /* Kill the application.  */
    abort();
  }

在 abort 函数中,在 glic 还是 2.23 版本时,会 fflush stream.

  /* Flush all streams.  We cannot close them now because the user
     might have registered a handler for SIGABRT.  */
  if (stage == 1)
    {
      ++stage;
      fflush (NULL);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值