what is iofile_vtable 2

전작에서는 대강 개요를 설명하였다. 이번에는 진짜 조금만 더 자세히 들어가보자

C
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 위치가 보인다

C
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 함수에서

C
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 구조체도

C
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 에 위치한 실제 함수들 위치들이기에 같다.

그럼 이제 각각의 함수가 어떨때 사용되는지 알아보자

C

$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로 조금만 가시화해 보면

C
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 함수의 코드는

C
fread(file_buf, 1, sizeof(file_buf)-1, fp);

다음과 같은데 아까보여준것과 같이

C
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 함수를 실행한다.

( 자세한 내용은 아래 글 참조 )

코드확인하기
C
	  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). 함수에는

C
  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
		       fp->_IO_buf_end - fp->_IO_buf_base);

다음과 같은 코드가 있다.

C
#define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)

_IO_SYSREAD 는 다음과 같은 매크로이며

C
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)

JUMP2 는 다음 매크로이다

C
#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 은 다음과 같은 매크로이다.

음. 이제 윤곽이 보인다.

C
#define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)

즉 이게 _IO_jump_t 에서 __read 함수를 가져오는 것이다.

C
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>
}

음, 그러면

C
  __read = 0x7ffff7a6d910 <__GI__IO_file_read>, 

결국 저 함수가 실행 된다. 근데 알겠지만 저 함수는 우리가 아는 read의 레퍼함수이다.

아 참고로 앞에 __GI_ 는 glibc internal symbols 라는 뜻으로 이 부분을 때어내어야 원래 함수 이름이 된다.

C
_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을 사용하고 있다.