ghkdtlwns987

stdout 으로 libc leak 본문

시스템

stdout 으로 libc leak

2020/03/31 2020. 11. 10. 22:56

이번엔 stdout 으로 libc 를 leak 하는 방법에 대해 알아보겠다.

 

stdout 으로 leak 하기 위해선 stdout 구조체에 값을 입력할 수 있어야 한다. 

 

1. stdout 이란? 

stdout 은 '표준 출력 스트림' 이다. 

printf 나, puts 등과 같은 함수를 사용할 시 stdout이 사용된다. 

얘를 들어보자면

  • puts("ABCD") 와 같은 함수가 실행되면  내부적으로 write_sys 를 호출하여 출력이 되는데 , write_sys 는 'kernel buffer' 을 의미한다. 하지만 문자열을 출력할 때마다 커널버퍼를 사용하면 메모리 사용이 상당히 비효율적이다. 
  • 따라서 glibc 내부의 임시버퍼에 해당 문자열을 임시로 저장하는 방식을 이용하는데, _IO_2_1_stdout  _IO_FILE 구조체 타입이다. 
  • 이때 _IO_2_1_stdout 를 이용하여 임시출력버퍼에 저장하는 로직을 거침

 

한번 코드를 통해 분석해 보자.

그냥 단순히 puts() 함수를 사용해 Hello 를 출력하는 코드이다. 

puts 함수를 사용했을 때의 stdout 값을 확인해 보자.

다음을 보면 flag 값이 fbad2084 로 설정되어 있다.

flag 값에 대한 설명은 전에 했으므로 생략, flag 값이 어떻게 구성되었는지 편하게 보기 위해

flag 값만 따로 올리도록 하겠다.

더보기
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

#define _IO_FLAGS2_MMAP 1
#define _IO_FLAGS2_NOTCANCEL 2
#ifdef _LIBC

다음 flag 를 보면 0xfbad2084 이므로 _IO_IS_FILE_BUF + _IO_LINKED + _IO_NO_READS 가 적용되어 있다.

 

뭐 대충 flags 를 확인해 보았으니, 이제부터 puts 함수 내부 루틴을 분석해 보겠다.

다음은 _IO_puts 함수 코드이다.

#include "libioP.h"
#include <string.h>
#include <limits.h>
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;
}
weak_alias (_IO_puts, puts)
libc_hidden_def (_IO_puts)

여기서 눈여겨 봐야 할 것은 _IO_sputn() 함수이다. 

_IO_sputn() 함수는 원하는 길이만큼 출력하기 위해서 호출되는 함수라고 한다. 

 

_IO_sputn() 함수 내부를 보자.

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n) 
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N) 
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) 
#define _IO_JUMPS_FUNC(THIS) \ 
	(IO_validate_vtable \ 
	(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \ 
							+ (THIS)->_vtable_offset))) 

#define _IO_JUMPS_FILE_plus(THIS) \ 
	_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, 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) 

여기서 vtable 어쩌고 하는데, 이건 jump_table 값이다. 

이는 _IO_FILE_vtable 에서 설명했으니 넘어가자.

 

아까 _IO_sputn() 함수를 눈여겨 보라고 했는데, 

vtable 내부에  _IO_new_file_xsputn 함수가 존재하는데, 이번엔 이 함수를 가지고 분석해보겠다.

_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
  const char *s = (const char *) data;
  _IO_size_t to_do = n;
  int must_flush = 0;
  _IO_size_t count = 0;
  if (n <= 0)
    return 0;
  /* This is an optimized implementation.
     If the amount to be written straddles a block boundary
     (or the filebuf is unbuffered), use sys_write directly. */
  /* First figure out how much space is available in the buffer. */
  if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
    {
      count = f->_IO_buf_end - f->_IO_write_ptr;
      if (count >= n)
	{
	  const char *p;
	  for (p = s + n; p > s; )
	    {
	      if (*--p == '\n')
		{
		  count = p - s + 1;
		  must_flush = 1;
		  break;
		}
	    }
	}
    }
  else if (f->_IO_write_end > f->_IO_write_ptr)
    count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
  /* Then fill the buffer. */
  if (count > 0)
    {
      if (count > to_do)
	count = to_do;
#ifdef _LIBC
      f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
#else
      memcpy (f->_IO_write_ptr, s, count);
      f->_IO_write_ptr += count;
#endif
      s += count;
      to_do -= count;
    }
  if (to_do + must_flush > 0)
    {
      _IO_size_t block_size, do_write;
      /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)
	/* If nothing else has to be written we must not signal the
	   caller that everything has been written.  */
	return to_do == 0 ? EOF : n - to_do;
      /* Try to maintain alignment: write a whole number of blocks.  */
      block_size = f->_IO_buf_end - f->_IO_buf_base;
      do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
      if (do_write)
	{
	  count = new_do_write (f, s, do_write);
	  to_do -= count;
	  if (count < do_write)
	    return n - to_do;
	}
      /* Now write out the remainder.  Normally, this will fit in the
	 buffer, but it's somewhat messier for line-buffered files,
	 so we let _IO_default_xsputn handle the general case. */
      if (to_do)
	to_do -= _IO_default_xsputn (f, s+do_write, to_do);
    }
  return n - to_do;
}

코드를 보면 _IO_OVERFLOW 를 호출하는 조건 등이 있는데, 

_IO_xputn.() 함수에 존재하고, 밑을 보면 if문이 존재한다.

  if (to_do + must_flush > 0)
    {
      _IO_size_t block_size, do_write;
      /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)
	/* If nothing else has to be written we must not signal the
	   caller that everything has been written.  */
	return to_do == 0 ? EOF : n - to_do;
      /* Try to maintain alignment: write a whole number of blocks.  */
      block_size = f->_IO_buf_end - f->_IO_buf_base;
      do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
      if (do_write)
	{
	  count = new_do_write (f, s, do_write);
	  to_do -= count;
	  if (count < do_write)
	    return n - to_do;
	}
      /* Now write out the remainder.  Normally, this will fit in the
	 buffer, but it's somewhat messier for line-buffered files,
	 so we let _IO_default_xsputn handle the general case. */
      if (to_do)
	to_do -= _IO_default_xsputn (f, s+do_write, to_do);
    }
  return n - to_do;
}

위 코드를 보면 if(to_do + must_flush) > 0 이 되면 _IO_OVERFLIW 함수를 호출 할 수 있는데,

여기서 to_do 는 출력할 문자의 길이 이다.  

_IO_OVERFLOW 가 호출되면 _IO_new_file_overflow() 함수가 호출된다.

 

_IO_new_file_overflow (FILE *f, int ch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {
      /* Allocate a buffer if needed. */
      if (f->_IO_write_base == NULL)
        {
          _IO_doallocbuf (f);
          _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
        }
      /* Otherwise must be currently reading.
         If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
         logically slide the buffer forwards one block (by setting the
         read pointers to all point at the beginning of the block).  This
         makes room for subsequent output.
         Otherwise, set the read pointers to _IO_read_end (leaving that
         alone, so it can continue to correspond to the external position). */
      if (__glibc_unlikely (_IO_in_backup (f)))
        {
          size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
          _IO_free_backup_area (f);
          f->_IO_read_base -= MIN (nbackup,
                                   f->_IO_read_base - f->_IO_buf_base);
          f->_IO_read_ptr = f->_IO_read_base;
        }
      if (f->_IO_read_ptr == f->_IO_buf_end)
        f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
      f->_IO_write_ptr = f->_IO_read_ptr;
      f->_IO_write_base = f->_IO_write_ptr;
      f->_IO_write_end = f->_IO_buf_end;
      f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
      f->_flags |= _IO_CURRENTLY_PUTTING;
      if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
        f->_IO_write_end = f->_IO_write_ptr;
    }
  if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
                         f->_IO_write_ptr - f->_IO_write_base);
  if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
    if (_IO_do_flush (f) == EOF)
      return EOF;
  *f->_IO_write_ptr++ = ch;
  if ((f->_flags & _IO_UNBUFFERED)
      || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
    if (_IO_do_write (f, f->_IO_write_base,
                      f->_IO_write_ptr - f->_IO_write_base) == EOF)
      return EOF;
  return (unsigned char) ch;
}

_IO_new_file_overflow() 함수의 맽 밑을 보면 _IO_do_write() 함수가 존재한다.

  if ((f->_flags & _IO_UNBUFFERED)
      || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
    if (_IO_do_write (f, f->_IO_write_base,
                      f->_IO_write_ptr - f->_IO_write_base) == EOF)
      return EOF;
  return (unsigned char) ch;
}

바로 이 부분인데, flags & _IO_UNBUFFERED || flags & _IO_LINE_BUF 조건문을 통과하게 되면 

_IO_do_write() 함수가 호출된다. 여기서 _IO_do_write() 함수는

(_IO_write_base, _IO_write_ptr - f -> _IO_write_base) 를 인자로 받는다.

 

-> 즉, flag에 UNBUFFERED 와 LINE_BUF가 flag로 성정되어 있고, ch 가 \n이라면

_IO_do_write() 함수가 호출되는 것이다.

 

그럼 _IO_do_write() 함수를 호출하는데, 이 함수가 무슨 함수인지 알아야 하기 때문에, 

소스코드를 참고하겠다.

int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
  return (to_do == 0
          || (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
  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)
    {
      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;
}

여기서 중요한 우리가 사용할 중요한 함수가 하나 존재한다. 

 

여기서 new_do_write() 함수가 있다.

new_do_write (FILE *fp, const char *data, size_t to_do)

 

 

new_do_write() 함수를 보면

static size_t new_do_write (FILE *fp, const char *data, size_t to_do)
{
  size_t count;
  if (fp->_flags & _IO_IS_APPENDING)                              
    fp->_offset = _IO_pos_BAD;
  else if (fp->_IO_read_end != fp->_IO_write_base)                  
    {
      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;
}

[여기서 중요하게 봐야 할 부분은 count = _IO_SYSWRITE (fp, data, to_do);  이다

우린 _IO_SYSWRITE 함수를 통해 libc leak 을 할 수 있게 된다.

 

=> 여기서 stdout 으로 libc를 leak 할 수 있는데, 

stdout 구조체를 원하는 대로 수정한다면 다음과 같이 변경시킬 수 있다.

(참고로 밑에 f 는 stdout 을 가리킨다.)

_IO_SYSWRITE(f, write_base, write_ptr - write_base) 와 같이 호출할 수 있다.

그렇다면 _IO_write_base 가 출력되고,

write_ptr - write_base 만큼이 출력되는 것이다. 

 

 

 

자! 그렇다면 _IO_SYSWRITE 로 libc 를 leak 할 수 있다는 것을 알게 되었다.

그럼 _IO_SYSWRITE() 함수를 호출하기 위해선 어떻게 해야할까?

위 코드를 분석했다면 알겠지만 flags 를 0xfbad0000 에서 0x1000 , 0x800 을 세팅해주면 된다.

즉, 0xfbad1800 으로 flags 를 변경해 주면 조건문을 통과할 것이다. (stdout 뿐만 아니라, stdin 도 동일하게 적용됨)

이후엔, flags 로부터 _IO_write_base 의 하위 1byte 까지 채워주면 된다.

 

=> p64(0xfbad1800) + '\x00'*25 를 해주면 된다.

여기서 write_base 의 하위 1byte 를 0으로 덮어주는 이유는 _IO_SYSWRITE() 함수에서

(write_ptr - write_base) 해주기 때문에, (_IO_SYSWRITE() 함수의 세번째 인자는 길이를 나타낸다) 

즉, (0x7ffffff8 - 0x7ffffff0) 을 해주면 8만큼의 데이터가 출력된다.

-> _IO_SYSWRITE(1, _IO_write_base , 8 ) 과 같이 될 것이다. 

 

더보기

+ 혹시 모르는 사람을 위해 적겠다.

stdout 에서 _IO_2_1_stdout 의 값은 해당 libc내에 data 영역에 속하기 때문에,

FULL RELRO 에서도 _IO_2_1_stdout 값을 덮을 수 있다. (stdin, stderr 도 마찬가지))

 

 

 

마지막으로 직접 leak 하는 것을 보고 이 글을 마치도록 하겠다.

 

다음과 같이 stdin 값이 있다고 하면(stdout 은 당연히 된다. 단지 stdin 은 안될거 같다는 사람을 위해 stdin 으로 했음)

 

다음은 일반적은 stdin 의 형태이다. 이를 gdb에서 다음과 같은 명령어로 변경해보자.

set {int}0x7ffff7dd18e0 = 0xfbad1800
set {double} 0x7ffff7dd18e8 = 0
set {double} 0x7ffff7dd18f0 = 0
set {double} 0x7ffff7dd18f8 = 0
set {char} 0x7ffff7dd1900 = 0

그럼 밑에 있는 그림과 같이 될 것이다. 

그리고, stdin 을 사용하는 함수 아무거나(fgets,gets .... ) 함수를 실행시키면 

다음과 같이 libc가 출력이 된다.

 

이로서 stdout으로 leak 하는 방법에 대해 마치도록 하겠다.

'시스템' 카테고리의 다른 글

[시스템] dl_fini  (0) 2020.11.18
[시스템] 쉘 코드 모음  (0) 2020.11.16
FSOB(_IO_flush_all_lockp )  (0) 2020.10.10
flose() 분석  (0) 2020.10.10
FSOB(File Stream Oriented Programming) 1  (0) 2020.10.10
Comments