fflush를 이용한 libc leak

chk_pass·2024년 3월 29일
0

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_writenew_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_writenew_do_write_IO_SYSWRITE

우리가 신경써야 할 것은 1)_IO_SYSWRITE 에 도달하기 위해 만족시켜야 할 조건과, 2)_IO_SYSWRITE 가 궁극적으로 무엇을 출력하는지이다.

1)_IO_SYSWRITE 에 도달하기 위해 만족시켜야 할 조건

  1. fp->_IO_write_ptr > fp->_IO_write_base
  2. fp->_IO_read_end != fp->_IO_write_base 가 성립 x

2)_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바이트 출력할 수 있게 된다.

따라서 결론적으로 우리가 해야할 일은 다음과 같다.

  1. stdout 구조체를 다음과 같이 변조한다.

    1) flag=0xfbad2802

    2) io_read_end=io_write_base  그리고 io_write_base 에는 got 적어주고, write_ptr에는 got+8 적어준다.

    3) buf_base는 0으로 세팅

  2. 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가 필요하고 바이너리 내에 출력함수가 전무한 상황이라면 이 방법을 사용할 수 있겠다.

0개의 댓글