Description
Shellcode as a Service.
Setup
Connect to the challenge server with netcat.
Download the binary to analyze the seccomp filter locally.
nc <challenge_host> <PORT_FROM_INSTANCE>wget <challenge_url>/saas # binary for local analysisSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Analyze the seccomp filter with seccomp-toolsObservationI 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).bashgem install seccomp-toolsbashseccomp-tools dump ./saasbash# Or disassemble the filter from binary directly:bashseccomp-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: onlywriteto fd 1,exit, andexit_groupare allowed.open,read, andexecveare all killed.Step 2
Write write-only memory-scanner shellcodeObservationI 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.pypythonfrom pwn import *bashcontext.arch = 'amd64'bashpythondef make_shellcode(addr):bashreturn asm(f'''bash/* write(1, {addr:#x}, 64) */bashmov rsi, {addr:#x}bashmov edi, 1bashmov edx, 64bashmov eax, 1bashsyscallbash/* exit(0) */bashxor edi, edibashmov eax, 60bashsyscallbash''')bashbashbase = 0x550000000000bashflag_offset = 0x2060 # typical PIE .bss offset for flag globalbashstep = 0x100000bashfor i in range(0x100):bashcandidate = (base + i * step) | flag_offsetbashp = remote('<host>', <PORT_FROM_INSTANCE>)bashsc = make_shellcode(candidate)bashp.send(sc)bashtry:bashdata = p.recvall(timeout=1)bashif b'picoCTF' in data:pythonprint(data)bashbreakbashexcept:bashpassbashp.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/flaginto achar flag[64]global, then callssetup()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) andread(syscall 0) isKILL. 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:
writeto fd 1 andexit/exit_groupare the only allowed syscalls. Writing to an unmapped address raisesSIGSEGV(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 containingpicoCTF.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.Step 3
Run the exploit and capture the flagObservationI 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.pythonpython3 exploit.pybash# 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.