stdout 구조체에 값을 쓸 수 있고, fflush(stdout)를 호출할 수 있을 경우 사용 가능한 libc leak 방법을 소개하려고 한다.
libc leak이 필요한데 바이너리 내에 출력 함수가 아예 존재하지 않을 경우 사용할 수 있는 방법이다.
다음은 _IO_fflush
의 코드이다. 이 중 우리가 이용할 것은 _IO_SYNC
이다.
int
_IO_fflush (_IO_FILE *fp)
{
if (fp == NULL)
return _IO_flush_all ();
else
{
int result;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
result = _IO_SYNC (fp) ? EOF : 0;
_IO_release_lock (fp);
return result;
}
}
다음은 _IO_SYNC(stdout)
에 의해 수행되는 _IO_new_file_sync
함수이다. 이 중 _IO_do_flush(fp)
에 주목하자.
int
_IO_new_file_sync (_IO_FILE *fp)
{
_IO_ssize_t delta;
int retval = 0;
/* char* ptr = cur_ptr(); */
if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_do_flush(fp)) return EOF;
delta = fp->_IO_read_ptr - fp->_IO_read_end;
if (delta != 0)
{
#ifdef TODO
if (_IO_in_backup (fp))
delta -= eGptr () - Gbase ();
#endif
_IO_off64_t new_pos = _IO_SYSSEEK (fp, delta, 1);
if (new_pos != (_IO_off64_t) EOF)
fp->_IO_read_end = fp->_IO_read_ptr;
else if (errno == ESPIPE)
; /* Ignore error from unseekable devices. */
else
retval = EOF;
}
if (retval != EOF)
fp->_offset = _IO_pos_BAD;
/* FIXME: Cleanup - can this be shared? */
/* setg(base(), ptr, ptr); */
return retval;
}
_IO_do_flush
는 매크로로 정의되어있고, 이는 내부에서 _IO_do_write
를 수행한다.
또한, _IO_do_write
는 new_do_write
를 수행한다.
#define _IO_do_flush(_f) \
((_f)->_mode <= 0 \
? _IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base) \
: _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \
((_f)->_wide_data->_IO_write_ptr \
- (_f)->_wide_data->_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)
new_do_write
의 내부를 살펴보면, _IO_SYSWRITE
가 존재하는 것을 확인할 수 있다.
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
흐름을 다시 정리해보면 다음과 같다.
_IO_fflush
⇒ _IO_new_file_sync
⇒ _IO_do_flush
⇒ _IO_do_write
⇒ new_do_write
⇒ _IO_SYSWRITE
우리가 신경써야 할 것은 1)_IO_SYSWRITE
에 도달하기 위해 만족시켜야 할 조건과, 2)_IO_SYSWRITE
가 궁극적으로 무엇을 출력하는지이다.
1)_IO_SYSWRITE
에 도달하기 위해 만족시켜야 할 조건
fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_read_end != fp->_IO_write_base
가 성립 x2)_IO_SYSWRITE
가 궁극적으로 무엇을 출력하는지
_IO_SYSWRITE
에 인자로 들어가는 것은 다음과 같다.
_f, (_f)->_IO_write_base, (_f)->_IO_write_ptr-(_f)->_IO_write_base
즉, stdout의 _IO_write_base
에 leak하고 싶은 영역의 주소(ex. 특정함수의 got)를 넣고, _IO_write_ptr
은 _IO_write_base
+ 8만큼의 값을 써주면 내가 원하는 값을 총 8바이트 출력할 수 있게 된다.
따라서 결론적으로 우리가 해야할 일은 다음과 같다.
stdout 구조체를 다음과 같이 변조한다.
1) flag=0xfbad2802
2) io_read_end=io_write_base 그리고 io_write_base 에는 got 적어주고, write_ptr에는 got+8 적어준다.
3) buf_base는 0으로 세팅
fflush(stdout)를 호출한다.
3번 조건은 fclose를 이용할 때 추후 종료 루틴 시 오류 방지를 위한 것이라는데 fflush에서는 굳이 필요 없는거 같기도 하다. 왜냐하면 직접 다른 값을 넣고 디버깅해봤더니 오류가 나지 않고 정상적으로 leak 이후 함수가 종료된다.
문제를 풀던 중 아예 출력함수가 바이너리 내에 존재하지 않아 rop가 가능함에도 libc leak을 할 방법이 보이지 않았다. 그래서 fflush를 이용해 libc leak하는 방법을 찾던 중, fclose를 이용한 leak 방법에 대해 찾게 되었고, fflush와 fclose의 내부 루틴 중 fclose에서 이용한 함수와 겹치는 것이 존재한다는 것을 발견해서 fflush에 직접 적용해보며 알게 된 방법이다. 따라서 이와 동일한 방식을 fclose에도 적용할 수 있다.
이제 직접 디버깅하면서 확인해주자.
일단 stdout구조체를 앞선 조건에 맞게 변조하고 fflush(stdout)
를 호출한 상황이다.
_IO_file_sync
가 호출되고 있다.
다음으로는 _IO_new_do_write
가 실행되는데, 인자를 살펴보면 우리가 변조한대로 stdout, got 주소, 출력할 size 순이다. 원래 size는 8이 되도록 하는 것이 일반적이지만 나는 그냥 내가 입력한대로 size가 정해지는지 확인하려고 16만큼 큰 수를 대입해봐서 0x10이 된 것이다.
다음으로는 _IO_file_write
가 실행된다.
그리고 최종적으로 그 내부에서 write
함수가 실행되면서 값이 leak된다.
pwntools 상에서도 libc관련 값이 받아지는 것을 확인할 수 있다. (디버깅은 로컬, 이 값은 리모트에서 확인해서 릭된 값은 다르다)
사용할 상황이 많을진 모르겠다만 만약 libc base가 필요하고 바이너리 내에 출력함수가 전무한 상황이라면 이 방법을 사용할 수 있겠다.