Phân tích chi tiết về house_of_apple2

Bài viết này sẽ giới thiệu về kỹ thuật house_of_apple2, một trong ba kỹ thuật house_of_apple. Kỹ thuật này được coi là đơn giản hơn so với các kỹ thuật khác.

Phiên bản glibc sử dụng trong bài viết là 2.35.

Kỹ thuật house_of_apple2

Nguyên lý:

Cấu trúc _IO_FILE cho stdin, stdout, stderr sử dụng bảng gọi hàm _IO_file_jumps. Khi cần gọi hàm từ bảng gọi, sẽ sử dụng các macro. Vì vậy, chúng ta chỉ cần thiết kế cấu trúc FILE giả mạo để thực hiện cuộc tấn công.

Điều kiện sử dụng:

  1. Có thể tiết lộ địa chỉ heap và địa chỉ libc.
  2. Có thể thực hiện một cuộc tấn công largebin.
  3. Có thể kiểm soát chương trình thực hiện các thao tác IO, bao gồm việc trở lại từ hàm main, gọi hàm exit hoặc kích hoạt thông qua __malloc_assert.
  4. Có thể kiểm soát vtable và _wide_data của _IO_FILE, thường sử dụng cuộc tấn công largebin.

Bỏ qua kiểm tra:

1. Kiểm tra vtable:

Từ phiên bản glibc 2.24, đã được thêm kiểm tra vtable:

IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  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))
    _IO_vtable_check ();
  return vtable;
}

Nếu vtable không nằm trong khoảng mong đợi, chương trình sẽ chuyển sang hàm kiểm tra chậm hơn _IO_vtable_check.

2. Cách bỏ qua kiểm tra:

Để bỏ qua kiểm tra, chúng ta có thể sử dụng các vtable nội bộ như _IO_wfile_jumps, _IO_wfile_jumps_mmap, hoặc _IO_wfile_jumps_maybe_mmap để thay đổi vtable, sau đó gọi _IO_wfile_overflow để kiểm soát _IO_wide_data thành không gian heap được kiểm soát, từ đó kiểm soát _IO_wide_data->_wide_vtable thành không gian heap được kiểm soát.

Phân tích nguyên lý cụ thể:

Quá trình thực thi exit:

Dưới đây là chuỗi gọi hàm:

exit -> __run_exit_handlers -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_wfile_overflow -> _IO_wdoallocbuf -> mục tiêu

Sau khi gọi __run_exit_handlers, chương trình sẽ gọi _IO_cleanup, sau đó là _IO_flush_all_lockp.

Hàm _IO_flush_all_lockp:

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;

  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
	   || (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);
    }

  return result;
}

Khi gọi hàm overflow, chương trình sẽ kiểm tra vtable.

Hàm _IO_wfile_overflow:

Để thực hiện cuộc tấn công, chúng ta cần thay đổi vtable thành _IO_wfile_jumps để tránh kiểm tra vtable, sau đó gọi _IO_wfile_overflow.

wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES)
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
    {
      if (f->_wide_data->_IO_write_base == 0)
	{
	  _IO_wdoallocbuf (f);
	  _IO_free_wbackup_area (f);
	  _IO_wsetg (f, f->_wide_data->_IO_buf_base,
		     f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

	  if (f->_IO_write_base == NULL)
	    {
	      _IO_doallocbuf (f);
	      _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
	    }
	}
      else
	{
	  if (f->_wide_data->_IO_read_ptr == f->_wide_data->_IO_buf_end)
	    {
	      f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
	      f->_wide_data->_IO_read_end = f->_wide_data->_IO_read_ptr =
		f->_wide_data->_IO_buf_base;
	    }
	}
      f->_wide_data->_IO_write_ptr = f->_wide_data->_IO_read_ptr;
      f->_wide_data->_IO_write_base = f->_wide_data->_IO_write_ptr;
      f->_wide_data->_IO_write_end = f->_wide_data->_IO_buf_end;
      f->_wide_data->_IO_read_base = f->_wide_data->_IO_read_ptr =
	f->_wide_data->_IO_read_end;

      f->_IO_write_ptr = f->_IO_read_ptr;
      f->_IO_write_base = f->_IO_write_ptr;
      f->_IO_write_end = f->_IO_buf_end;
      f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

      f->_flags |= _IO_CURRENTLY_PUTTING;
      if (f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
	f->_wide_data->_IO_write_end = f->_wide_data->_IO_write_ptr;
    }
  if (wch == WEOF)
    return _IO_do_flush (f);
  if (f->_wide_data->_IO_write_ptr == f->_wide_data->_IO_buf_end)
    if (_IO_do_flush (f) == EOF)
      return WEOF;
  *f->_wide_data->_IO_write_ptr++ = wch;
  if ((f->_flags & _IO_UNBUFFERED)
      || ((f->_flags & _IO_LINE_BUF) && wch == L'\n'))
    if (_IO_do_flush (f) == EOF)
      return WEOF;
  return wch;
}

Chúng ta cần vượt qua ba kiểm tra:

  1. f->_flags & _IO_NO_WRITES
  2. (f->_flags & _IO_CURRENTLY_PUTTING) == 0
  3. f->_wide_data->_IO_write_base == 0

Sau đó, chúng ta sẽ gọi hàm _IO_wdoallocbuf.

Hàm _IO_wdoallocbuf:

void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
      return;
  _IO_wsetb (fp, fp->_wide_data->_shortbuf,
		     fp->_wide_data->_shortbuf + 1, 0);
}

Chúng ta cần vượt qua hai điều kiện:

  1. fp->_wide_data->_IO_buf_base == 0
  2. (fp->_flags & _IO_UNBUFFERED) == 0

Sau cùng, chúng ta sẽ gọi _IO_WDOALLOCATE.

Hàm _IO_WDOALLOCATE:

#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

Chúng ta sẽ gọi đến hàm __doallocate thông qua các macro.

Tóm tắt:

Quy trình tấn công:

  1. Thay đổi vtable của cấu trúc IO giả mạo thành _IO_wfile_jumps để bỏ qua kiểm tra vtable.
  2. Gọi hàm _IO_wfile_overflow.
  3. Vượt qua các kiểm tra.
  4. Gọi hàm _IO_wdoallocbuf.
  5. Gọi hàm _IO_WDOALLOCATE.
  6. Gọi hàm __doallocate.

Điều kiện cần đáp ứng:

  • f->flags != 0x8 && f->flags != 0x800 && f->flags != 0x2
  • vtable được thiết lập thành _IO_wfile_jumps để gọi được _IO_wfile_overflow.
  • _wide_data được thiết lập thành địa chỉ heap kiểm soát, tức là *(f + 0xa0) = heap_addr1.
  • _wide_data->_IO_write_base được thiết lập thành 0, tức là *(heap_addr1 + 0x18) = 0.
  • _wide_data->_IO_buf_base được thiết lập thành 0, tức là *(heap_addr1 + 0x30) = 0.
  • _wide_data->_wide_vtable được thiết lập thành địa chỉ heap kiểm soát, tức là *(heap_addr1 + 0xe0) = heap_addr2.
  • _wide_data->_wide_vtable->doallocate được thiết lập thành địa chỉ C để kiểm soát luồng thực thi, tức là *(heap_addr2 + 0x68) = C.

Thẻ: glibc house_of_apple IO_exploitation

Đăng vào ngày 3 tháng 6 lúc 20:33