[总结型]记CTF PWN中过气的堆利用

前言

how2heap(https://github.com/shellphish/how2heap)可以说是目前"权威"的glibc中的heap exploitation techniques文档。在学术界近年的一些顶会文章也有引用这个github项目。
攻防对抗是一直持续发展的,很容易在学习中,复现一些过时的攻击技术出现困难,原因就是已经有补丁打上了。本文大致总结一些在比赛中常见的过时技术,持续更新…吧

不再好用的unsorted bin attack

2021.4
都2021了还能在一些人的wp上看到如果有unsorted bin attack可以怎么怎么样。
how2heap上的描述是<libc-2.29。实测该补丁已经给到了glibc2.27-3ubuntu1.4。

CTFWiKi中描述
Unsorted Bin Attack 可以达到的效果是实现修改任意地址值为一个较大的数值。

已经不可能实现任意地址的修改了。
新的unsorted bin attack要求修改的地址存放者的是一个特定的指针,类似新unlink技术。补丁如下:

+          if (__glibc_unlikely (bck->fd != victim)
+              || __glibc_unlikely (victim->fd != unsorted_chunks (av)))
+            malloc_printerr ("malloc(): unsorted double linked list corrupted");

一个普遍替代的方案,适用于libc-2.31,那就是large_bin_attack
其效果可以把一个任意地址的内容写成一个largin bin的地址

另一个替代方案,适用与libc-2.31,[tcache_stashing_unlink],可以参考我另一个文章。
其效果有多种,其中之一可以把一个任意地址的内容写成一个main_area的地址(很大的数)。

严格检查的tcache double free

2021.4
tcache对double free有了严格的检查,其补丁已经给到了libc2.27。可以说在只有tache内的double free的情况下,必定能检查出来。
这里做一个笔记吧,一个很巧妙的检查方法。如果要严格检查,思路很简单,那就暴力查找这个链上所有的free chunk。那么这个补丁是怎么权衡性能开销和安全的?

//第一次free的时候,走到这里时
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache; //关键, key被赋值成一个地址,可以理解为一个随机数
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

//第二次free的时候,走到这里时
static void
_int_free (mstate av, mchunkptr p, int have_lock) {
	...
	...
	...
 
    if (tcache != NULL && tc_idx < mp_.tcache_bins)
      {
        /* Check to see if it's already in the tcache.  */
        tcache_entry *e = (tcache_entry *) chunk2mem (p);
        /* This test succeeds on double free.  However, we don't 100%
           trust it (it also matches random payload data at a 1 in
           2^<size_t> chance), so verify it's not an unlikely
           coincidence before aborting.  */
        if (__glibc_unlikely (e->key == tcache)) // 关键,检查这个数。一般来说用户使用后这个随机数会被覆盖。如果没有被覆盖的话,进一步检查。
          {
          
            tcache_entry *tmp;
            //暴力检查链上所有的chunks
            for (tmp = tcache->entries[tc_idx];
                 tmp;
                 tmp = tmp->next)
              if (tmp == e)
                malloc_printerr ("free(): double free detected in tcache 2");
            /* If we get here, it was a coincidence.  We've wasted a
               few cycles, but don't abort.  */
          }
      	...
		...
      }
      ...
      ...
      ...
}

劫持IO_FILE的vtable,_IO_strfile不好用

2021.7
libc2.24以后,vtable会增加检查,所有的vtable跳转都会经过IO_validate_vtable(),添加的逻辑是在宏定义修改的。实测目前最新的libc-2.23-0ubuntu11.2仍然未加小补丁,估计以后也不会加。

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  uintptr_t ptr = (uintptr_t) vtable;
  uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

ctfwiki介绍的一个绕开的思路是使用_IO_strfile的vtable。可惜其中的函数指针libc>=2.29已经弃用,修改的代码如下。实测libc-2.27-3ubuntu1.4已经打上这个补丁,故也无效了。

int
_IO_str_overflow (FILE *fp, int c)
{
...
//new_buf
        = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
        //写死成malloc()了,变成绝对地址call
new_buf = malloc (new_size);
...
}

struct _IO_str_fields
{
  /* These members are preserved for ABI compatibility.  The glibc
     implementation always calls malloc/free for user buffers if
     _IO_USER_BUF or _IO_FLAGS2_USER_WBUF are not set.  */
  _IO_alloc_type _allocate_buffer_unused;
  _IO_free_type _free_buffer_unused;
};
#一个判断是否能用的方法
from pwn import *
f = libc.functions['_IO_str_overflow']
print(libc.disasm(f.address, f.size))

#这样的就是能用的 libc-2.23
   7cd0e:       4c 89 f7                mov    rdi, r14
   7cd11:       ff 93 e0 00 00 00       call   QWORD PTR [rbx+0xe0]
   7cd17:       48 85 c0                test   rax, rax
   7cd1a:       49 89 c7                mov    r15, rax
   7cd1d:       0f 84 cd 00 00 00       je     0x7cdf0
   7cd23:       4d 85 e4                test   r12, r12
   7cd26:       74 1f                   je     0x7cd47
   7cd28:       4c 89 ea                mov    rdx, r13
   7cd2b:       4c 89 e6                mov    rsi, r12
   7cd2e:       48 89 c7                mov    rdi, rax
   7cd31:       e8 ba 77 01 00          call   0x944f0
   7cd36:       4c 89 e7                mov    rdi, r12
   7cd39:       ff 93 e8 00 00 00       call   QWORD PTR [rbx+0xe8]

#这样的是不能用的 libc-2.27
   900ae:       4c 89 f7                mov    rdi, r14
   900b1:       e8 0a 12 f9 ff          call   0x212c0 #call malloc@plt 不可写的got
   900b6:       48 85 c0                test   rax, rax
   900b9:       49 89 c5                mov    r13, rax
   900bc:       0f 84 fe 00 00 00       je     0x901c0
   900c2:       4d 85 e4                test   r12, r12
   900c5:       74 1e                   je     0x900e5
   900c7:       4c 89 fa                mov    rdx, r15
   900ca:       4c 89 e6                mov    rsi, r12
   900cd:       48 89 c7                mov    rdi, rax
   900d0:       e8 8b 10 f9 ff          call   0x21160
   900d5:       4c 89 e7                mov    rdi, r12
   900d8:       e8 eb 11 f9 ff          call   0x212c8 #call free@plt 不可写的got

严格来说,如果假设攻击者有更多的能力,那么IO_str_file在libc-2.31仍然也是可以使用的。

例如,假设已经劫持了malloc_hook或者free_hook,就可以通过_IO_str_overflow来触发。
其触发的参数都是_IO_FILE内相关,很容易控制
具体可参考这篇文章[https://www.anquanke.com/post/id/216290]

glibc2.29以后的vtable是处于可写的数据区。当有足够的写能力时,不必纠结于IO_FILE中的vtable ptr,直接写vtable里面的函数指针即可。

fastbin在malloc_consolidate()也要求chunk_size正确

2021.5
实测libc-2.27.so已有如下代码(但是libc-2.23.so没有补丁),所以如同CTFWIFI中所说的house of rabbit已经过时了

而house of rabbit 就利用了在 malloc consolidate 的时候 fastbin 中的堆块进行合并时 size 没有进行检查从而伪造一个假的堆块,为进一步的利用做准备。

static void malloc_consolidate(mstate av) {
	...
	...
        {
          unsigned int idx = fastbin_index (chunksize (p));
          if ((&fastbin (av, idx)) != fb)
            malloc_printerr ("malloc_consolidate(): invalid chunk size");
        }

abort() 不再刷新标准输入输出缓存

2021.8
偶然发现一些萌新(我也是)分享FSOP或者是house of orange时,会错误提到其触发_IO_flush_all_lockp()的条件可以是abort(),ctf-wiki也是如此说明,然而现在看来这是不对的。
自libc-2.27以来abort()不再fflush(NULL),libc-2.26及以下倒是还在。

这意味着:

  • malloc_printerr()里面走的是abort(),此路不通。
  • 想继续利用FSOP的思路,可能只能走exit()了。
// glibc-2.23 
// in stdlib/abort.c
void
abort (void)
{
  ...
  
  /* Flush all streams.  We cannot close them now because the user
     might have registered a handler for SIGABRT.  */
  if (stage == 1)
    {
      ++stage;
      fflush (NULL);
    }
  ...
  
  /* Now close the streams which also flushes the output the user
     defined handler might has produced.  */
  if (stage == 4)
    {
      ++stage;
      __fcloseall ();
    }
    
  ...
}

// glibc-2.27以后
// in stdlib/abort.c
void
abort (void)
{
//相关flush代码已被删除
...
}

为啥现在libc出错不会打印 Backtrace?

2021.8
萌新(我也是)在打glibc-2.23的时候可能会发现这样的现象:
在这里插入图片描述
实际上在比赛中也会发现服务器打印出这些Backtrace,如此不就是轻松知道基地址了吗?就算开了ALSR,这样的信息有助于寻找libc的小版本。然而,glibc-2.27以后就消失了,这是为啥?

// in glibc-2.23
/* Abort with an error message.  */
void
__libc_message (enum __libc_message_action action, const char *fmt, ...){
  ...
  ...
  if (do_abort)
    {
      BEFORE_ABORT (do_abort, written, fd);

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

// in glibc-2.27
/* Abort with an error message.  */
void
__libc_message (enum __libc_message_action action, const char *fmt, ...){
  ...
  ...
    if ((action & do_abort))
    {
      if ((action & do_backtrace))
	  BEFORE_ABORT (do_abort, written, fd);

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

// in glibc-2.31
/* Abort with an error message.  */
void
__libc_message (enum __libc_message_action action, const char *fmt, ...){
  ...
  ...
  if ((action & do_abort))
    /* Kill the application.  */
    abort ();
}

这是一个悲伤的故事,我本以为是abort()变换,实际上是被哪个大佬把这块给咔嚓了。glibc-2.23的时候是无条件BEFORE_ABORT()的,这里会打印Backtrace。glibc-2.27加了额外的判断,走malloc_printerr()是不打印的,(但是实测这里似乎写的有点问题)。glibc-2.31就直接删掉了。自然Backtrace消失了。下面贴一下BEFORE_ABORT()的实现方便理解。

static void
backtrace_and_maps (int do_abort, bool written, int fd)
{
  if (do_abort > 1 && written)
    {
      void *addrs[64];
#define naddrs (sizeof (addrs) / sizeof (addrs[0]))
      int n = __backtrace (addrs, naddrs);
      if (n > 2)
        {
#define strnsize(str) str, strlen (str)
#define writestr(str) write_not_cancel (fd, str)
          writestr (strnsize ("======= Backtrace: =========\n"));
          __backtrace_symbols_fd (addrs + 1, n - 1, fd);

          writestr (strnsize ("======= Memory map: ========\n"));
          int fd2 = open_not_cancel_2 ("/proc/self/maps", O_RDONLY);
          char buf[1024];
          ssize_t n2;
          while ((n2 = read_not_cancel (fd2, buf, sizeof (buf))) > 0)
            if (write_not_cancel (fd, buf, n2) != n2)
              break;
          close_not_cancel_no_status (fd2);
        }
    }
}
#define BEFORE_ABORT		backtrace_and_maps
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值