You land code execution, reach for execve("/bin/sh"), and the process dies with
Bad system call (SIGSYS). A seccomp-bpf filter is in the way. The good news:
most filters are blacklists, and a blacklist is only as good as its author's
memory. Read it, find what they forgot, and walk through.
- arch
- amd64-64-little
- seccomp
- blacklist (execve killed)
- goal
- read ./flag despite the filter
read the filter first#
Never guess at the rules — dump them. seccomp-tools runs the binary and prints
the BPF program as readable logic:
The filter operates on struct seccomp_data: nr at offset 0, arch at offset
4, then the six syscall args. Each rule is a comparison; the last one wins. Here:
"if you're x86-64 and calling syscall 59, die; otherwise allow." That and
is the whole vulnerability.
hole 1 — the arch check it skipped#
Notice line 0001: if the arch is not AUDIT_ARCH_X86_64, the filter jumps
straight to ALLOW. The CPU will happily run 32-bit syscalls via int 0x80,
where the same numbers mean different calls — and they arrive tagged
AUDIT_ARCH_I386, which this filter waves through:
So execve is blocked at nr 59, but the i386 execve is nr 11 and a different
arch — both checks miss it. Switch to 32-bit and call it:
x32, the other arch trick
Even when arch is pinned to x86-64, an exact nr == 59 match can be dodged
with the x32 ABI: OR __X32_SYSCALL_BIT (0x40000000) into the number.
0x4000003b isn't 0x3b, so a jeq 0x3b blacklist misses it while the kernel
still dispatches it. Robust filters check nr >= 0x40000000 and reject.
hole 2 — when execve really is gone: ORW#
A tighter jail kills execve and execveat and pins the arch. Fine — you
rarely need a shell, you need the flag. If open/openat, read, and write
survive, just open-read-write it out:
The recurring blacklist gaps, in the order I check them:
openblocked,openat/openat2allowed — nr 2 vs 257/437. Almost always the oversight;openat(AT_FDCWD, …)is identical in effect.read/writeblocked — fall back topread64,preadv,readv,sendfile, orsplice; ormmapthe file and copy it out.- arch not pinned — hole 1; switch to i386/int 0x80 numbering.
ptraceallowed — escape sideways into another process entirely.
the flag
openat was never on the blacklist, so the jail that "blocks file access"
reads its own flag out:
segfault{a_blacklist_is_a_todo_list_for_the_attacker}
The lesson is the same one the filter's author should have internalised:
whitelist, don't blacklist. A blacklist is a list of syscalls you remembered
to fear; the kernel offers hundreds, across multiple ABIs, several of which are
synonyms. A whitelist that allows exactly {openat, read, write, exit_group} and
pins the architecture has no holes to find — which is precisely why you so rarely
see one under time pressure.