Help Ukraine, click for information
root@sovietghost:/blog/36-ctf-sandbox-writeup# cat post.md
Title: Breaking the Sandbox: Memory Mapping & Seccomp Escape
Author: SovietGhost
Date: 10/10/2025
Description: CTF writeup detailing a sandbox escape challenge: fixed RWX mmap, fork-before-seccomp, and non-interactive payloads to retrieve the flag.
Tags: [ctf, sandbox, seccomp, mmap, exploitation, writeup]
Status: published

> Breaking the Sandbox: Memory Mapping & Seccomp Escape_

Quote:

Quick TL;DR: The service mmap()s a fixed RWX page, fork()s, applies seccomp only in the child, and the parent executes attacker-supplied bytes from that RWX page. Proof-of-concept: send compact shellcode that opens and prints the flag (non-interactive). No ROP, no libc leaks — one-shot shellcode.


## Overview

  • >Target: nc 34.107.44.96 11773 (remote service)
  • >Goal: read the flag and print it to the socket (non-interactive).
  • >Constraints: the service reads up to 0x100 bytes from the connection into a fixed RWX buffer and then executes those bytes.
  • >Key weakness: fork() occurs before the child applies seccomp; only the child is sandboxed.

This is a classic CTF trick — the service tries to be clever with seccomp but forgets the order of operations. The execution occurs in the unsandboxed parent.


## RE (what matters)

From the decompilation (relevant excerpts):

terminal
puVar1 = (undefined8 *)mmap((void *)0x2875890000,0x200000,7,0x22,-1,0); ... sandbox_call(); void sandbox_call(long *param_1) { // fill mapped region with 0x90 fork(); read(0, (void *)(*param_1 + 8), 0x100); // read attacker bytes into base+8 if (is_parent) { (*(code *)(psVar4 + 1))(); // call base+8 -> execute injected bytes } else { sandbox_check(param_1); // child applies seccomp } }
  • >Mapping: fixed base 0x28758a0000 (absolute addressing possible).
  • >Execution: the parent executes pointer base+8 after reading up to 0x100 bytes there.
  • >Seccomp: applied to child only. Parent keeps syscalls.

Result: Parent executes your code unsandboxed; you can do open/read/write as long as parent has permissions.


## Exploit strategy

Goals:

  1. >Avoid interactive shell — the CTF expects flag output.
  2. >Fit payload into 0x100 bytes.
  3. >Use absolute addresses inside the mapped region to store small pointer table + strings + loop code.
  4. >Try multiple likely flag paths in a compact loop; on success read and write the flag back to the socket.

Why this works:

  • >The mapped region is RWX and at a fixed address: we can place code and data together and reference them by absolute addresses.
  • >The parent executes the injected code unsandboxed, so syscalls like open, read, write succeed if permitted by OS permissions.

## PoC payload (concept)

  • >

    Layout inside the single read payload:

    • >Code (loop + syscalls)
    • >Pointer table (QWORDs) pointing into the string area
    • >Concatenated null-terminated path strings
  • >

    Loop (pseudocode):

    • >for each pointer in table:
      • >fd = open(ptr, O_RDONLY)
      • >if fd >= 0:
        • >n = read(fd, BUF, 0x100)
        • >write(1, BUF, n)
        • >exit(0)
    • >exit(1)
  • >

    Addresses used (from binary):

    • >MMAP_BASE = 0x28758a0000
    • >BUF = MMAP_BASE + 0x100
    • >PTR_TABLE = MMAP_BASE + 0x140
    • >DATA = MMAP_BASE + 0x180

The PoC assembly assembles into a single ≤0x100-byte blob which is sent straight to the target; the parent executes it and returns the flag contents.


## Exploit script (what we used)

We built a Python (pwntools) script that:

  • >Assembles compact loop-based shellcode.
  • >Packs pointer table + strings after the code.
  • >Automatically trims the candidate list until the full payload fits into 0x100 bytes.
  • >Sends the payload and prints whatever the server returns.

This approach avoids heavy per-string writes (which blow up the code size) and relies on placing the strings as data.

(Full exploit code omitted here — included in appendix if you want the exact script.)


## Robustness & edge cases

  • >Why not use rsp for buffers? Using rsp can overwrite the shellcode or stack and crash the parent. We instead used a fixed buffer inside the mapped region to be safe.
  • >Why not ROP/mprotect? No need: the mapping is RWX. ROP would be heavier and unnecessary.
  • >Seccomp concerns: Parent is unsandboxed per decompilation; if the challenge had applied seccomp to the parent as well, this would fail. Always confirm which process gets filters.
  • >Path unknown: Since the binary doesn't hardcode the flag path, we probe likely locations in one compact payload. If none succeed, send additional batches with extended candidate lists.

## Lessons learned / mitigation

  • >Order matters: applying seccomp to the wrong process defeats the point of the sandbox. Always ensure policies are applied to all code paths that execute untrusted input.
  • >Avoid RWX when possible: use W^X (write xor execute) or mmap without exec to store untrusted data.
  • >Don't execute network-provided bytes: code should not execute untrusted input; if you must, run it in a properly isolated sandbox (separate user, namespace, cgroup, and seccomp filters applied to the executing process).
  • >Use capability drops & least privilege: reduce available syscalls and capabilities for processes that must interact with untrusted input.

## Appendix: example candidate paths tried

  • >/flag
  • >/flag.txt
  • >/home/ctf/flag
  • >/home/ctf/flag.txt
  • >/tmp/flag
  • >/root/flag
  • >/etc/flag
  • >/app/flag
  • >/var/flag
  • >/opt/flag
  • >/challenge/flag

(Exploit auto-trims the list to fit payload size.)


## Closing

This was a clean challenge: a clever mix of mmap and seccomp, but the ordering mistake made it solvable without complicated exploitation techniques. The simplest, most reliable PoC: send compact shellcode that opens likely flag files and prints the contents. Keep your sandboxes correctly ordered next time.


root@sovietghost:/blog/36-ctf-sandbox-writeup# ls -la ../

> Thanks for visiting. Stay curious and stay secure. _