함수 코드는 다음과 같다.
/* Perform vtable pointer validation. If validation fails, terminate
the process. */
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 값이 __libc_IO_vtables
section을 벗어나지 않는다면 함수는 종료되고, 벗어난다면 _IO_vtable_check()
함수가 실행된다.
_IO_vtable_check
함수까지 분석하기에는 너무 간 것 같아서 하지 않고 넘어간다.
대신, __libc_IO_vtables
section 안에 존재하는 공격가능한 함수를 찾은 후, vtable값을 섹션 안의 값으로 잘 조정하면 exploit할 수 있게 된다.
_IO_str_overflow
함수 - glibc-2.37 버전int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
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 >= (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);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
if (flush_only)
return 0;
else
return c;
}
libc_hidden_def (_IO_str_overflow)
어... write하는 부분도 없고 read하는 부분도 없고 malloc하고 free만 이루어진다.
너무 최신 버전이라 그런가..
dreamhack에서 참고하는 2.27버전을 보자.
_IO_str_overflow
함수 - glibc-2.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);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
libc_hidden_def (_IO_str_overflow)
오.. 뭔가 다르다.
_IO_strfile
구조체를 사용하는 것이 보인다. 구조체를 살펴보자.
typedef void *(*_IO_alloc_type) (_IO_size_t);
typedef void (*_IO_free_type) (void*);
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};
struct _IO_streambuf
{
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
};
typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;
_IO_streambuf
를 보니 뭔가 익숙해서 찾아봤더니 _IO_FILE_plus
와는 다른 struct였다..
glibc 2.37과 비교해 보면, 이 부분이 딱 malloc, free가 이루어지는 부분에 해당한다.
실제로 위에 struct, typedef 들을 확인해 보면
_IO_alloc_type
: size_t
형 인자를 받고, void *
형을 리턴해주는 함수_IO_free_type
: void *
형 인자를 받고, void
리턴해주는 함수즉, 각자가 malloc, free 대신 역할을 해주는 함수 포인터들이다.
아마 여기가 취약점인 것 같다.
dreamhack에서 2.29 이후로는 쓸 수 없는 방법이라고 한 이유가 이거인듯.
struct _IO_str_fields
의 멤버를 원하는 함수로 덮을 수 있다면, exploit이 가능할 것이다. (특히 _IO_free_type
- "/bin/sh"를 인자로 system 함수를 호출해주면 될 듯하다.)
2.27버전 구조체 분석
혹시나 다를까 해서 봤더니 다를 것은 없더라.
그러면 _IO_str_fields
는 어디에 위치해 있는 것인가?
그림으로 확인해 보자. 짱림판 최고!
exploit 하기
파일 관련 함수가 딱 _IO_str_overflow
를 호출하게끔 vtable값을 조작.
_free_buffer
함수포인터가 system
함수를 가리키게 조작.
_free_buffer
함수포인터가 실행되도록 _IO_str_overflow
에서 조건 충족시키기.
참고 자료