what is iofile_vtable 2
전작에서는 대강 개요를 설명하였다. 이번에는 진짜 조금만 더 자세히 들어가보자
pwndbg> p *(struct _IO_FILE_plus *) 0x7ffff7dce760
$13 = {
file = {
_flags = -72537977,
_IO_read_ptr = 0x7ffff7dce7e3 <_IO_2_1_stdout_+131> "",
_IO_read_end = 0x7ffff7dce7e3 <_IO_2_1_stdout_+131> "",
_IO_read_base = 0x7ffff7dce7e3 <_IO_2_1_stdout_+131> "",
_IO_write_base = 0x7ffff7dce7e3 <_IO_2_1_stdout_+131> "",
_IO_write_ptr = 0x7ffff7dce7e3 <_IO_2_1_stdout_+131> "",
_IO_write_end = 0x7ffff7dce7e3 <_IO_2_1_stdout_+131> "",
_IO_buf_base = 0x7ffff7dce7e3 <_IO_2_1_stdout_+131> "",
_IO_buf_end = 0x7ffff7dce7e4 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dcda00 <_IO_2_1_stdin_>,
_fileno = 1,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7dcf8c0 <_IO_stdfile_1_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7dcd8c0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dca2a0 <_IO_file_jumps>
}먼저 다음과 같이 _IO_FILE_plus 구조체가 있고 vtable 인 0x7ffff7dca2a0 위치가 보인다
pwndbg> p *(struct _IO_jump_t *) 0x7ffff7dca2a0
$14 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a6e330 <_IO_new_file_finish>,
__overflow = 0x7ffff7a6f300 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a6f020 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a703c0 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a71c50 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a6d930 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a6d590 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a6cb90 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a70990 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a6c850 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a6c6d0 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a60100 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a6d910 <__GI__IO_file_read>,
__write = 0x7ffff7a6d190 <_IO_new_file_write>,
__seek = 0x7ffff7a6c910 <__GI__IO_file_seek>,
__close = 0x7ffff7a6c840 <__GI__IO_file_close>,
__stat = 0x7ffff7a6d180 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a71dd0 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a71de0 <_IO_default_imbue>
}다음과 같이 볼 수 있다.
왜냐면 glibc에서는 실제로 struct _IO_jump_t {. 다음과 같이 정의가 되어 있기 때문이다.
참고로 이 _IO_jump_t 들은 모든 _IO_FILE_plus 가 공유를 한다.
main 함수에서
int read_account() {
FILE *fp;
fp = fopen("/etc/passwd", "r");
fread(account_buf, sizeof(char), sizeof(account_buf), fp);
write(1, account_buf, sizeof(account_buf));
fclose(fp);
}다음과 같이 열린_IO_FILE_plus 구조체도
pwndbg> p *(struct _IO_FILE_plus *)0x602260
$15 = {
file = {
_flags = 875770416,
_IO_read_ptr = 0x602490 "",
_IO_read_end = 0x602490 "",
_IO_read_base = 0x602490 "",
_IO_write_base = 0x602490 "",
_IO_write_ptr = 0x602490 "",
_IO_write_end = 0x602490 "",
_IO_buf_base = 0x602490 "",
_IO_buf_end = 0x603490 "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dce680 <_IO_2_1_stderr_>,
_fileno = 3,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x602340,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x602350,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7dca2a0 <_IO_file_jumps>
}
0x7ffff7dca2a0. 같은 _IO_jump_t 주소를 가지고 있다. 즉, 공유를 하고 있다.
당연히 테이블 안에 있는 주소들도 glibc 에 위치한 실제 함수들 위치들이기에 같다.
그럼 이제 각각의 함수가 어떨때 사용되는지 알아보자
$14 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a6e330 <_IO_new_file_finish>,
__overflow = 0x7ffff7a6f300 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a6f020 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a703c0 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a71c50 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a6d930 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a6d590 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a6cb90 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a70990 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a6c850 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a6c6d0 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a60100 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a6d910 <__GI__IO_file_read>,
__write = 0x7ffff7a6d190 <_IO_new_file_write>,
__seek = 0x7ffff7a6c910 <__GI__IO_file_seek>,
__close = 0x7ffff7a6c840 <__GI__IO_file_close>,
__stat = 0x7ffff7a6d180 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a71dd0 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a71de0 <_IO_default_imbue>
}__finish <_IO_new_file_finish>
→ 스트림 종료 시, fclose()
__overflow <_IO_new_file_overflow>,
→ 출력 버퍼가 가득 찼을 때 → putc(), printf() 등에서 사용
__underflow = 0x7ffff7a6f020 <_IO_new_file_underflow> →
입력 버퍼가 비었을 때 → getc(), fgets() 등에서 사용
__uflow = 0x7ffff7a703c0 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7a71c50 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a6d930 <_IO_new_file_xsputn>, →
여러 바이트를 출력할 때 사용 → fwrite(), printf() 내부
__xsgetn = 0x7ffff7a6d590 <__GI__IO_file_xsgetn>,
여러 바이트 입력할 때 사용 → fread() 내부
__seekoff = 0x7ffff7a6cb90 <_IO_new_file_seekoff>, __seekpos = 0x7ffff7a70990 <_IO_default_seekpos>, __setbuf = 0x7ffff7a6c850 <_IO_new_file_setbuf>, __sync = 0x7ffff7a6c6d0 <_IO_new_file_sync>, __doallocate = 0x7ffff7a60100 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a6d910 <__GI__IO_file_read>,
read() 래퍼로 fread(), getc() 등에서 사용
__write = 0x7ffff7a6d190 <_IO_new_file_write>,
write() 래퍼로 printf(), fputs() 등에서 사용
__seek = 0x7ffff7a6c910 <__GI__IO_file_seek>,
__close = 0x7ffff7a6c840 <__GI__IO_file_close>,
fclose() 호출 시
__stat = 0x7ffff7a6d180 <__GI__IO_file_stat>, __showmanyc = 0x7ffff7a71dd0 <_IO_default_showmanyc>, __imbue = 0x7ffff7a71de0 <_IO_default_imbue>
이렇게만 보면 어려울 수 있는데
gdb로 조금만 가시화해 보면
pwndbg> bt
#0 __GI__IO_file_read (fp=0x602260, buf=0x602490, size=4096) at fileops.c:1153
#1 0x00007ffff7a6f148 in _IO_new_file_underflow (fp=0x602260) at fileops.c:531
#2 0x00007ffff7a6d748 in __GI__IO_file_xsgetn (fp=0x602260, data=<optimized out>, n=1023) at fileops.c:1344
#3 0x00007ffff7a613c1 in __GI__IO_fread (buf=<optimized out>, size=1, count=1023, fp=0x602260) at iofread.c:38
#4 0x00000000004008b6 in main ()
#5 0x00007ffff7a03b97 in __libc_start_main (main=0x40082b <main>, argc=1, argv=0x7fffffffe668, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe658) at ../csu/libc-start.c:310
#6 0x00000000004006ca in _start ()
pwndbg> bt 명령어서 트레이서 해보면 main 함수에서 fread() 를 사용하니깐 __GI__IO_fread 함수를 거치고
iofile_vtable에 있는 __GI__IO_file_xsgetn 함수가 실행된것을 알 수 있다.
이것을 이용한 overwrite 공격도 있다.
근데 여기서 의구심이 들 수 있다.
그럼 일반적인 read(), write() 함수들도 stdout , stdin 구조체들이나 같이 쓰는데 이 함수들은 vtable을 쓰나요? 정답은 얘내는 안쓴다. 얘내들은 완전 낮은 레벨의 syscall이랑 바로 연결되어 있는 함수이다.
그에 반해 printf나 fread는 조금더 버퍼나 파일 디스크립터에 대해서 생각해야 할 것이 많고 처리해줘야 하는 부분이 많아서 중간에 vtable 함수들을 거친다음에 결국 read, write 시스템콜을 사용하게 된다.
예시를 보여주기 위해 fread 함수를 예시로 들어주겠다.
main 함수의 코드는
fread(file_buf, 1, sizeof(file_buf)-1, fp);다음과 같은데 아까보여준것과 같이
pwndbg> bt
#0 __GI__IO_file_read (fp=0x602260, buf=0x602490, size=4096) at fileops.c:1153
#1 0x00007ffff7a6f148 in _IO_new_file_underflow (fp=0x602260) at fileops.c:531
#2 0x00007ffff7a6d748 in __GI__IO_file_xsgetn (fp=0x602260, data=<optimized out>, n=1023) at fileops.c:1344
#3 0x00007ffff7a613c1 in __GI__IO_fread (buf=<optimized out>, size=1, count=1023, fp=0x602260) at iofread.c:38
#4 0x00000000004008b6 in main ()
#5 0x00007ffff7a03b97 in __libc_start_main (main=0x40082b <main>, argc=1, argv=0x7fffffffe668, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe658) at ../csu/libc-start.c:310
#6 0x00000000004006ca in _start ()다음과 같은 흐름이다.
https://elixir.bootlin.com/glibc/glibc-2.27/source/libio/fileops.c#L469
__GI__IO_file_xsgetn.도 vtable 함수이며 이 함수에서 내부버퍼가 비면 또 vtable을 사용해
_IO_new_file_underflow 함수를 실행한다.
( 자세한 내용은 아래 글 참조 )
코드확인하기
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
if (__underflow (fp) == EOF)
break;
continue;
}다음 __underflow 부분이 _IO_new_file_underflow 실행 부분이다.
_IO_new_file_underflow (_IO_FILE *fp). 함수에는
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);다음과 같은 코드가 있다.
#define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)_IO_SYSREAD 는 다음과 같은 매크로이며
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)JUMP2 는 다음 매크로이다
#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_JUMPS_FUNC 은 다음과 같은 매크로이다.
음. 이제 윤곽이 보인다.
#define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)즉 이게 _IO_jump_t 에서 __read 함수를 가져오는 것이다.
pwndbg> p *(struct _IO_jump_t *) 0x7ffff7dca2a0
$16 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a6e330 <_IO_new_file_finish>,
__overflow = 0x7ffff7a6f300 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a6f020 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a703c0 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a71c50 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a6d930 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a6d590 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a6cb90 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a70990 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a6c850 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a6c6d0 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a60100 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a6d910 <__GI__IO_file_read>,
__write = 0x7ffff7a6d190 <_IO_new_file_write>,
__seek = 0x7ffff7a6c910 <__GI__IO_file_seek>,
__close = 0x7ffff7a6c840 <__GI__IO_file_close>,
__stat = 0x7ffff7a6d180 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a71dd0 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a71de0 <_IO_default_imbue>
}음, 그러면
__read = 0x7ffff7a6d910 <__GI__IO_file_read>, 결국 저 함수가 실행 된다. 근데 알겠지만 저 함수는 우리가 아는 read의 레퍼함수이다.
아 참고로 앞에 __GI_ 는 glibc internal symbols 라는 뜻으로 이 부분을 때어내어야 원래 함수 이름이 된다.
_IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
{
return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
? __read_nocancel (fp->_fileno, buf, size)
: __read (fp->_fileno, buf, size));
}따라서 다음과 같이 우리가 아는 시스콜의 __read 함수를 호출 하는 것을 알 수 있다.
즉, 결국 system call을 호출하는 것은 같다. 그러나 fread와 fwrite 등등 많은 함수들은 이러한
_IO_FILE_plus 구조체의 vtable을 사용하고 있다.