서론
우리는 파일을 열 때 파일 접근 유형을 명시해야 하는데, 대표적으로 읽기 및 쓰기 모드가 있다.
읽기 모드로 파일을 열고, 파일에 데이터를 작성하려 하면 에러가 발생하지는 않지만 기능이 수행되지 않는다.
모든 파일 함수는 fopen 함수에서 반환한 파일 포인터를 인자로 전달받고, 기능을 수행하기에 앞서 파일 포인터를 참조해 파일 정보를 먼저 확인한다. 파일의 정보로는 파일이 어떤 모드로 열렸으며, 파일 작업을 수행하기 위한 함수의 주소가 포함된다. 따라서 파일 작업이 어떻게 이뤄지는지 알기 위해서는 파일 구조체를 이해하고 있어야 한다.
_ IO_FILE
_IO_FILE은 리눅스 시스템의 표준 라이브러리에서 파일 스트림을 나타내기 위한 구조체이다. 이는 파일을 열기 위한 fopen 함수를 사용할 때 힙 영역에 할당된다.
아래는 _IO_FILE 파일 구조체이다.
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
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
};
_IO_FILE: _flags
_flags 멤버 변수는 파일의 성질을 나타내는 필드이다. 해당 필드는 fopen 함수로 파일을 열 때 전달한 모드에 따라 값이 설정된다. 아래는 _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
예제 코드는 다음과 같다.
// 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;
}
이를 컴파일하고 실행해보면 다음과 같은 결과가 출력되는데, 0xfbad2c84는 _IO_MAGIC이라는 매직 넘버를 포함한 각 권한을 의미한다. 해당 값은 _IO_MAGIC, _IO_NO_READS, _IO_LINKED, _IO_TIED_PUT_GET, _ID_CURRENTLY_PUTTING, _IO_IS_FILEBUF 비트가 포함된 것을 알 수 있다.
$ ./iofile
_flags: fbad2c84
_fileno: 3
_flags 멤버 변수는 fopen 함수로 파일을 열 때 전달한 모드에 따라 값이 설정된다. 아래 코드는 fopen 함수가 호출될 때 실행되는 내부 함수인 _IO_new_file_fopen이다.
코드를 살펴보면, fopen 함수의 두 번째 인자인 mode 변수가 'r', 'w', 'a' 문자 인지를 확인하고, 각 권한에 해당하는 비트가 할당되는 것을 확인할 수 있다.
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;
...
}
더 자세히 살펴보면, read_write 뿐만이 아니라 omode 변수에 O_RDONLY, O_WRONLY 등의 값이 저장되는 것을 볼 수 있다. fopen 함수는 결국 open 시스템 콜을 호출해 파일을 열게 되는데, 이때 해당 시스템 콜의 인자로 전달된다.
_IO_FILE: vtable
앞서 _IO_FILE 구조체가 포함된 _IO_FILE_plus 구조체를 살펴보면 vtable 포인터가 존재하는 것을 확인할 수 있다. Virtual function Table (vtable)은 객체 지향 프로그래밍 언어에서 클래스를 정의하고 가상 함수를 사용할 때 할당되는 테이블이다. 이는 메모리에 가상 함수를 담을 영역을 할당하고, 함수의 주소를 기록한다.
파일 구조체는 각 파일마다 _IO_FILE 구조체뿐만 아니라 함수 테이블을 가지고 있다. 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);
};
구조체를 살펴보면, 16바이트 크기의 더미 바이트를 포함해 다양한 함수가 정의된 것을 확인할 수 있다.
아래 코드는 디버깅을 통해 fopen 함수로부터 반환된 파일 포인터의 vtable 모습을 확인한 결과이다.
gdb-peda$ p *(struct _IO_jump_t *)0x00007ffff7dca2a0
$32 = {
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x7ffff7a6e400 <_IO_new_file_finish>,
__overflow = 0x7ffff7a6f3d0 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a6f0f0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a70490 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a71d20 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a6da00 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a6d660 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a6cc60 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a70a60 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a6c920 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a6c7a0 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a601e0 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a6d9e0 <__GI__IO_file_read>,
__write = 0x7ffff7a6d260 <_IO_new_file_write>,
__seek = 0x7ffff7a6c9e0 <__GI__IO_file_seek>,
__close = 0x7ffff7a6c910 <__GI__IO_file_close>,
__stat = 0x7ffff7a6d250 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a71ea0 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a71eb0 <_IO_default_imbue>
}
_IO_FILE: vtable 호출 과정
아래 코드를 살펴보면 fread 함수는 _IO_fread 함수와 동일하게 정의된 것을 볼 수 있으며, 해당 함수 내부에서 _IO_sgetn 함수를 호출한다.
#define fread(p, m, n, s) _IO_fread (p, m, n, s)
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;
}
또한, 아래는 _IO_sgetn 함수의 구현체로, _IO_XGETN을 호출한다. 코드의 상단에서 정의된 매크로를 순차적으로 확인해보면 결국 앞서 살펴본 vtable 변수를 참조하는 것을 확인할 수 있다.
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable ( (THIS)))
size_t
_IO_sgetn (FILE *fp, void *data, size_t n)
{
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n);
}
fread를 포함한 모든 파일 함수는 이처럼 가상 함수 테이블을 참조해 파일 작업을 시도한다. 그러나 해당 함수 테이블은 동적으로 할당되는 영역으로 쓰기 권한이 있기 때문에 공격에 악용될 수 있다.
'시스템해킹 > 개념' 카테고리의 다른 글
| Unsorted bin 공격 정리 (1) | 2025.07.13 |
|---|---|
| Double Free bug와 Fastbin dup 복습 (0) | 2025.06.25 |
| [Dreamhack-system] SigReturn-Oriented Programming (3) | 2024.12.18 |
| [Dreamhack-system] Background : _rtld_global (3) | 2024.12.17 |
| [Dreamhack-system] Background: Master Canary (2) | 2024.12.16 |