Off-by-null 的利用方法


不知道为什么以前都不把这些记录下来,然后每次做到的时候都会忘了从零开始学。

本文将主要介绍 2.23 和 2.31 版本下的利用,2.27 和 2.23 差不多,多填一个 tcache 即可。2.29 和 2.31 也差不多,多了一个 key。

因此,阅读本文你需要对 Heap 的分配有一定的基础,本文将更多涉及方法而非原理。

本文涉及的挑战主要有以下几个特征

  • 存在 off-by-null
  • 分配次数几乎不受限
  • 分配大小几乎不受限或是能分配 largebin 范围
  • 不存在 edit 函数或只能 edit 一次
  • 只能 show 一次(按理来说不能 show 也可以,但是堆块分配太麻烦了,做这种题不如睡觉)

off-by-null

一般来说 off by null 有以下几种可能

  • strcpy 之类会往末尾额外添加 \x00 的函数
  • 一个循环中 read(0, buf, 1) 然后判断 *buf = 0xA ? break,在循环外 *buf = 0 的情况

而它仅仅能造成 1 字节的越界写,且只能填写 \x00,因此这种攻击原语能力小于 off-by-one。

一般而言,我们都会用这个攻击原语修改 chunk 的 prev_inuse 位,或是修改 fdbk 指针等使其指向一个最低字节为 \x00 的堆块,进而利用 malloc_consolidate 造成堆块重叠 (chunk overlapping) 或是利用 fd 指针使得空闲链表指向一个已分配的堆块,从而达到 UAF 的效果。

源码分析

malloc_consolidate

在 free 堆块大于 get_max_fast() 时,就会进入 consolidate 过程,在这里,我们的 backward 指的是低地址的堆块,forward 则是高地址的堆块。

backward_consolidate

可以看到,对于当前 free 的堆块 p,它首先检查自己的 prev_inuse 位,如果为 0,那么就根据 prev_size 找到上一个堆块,然后对上一个堆块进行 unlink 操作。

/* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }

    if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

      /* consolidate forward */
      if (!nextinuse) {
	unlink(av, nextchunk, bck, fwd);
	size += nextsize;
      } else
	clear_inuse_bit_at_offset(nextchunk, 0);

forward_consolidate

接着,在下一个 chunk 不是 topchunk 的情况下,它检查下一个 chunk 的再下一个 chunk 的 prev_inuse 位,来确定下一个 chunk 是否被使用,同样的,它对下一个 chunk 进行 unlink 操作。

不难发现,在 2.23 下,我们的 consolidate 过程完全没有保护。

在 2.23 下,我们注意到 2.23 仅有一个 check 需要绕过。

也就是对于 unlink 的堆块 P,我们要求 P->fd->bk == P == P->bk->fd

#define unlink(AV, P, BK, FD) {                                            \
    FD = P->fd;								      \
    BK = P->bk;								      \
    // highlight-next-line
    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;		      \
              }								      \
          }								      \
      }									      \
}

利用思路

在 off-by-null 的情况下,我们一个非常直观的想法就是去覆写一个 chunk 的 prev_inuse 位,使其变为 0,因此,chunk 大小应该在 0x100 以上。

紧接着,如果我们对这个修改后的堆块进行 free 操作,那么它便会进入 consolidate backward 的环节,取出 prev_size 的堆块进行 unlink

此时,由于我们可以设置 prev_inuse,因此,我们也就可以控制这个堆块,使其指向一个较为低地址的堆块,包裹我们已经分配的堆块,造成堆块重叠。

2.23 下我们仅需要绕过 unlink 的检查。这是极其容易的,我们可以利用 unsortedbinfdbk,初始情况下,这个双链就是这么链接的。

image-20240913162609542

/*
┌──────────────────────────────────────────────────────────────────────┐
│                                                                      │
│                                                                      │
│         ┌────────────────┐                    ┌─────────────────┐    │
│         │                │                    │                 │    │
└─────────►           fd   ─────────────────────►            fd   ─────┘
          │                │                    │                 │     
          │    unsorted    │                    │   chunk         │     
          │                │                    │                 │     
┌────────── bk             ◄───────────────────── bk              ◄────┐
│         │                │                    └─────────────────┘    │
│         └────────────────┘                                           │
│                                                                      │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘
*/

用图画一下大概就是这个感觉

利用手法

那么在 2.23 下,我们可以这样构造

分配四个堆块 A: 0x90, B: 0x20, C: 0x100, D: 0x20

  1. 首先释放 Aunsorted bin
  2. 此时我们在 B 上做 obn,修改 Cprev_size0x90+0x20,并且利用 null 修改 Cprev_inuse0
  3. 释放堆块 C,触发 backward consolidate,此时有 P = C - prev_size = A,触发 unlink(P)unlink 会判断 P->fd->bk == P == P->bk->fd,由于 Aunsorted bin 里,所以此时这个是满足的,因此 A size 被修改为 0x100+0x20+0x90,造成 overlap
add(0x80)  # A
add(0x18)  # B
add(0xf0)  # C
add(0x10)  # D

free(B)  # prepare for later obn
free(A)  # A <-> unsorted
add(0x18, b'A*0x10' + p64(0x90+0x20) + b'\n')  # obn
free(C)  # A->size = 0x100+0x20+0x90
add(0x80)  # malloc remains
add(0x18)  # same as B, E
目录