rosieblue
article thumbnail
Published 2023. 7. 18. 22:04
[FSOP] _IO_FILE Linux Exploitation/FSOP
728x90

_IO_FILE

 

_IO_FILE이란, fopen 등의 함수를 통해서 반환하는 함수 포인터가 가리키는 구조체를 말한다.

 

예를 들어 우리는 파일을 열 때 아래와 같은 코드를 사용했다.
FILE *fp=fopen("./test.txt",'r')

이때 fopen을 통해 반환되는 포인터가 _IO_FILE 구조체 포인터가 되는 것이다.

/* The opaque type of streams.  This is the definition used elsewhere.  */
typedef struct _IO_FILE FILE;

_IO_FILE 구조체가 FILE로 쓰이는 것을 알 수 있다.

 

_IO_FILE이란 구조체는 파일의 정보를 담고 있는 구조체이다. fopen을 통해서 파일을 여므로 이때 파일의 정보를 담고 있는 구조체가 생성되는 것이다. _IO_FILE 구조체는 heap 영역에 할당된다.

 

fopen의 반환값 rax의 위치를 출력해보았다.

위를 보면 rax값이 heap에 할당된 것을 볼 수 있다.

 

그렇다면 해당 구조체가 어떤 정보를 가지고 있는지 살펴보자.

 

구조체 정의

아래는 _IO_FILE 구조체의 정의이다.

struct _IO_FILE
{
  int _flags;		   /* (4bytes) High-order word is _IO_MAGIC; rest is flags. */
  //align때문에 flag뒤에 4yte는 더미로 저장되는듯
  /* The following pointers correspond to the C++ streambuf protocol. */
  //8*8=64bytes
  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. */
  //8*5=40bytes
  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; /* Pointer00 to end of non-current get area. */
  struct _IO_marker *_markers;
  struct _IO_FILE *_chain;
  
  int _fileno; //4bytes
  int _flags2; //4bytes
  __off_t _old_offset; /* (8bytes) This used to be _offset but it's too small.  */
  
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column; //2bytes
  signed char _vtable_offset; //1byte
  char _shortbuf[1]; //1byte
  _IO_lock_t *_lock; //8bytes
  //그리고 보통 dummy로 0x0000000000 * 9 있음
#ifdef _IO_USE_OLD_IO_FILE
};

 

  • 맨 위에 있는 flag는 파일의 읽기, 쓰기, 추가 등의 권한을 나타낸다.
    • 0xfbad0000가 매직 넘버로 쓰이고 나머지 비트가 플래그로 사용된다
  • 밑에 있는 8개의 포인터 파일의 읽기 혹은 쓰기 버퍼 주소 등을 나타낸다.
  • _IO_FILE은 _chain 필드를 이용해 singly linked list를 구성한다
    • 해당 리스트의 해더는 라이브러리 전역변수 _IO_list_all에 저장된다.
  • _fileno는 파일 디스크립터의 값을 나타낸다.

 

멤버 변수 설명
_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 필드를 통해 링크드 리스트를 만듭니다. 링크드 리스트의 헤더는 라이브러리의 전역 변수인 _IO_list_all에 저장됩니다.
_fileno 파일 디스크립터의 값입니다.
_IO_jump_ t *vtable 파일 관련 작업을 수행하는 가상 함수 테이블입니다. //얘는 조금 더 뒤에 다루겠다.

 

이러한 입력/출력 버퍼들은 domallocbuf라는 함수에 의해 초기화 및 할당이 되는 것 같다

그리고 _IO_setg등의 함수를 통해 write ptr이런애들을 초기화하는 것 같다. (아래 gpt 설명 참고)

그리고 fwrite 등을 하는 경우 일단 데이터에서 write buf로 복사한다음 write buf에서 file로 쓰는 것 같다(?)

더보기

_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base); 얘는 뭘 하는거야

 

_IO_setg는 C 표준 라이브러리 내부에서 사용되는 함수로서, 파일 스트림(_IO_FILE 구조체)의 입력 버퍼를 설정하는 역할을 합니다.

입력 버퍼는 파일로부터 데이터를 읽어오는데 사용되는 버퍼로, 파일 스트림의 내부에서 관리됩니다. _IO_setg 함수는 해당 입력 버퍼를 설정하기 위해 사용되며, 파일 스트림의 gptr (get pointer), egptr (end get pointer), base (buffer base)를 초기화합니다.

다음은 _IO_setg 함수의 세 가지 인자와 역할에 대한 설명입니다:

  1. f: _IO_FILE 구조체를 가리키는 포인터입니다. 파일 스트림의 입력 버퍼와 관련된 정보를 설정하기 위해 사용됩니다.
  2. f->_IO_buf_base: 입력 버퍼의 시작 주소를 나타냅니다. gptr과 base가 같은 값을 가리키도록 합니다.
  3. f->_IO_buf_base: 입력 버퍼의 끝 주소를 나타냅니다. egptr가 base와 같은 값을 가리키도록 합니다.
  4. f->_IO_buf_base: 입력 버퍼의 현재 위치를 나타냅니다. gptr이 base와 같은 값을 가리키도록 합니다.

이렇게 _IO_setg 함수를 호출하여 파일 스트림의 입력 버퍼를 설정하면, 파일로부터 데이터를 읽어오는 작업에 사용할 수 있게 됩니다. 이 함수는 주로 C 표준 라이브러리의 내부 구현에 사용되며, 일반적으로 사용자가 직접 호출하거나 다루는 일은 없습니다. 사용자는 보통 fopen, fread, getc 등의 C 표준 라이브러리 함수를 사용하여 파일로부터 데이터를 읽어오는데 사용됩니다.

 

아래처럼 FILE의 구조체를 확인해 보았다.

 

 

flags

#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

위를 보면 막 _IO_NO_READS, _IO_NO_WRITES 등의 값이 있는 것을 볼 수 있다.

이처럼 flag은 파일의 읽기, 쓰기, 수정 권한 등이 어떻게 설정되어있는지를 나타낸다.

0xfbad0000은 위에서 언급했듯 매직넘버이다. 따라서 뒤 두 바이트만 플래그로 설정되는 것이다.

 

예를 들어 flag가 0xfbad2c84라면 

0xfbad2c84=0xfbad000+

+0x2000 (_IO_IS_FILEBUF)

+0x0800(_IO_USER_LOCK)+0x0400(_IO_TIRED_PUT_GET) 

+0x80(_IO_LINKED)

+0x4(_IO_NO_READS)

이다.

 

즉, _IO_MAGIC_IO_NO_READS_IO_LINKED_IO_TIED_PUT_GET_IO_CURRENTLY_PUTTING_IO_IS_FILEBUF 비트가 포함된 것을 알 수 있다.

 

 

플래그들은 fopen을 실행하는 과정에서 설정된다.

fopen 은 내부적으로 __fopen_internal을 호출하게되며 __fopen_internal에서 _IO_new_file_fopen을 호출하는 것을 볼 수 있다. _IO_new_file_fopen은 여기서 사용자가 입력한 모드('r', 'w' 등)에 따라 위에 나온 플래그 _IO_NO_WRITES 등을 설정해준다. (fopen -> __fopen_internal -> _IO_new_file_fopen 순으로 호출)

 

 

아래 코드를 보면 mode가 'r'인지 'w'인지 'a' 인지 등에 따라서 read_write 변수의 값을 위에 나온 플래그로 설정해주고 있다.

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;
      ...
  }

 

아래는 _IO_FILE 구조체의 예시인 stdin과 stdout의 메모리 구조이다.

 

예제

위에서 배운 내용을 복습하기 위해 간단한 코드를 작성하였다.

"./tmp.txt"의 내용을 buf에 저장한 후 화면에 출력하는 예제이다.

"./tmp.txt"에는 "hi my name is haeun" 이라는 텍스트가 저장되어 있다.

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(){

    FILE * fp=fopen("./tmp.txt","r");
    char buf[256]={0};

    fread(buf,1,sizeof(buf),fp);
    fclose(fp);

    printf("%s",buf);

    return 0;
}

 

fopen 끝난 후에 fp의 메모리를 확인해 보았다.

flag가 0xfbad2488로 설정되어있고 아직 나머지 IO관련 포인터는 설정되지 않았다.

fopen 후 fp의 메모리

빨간 표시 전까지가 FILE 구조체이다.참고로 빨간 표시에 나타난 0x7ff~5e0은 vtable의 주소인데 후에 설명하겠다.

 

 

fread 후에 fp의 메모리를 다시 확인해 보았다.

 

fread 후 fp의 메모리

0x555555559480이라는 값이 8번 적혀있다. 적힌 위치는 위에서 보았던 포인터들이다.

 

0x555555559480에는 무슨 값이 적혀있을까?

아직 아무 값도 안 적혀있다. 읽기 혹은 쓰기 작업을 하면 해당 위치에 데이터가 적힐 것이다.

 

_IO_FILE_plus

한편 fopen을 통해 생성되는 값은 사실 _IO_FILE이 아니라 _IO_FILE_plus이다.

엥 !?? 뜬금없을 수 있긴 한데 암튼 그렇다...

 

/* We always allocate an extra word following an _IO_FILE.
   This contains a pointer to the function jump table used.
   This is for compatibility with C++ streambuf; the word can
   be used to smash to a pointer to a virtual function table. */
struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};

 

_IO_FILE_plus는 _IO_FILE에다가 vtable이라는 가상함수 테이블을 추가한 것이다.

fopen, fread 등 파일 관련 표준 함수를 호출하면 이 함수들은 vtable안에있는 함수들을 참조하여 호출한다. 

이처럼 _IO_FILE_plus는 함수의 호출을 조금 더 용이하게 할 수 있기 위해 만들어진 것 같다.

 

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

 

한편, vtable안의 값을 조작할 수 있다면 우리가 원하는 함수가 실행되게끔 할 수 있다. 이에 관련해서는 다른 포스트에서 다루도록 하겠다!

 

 

아래 __fopen_internal은 fopen의 internal implementation이다.

__fopen_internal 함수는 malloc으로 locked_FILE을 할당하고 거기에서 FILE을 반환한다.

FILE * __fopen_internal (const char *filename, const char *mode, int is32)
{
  struct locked_FILE
  {
    struct _IO_FILE_plus fp; 
#ifdef _IO_MTSAFE_IO
    _IO_lock_t lock;
#endif
    struct _IO_wide_data wd;
  } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
  //malloc으로 locked_FILE을 할당하고 해당 주소를 new_f에 넣음
  
  if (new_f == NULL)
    return NULL;
#ifdef _IO_MTSAFE_IO
  new_f->fp.file._lock = &new_f->lock;
#endif
  _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
  _IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
  _IO_new_file_init_internal (&new_f->fp);
  if (_IO_file_fopen ((FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file); 
    //제대로 실행이되면 위처럼 new_f가 가리키는 fp(_IO_FILE_plus)의 file(_IO_FILE_plus.FILE)을 반환
  _IO_un_link (&new_f->fp);
  free (new_f);
  return NULL;
}

(1) Allocate a locked_file. (locked_file 반환)
(2) Invoke function _IO_new_file_init_internal. In this function, the newly allocated fp will be inserted into the singly linked list. (_IO_new_file_init_internal에서 singly linked list에 fp 삽입)
(3) Invoke syscall fopen to get a file descriptor of the target file and assign the file descriptor number to the fp->fileno.

(fopen을 통해 target file의 file descriptor 가져오고 걔를 fp->fileno에 넣음)

 

 

참고로 FILE 구조체를 조작할 때 pwntools에서 이런것도 지원해준다고 한다.. 처음 알음 ㅠㅠ

https://docs.pwntools.com/en/stable/filepointer.html

 

pwnlib.filepointer — FILE* structure exploitation — pwntools 4.10.0 documentation

© Copyright 2016, Gallopsled et al. Revision a0c9da07.

docs.pwntools.com

References

https://learn.dreamhack.io/11#41

https://aidencom.tistory.com/187

https://skysquirrel.tistory.com/268

https://learn.dreamhack.io/271#5 

https://dangokyo.me/2018/01/01/advanced-heap-exploitation-file-stream-oriented-programming/

profile

rosieblue

@Rosieblue

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!